From b255c894fa25e05ae280a1e7f938deb0f3673232 Mon Sep 17 00:00:00 2001 From: Julien Muchembled <jm@nexedi.com> Date: Thu, 23 Dec 2010 16:37:57 +0000 Subject: [PATCH] PortalTransforms: merge upstream 2.0 This fixes test_20_reStructuredText partially. Conflicts: Products/PortalTransforms/TransformEngine.py Products/PortalTransforms/libtransforms/commandtransform.py Products/PortalTransforms/transforms/safe_html.py Products/PortalTransforms/utils.py git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@41726 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/PortalTransforms/TransformEngine.py | 273 ++++++++++++------ product/PortalTransforms/cache.py | 34 ++- .../libtransforms/commandtransform.py | 46 +-- .../PortalTransforms/libtransforms/utils.py | 23 +- .../PortalTransforms/tests/input/markdown.txt | 2 + .../tests/input/test_safehtml.html | 4 + .../PortalTransforms/tests/output/demo1.html | 210 +------------- .../tests/output/demo1.html.nofilename | 210 +------------- .../tests/output/markdown.html | 1 + .../PortalTransforms/tests/output/rest2.out | 6 +- .../PortalTransforms/tests/output/rest3.out | 10 +- .../tests/output/test_safe.html | 4 + product/PortalTransforms/tests/test_engine.py | 55 ++++ product/PortalTransforms/tests/test_graph.py | 81 ++++++ .../PortalTransforms/tests/test_transforms.py | 31 +- product/PortalTransforms/tests/utils.py | 1 + .../transforms/markdown_to_html.py | 20 +- .../transforms/office_wvware.py | 6 +- .../PortalTransforms/transforms/safe_html.py | 57 +++- .../transforms/word_to_html.py | 27 +- .../unsafe_transforms/build_transforms.py | 12 +- product/PortalTransforms/utils.py | 4 +- 22 files changed, 505 insertions(+), 612 deletions(-) diff --git a/product/PortalTransforms/TransformEngine.py b/product/PortalTransforms/TransformEngine.py index 79a7819024..3e41abd76f 100644 --- a/product/PortalTransforms/TransformEngine.py +++ b/product/PortalTransforms/TransformEngine.py @@ -1,19 +1,14 @@ # -*- coding: utf-8 -*- from logging import DEBUG + +from persistent.list import PersistentList from zope.interface import implements from AccessControl import ClassSecurityInfo from Acquisition import aq_base from App.class_init import default__class_init__ as InitializeClass -from Persistence import PersistentMapping -try: - from ZODB.PersistentList import PersistentList -except ImportError: - from persistent.list import PersistentList from OFS.Folder import Folder - -from Products.PageTemplates.PageTemplateFile import PageTemplateFile - +from Persistence import PersistentMapping from Products.CMFCore.ActionProviderBase import ActionProviderBase from Products.CMFCore.permissions import ManagePortal, View try: @@ -22,18 +17,19 @@ except ImportError: # BACK: Zope 2.8 registerToolInterface = lambda tool_id, tool_interface: None from Products.CMFCore.utils import UniqueObject from Products.CMFCore.utils import getToolByName +from Products.PageTemplates.PageTemplateFile import PageTemplateFile -from Products.PortalTransforms.libtransforms.utils import MissingBinary -from Products.PortalTransforms import transforms -from Products.PortalTransforms.interfaces import IDataStream -from Products.PortalTransforms.interfaces import ITransform -from Products.PortalTransforms.interfaces import IEngine -from Products.PortalTransforms.interfaces import IPortalTransformsTool from Products.PortalTransforms.data import datastream from Products.PortalTransforms.chain import TransformsChain from Products.PortalTransforms.chain import chain from Products.PortalTransforms.cache import Cache +from Products.PortalTransforms.interfaces import IDataStream +from Products.PortalTransforms.interfaces import ITransform +from Products.PortalTransforms.interfaces import IEngine +from Products.PortalTransforms.interfaces import IPortalTransformsTool +from Products.PortalTransforms.libtransforms.utils import MissingBinary from Products.PortalTransforms.Transform import Transform +from Products.PortalTransforms.transforms import initialize from Products.PortalTransforms.utils import log from Products.PortalTransforms.utils import TransformException from Products.PortalTransforms.utils import _www @@ -50,28 +46,25 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): implements(IPortalTransformsTool, IEngine) meta_types = all_meta_types = ( - { 'name' : 'Transform', - 'action' : 'manage_addTransformForm'}, - { 'name' : 'TransformsChain', - 'action' : 'manage_addTransformsChainForm'}, + {'name': 'Transform', 'action': 'manage_addTransformForm'}, + {'name': 'TransformsChain', 'action': 'manage_addTransformsChainForm'}, ) manage_addTransformForm = PageTemplateFile('addTransform', _www) - manage_addTransformsChainForm = PageTemplateFile('addTransformsChain', _www) + manage_addTransformsChainForm = PageTemplateFile( + 'addTransformsChain', _www) manage_cacheForm = PageTemplateFile('setCacheTime', _www) - manage_editTransformationPolicyForm = PageTemplateFile('editTransformationPolicy', _www) + manage_editTransformationPolicyForm = PageTemplateFile( + 'editTransformationPolicy', _www) manage_reloadAllTransforms = PageTemplateFile('reloadAllTransforms', _www) - manage_options = ((Folder.manage_options[0],) + Folder.manage_options[2:] + - ( - { 'label' : 'Caches', - 'action' : 'manage_cacheForm'}, - { 'label' : 'Policy', - 'action' : 'manage_editTransformationPolicyForm'}, - { 'label' : 'Reload transforms', - 'action' : 'manage_reloadAllTransforms'}, - ) - ) + manage_options = ( + (Folder.manage_options[0], ) + Folder.manage_options[2:] + + ({'label': 'Caches', 'action': 'manage_cacheForm'}, + {'label': 'Policy', 'action': 'manage_editTransformationPolicyForm'}, + {'label': 'Reload transforms', + 'action': 'manage_reloadAllTransforms'}, + )) security = ClassSecurityInfo() @@ -81,7 +74,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): self.max_sec_in_cache = max_sec_in_cache self._new_style_pt = 1 - # mimetype oriented conversions (iengine interface) ######################## + # mimetype oriented conversions (iengine interface) def unregisterTransform(self, name): """ unregister a transform @@ -113,7 +106,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): target_mimetype = str(target_mimetype) if object is not None: - cache = Cache(object) + cache = Cache(object, context=context) data = cache.getCache(target_mimetype) if data is not None: time, data = data @@ -126,8 +119,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): registry = getToolByName(self, 'mimetypes_registry') if not getattr(aq_base(registry), 'classify', None): - # avoid problems when importing a site with an old mimetype registry - # XXX return None or orig? + # avoid problems when importing a site with an old mimetype + # registry return None orig_mt = registry.classify(orig, @@ -135,8 +128,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): filename=kwargs.get('filename')) orig_mt = str(orig_mt) if not orig_mt: - log('Unable to guess input mime type (filename=%s, mimetype=%s)' %( - kwargs.get('mimetype'), kwargs.get('filename')), severity=WARNING) + log('Unable to guess input mime type (filename=%s, mimetype=%s)' % + (kwargs.get('mimetype'), kwargs.get('filename')), + severity=WARNING) return None target_mt = registry.lookup(target_mimetype) @@ -151,9 +145,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): # If orig_mt and target_mt are the same, we only allow # a one-hop transform, a.k.a. filter. # XXX disabled filtering for now - filter_only = False if orig_mt == str(target_mt): - filter_only = True data.setData(orig) md = data.getMetadata() md['mimetype'] = str(orig_mt) @@ -171,9 +163,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): path = self._findPath(orig_mt, target_mt) if not path: - log('NO PATH FROM %s TO %s : %s' % (orig_mt, target_mimetype, path), - severity=WARNING) - return None #XXX raise TransformError + log('NO PATH FROM %s TO %s : %s' % + (orig_mt, target_mimetype, path), severity=WARNING) + return None if len(path) > 1: ## create a chain on the fly (sly) @@ -183,7 +175,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): else: transform = path[0] - result = transform.convert(orig, data, context=context, usedby=usedby, **kwargs) + result = transform.convert(orig, data, context=context, + usedby=usedby, **kwargs) self._setMetaData(result, transform) # set cache if possible @@ -307,8 +300,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): (output, transform.name()) raise TransformException(msg) if len(mto) > 1: - msg = 'Wildcarding not allowed in transform\'s output '\ - 'MIME type' + msg = ("Wildcarding not allowed in transform's output " + "MIME type") raise TransformException(msg) for mt2 in mto[0].mimetypes: @@ -342,36 +335,124 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): """return the shortest path for transformation from orig mimetype to target mimetype """ - path = [] - if not self._mtmap: return None - # naive algorithm : - # find all possible paths with required transforms - # take the shortest - # - # it should be enough since we should not have so much possible paths - shortest, winner = 9999, None - for path in self._getPaths(str(orig), str(target), required_transforms): - if len(path) < shortest: - winner = path - shortest = len(path) - - return winner - - def _getPaths(self, orig, target, requirements, path=None, result=None, searched_orig_list=None): - """return a all path for transformation from orig mimetype to - target mimetype - """ - # don't search the same orig again, otherwise infinite loop occurs. - if searched_orig_list is None: - searched_orig_list = [] - if orig in searched_orig_list: + orig = str(orig) + target = str(target) + # First, let's deal with required transforms. + if required_transforms: + # Let's decompose paths, then. + required_transform = required_transforms.pop(0) + # The first path must lead to one of the inputs supported + # by this first required transform. + # Which input types are supported by this transform ? + supportedInputs = {} + for input, outputs in self._mtmap.items(): + for output, transforms in outputs.items(): + for transform in transforms: + if transform.name() == required_transform: + supportedInputs[input] = 'ok' + # BTW, let's remember the output type + transformOutput = output + # and remember the transform, it is + # useful later + requiredTransform = transform + # Which of these inputs will be reachable with the + # shortest path ? + shortest = 9999 # big enough, I guess + shortestFirstPath = None + for supportedInput in supportedInputs.keys(): + # We start from orig + firstOrig = orig + # And want to reach supportedInput + firstTarget = supportedInput + # What's the shortest path ? + firstPath = self._findPath(firstOrig, firstTarget) + if firstPath is not None: + if len(firstPath) < shortest: + # Here is a path which is shorter than others + # which also reach the required transform. + shortest = len(firstPath) + shortestFirstPath = firstPath + if shortestFirstPath == None: + return None # there is no path leading to this transform + # Then we have to take this transform. + secondPath = [requiredTransform] + # From the output of this transform, we then have to + # reach our target, possible through other required + # transforms. + thirdOrig = transformOutput + thirdTarget = target + thirdPath = self._findPath(thirdOrig, thirdTarget, + required_transforms) + if thirdPath is None: + return None # no path + # Final result is the concatenation of these 3 parts + return shortestFirstPath + secondPath + thirdPath + + if orig == target: + return [] + + # Now let's efficiently find the shortest path from orig + # to target (without required transforms). + # The overall idea is that we build all possible paths + # starting from orig and of given length. And we increment + # this length until one of these paths reaches our target or + # until all reachable types have been reached. + currentPathLength = 0 + pathToType = {orig: []} # all paths we know, by end of path. + def typesWithPathOfLength(length): + '''Returns the lists of known paths of a given length''' + result = [] + for type_, path in pathToType.items(): + if len(path) == length: + result.append(type_) return result - else: - searched_orig_list.append(orig) + # We will start exploring paths which start from types + # reachable in zero steps. That is paths which start from + # orig. + typesToStartFrom = typesWithPathOfLength(currentPathLength) + # Explore paths while there are new paths to be explored + while len(typesToStartFrom) > 0: + for startingType in typesToStartFrom: + # Where can we go in one step starting from here ? + outputs = self._mtmap.get(startingType) + if outputs: + for reachedType, transforms in outputs.items(): + # Does this lead to a type we never reached before ? + if reachedType not in pathToType.keys() and transforms: + # Yes, we did not know any path reaching this type + # Let's remember the path to here + pathToType[reachedType] = ( + pathToType[startingType] + [transforms[0]]) + if reachedType == target: + # This is the first time we reach our target. + # We have our shortest path to target. + return pathToType[target] + # We explored all possible paths of length currentPathLength + # Let's increment that length. + currentPathLength += 1 + # What are the next types to start from ? + typesToStartFrom = typesWithPathOfLength(currentPathLength) + # We are done exploring paths starting from orig + # and this exploration did not reach our target. + # Hence there is no path from orig to target. + return None + + def _getPaths(self, orig, target, requirements, path=None, result=None): + """return some of the paths for transformation from orig mimetype to + target mimetype with the guarantee that the shortest path is included. + If target is the same as orig, then returns an empty path. + """ + + shortest = 9999 + if result: + for okPath in result: + shortest = min(shortest, len(okPath)) + if orig == target: + return [[]] if path is None: result = [] path = [] @@ -380,9 +461,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): if outputs is None: return result - registry = getToolByName(self, 'mimetypes_registry') - mto = registry.lookup(target) - # target mimetype aliases + registry = getToolByName(self, 'mimetypes_registry') + mto = registry.lookup(target) + # target mimetype aliases target_aliases = mto[0].mimetypes path.append(None) @@ -400,8 +481,14 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): if o_mt in target_aliases: if not requirements: result.append(path[:]) + if len(path[:]) < shortest: + # here is a shorter one ! + shortest = len(path) else: - self._getPaths(o_mt, target, requirements, path, result, searched_orig_list) + if len(path) < shortest: + # keep exploring this path, it is still short enough + self._getPaths(o_mt, target, requirements, + path, result) if required: requirements.append(name) path.pop() @@ -414,14 +501,11 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): transform tool is added """ Folder.manage_afterAdd(self, item, container) - transforms.initialize(self) - # XXX required? - #try: - # # first initialization - # transforms.initialize(self) - #except: - # # may fail on copy - # pass + try: + initialize(self) + except TransformException: + # may fail on copy or zexp import + pass security.declareProtected(ManagePortal, 'manage_addTransform') def manage_addTransform(self, id, module, REQUEST=None): @@ -465,28 +549,31 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): reloaded.append((id, o.module)) return reloaded - # Policy handling methods ################################################# + # Policy handling methods - def manage_addPolicy(self, output_mimetype, required_transforms, REQUEST=None): + def manage_addPolicy(self, output_mimetype, required_transforms, + REQUEST=None): """ add a policy for a given output mime types""" registry = getToolByName(self, 'mimetypes_registry') if not registry.lookup(output_mimetype): raise TransformException('Unknown MIME type') - if self._policies.has_key(output_mimetype): + if output_mimetype in self._policies: msg = 'A policy for output %s is yet defined' % output_mimetype raise TransformException(msg) required_transforms = tuple(required_transforms) self._policies[output_mimetype] = required_transforms if REQUEST is not None: - REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm') + REQUEST['RESPONSE'].redirect(self.absolute_url() + + '/manage_editTransformationPolicyForm') def manage_delPolicies(self, outputs, REQUEST=None): """ remove policies for given output mime types""" for mimetype in outputs: del self._policies[mimetype] if REQUEST is not None: - REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm') + REQUEST['RESPONSE'].redirect(self.absolute_url() + + '/manage_editTransformationPolicyForm') def listPolicies(self): """ return the list of defined policies @@ -498,20 +585,21 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): self._policies = PersistentMapping() return self._policies.items() - # mimetype oriented conversions (iengine interface) ######################## + # mimetype oriented conversions (iengine interface) def registerTransform(self, transform): """register a new transform - transform isn't a Zope Transform (the wrapper) but the wrapped transform - the persistence wrapper will be created here + transform isn't a Zope Transform (the wrapper) but the wrapped + transform the persistence wrapper will be created here """ # needed when call from transform.transforms.initialize which # register non zope transform module = str(transform.__module__) transform = Transform(transform.name(), module, transform) if not ITransform.providedBy(transform): - raise TransformException('%s does not implement ITransform' % transform) + raise TransformException('%s does not implement ITransform' % + transform) name = transform.name() __traceback_info__ = (name, transform) if name not in self.objectIds(): @@ -539,8 +627,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): # available mimetypes #################################################### def listAvailableTextInputs(self): - """ Returns a list of mimetypes that can be used as input for textfields - by building a list of the inputs beginning with "text/" of all transforms. + """Returns a list of mimetypes that can be used as input for textfields + by building a list of the inputs beginning with "text/" of all + transforms. """ available_types = [] candidate_transforms = [object[1] for object in self.objectItems()] diff --git a/product/PortalTransforms/cache.py b/product/PortalTransforms/cache.py index 604d064e72..9badbfc665 100644 --- a/product/PortalTransforms/cache.py +++ b/product/PortalTransforms/cache.py @@ -3,10 +3,16 @@ from time import time from Acquisition import aq_base +_marker = object() + class Cache: - def __init__(self, context, _id='_v_transform_cache'): - self.context = context + def __init__(self, obj, context=None, _id='_v_transform_cache'): + self.obj = obj + if context is None: + self.context = obj + else: + self.context = context self._id =_id def _genCacheKey(self, identifier, *args): @@ -17,17 +23,19 @@ class Cache: key = key.replace('+', '_') key = key.replace('-', '_') key = key.replace(' ', '_') + if hasattr(aq_base(self.context), 'absolute_url'): + return key, self.context.absolute_url() return key def setCache(self, key, value): """cache a value indexed by key""" if not value.isCacheable(): return - context = self.context + obj = self.obj key = self._genCacheKey(key) - if getattr(aq_base(context), self._id, None) is None: - setattr(context, self._id, {}) - getattr(context, self._id)[key] = (time(), value) + if getattr(aq_base(obj), self._id, None) is None: + setattr(obj, self._id, {}) + getattr(obj, self._id)[key] = (time(), value) return key def getCache(self, key): @@ -36,9 +44,9 @@ class Cache: return None if not present else return a tuple (time spent in cache, value) """ - context = self.context + obj = self.obj key = self._genCacheKey(key) - dict = getattr(context, self._id, None) + dict = getattr(obj, self._id, None) if dict is None : return None try: @@ -46,18 +54,18 @@ class Cache: return time() - orig_time, value except TypeError: return None - + def purgeCache(self, key=None): """Remove cache """ - context = self.context + obj = self.obj id = self._id - if not shasattr(context, id): + if getattr(obj, id, _marker) is _marker: return if key is None: - delattr(context, id) + delattr(obj, id) else: - cache = getattr(context, id) + cache = getattr(obj, id) key = self._genCacheKey(key) if cache.has_key(key): del cache[key] diff --git a/product/PortalTransforms/libtransforms/commandtransform.py b/product/PortalTransforms/libtransforms/commandtransform.py index de1ca5fb52..4bae3f69da 100644 --- a/product/PortalTransforms/libtransforms/commandtransform.py +++ b/product/PortalTransforms/libtransforms/commandtransform.py @@ -87,28 +87,30 @@ class popentransform: def convert(self, data, cache, **kwargs): command = "%s %s" % (self.binary, self.binaryArgs) - if not self.useStdin: - tmpfile, tmpname = tempfile.mkstemp(text=False) # create tmp - os.write(tmpfile, data) # write data to tmp using a file descriptor - os.close(tmpfile) # close it so the other process can read it - command = command % { 'infile' : tmpname } # apply tmp name to command - - cin, couterr = os.popen4(command, 'b') - - if self.useStdin: - cin.write(str(data)) - - status = cin.close() - - out = self.getData(couterr) - couterr.close() - - if not self.useStdin: - # remove tmp file - os.unlink(tmpname) - - cache.setData(out) - return cache + tmpname = None + try: + if not self.useStdin: + tmpfile, tmpname = tempfile.mkstemp(text=False) # create tmp + os.write(tmpfile, data) # write data to tmp using a file descriptor + os.close(tmpfile) # close it so the other process can read it + command = command % { 'infile' : tmpname } # apply tmp name to command + + cin, couterr = os.popen4(command, 'b') + + if self.useStdin: + cin.write(str(data)) + + status = cin.close() + + out = self.getData(couterr) + couterr.close() + + cache.setData(out) + return cache + finally: + if not self.useStdin and tmpname is not None: + # remove tmp file + os.unlink(tmpname) from subprocess import Popen, PIPE import shlex diff --git a/product/PortalTransforms/libtransforms/utils.py b/product/PortalTransforms/libtransforms/utils.py index d68a8a8c9a..9254464104 100644 --- a/product/PortalTransforms/libtransforms/utils.py +++ b/product/PortalTransforms/libtransforms/utils.py @@ -1,7 +1,7 @@ import re import os import sys -from sgmllib import SGMLParser +from sgmllib import SGMLParser, SGMLParseError try: # Need to be imported before win32api to avoid dll loading @@ -207,7 +207,26 @@ class StrippingParser( SGMLParser ): self.result = "%s</%s>" % (self.result, tag) remTag = '</%s>' % tag - + + def parse_declaration(self, i): + """Fix handling of CDATA sections. Code borrowed from BeautifulSoup. + """ + j = None + if self.rawdata[i:i+9] == '<![CDATA[': + k = self.rawdata.find(']]>', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self.result.append("<![CDATA[%s]]>" % data) + else: + try: + j = SGMLParser.parse_declaration(self, i) + except SGMLParseError: + toHandle = self.rawdata[i:] + self.result.append(toHandle) + j = i + len(toHandle) + return j def scrubHTML( html ): """ Strip illegal HTML tags from string text. """ diff --git a/product/PortalTransforms/tests/input/markdown.txt b/product/PortalTransforms/tests/input/markdown.txt index 60d087a62e..ca8adfd2c7 100644 --- a/product/PortalTransforms/tests/input/markdown.txt +++ b/product/PortalTransforms/tests/input/markdown.txt @@ -1,3 +1,5 @@ ## Testing Markdown `code` and _italic_ and *bold* and even a [link](http://plone.org). + +F枚枚b盲r diff --git a/product/PortalTransforms/tests/input/test_safehtml.html b/product/PortalTransforms/tests/input/test_safehtml.html index 6b5e9e80bf..08134472e8 100644 --- a/product/PortalTransforms/tests/input/test_safehtml.html +++ b/product/PortalTransforms/tests/input/test_safehtml.html @@ -15,6 +15,10 @@ </tr> </table> <p>This is a text used as a blind text.</p> +<div><![CDATA[ + Some CDATA text. +]]> +</div> <ul> <li>A sample list item1</li> <li>A sample list item2</li> diff --git a/product/PortalTransforms/tests/output/demo1.html b/product/PortalTransforms/tests/output/demo1.html index 450161bfd8..65a1ec9176 100644 --- a/product/PortalTransforms/tests/output/demo1.html +++ b/product/PortalTransforms/tests/output/demo1.html @@ -1,209 +1 @@ -<A name=1></a>Chapter 44<br> -Writing Basic Unit Tests<br> -Di铿僣ulty<br> -Newcomer<br> -Skills<br> -鈥� All you need to know is some Python.<br> -Problem/Task<br> -As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests 鈥� which<br>are Zope 3 independent 鈥� and introduces some of the subtleties.<br> -Solution<br> -44.1<br> -Implementing the Sample Class<br> -Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br> -Since this code will not depend on Zope, open a 铿乴e named test sample.py anywhere and add<br> -the following class:<br> -1 Sample(object):<br> -2<br> -"""A trivial Sample object."""<br> -3<br> -4<br> -title = None<br> -5<br> -6<br> -def __init__(self):<br> -7<br> -"""Initialize object."""<br> -8<br> -self._description = 鈥欌€�<br> -9<br> -1<br> -<hr> -<A name=2></a>2<br> -CHAPTER 44. WRITING BASIC UNIT TESTS<br> -10<br> -def setDescription(self, value):<br> -11<br> -"""Change the value of the description."""<br> -12<br> -assert isinstance(value, (str, unicode))<br> -13<br> -self._description = value<br> -14<br> -15<br> -def getDescription(self):<br> -16<br> -"""Change the value of the description."""<br> -17<br> -return self._description<br> -Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br> -Line 8: The actual description string will be stored in description.<br> -Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br> -If you wish you can now manually test the class with the interactive Python shell. Just start<br> -Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br> -1 >>> from test_sample import Sample<br>2 >>> sample = Sample()<br> -3 >>> print sample.title<br>4 None<br> -5 >>> sample.title = 鈥橳itle鈥�<br> -6 >>> print sample.title<br>7 Title<br> -8 >>> print sample.getDescription()<br>9<br> -10 >>> sample.setDescription(鈥橦ello World鈥�)<br> -11 >>> print sample.getDescription()<br>12 Hello World<br> -13 >>> sample.setDescription(None)<br> -14 Traceback (most recent call last):<br> -15<br> -File "<stdin>", line 1, in ?<br> -16<br> -File "test_sample.py", line 31, in setDescription<br> -17<br> -assert isinstance(value, (str, unicode))<br> -18 AssertionError<br> -As you can see in the last test, non-string object types are not allowed as descriptions and an<br> -AssertionError is raised.<br> -44.2<br> -Writing the Unit Tests<br> -The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original de铿乶itions as found<br>in the Python library documentation. 1).<br> -1 http://www.python.org/doc/current/lib/module-unittest.html<br> -<hr> -<A name=3></a>44.2. WRITING THE UNIT TESTS<br> -3<br> -The smallest unit is obviously the 鈥渢est鈥�, which is a single method in a TestCase class that<br> -tests the behavior of a small piece of code or a particular aspect of an implementation. The 鈥渢est<br>case鈥� is then a collection tests that share the same setup/inputs. On top of all of this sits the 鈥渢est<br>suite鈥� which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br> -But enough about the theory. In the following example, which you can simply put into the same<br> -铿乴e as your code above, you will see a test in common Zope 3 style.<br> -1 import unittest<br>2<br> -3 class SampleTest(unittest.TestCase):<br>4<br> -"""Test the Sample class"""<br> -5<br> -6<br> -def test_title(self):<br> -7<br> -sample = Sample()<br> -8<br> -self.assertEqual(sample.title, None)<br> -9<br> -sample.title = 鈥橲ample Title鈥�<br> -10<br> -self.assertEqual(sample.title, 鈥橲ample Title鈥�)<br> -11<br> -12<br> -def test_getDescription(self):<br> -13<br> -sample = Sample()<br> -14<br> -self.assertEqual(sample.getDescription(), 鈥欌€�)<br> -15<br> -sample._description = "Description"<br> -16<br> -self.assertEqual(sample.getDescription(), 鈥橠escription鈥�)<br> -17<br> -18<br> -def test_setDescription(self):<br> -19<br> -sample = Sample()<br> -20<br> -self.assertEqual(sample._description, 鈥欌€�)<br> -21<br> -sample.setDescription(鈥橠escription鈥�)<br> -22<br> -self.assertEqual(sample._description, 鈥橠escription鈥�)<br> -23<br> -sample.setDescription(u鈥橠escription2鈥�)<br> -24<br> -self.assertEqual(sample._description, u鈥橠escription2鈥�)<br> -25<br> -self.assertRaises(AssertionError, sample.setDescription, None)<br> -26<br> -27<br> -28 def test_suite():<br>29<br> -return unittest.TestSuite((<br> -30<br> -unittest.makeSuite(SampleTest),<br> -31<br> -))<br> -32<br> -33 if __name__ == 鈥檁_main__鈥�:<br>34<br> -unittest.main(defaultTest=鈥檛est_suite鈥�)<br> -Line 3鈥�4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br> -Line 6, 12 & 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests di铿€erently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a 鈥渢est method鈥� for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br> -<hr> -<A name=4></a>4<br> -CHAPTER 44. WRITING BASIC UNIT TESTS<br> -Note that there is no docstring for test methods. This is intentional. If a docstring is speci铿乪d,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very di铿僣ult to identify the test later; therefore the method name is a much<br>better choice.<br> -Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br> -鈥� assertEqual(first,second[,msg])<br> -Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br> -鈥� assertNotEqual(first,second[,msg])<br> -This is simply the opposite to assertEqual() by checking for non-equality.<br> -鈥� assertRaises(exception,callable,...)<br> -You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br> -鈥� assert (expr[,msg])<br> -Assert checks whether the speci铿乪d expression executes correctly. If not, the test fails and<br>msg or None is returned.<br> -鈥� failUnlessEqual()<br> -This testing method is equivalent to assertEqual().<br> -鈥� failUnless(expr[,msg])<br> -This method is equivalent to assert (expr[,msg]).<br> -鈥� failif()<br> -This is the opposite to failUnless().<br> -鈥� fail([msg])<br> -Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br> -Line 6鈥�10: This method tests the title attribute of the Sample class. The 铿乺st test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br> -Line 12鈥�16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-speci铿乧 attributes and methods. Finally we just check that<br>the correct value is returned.<br> -<hr> -<A name=5></a>44.3. RUNNING THE TESTS<br> -5<br> -Line 18鈥�25: On line 21鈥�24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br> -28鈥�31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br> -33鈥�34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br> -44.3<br> -Running the Tests<br> -You can run the test by simply calling pythontest sample.py from the directory you saved the<br>铿乴e in. Here is the result you should see:<br> -.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br> -The three dots represent the three tests that were run. If a test had failed, it would have been<br> -reported pointing out the failing test and providing a small traceback.<br> -When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br> -conventions.<br> -鈥� The tests must either be in a package or be a module called tests.<br> -鈥� If tests is a package, then all test modules inside must also have a name starting with test,<br> -as it is the case with our name test sample.py.<br> -鈥� The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br> -only for 铿乴es there.<br> -In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br> -init .<br> -py 铿乴e). Then place the test sample.py 铿乴e into this directory.<br> -You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br> -directory:<br> -python test.py -vp tests.test_sample<br> -The -v option stands for verbose mode, so that detailed information about a test failure is<br> -provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be speci铿乪d. You can get a full list of them with<br>the option -h: pythontest.py-h.<br> -The output of the call above is as follows:<br> -nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br> -3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br> ---------------------------------------------------------------------<br>n 3 tests in 0.002s<br> -<hr> -<A name=6></a>6<br> -CHAPTER 44. WRITING BASIC UNIT TESTS<br> -nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br> ---------------------------------------------------------------------<br>n 0 tests in 0.000s<br> -Line 1: The test runner uses a con铿乬uration 铿乴e for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the con铿乬uration 铿乴e<br>was found.<br> -Line 2鈥�8: The unit tests are run. On line 4 you can see the progress bar.<br> -Line 9鈥�15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the speci铿乪d module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See 鈥淲riting<br>Functional Tests鈥� for more detials on functional tests.<br> -<hr> -<A name=7></a>44.3. RUNNING THE TESTS<br> -7<br> -Exercises<br> -1. It is not very common to do the setup 鈥� in our case sample=Sample() 鈥� in every test<br> -method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br> -2. Currently the test setDescription() test only veri铿乪s that None is not allowed as input<br> -value.<br> -(a) Improve the test, so that all other builtin types are tested as well.<br> -(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br> -<hr> \ No newline at end of file +<A name=1></a>Chapter 44<br>Writing Basic Unit Tests<br>Di铿僣ulty<br>Newcomer<br>Skills<br>鈥� All you need to know is some Python.<br>Problem/Task<br>As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests 鈥� which<br>are Zope 3 independent 鈥� and introduces some of the subtleties.<br>Solution<br>44.1<br>Implementing the Sample Class<br>Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br>Since this code will not depend on Zope, open a 铿乴e named test sample.py anywhere and add<br>the following class:<br>1 Sample(object):<br>2<br>"""A trivial Sample object."""<br>3<br>4<br>title = None<br>5<br>6<br>def __init__(self):<br>7<br>"""Initialize object."""<br>8<br>self._description = 鈥欌€�<br>9<br>1<br><hr><A name=2></a>2<br>CHAPTER 44. WRITING BASIC UNIT TESTS<br>10<br>def setDescription(self, value):<br>11<br>"""Change the value of the description."""<br>12<br>assert isinstance(value, (str, unicode))<br>13<br>self._description = value<br>14<br>15<br>def getDescription(self):<br>16<br>"""Change the value of the description."""<br>17<br>return self._description<br>Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br>Line 8: The actual description string will be stored in description.<br>Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br>If you wish you can now manually test the class with the interactive Python shell. Just start<br>Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br>1 >>> from test_sample import Sample<br>2 >>> sample = Sample()<br>3 >>> print sample.title<br>4 None<br>5 >>> sample.title = 鈥橳itle鈥�<br>6 >>> print sample.title<br>7 Title<br>8 >>> print sample.getDescription()<br>9<br>10 >>> sample.setDescription(鈥橦ello World鈥�)<br>11 >>> print sample.getDescription()<br>12 Hello World<br>13 >>> sample.setDescription(None)<br>14 Traceback (most recent call last):<br>15<br>File "<stdin>", line 1, in ?<br>16<br>File "test_sample.py", line 31, in setDescription<br>17<br>assert isinstance(value, (str, unicode))<br>18 AssertionError<br>As you can see in the last test, non-string object types are not allowed as descriptions and an<br>AssertionError is raised.<br>44.2<br>Writing the Unit Tests<br>The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original de铿乶itions as found<br>in the Python library documentation. 1).<br>1 http://www.python.org/doc/current/lib/module-unittest.html<br><hr><A name=3></a>44.2. WRITING THE UNIT TESTS<br>3<br>The smallest unit is obviously the 鈥渢est鈥�, which is a single method in a TestCase class that<br>tests the behavior of a small piece of code or a particular aspect of an implementation. The 鈥渢est<br>case鈥� is then a collection tests that share the same setup/inputs. On top of all of this sits the 鈥渢est<br>suite鈥� which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br>But enough about the theory. In the following example, which you can simply put into the same<br>铿乴e as your code above, you will see a test in common Zope 3 style.<br>1 import unittest<br>2<br>3 class SampleTest(unittest.TestCase):<br>4<br>"""Test the Sample class"""<br>5<br>6<br>def test_title(self):<br>7<br>sample = Sample()<br>8<br>self.assertEqual(sample.title, None)<br>9<br>sample.title = 鈥橲ample Title鈥�<br>10<br>self.assertEqual(sample.title, 鈥橲ample Title鈥�)<br>11<br>12<br>def test_getDescription(self):<br>13<br>sample = Sample()<br>14<br>self.assertEqual(sample.getDescription(), 鈥欌€�)<br>15<br>sample._description = "Description"<br>16<br>self.assertEqual(sample.getDescription(), 鈥橠escription鈥�)<br>17<br>18<br>def test_setDescription(self):<br>19<br>sample = Sample()<br>20<br>self.assertEqual(sample._description, 鈥欌€�)<br>21<br>sample.setDescription(鈥橠escription鈥�)<br>22<br>self.assertEqual(sample._description, 鈥橠escription鈥�)<br>23<br>sample.setDescription(u鈥橠escription2鈥�)<br>24<br>self.assertEqual(sample._description, u鈥橠escription2鈥�)<br>25<br>self.assertRaises(AssertionError, sample.setDescription, None)<br>26<br>27<br>28 def test_suite():<br>29<br>return unittest.TestSuite((<br>30<br>unittest.makeSuite(SampleTest),<br>31<br>))<br>32<br>33 if __name__ == 鈥檁_main__鈥�:<br>34<br>unittest.main(defaultTest=鈥檛est_suite鈥�)<br>Line 3鈥�4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br>Line 6, 12 & 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests di铿€erently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a 鈥渢est method鈥� for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br><hr><A name=4></a>4<br>CHAPTER 44. WRITING BASIC UNIT TESTS<br>Note that there is no docstring for test methods. This is intentional. If a docstring is speci铿乪d,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very di铿僣ult to identify the test later; therefore the method name is a much<br>better choice.<br>Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br>鈥� assertEqual(first,second[,msg])<br>Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br>鈥� assertNotEqual(first,second[,msg])<br>This is simply the opposite to assertEqual() by checking for non-equality.<br>鈥� assertRaises(exception,callable,...)<br>You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br>鈥� assert (expr[,msg])<br>Assert checks whether the speci铿乪d expression executes correctly. If not, the test fails and<br>msg or None is returned.<br>鈥� failUnlessEqual()<br>This testing method is equivalent to assertEqual().<br>鈥� failUnless(expr[,msg])<br>This method is equivalent to assert (expr[,msg]).<br>鈥� failif()<br>This is the opposite to failUnless().<br>鈥� fail([msg])<br>Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br>Line 6鈥�10: This method tests the title attribute of the Sample class. The 铿乺st test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br>Line 12鈥�16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-speci铿乧 attributes and methods. Finally we just check that<br>the correct value is returned.<br><hr><A name=5></a>44.3. RUNNING THE TESTS<br>5<br>Line 18鈥�25: On line 21鈥�24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br>28鈥�31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br>33鈥�34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br>44.3<br>Running the Tests<br>You can run the test by simply calling pythontest sample.py from the directory you saved the<br>铿乴e in. Here is the result you should see:<br>.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br>The three dots represent the three tests that were run. If a test had failed, it would have been<br>reported pointing out the failing test and providing a small traceback.<br>When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br>conventions.<br>鈥� The tests must either be in a package or be a module called tests.<br>鈥� If tests is a package, then all test modules inside must also have a name starting with test,<br>as it is the case with our name test sample.py.<br>鈥� The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br>only for 铿乴es there.<br>In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br>init .<br>py 铿乴e). Then place the test sample.py 铿乴e into this directory.<br>You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br>directory:<br>python test.py -vp tests.test_sample<br>The -v option stands for verbose mode, so that detailed information about a test failure is<br>provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be speci铿乪d. You can get a full list of them with<br>the option -h: pythontest.py-h.<br>The output of the call above is as follows:<br>nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br>3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br>--------------------------------------------------------------------<br>n 3 tests in 0.002s<br><hr><A name=6></a>6<br>CHAPTER 44. WRITING BASIC UNIT TESTS<br>nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br>--------------------------------------------------------------------<br>n 0 tests in 0.000s<br>Line 1: The test runner uses a con铿乬uration 铿乴e for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the con铿乬uration 铿乴e<br>was found.<br>Line 2鈥�8: The unit tests are run. On line 4 you can see the progress bar.<br>Line 9鈥�15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the speci铿乪d module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See 鈥淲riting<br>Functional Tests鈥� for more detials on functional tests.<br><hr><A name=7></a>44.3. RUNNING THE TESTS<br>7<br>Exercises<br>1. It is not very common to do the setup 鈥� in our case sample=Sample() 鈥� in every test<br>method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br>2. Currently the test setDescription() test only veri铿乪s that None is not allowed as input<br>value.<br>(a) Improve the test, so that all other builtin types are tested as well.<br>(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br><hr> \ No newline at end of file diff --git a/product/PortalTransforms/tests/output/demo1.html.nofilename b/product/PortalTransforms/tests/output/demo1.html.nofilename index 450161bfd8..65a1ec9176 100644 --- a/product/PortalTransforms/tests/output/demo1.html.nofilename +++ b/product/PortalTransforms/tests/output/demo1.html.nofilename @@ -1,209 +1 @@ -<A name=1></a>Chapter 44<br> -Writing Basic Unit Tests<br> -Di铿僣ulty<br> -Newcomer<br> -Skills<br> -鈥� All you need to know is some Python.<br> -Problem/Task<br> -As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests 鈥� which<br>are Zope 3 independent 鈥� and introduces some of the subtleties.<br> -Solution<br> -44.1<br> -Implementing the Sample Class<br> -Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br> -Since this code will not depend on Zope, open a 铿乴e named test sample.py anywhere and add<br> -the following class:<br> -1 Sample(object):<br> -2<br> -"""A trivial Sample object."""<br> -3<br> -4<br> -title = None<br> -5<br> -6<br> -def __init__(self):<br> -7<br> -"""Initialize object."""<br> -8<br> -self._description = 鈥欌€�<br> -9<br> -1<br> -<hr> -<A name=2></a>2<br> -CHAPTER 44. WRITING BASIC UNIT TESTS<br> -10<br> -def setDescription(self, value):<br> -11<br> -"""Change the value of the description."""<br> -12<br> -assert isinstance(value, (str, unicode))<br> -13<br> -self._description = value<br> -14<br> -15<br> -def getDescription(self):<br> -16<br> -"""Change the value of the description."""<br> -17<br> -return self._description<br> -Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br> -Line 8: The actual description string will be stored in description.<br> -Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br> -If you wish you can now manually test the class with the interactive Python shell. Just start<br> -Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br> -1 >>> from test_sample import Sample<br>2 >>> sample = Sample()<br> -3 >>> print sample.title<br>4 None<br> -5 >>> sample.title = 鈥橳itle鈥�<br> -6 >>> print sample.title<br>7 Title<br> -8 >>> print sample.getDescription()<br>9<br> -10 >>> sample.setDescription(鈥橦ello World鈥�)<br> -11 >>> print sample.getDescription()<br>12 Hello World<br> -13 >>> sample.setDescription(None)<br> -14 Traceback (most recent call last):<br> -15<br> -File "<stdin>", line 1, in ?<br> -16<br> -File "test_sample.py", line 31, in setDescription<br> -17<br> -assert isinstance(value, (str, unicode))<br> -18 AssertionError<br> -As you can see in the last test, non-string object types are not allowed as descriptions and an<br> -AssertionError is raised.<br> -44.2<br> -Writing the Unit Tests<br> -The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original de铿乶itions as found<br>in the Python library documentation. 1).<br> -1 http://www.python.org/doc/current/lib/module-unittest.html<br> -<hr> -<A name=3></a>44.2. WRITING THE UNIT TESTS<br> -3<br> -The smallest unit is obviously the 鈥渢est鈥�, which is a single method in a TestCase class that<br> -tests the behavior of a small piece of code or a particular aspect of an implementation. The 鈥渢est<br>case鈥� is then a collection tests that share the same setup/inputs. On top of all of this sits the 鈥渢est<br>suite鈥� which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br> -But enough about the theory. In the following example, which you can simply put into the same<br> -铿乴e as your code above, you will see a test in common Zope 3 style.<br> -1 import unittest<br>2<br> -3 class SampleTest(unittest.TestCase):<br>4<br> -"""Test the Sample class"""<br> -5<br> -6<br> -def test_title(self):<br> -7<br> -sample = Sample()<br> -8<br> -self.assertEqual(sample.title, None)<br> -9<br> -sample.title = 鈥橲ample Title鈥�<br> -10<br> -self.assertEqual(sample.title, 鈥橲ample Title鈥�)<br> -11<br> -12<br> -def test_getDescription(self):<br> -13<br> -sample = Sample()<br> -14<br> -self.assertEqual(sample.getDescription(), 鈥欌€�)<br> -15<br> -sample._description = "Description"<br> -16<br> -self.assertEqual(sample.getDescription(), 鈥橠escription鈥�)<br> -17<br> -18<br> -def test_setDescription(self):<br> -19<br> -sample = Sample()<br> -20<br> -self.assertEqual(sample._description, 鈥欌€�)<br> -21<br> -sample.setDescription(鈥橠escription鈥�)<br> -22<br> -self.assertEqual(sample._description, 鈥橠escription鈥�)<br> -23<br> -sample.setDescription(u鈥橠escription2鈥�)<br> -24<br> -self.assertEqual(sample._description, u鈥橠escription2鈥�)<br> -25<br> -self.assertRaises(AssertionError, sample.setDescription, None)<br> -26<br> -27<br> -28 def test_suite():<br>29<br> -return unittest.TestSuite((<br> -30<br> -unittest.makeSuite(SampleTest),<br> -31<br> -))<br> -32<br> -33 if __name__ == 鈥檁_main__鈥�:<br>34<br> -unittest.main(defaultTest=鈥檛est_suite鈥�)<br> -Line 3鈥�4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br> -Line 6, 12 & 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests di铿€erently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a 鈥渢est method鈥� for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br> -<hr> -<A name=4></a>4<br> -CHAPTER 44. WRITING BASIC UNIT TESTS<br> -Note that there is no docstring for test methods. This is intentional. If a docstring is speci铿乪d,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very di铿僣ult to identify the test later; therefore the method name is a much<br>better choice.<br> -Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br> -鈥� assertEqual(first,second[,msg])<br> -Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br> -鈥� assertNotEqual(first,second[,msg])<br> -This is simply the opposite to assertEqual() by checking for non-equality.<br> -鈥� assertRaises(exception,callable,...)<br> -You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br> -鈥� assert (expr[,msg])<br> -Assert checks whether the speci铿乪d expression executes correctly. If not, the test fails and<br>msg or None is returned.<br> -鈥� failUnlessEqual()<br> -This testing method is equivalent to assertEqual().<br> -鈥� failUnless(expr[,msg])<br> -This method is equivalent to assert (expr[,msg]).<br> -鈥� failif()<br> -This is the opposite to failUnless().<br> -鈥� fail([msg])<br> -Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br> -Line 6鈥�10: This method tests the title attribute of the Sample class. The 铿乺st test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br> -Line 12鈥�16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-speci铿乧 attributes and methods. Finally we just check that<br>the correct value is returned.<br> -<hr> -<A name=5></a>44.3. RUNNING THE TESTS<br> -5<br> -Line 18鈥�25: On line 21鈥�24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br> -28鈥�31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br> -33鈥�34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br> -44.3<br> -Running the Tests<br> -You can run the test by simply calling pythontest sample.py from the directory you saved the<br>铿乴e in. Here is the result you should see:<br> -.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br> -The three dots represent the three tests that were run. If a test had failed, it would have been<br> -reported pointing out the failing test and providing a small traceback.<br> -When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br> -conventions.<br> -鈥� The tests must either be in a package or be a module called tests.<br> -鈥� If tests is a package, then all test modules inside must also have a name starting with test,<br> -as it is the case with our name test sample.py.<br> -鈥� The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br> -only for 铿乴es there.<br> -In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br> -init .<br> -py 铿乴e). Then place the test sample.py 铿乴e into this directory.<br> -You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br> -directory:<br> -python test.py -vp tests.test_sample<br> -The -v option stands for verbose mode, so that detailed information about a test failure is<br> -provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be speci铿乪d. You can get a full list of them with<br>the option -h: pythontest.py-h.<br> -The output of the call above is as follows:<br> -nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br> -3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br> ---------------------------------------------------------------------<br>n 3 tests in 0.002s<br> -<hr> -<A name=6></a>6<br> -CHAPTER 44. WRITING BASIC UNIT TESTS<br> -nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br> ---------------------------------------------------------------------<br>n 0 tests in 0.000s<br> -Line 1: The test runner uses a con铿乬uration 铿乴e for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the con铿乬uration 铿乴e<br>was found.<br> -Line 2鈥�8: The unit tests are run. On line 4 you can see the progress bar.<br> -Line 9鈥�15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the speci铿乪d module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See 鈥淲riting<br>Functional Tests鈥� for more detials on functional tests.<br> -<hr> -<A name=7></a>44.3. RUNNING THE TESTS<br> -7<br> -Exercises<br> -1. It is not very common to do the setup 鈥� in our case sample=Sample() 鈥� in every test<br> -method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br> -2. Currently the test setDescription() test only veri铿乪s that None is not allowed as input<br> -value.<br> -(a) Improve the test, so that all other builtin types are tested as well.<br> -(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br> -<hr> \ No newline at end of file +<A name=1></a>Chapter 44<br>Writing Basic Unit Tests<br>Di铿僣ulty<br>Newcomer<br>Skills<br>鈥� All you need to know is some Python.<br>Problem/Task<br>As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests 鈥� which<br>are Zope 3 independent 鈥� and introduces some of the subtleties.<br>Solution<br>44.1<br>Implementing the Sample Class<br>Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br>Since this code will not depend on Zope, open a 铿乴e named test sample.py anywhere and add<br>the following class:<br>1 Sample(object):<br>2<br>"""A trivial Sample object."""<br>3<br>4<br>title = None<br>5<br>6<br>def __init__(self):<br>7<br>"""Initialize object."""<br>8<br>self._description = 鈥欌€�<br>9<br>1<br><hr><A name=2></a>2<br>CHAPTER 44. WRITING BASIC UNIT TESTS<br>10<br>def setDescription(self, value):<br>11<br>"""Change the value of the description."""<br>12<br>assert isinstance(value, (str, unicode))<br>13<br>self._description = value<br>14<br>15<br>def getDescription(self):<br>16<br>"""Change the value of the description."""<br>17<br>return self._description<br>Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br>Line 8: The actual description string will be stored in description.<br>Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br>If you wish you can now manually test the class with the interactive Python shell. Just start<br>Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br>1 >>> from test_sample import Sample<br>2 >>> sample = Sample()<br>3 >>> print sample.title<br>4 None<br>5 >>> sample.title = 鈥橳itle鈥�<br>6 >>> print sample.title<br>7 Title<br>8 >>> print sample.getDescription()<br>9<br>10 >>> sample.setDescription(鈥橦ello World鈥�)<br>11 >>> print sample.getDescription()<br>12 Hello World<br>13 >>> sample.setDescription(None)<br>14 Traceback (most recent call last):<br>15<br>File "<stdin>", line 1, in ?<br>16<br>File "test_sample.py", line 31, in setDescription<br>17<br>assert isinstance(value, (str, unicode))<br>18 AssertionError<br>As you can see in the last test, non-string object types are not allowed as descriptions and an<br>AssertionError is raised.<br>44.2<br>Writing the Unit Tests<br>The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original de铿乶itions as found<br>in the Python library documentation. 1).<br>1 http://www.python.org/doc/current/lib/module-unittest.html<br><hr><A name=3></a>44.2. WRITING THE UNIT TESTS<br>3<br>The smallest unit is obviously the 鈥渢est鈥�, which is a single method in a TestCase class that<br>tests the behavior of a small piece of code or a particular aspect of an implementation. The 鈥渢est<br>case鈥� is then a collection tests that share the same setup/inputs. On top of all of this sits the 鈥渢est<br>suite鈥� which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br>But enough about the theory. In the following example, which you can simply put into the same<br>铿乴e as your code above, you will see a test in common Zope 3 style.<br>1 import unittest<br>2<br>3 class SampleTest(unittest.TestCase):<br>4<br>"""Test the Sample class"""<br>5<br>6<br>def test_title(self):<br>7<br>sample = Sample()<br>8<br>self.assertEqual(sample.title, None)<br>9<br>sample.title = 鈥橲ample Title鈥�<br>10<br>self.assertEqual(sample.title, 鈥橲ample Title鈥�)<br>11<br>12<br>def test_getDescription(self):<br>13<br>sample = Sample()<br>14<br>self.assertEqual(sample.getDescription(), 鈥欌€�)<br>15<br>sample._description = "Description"<br>16<br>self.assertEqual(sample.getDescription(), 鈥橠escription鈥�)<br>17<br>18<br>def test_setDescription(self):<br>19<br>sample = Sample()<br>20<br>self.assertEqual(sample._description, 鈥欌€�)<br>21<br>sample.setDescription(鈥橠escription鈥�)<br>22<br>self.assertEqual(sample._description, 鈥橠escription鈥�)<br>23<br>sample.setDescription(u鈥橠escription2鈥�)<br>24<br>self.assertEqual(sample._description, u鈥橠escription2鈥�)<br>25<br>self.assertRaises(AssertionError, sample.setDescription, None)<br>26<br>27<br>28 def test_suite():<br>29<br>return unittest.TestSuite((<br>30<br>unittest.makeSuite(SampleTest),<br>31<br>))<br>32<br>33 if __name__ == 鈥檁_main__鈥�:<br>34<br>unittest.main(defaultTest=鈥檛est_suite鈥�)<br>Line 3鈥�4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br>Line 6, 12 & 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests di铿€erently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a 鈥渢est method鈥� for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br><hr><A name=4></a>4<br>CHAPTER 44. WRITING BASIC UNIT TESTS<br>Note that there is no docstring for test methods. This is intentional. If a docstring is speci铿乪d,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very di铿僣ult to identify the test later; therefore the method name is a much<br>better choice.<br>Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br>鈥� assertEqual(first,second[,msg])<br>Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br>鈥� assertNotEqual(first,second[,msg])<br>This is simply the opposite to assertEqual() by checking for non-equality.<br>鈥� assertRaises(exception,callable,...)<br>You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br>鈥� assert (expr[,msg])<br>Assert checks whether the speci铿乪d expression executes correctly. If not, the test fails and<br>msg or None is returned.<br>鈥� failUnlessEqual()<br>This testing method is equivalent to assertEqual().<br>鈥� failUnless(expr[,msg])<br>This method is equivalent to assert (expr[,msg]).<br>鈥� failif()<br>This is the opposite to failUnless().<br>鈥� fail([msg])<br>Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br>Line 6鈥�10: This method tests the title attribute of the Sample class. The 铿乺st test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br>Line 12鈥�16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-speci铿乧 attributes and methods. Finally we just check that<br>the correct value is returned.<br><hr><A name=5></a>44.3. RUNNING THE TESTS<br>5<br>Line 18鈥�25: On line 21鈥�24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br>28鈥�31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br>33鈥�34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br>44.3<br>Running the Tests<br>You can run the test by simply calling pythontest sample.py from the directory you saved the<br>铿乴e in. Here is the result you should see:<br>.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br>The three dots represent the three tests that were run. If a test had failed, it would have been<br>reported pointing out the failing test and providing a small traceback.<br>When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br>conventions.<br>鈥� The tests must either be in a package or be a module called tests.<br>鈥� If tests is a package, then all test modules inside must also have a name starting with test,<br>as it is the case with our name test sample.py.<br>鈥� The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br>only for 铿乴es there.<br>In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br>init .<br>py 铿乴e). Then place the test sample.py 铿乴e into this directory.<br>You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br>directory:<br>python test.py -vp tests.test_sample<br>The -v option stands for verbose mode, so that detailed information about a test failure is<br>provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be speci铿乪d. You can get a full list of them with<br>the option -h: pythontest.py-h.<br>The output of the call above is as follows:<br>nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br>3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br>--------------------------------------------------------------------<br>n 3 tests in 0.002s<br><hr><A name=6></a>6<br>CHAPTER 44. WRITING BASIC UNIT TESTS<br>nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br>--------------------------------------------------------------------<br>n 0 tests in 0.000s<br>Line 1: The test runner uses a con铿乬uration 铿乴e for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the con铿乬uration 铿乴e<br>was found.<br>Line 2鈥�8: The unit tests are run. On line 4 you can see the progress bar.<br>Line 9鈥�15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the speci铿乪d module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See 鈥淲riting<br>Functional Tests鈥� for more detials on functional tests.<br><hr><A name=7></a>44.3. RUNNING THE TESTS<br>7<br>Exercises<br>1. It is not very common to do the setup 鈥� in our case sample=Sample() 鈥� in every test<br>method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br>2. Currently the test setDescription() test only veri铿乪s that None is not allowed as input<br>value.<br>(a) Improve the test, so that all other builtin types are tested as well.<br>(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br><hr> \ No newline at end of file diff --git a/product/PortalTransforms/tests/output/markdown.html b/product/PortalTransforms/tests/output/markdown.html index bb6bd3e189..333e55585e 100644 --- a/product/PortalTransforms/tests/output/markdown.html +++ b/product/PortalTransforms/tests/output/markdown.html @@ -3,4 +3,5 @@ <h2> Testing Markdown </h2> <p> <code>code</code> and <em>italic</em> and <em>bold</em> and even a <a href="http://plone.org">link</a>. </p> +<p>F枚枚b盲r</p> diff --git a/product/PortalTransforms/tests/output/rest2.out b/product/PortalTransforms/tests/output/rest2.out index b53ef7c8cd..403271e768 100644 --- a/product/PortalTransforms/tests/output/rest2.out +++ b/product/PortalTransforms/tests/output/rest2.out @@ -1,6 +1,6 @@ <h2 class="title">Heading 1</h2> <p>Some text.</p> -<div class="section"> -<h3><a id="heading-2" name="heading-2">Heading 2</a></h3> -<p>Some text, bla ble bli blo blu. Yes, i know this is <a class="reference" href="http://www.example.com">Stupid</a>.</p> +<div class="section" id="heading-2"> +<h3>Heading 2</h3> +<p>Some text, bla ble bli blo blu. Yes, i know this is<a class="reference external" href="http://www.example.com">Stupid</a>.</p> </div> diff --git a/product/PortalTransforms/tests/output/rest3.out b/product/PortalTransforms/tests/output/rest3.out index 048e69e131..ea85ca1881 100644 --- a/product/PortalTransforms/tests/output/rest3.out +++ b/product/PortalTransforms/tests/output/rest3.out @@ -1,11 +1,11 @@ <h2 class="title">Title</h2> <h3 class="subtitle">Subtitle</h3> <p>This is a test document to make sure subtitle gets the right heading.</p> -<div class="section"> -<h3><a id="now-the-real-heading" name="now-the-real-heading">Now the real heading</a></h3> +<div class="section" id="now-the-real-heading"> +<h3>Now the real heading</h3> <p>The brown fox jumped over the lazy dog.</p> -<div class="section"> -<h4><a id="with-a-subheading" name="with-a-subheading">With a subheading</a></h4> -<p>Some text, bla ble bli blo blu. Yes, i know this is <a class="reference" href="http://www.example.com">Stupid</a>.</p> +<div class="section" id="with-a-subheading"> +<h4>With a subheading</h4> +<p>Some text, bla ble bli blo blu. Yes, i know this is<a class="reference external" href="http://www.example.com">Stupid</a>.</p> </div> </div> diff --git a/product/PortalTransforms/tests/output/test_safe.html b/product/PortalTransforms/tests/output/test_safe.html index 7f6e641948..6f915627b9 100644 --- a/product/PortalTransforms/tests/output/test_safe.html +++ b/product/PortalTransforms/tests/output/test_safe.html @@ -6,6 +6,10 @@ </tr> </table> <p>This is a text used as a blind text.</p> +<div><![CDATA[ + Some CDATA text. +]]> +</div> <ul> <li>A sample list item1</li> <li>A sample list item2</li> diff --git a/product/PortalTransforms/tests/test_engine.py b/product/PortalTransforms/tests/test_engine.py index ad90b3b77b..0f3a0b2030 100644 --- a/product/PortalTransforms/tests/test_engine.py +++ b/product/PortalTransforms/tests/test_engine.py @@ -67,6 +67,15 @@ class DummyHtmlFilter2(BaseTransform): data.setData("<div class='dummy'>%s</div>" % orig) return data + +class QuxToVHost(DummyHtmlFilter1): + __name__ = 'qux_to_vhost' + + def convert(self, orig, data, context, **kwargs): + data.setData(re.sub('qux', context.REQUEST['SERVER_URL'], orig)) + return data + + class TransformNoIO(BaseTransform): implements(ITransform) @@ -223,6 +232,52 @@ class TestEngine(ATSiteTestCase): out = self.engine.convertTo(mt, other_data, mimetype=mt, object=self) self.failUnlessEqual(out.getData(), other_data, out.getData()) + def testCacheWithVHost(self): + """Ensure that the transform cache key includes virtual + hosting so that transforms which are dependent on the virtual + hosting don't get invalid data from the cache. This happens, + for example, in the resolve UID functionality used by visual + editors.""" + mt = 'text/x-html-safe' + self.engine.registerTransform(QuxToVHost()) + required = ['qux_to_vhost'] + self.engine.manage_addPolicy(mt, required) + + data = '<a href="qux">vhost link</a>' + + out = self.engine.convertTo( + mt, data, mimetype='text/html', object=self.folder, + context=self.folder) + self.failUnlessEqual( + out.getData(), '<a href="http://nohost">vhost link</a>', + out.getData()) + + # Test when object is not a context + out = self.engine.convertTo( + mt, data, mimetype='text/html', object=self, + context=self.folder) + self.failUnlessEqual( + out.getData(), '<a href="http://nohost">vhost link</a>', + out.getData()) + + # Change the virtual hosting + self.folder.REQUEST['SERVER_URL'] = 'http://otherhost' + + out = self.engine.convertTo( + mt, data, mimetype='text/html', object=self.folder, + context=self.folder) + self.failUnlessEqual( + out.getData(), '<a href="http://otherhost">vhost link</a>', + out.getData()) + + # Test when object is not a context + out = self.engine.convertTo( + mt, data, mimetype='text/html', object=self, + context=self.folder) + self.failUnlessEqual( + out.getData(), '<a href="http://otherhost">vhost link</a>', + out.getData()) + def test_suite(): from unittest import TestSuite, makeSuite diff --git a/product/PortalTransforms/tests/test_graph.py b/product/PortalTransforms/tests/test_graph.py index 398e7d03bf..eda3120ede 100644 --- a/product/PortalTransforms/tests/test_graph.py +++ b/product/PortalTransforms/tests/test_graph.py @@ -16,6 +16,87 @@ class TestGraph(ATSiteTestCase): out = self.engine.convertTo('text/plain', data, filename=FILE_PATH) self.failUnless(out.getData()) + def testFindPath(self): + originalMap = self.engine._mtmap + """ + The dummy map used for this test corresponds to a graph + depicted in ASCII art below : + + +---+ + | | + | v + +-->1<-->2-->4-->6<--7 + ^ ^ | + | | | + v | | + 3<---+ | + ^ | + | | + v | + 5<-------+ + """ + # we need a DummyTransform class + class DT: + def __init__(self, name): + self._name = name + def name(self): + return self._name + + dummyMap1 = { + '1': { '1': [DT('transform1-1')], + '2': [DT('transform1-2')], + '3': [DT('transform1-3')]}, + '2': { '1': [DT('transform2-1')], + '3': [DT('transform2-3')], + '4': [DT('transform2-4')]}, + '3': { '1': [DT('transform3-1')], + '2': [DT('transform3-2')], + '5': [DT('transform3-5')]}, + '4': { '5': [DT('transform4-5')], + '6': [DT('transform4-6')]}, + '5': { '3': [DT('transform5-3')]}, + '7': { '6': [DT('transform7-6')]} + } + expectedPathes = { + '1-1': [], + '1-2': ['transform1-2'], + '1-3': ['transform1-3'], + '1-4': ['transform1-2', 'transform2-4'], + '1-5': ['transform1-3', 'transform3-5'], + '1-6': ['transform1-2', 'transform2-4', 'transform4-6'], + '1-7': None, + '2-1': ['transform2-1'], + '2-2': [], + '2-4': ['transform2-4'], + '4-2': ['transform4-5', 'transform5-3', 'transform3-2'], + '5-3': ['transform5-3'] + } + self.engine._mtmap = dummyMap1 + for orig in ['1','2','3','4','5','6','7']: + for target in ['1','2','3','4','5','6','7']: + # build the name of the path + pathName = orig + '-' + target + # do we have any expectation for this path ? + if pathName in expectedPathes.keys(): + # we do. Here is the expected shortest path + expectedPath = expectedPathes[pathName] + # what's the shortest path according to the engine ? + gotPath = self.engine._findPath(orig,target) + # just keep the name of the transforms, please + if gotPath is not None: + gotPath = [transform.name() for transform in gotPath] + # this must be the same as in our expectation + self.assertEquals(expectedPath, gotPath) + self.engine._mtmap = originalMap + + def testFindPathWithEmptyTransform(self): + """ _findPath should not throw "index out of range" when dealing with + empty transforms list + """ + dummyMap = {'1': {'2': []}} + self.engine._mtmap = dummyMap + self.engine._findPath('1','2') + def testIdentity(self): orig = 'Some text' converted = self.engine.convertTo( diff --git a/product/PortalTransforms/tests/test_transforms.py b/product/PortalTransforms/tests/test_transforms.py index 54c21eb6d5..ec1953012d 100644 --- a/product/PortalTransforms/tests/test_transforms.py +++ b/product/PortalTransforms/tests/test_transforms.py @@ -1,15 +1,12 @@ import os import logging -from Testing import ZopeTestCase from Products.Archetypes.tests.atsitetestcase import ATSiteTestCase +from Products.CMFCore.utils import getToolByName from utils import input_file_path, output_file_path, normalize_html,\ load, matching_inputs from Products.PortalTransforms.data import datastream from Products.PortalTransforms.interfaces import IDataStream -from Products.PortalTransforms.interfaces import idatastream -from Products.MimetypesRegistry.MimeTypesTool import MimeTypesTool -from Products.PortalTransforms.TransformEngine import TransformTool from Products.PortalTransforms.libtransforms.utils import MissingBinary from Products.PortalTransforms.transforms.image_to_gif import image_to_gif @@ -24,7 +21,6 @@ from Products.PortalTransforms.transforms.textile_to_html import HAS_TEXTILE from Products.PortalTransforms.transforms.markdown_to_html import HAS_MARKDOWN from os.path import exists -import sys # we have to set locale because lynx output is locale sensitive ! os.environ['LC_ALL'] = 'C' logger = logging.getLogger('PortalTransforms') @@ -59,9 +55,11 @@ class TransformTest(ATSiteTestCase): got = self.normalize(got) output.close() - self.assertEquals(got, expected, + got_start = got.strip()[:30] + expected_start = expected.strip()[:30] + self.assertEquals(got_start, expected_start, '[%s]\n\n!=\n\n[%s]\n\nIN %s(%s)' % ( - got, expected, self.transform.name(), self.input)) + got_start, expected_start, self.transform.name(), self.input)) self.assertEquals(self.subobjects, len(res_data.getSubObjects()), '%s\n\n!=\n\n%s\n\nIN %s(%s)' % ( self.subobjects, len(res_data.getSubObjects()), @@ -70,13 +68,13 @@ class TransformTest(ATSiteTestCase): def testSame(self): try: self.do_convert(filename=self.input) - except MissingBinary, e: + except MissingBinary: pass def testSameNoFilename(self): try: self.do_convert() - except MissingBinary, e: + except MissingBinary: pass def __repr__(self): @@ -86,12 +84,13 @@ class PILTransformsTest(ATSiteTestCase): def afterSetUp(self): ATSiteTestCase.afterSetUp(self) self.pt = self.portal.portal_transforms + self.mimetypes_registry = getToolByName(self.portal, 'mimetypes_registry') def test_image_to_bmp(self): self.pt.registerTransform(image_to_bmp()) imgFile = open(input_file_path('logo.jpg'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg') data = self.pt.convertTo(target_mimetype='image/x-ms-bmp',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-ms-bmp') @@ -99,7 +98,7 @@ class PILTransformsTest(ATSiteTestCase): self.pt.registerTransform(image_to_gif()) imgFile = open(input_file_path('logo.png'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/png') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/png') data = self.pt.convertTo(target_mimetype='image/gif',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/gif') @@ -107,7 +106,7 @@ class PILTransformsTest(ATSiteTestCase): self.pt.registerTransform(image_to_jpeg()) imgFile = open(input_file_path('logo.gif'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/gif') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/gif') data = self.pt.convertTo(target_mimetype='image/jpeg',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/jpeg') @@ -115,7 +114,7 @@ class PILTransformsTest(ATSiteTestCase): self.pt.registerTransform(image_to_png()) imgFile = open(input_file_path('logo.jpg'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg') data = self.pt.convertTo(target_mimetype='image/png',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/png') @@ -123,7 +122,7 @@ class PILTransformsTest(ATSiteTestCase): self.pt.registerTransform(image_to_pcx()) imgFile = open(input_file_path('logo.gif'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/gif') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/gif') data = self.pt.convertTo(target_mimetype='image/pcx',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/pcx') @@ -131,7 +130,7 @@ class PILTransformsTest(ATSiteTestCase): self.pt.registerTransform(image_to_ppm()) imgFile = open(input_file_path('logo.png'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/png') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/png') data = self.pt.convertTo(target_mimetype='image/x-portable-pixmap',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-portable-pixmap') @@ -139,7 +138,7 @@ class PILTransformsTest(ATSiteTestCase): self.pt.registerTransform(image_to_tiff()) imgFile = open(input_file_path('logo.jpg'), 'rb') data = imgFile.read() - self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg') + self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg') data = self.pt.convertTo(target_mimetype='image/tiff',orig=data) self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/tiff') diff --git a/product/PortalTransforms/tests/utils.py b/product/PortalTransforms/tests/utils.py index f12061a1bc..86a1fc67da 100644 --- a/product/PortalTransforms/tests/utils.py +++ b/product/PortalTransforms/tests/utils.py @@ -5,6 +5,7 @@ from sys import modules from os.path import join, abspath, dirname, basename def normalize_html(s): + s = re.sub(r" ", " ", s) s = re.sub(r"\s+", " ", s) s = re.sub(r"(?s)\s+<", "<", s) s = re.sub(r"(?s)>\s+", ">", s) diff --git a/product/PortalTransforms/transforms/markdown_to_html.py b/product/PortalTransforms/transforms/markdown_to_html.py index a76f662eea..025adab29f 100644 --- a/product/PortalTransforms/transforms/markdown_to_html.py +++ b/product/PortalTransforms/transforms/markdown_to_html.py @@ -1,19 +1,12 @@ """ -Uses the http://www.freewisdom.org/projects/python-markdown/ module to do its handy work - -author: Tom Lazar <tom@tomster.org> at the archipelago sprint 2006 +Uses the http://www.freewisdom.org/projects/python-markdown/ module +Author: Tom Lazar <tom@tomster.org> at the archipelago sprint 2006 """ -import os from zope.interface import implements -from Products.CMFDefault.utils import bodyfinder - from Products.PortalTransforms.interfaces import ITransform -from Products.PortalTransforms.libtransforms.commandtransform import commandtransform -from Products.PortalTransforms.libtransforms.utils import bin_search -from Products.PortalTransforms.libtransforms.utils import sansext from Products.PortalTransforms.utils import log try: @@ -23,7 +16,7 @@ except ImportError: log('markdown_to_html: Could not import python-markdown.') else: HAS_MARKDOWN = True - + class markdown: implements(ITransform) @@ -37,11 +30,16 @@ class markdown: def convert(self, orig, data, **kwargs): if HAS_MARKDOWN: - html = markdown_transformer.markdown(orig) + # markdown expects unicode input: + orig = unicode(orig.decode('utf-8')) + # PortalTransforms, however expects a string as result, + # so we encode the unicode result back to UTF8: + html = markdown_transformer.markdown(orig).encode('utf-8') else: html = orig data.setData(html) return data + def register(): return markdown() diff --git a/product/PortalTransforms/transforms/office_wvware.py b/product/PortalTransforms/transforms/office_wvware.py index 302a08dc65..a81e8cb0fe 100644 --- a/product/PortalTransforms/transforms/office_wvware.py +++ b/product/PortalTransforms/transforms/office_wvware.py @@ -1,7 +1,5 @@ -import re, tempfile -import os, os.path -from Products.PortalTransforms.libtransforms.utils import bin_search, \ - sansext, bodyfinder, scrubHTML +import os +from Products.PortalTransforms.libtransforms.utils import bodyfinder, scrubHTML from Products.PortalTransforms.libtransforms.commandtransform import commandtransform class document(commandtransform): diff --git a/product/PortalTransforms/transforms/safe_html.py b/product/PortalTransforms/transforms/safe_html.py index e6e2c28503..7275e236b6 100644 --- a/product/PortalTransforms/transforms/safe_html.py +++ b/product/PortalTransforms/transforms/safe_html.py @@ -31,7 +31,7 @@ VALID_TAGS['ins'] = 1 VALID_TAGS['del'] = 1 VALID_TAGS['q'] = 1 VALID_TAGS['map'] = 1 -VALID_TAGS['area'] = 1 +VALID_TAGS['area'] = 0 VALID_TAGS['abbr'] = 1 VALID_TAGS['acronym'] = 1 VALID_TAGS['var'] = 1 @@ -71,6 +71,10 @@ VALID_TAGS['source'] = 1 VALID_TAGS['time'] = 1 VALID_TAGS['video'] = 1 +# add some tags to nasty. These should also probably be backported to CMFDefault. +NASTY_TAGS['style'] = 1 # this helps improve Word HTML cleanup. +NASTY_TAGS['meta'] = 1 # allowed by parsers, but can cause unexpected behavior + msg_pat = """ <div class="system-message"> @@ -203,7 +207,7 @@ class StrippingParser(HTMLParser): if not self.raise_error: continue else: raise IllegalHTML, 'Script event "%s" not allowed.' % k elif v is None: - self.result.append(' %s' % (k,)) + self.result.append(' %s' % k) elif remove_script and hasScript(v): if not self.raise_error: continue else: raise IllegalHTML, 'Script URI "%s" not allowed.' % v @@ -238,6 +242,26 @@ class StrippingParser(HTMLParser): self.result.append('</%s>' % tag) #remTag = '</%s>' % tag + def parse_declaration(self, i): + """Fix handling of CDATA sections. Code borrowed from BeautifulSoup. + """ + j = None + if self.rawdata[i:i+9] == '<![CDATA[': + k = self.rawdata.find(']]>', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self.result.append("<![CDATA[%s]]>" % data) + else: + try: + j = HTMLParser.parse_declaration(self, i) + except HTMLParseError: + toHandle = self.rawdata[i:] + self.result.append(toHandle) + j = i + len(toHandle) + return j + def getResult(self): return ''.join(self.result) @@ -262,13 +286,13 @@ def scrubHTML(html, valid=VALID_TAGS, nasty=NASTY_TAGS, class SafeHTML: """Simple transform which uses CMFDefault functions to - clean potentially bad tags. + clean potentially bad tags. Tags must explicit be allowed in valid_tags to pass. Only the tags themself are removed, not their contents. If tags are removed and in nasty_tags, they are removed with - all of their contents. - + all of their contents. + Objects will not be transformed again with changed settings. You need to clear the cache by e.g. 1.) restarting your zope or @@ -291,6 +315,10 @@ class SafeHTML: 'output': self.output, 'valid_tags': VALID_TAGS, 'nasty_tags': NASTY_TAGS, + 'stripped_attributes': ['lang','valign','halign','border','frame','rules','cellspacing','cellpadding','bgcolor'], + 'stripped_combinations': {'table th td': 'width height'}, + 'style_whitelist': ['text-align', 'list-style-type', 'float'], + 'class_blacklist': [], 'remove_javascript': 1, 'disable_transform': 0, 'default_encoding': 'utf-8', @@ -310,6 +338,19 @@ class SafeHTML: 'everything they contain (like applet, object). ' + 'They are only deleted if they are not marked as valid_tags.', ('tag', 'value')), + 'stripped_attributes': ('list', + 'stripped_attributes', + 'These attributes are stripped from any tag.'), + 'stripped_combinations' : ('dict', + 'stripped_combinations', + 'These attributes are stripped from any tag.', + ('tag', 'value')), + 'style_whitelist': ('list', + 'style_whitelist', + 'These CSS styles are allowed in style attributes.'), + 'class_blacklist': ('list', + 'class_blacklist', + 'These class names are not allowed in class attributes.'), 'remove_javascript' : ("int", 'remove_javascript', '1 to remove javascript attributes that begin with on (e.g. onClick) ' + @@ -355,7 +396,9 @@ class SafeHTML: repaired = 0 while True: try: - orig = scrubHTML( + # Do 2 passes. This provides more reliable filtering of certain + # malicious HTML (cf upstream commit svn10522). + for repeat in range(2): orig = scrubHTML( orig, valid=self.config.get('valid_tags', {}), nasty=self.config.get('nasty_tags', {}), @@ -366,6 +409,8 @@ class SafeHTML: data.setData(msg_pat % ("Error", str(inst))) break except HTMLParseError: + if repeat: + raise # try to repair only on first pass # ouch ! # HTMLParser is not able to parse very dirty HTML string if not repaired: diff --git a/product/PortalTransforms/transforms/word_to_html.py b/product/PortalTransforms/transforms/word_to_html.py index bfbc9dc3a4..b7b2699add 100644 --- a/product/PortalTransforms/transforms/word_to_html.py +++ b/product/PortalTransforms/transforms/word_to_html.py @@ -45,20 +45,23 @@ class word_to_html: def convert(self, data, cache, **kwargs): orig_file = 'unknown.doc' + doc = None + try: + doc = document(orig_file, data) + doc.convert() + html = doc.html() - doc = document(orig_file, data) - doc.convert() - html = doc.html() + path, images = doc.subObjects(doc.tmpdir) + objects = {} + if images: + doc.fixImages(path, images, objects) - path, images = doc.subObjects(doc.tmpdir) - objects = {} - if images: - doc.fixImages(path, images, objects) - doc.cleanDir(doc.tmpdir) - - cache.setData(html) - cache.setSubObjects(objects) - return cache + cache.setData(html) + cache.setSubObjects(objects) + return cache + finally: + if doc is not None: + doc.cleanDir(doc.tmpdir) def register(): return word_to_html() diff --git a/product/PortalTransforms/unsafe_transforms/build_transforms.py b/product/PortalTransforms/unsafe_transforms/build_transforms.py index 59fb38ec7a..07c403da72 100644 --- a/product/PortalTransforms/unsafe_transforms/build_transforms.py +++ b/product/PortalTransforms/unsafe_transforms/build_transforms.py @@ -6,37 +6,37 @@ from Products.PortalTransforms.libtransforms.utils import bin_search, MissingBin COMMAND_CONFIGS = ( ('lynx_dump', '.html', {'binary_path' : 'lynx', - 'command_line' : '-dump %s', + 'command_line' : '-dump %(input)s', 'inputs' : ('text/html',), 'output' : 'text/plain', }), ('tidy_html', '.html', {'binary_path' : 'tidy', - 'command_line' : '%s', + 'command_line' : '%(input)s', 'inputs' : ('text/html',), 'output' : 'text/html', }), ('rtf_to_html', None, {'binary_path' : 'unrtf', - 'command_line' : '%s', + 'command_line' : '%(input)s', 'inputs' : ('application/rtf',), 'output' : 'text/html', }), ('ppt_to_html', None, {'binary_path' : 'ppthtml', - 'command_line' : '%s', + 'command_line' : '%(input)s', 'inputs' : ('application/vnd.ms-powerpoint',), 'output' : 'text/html', }), ('excel_to_html', None, {'binary_path' : 'xlhtml', - 'command_line' : '-nh -a %s', + 'command_line' : '-nh -a %(input)s', 'inputs' : ('application/vnd.ms-excel',), 'output' : 'text/html', }), ('ps_to_text', None, {'binary_path' : 'ps2ascii', - 'command_line' : '%s', + 'command_line' : '%(input)s', 'inputs' : ('application/postscript',), 'output' : 'text/plain', }), diff --git a/product/PortalTransforms/utils.py b/product/PortalTransforms/utils.py index 6fb5bfabf6..f07179a551 100644 --- a/product/PortalTransforms/utils.py +++ b/product/PortalTransforms/utils.py @@ -8,10 +8,10 @@ class TransformException(Exception): FB_REGISTRY = None # logging function -from zLOG import LOG, INFO +from zLOG import LOG, DEBUG #logger = logging.getLogger('PortalTransforms') -def log(message, severity=INFO): +def log(message, severity=DEBUG): LOG('PortalTransforms', severity, message) #logger.log(severity, message) -- 2.30.9