[Checkins] SVN: hurry.resource/ Initial import.
Martijn Faassen
faassen at infrae.com
Mon Sep 22 07:37:32 EDT 2008
Log message for revision 91321:
Initial import.
Changed:
A hurry.resource/
A hurry.resource/trunk/
A hurry.resource/trunk/buildout.cfg
A hurry.resource/trunk/setup.py
A hurry.resource/trunk/src/
A hurry.resource/trunk/src/hurry/
A hurry.resource/trunk/src/hurry/__init__.py
A hurry.resource/trunk/src/hurry/resource/
A hurry.resource/trunk/src/hurry/resource/README.txt
A hurry.resource/trunk/src/hurry/resource/__init__.py
A hurry.resource/trunk/src/hurry/resource/core.py
A hurry.resource/trunk/src/hurry/resource/interfaces.py
A hurry.resource/trunk/src/hurry/resource/tests.py
-=-
Added: hurry.resource/trunk/buildout.cfg
===================================================================
--- hurry.resource/trunk/buildout.cfg (rev 0)
+++ hurry.resource/trunk/buildout.cfg 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,11 @@
+[buildout]
+develop = .
+parts = test
+versions = versions
+
+[versions]
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = hurry.resource
+defaults = ['--tests-pattern', '^f?tests$', '-v']
Added: hurry.resource/trunk/setup.py
===================================================================
--- hurry.resource/trunk/setup.py (rev 0)
+++ hurry.resource/trunk/setup.py 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,23 @@
+from setuptools import setup, find_packages
+import sys, os
+
+setup(
+ name='hurry.resource',
+ version='0.1dev',
+ description="Flexible resources.",
+ classifiers=[],
+ keywords='',
+ author='Martijn Faassen',
+ author_email='faassen at startifact.com',
+ license='',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'zope.interface',
+ 'zope.component',
+ ],
+ entry_points={},
+ )
Added: hurry.resource/trunk/src/hurry/__init__.py
===================================================================
--- hurry.resource/trunk/src/hurry/__init__.py (rev 0)
+++ hurry.resource/trunk/src/hurry/__init__.py 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+ import pkg_resources
+ pkg_resources.declare_namespace(__name__)
+except ImportError:
+ import pkgutil
+ __path__ = pkgutil.extend_path(__path__, __name__)
Added: hurry.resource/trunk/src/hurry/resource/README.txt
===================================================================
--- hurry.resource/trunk/src/hurry/resource/README.txt (rev 0)
+++ hurry.resource/trunk/src/hurry/resource/README.txt 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,345 @@
+hurry.resource
+==============
+
+Introduction
+------------
+
+Resources are files that are used as resources in the display of a web
+page, such as CSS files, Javascript files and images. Resources
+packaged together in a directory to be published as such are called a
+resource *library*.
+
+When a resource is included in the ``head`` section of a HTML page, we
+call this a resource *inclusion*. An inclusion is of a particular
+resource in a particular library. There are two forms of this kind of
+inclusion in HTML: javascript is included using the ``script`` tag,
+and CSS (and KSS) are included using a ``link`` tag.
+
+Inclusions may depend on other inclusions. A javascript resource may
+for instance be built on top of another javascript resource. This
+means both of them should be loaded when the page display, the
+dependency before the resource that depends on it.
+
+Page components may actually require a certain inclusion in order to
+be functional. A widget may for instance expect a particular
+Javascript library to loaded. We call this an *inclusion requirement* of
+the component.
+
+``hurry.resource`` provides a simple API to specify resource
+libraries, inclusion and inclusion requirements.
+
+A resource library
+------------------
+
+We define a library ``foo``::
+
+ >>> from hurry.resource import Library
+ >>> foo = Library('foo')
+
+Inclusion
+---------
+
+We now create an inclusion of a particular resource in a library. This
+inclusion needs ``a.js`` from ``library`` and ``b.js`` as well::
+
+ >>> from hurry.resource import Inclusion, ResourceSpec
+ >>> x = Inclusion([ResourceSpec(foo, 'a.js'),
+ ... ResourceSpec(foo, 'b.css')])
+
+Let's examine the resource specs in this inclusion::
+
+ >>> x.resources_of_ext('.css')
+ [<Resource 'b.css' in library 'foo'>]
+
+ >>> x.resources_of_ext('.js')
+ [<Resource 'a.js' in library 'foo'>]
+
+Let's now make an inclusion ``y`` that depends on ``x``, but also includes
+some other resources itself::
+
+ >>> y = Inclusion([ResourceSpec(foo, 'c.js'),
+ ... ResourceSpec(foo, 'd.css')], depends=[x])
+
+ >>> y.resources_of_ext('.css')
+ [<Resource 'b.css' in library 'foo'>, <Resource 'd.css' in library 'foo'>]
+
+ >>> y.resources_of_ext('.js')
+ [<Resource 'a.js' in library 'foo'>, <Resource 'c.js' in library 'foo'>]
+
+As we can see the resources required by the dependency are sorted
+before the resources listed in this inclusion.
+
+Inclusion requirements
+----------------------
+
+We can also require an inclusion in a particular code path, using
+``inclusion.need()``. This mean that this inclusion is added to the
+inclusions that should be on the page template when it is rendered.
+
+ >>> from hurry.resource import NeededInclusions
+ >>> needed = NeededInclusions()
+ >>> needed.need(y)
+
+Let's now see what resources are needed::
+
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+A simplified spelling
+---------------------
+
+Let's introduce a more convenient spelling of needs now::
+
+ y.need()
+
+This can be done without reference to the needed inclusions directly
+as there is typically only a single set of needed inclusions that is
+generated during the rendering of a page. Let's try it::
+
+ >>> y.need()
+ Traceback (most recent call last):
+ ...
+ ComponentLookupError: (<InterfaceClass hurry.resource.interfaces.ICurrentNeededInclusions>, '')
+
+We get an error. The system says it cannot find the component
+``ICurrentNeededInclusions``. This is the component we need to define
+in order to specify how the system can know what the
+``INeededInclusions`` object is that the inclusion ``y`` should be
+added to. So, our task is to provide a ``ICurrentNeededInclusions``
+component that can give us the current needed inclusions object.
+
+This needed inclusions should be maintained on an object that is going
+to be present throughout the request/response cycle that generates the
+web page that has the inclusions on them. The most obvious location on
+which to maintain the needed inclusions the request object
+itself. Let's introduce such a simple request object (your mileage may
+vary in your own web framework)::
+
+ >>> class Request(object):
+ ... def __init__(self):
+ ... self.needed = NeededInclusions()
+
+We now make a request, imitating what happens during a typical
+request/response cycle in a web framework::
+
+ >>> request = Request()
+
+We now should define a ``ICurrentNeededInclusion`` utility that knows
+how to get the current needed inclusions from that request::
+
+ >>> def currentNeededInclusions():
+ ... return request.needed
+
+ >>> c = currentNeededInclusions
+
+We now need to register the utility to complete plugging into our pluggability
+point::
+
+ >>> from zope import component
+ >>> from hurry.resource.interfaces import ICurrentNeededInclusions
+ >>> component.provideUtility(currentNeededInclusions,
+ ... ICurrentNeededInclusions)
+
+Okay, let's check which resources our request needs currently::
+
+ >>> c().resources()
+ []
+
+Nothing yet.
+
+Let's now make ``y`` needed using our simplified spelling::
+
+ >>> y.need()
+
+The resource will now indeed be needed::
+
+ >>> c().resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+By the way, we have a handy reference to ``c`` to get us the current needed
+inclusions, but that doesn't work as soon as we lose that reference. Here is
+how can get it back again::
+
+ >>> c_retrieved = component.getUtility(ICurrentNeededInclusions)
+ >>> c_retrieved is c
+ True
+
+rewrite below XXX
+
+Let's now see what resources are needed::
+
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+
+
+Needing the same inclusion twice won't make any difference for the
+resources needed::
+
+ >>> needed.need(y)
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+Needing ``x`` won't make any difference either, as ``y`` already
+required ``x``::
+
+ >>> needed.need(x)
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+Let's do it in the reverse, and require the ``x`` resources before we
+need those in ``y``. Again this makes no difference::
+
+ >>> needed = NeededInclusions()
+ >>> needed.need(x)
+ >>> needed.need(y)
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+Let's introduce an inclusion that also needs ``d.css``::
+
+ >>> z = Inclusion([ResourceSpec(foo, 'd.css')])
+
+We'll also require this. Again this makes no difference::
+
+ >>> needed.need(z)
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+We can also state the need for ``z`` first, then for ``y``::
+
+ >>> needed = NeededInclusions()
+ >>> needed.need(z)
+ >>> needed.need(y)
+ >>> needed.resources()
+ [<Resource 'b.css' in library 'foo'>,
+ <Resource 'd.css' in library 'foo'>,
+ <Resource 'a.js' in library 'foo'>,
+ <Resource 'c.js' in library 'foo'>]
+
+Note that the sort order is still the same as before; inclusions with
+less depth are sorted after ones with more depth.
+
+Modes
+-----
+
+A resource can optionally exist in several modes, such as for instance
+a minified and a debug version. Let's define a resource that exists in
+two modes (a main one and a debug alternative)::
+
+ >>> a1 = ResourceSpec(foo, 'a.js', debug='a-debug.js')
+
+Let's need this resource::
+
+ >>> inclusion = Inclusion([a1])
+ >>> needed = NeededInclusions()
+ >>> needed.need(inclusion)
+
+By default, we get ``a.js``::
+
+ >>> needed.resources()
+ [<Resource 'a.js' in library 'foo'>]
+
+We can however also get the resource for mode ``debug`` and get
+``a-debug.js``::
+
+ >>> needed.resources(mode='debug')
+ [<Resource 'a-debug.js' in library 'foo'>]
+
+Modes can also be specified fully with a resource spec, which allows
+you to specify a different ``library`` and ``part_of`` argumnent::
+
+ >>> a2 = ResourceSpec(foo, 'a2.js', debug=ResourceSpec(foo, 'a2-debug.js'))
+ >>> inclusion = Inclusion([a2])
+ >>> needed = NeededInclusions()
+ >>> needed.need(inclusion)
+
+By default we get ``a2.js``::
+
+ >>> needed.resources()
+ [<Resource 'a2.js' in library 'foo'>]
+
+We can however also get the resource for mode ``debug`` and get
+``a2-debug.js``::
+
+ >>> needed.resources(mode='debug')
+ [<Resource 'a2-debug.js' in library 'foo'>]
+
+Consolidation
+-------------
+
+For performance reasons it's often useful to consolidate multiple
+resources into a single, larger resource. Multiple javascript files
+could for instance be offered in a single, larger one. These
+consolidations can be specified when specifying the resource::
+
+ >>> b1 = ResourceSpec(foo, 'b1.js', part_of='giant.js')
+ >>> b2 = ResourceSpec(foo, 'b2.js', part_of='giant.js')
+
+If we find multiple resources that are also part of a consolidation, the
+system automatically collapses them::
+
+ >>> inclusion1 = Inclusion([b1])
+ >>> inclusion2 = Inclusion([b2])
+ >>> needed = NeededInclusions()
+ >>> needed.need(inclusion1)
+ >>> needed.need(inclusion2)
+
+ >>> needed.resources()
+ [<Resource 'giant.js' in library 'foo'>]
+
+Consolidation will not take place if only a single resource in a consolidation
+is present::
+
+ >>> needed = NeededInclusions()
+ >>> needed.need(inclusion1)
+ >>> needed.resources()
+ [<Resource 'b1.js' in library 'foo'>]
+
+``part_of`` can also be expressed as a fully specified ``ResourceSpec``::
+
+ >>> b3 = ResourceSpec(foo, 'b3.js', part_of=ResourceSpec(foo, 'giant.js'))
+ >>> inclusion3 = Inclusion([b1, b2, b3])
+ >>> needed = NeededInclusions()
+ >>> needed.need(inclusion3)
+ >>> needed.resources()
+ [<Resource 'giant.js' in library 'foo'>]
+
+Consolidation also can work with modes::
+
+ >>> b4 = ResourceSpec(foo, 'b4.js',
+ ... part_of='giant.js',
+ ... debug=ResourceSpec(foo, 'b4-debug.js', part_of='giant-debug.js'))
+
+ >>> b5 = ResourceSpec(foo, 'b5.js',
+ ... part_of='giant.js',
+ ... debug=ResourceSpec(foo, 'b5-debug.js', part_of='giant-debug.js'))
+
+ >>> inclusion4 = Inclusion([b4, b5])
+ >>> needed = NeededInclusions()
+ >>> needed.need(inclusion4)
+ >>> needed.resources()
+ [<Resource 'giant.js' in library 'foo'>]
+ >>> needed.resources(mode='debug')
+ [<Resource 'giant-debug.js' in library 'foo'>]
Added: hurry.resource/trunk/src/hurry/resource/__init__.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/__init__.py (rev 0)
+++ hurry.resource/trunk/src/hurry/resource/__init__.py 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,2 @@
+from hurry.resource.core import (Library, Inclusion, ResourceSpec,
+ NeededInclusions)
Added: hurry.resource/trunk/src/hurry/resource/core.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/core.py (rev 0)
+++ hurry.resource/trunk/src/hurry/resource/core.py 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,212 @@
+import os
+from types import TupleType
+
+from zope.interface import implements
+from zope import component
+
+from hurry.resource import interfaces
+
+EXTENSIONS = ['.css', '.kss', '.js']
+
+class Library(object):
+ implements(interfaces.ILibrary)
+
+ def __init__(self, name):
+ self.name = name
+
+class ResourceSpec(object):
+ """Resource specification
+
+ A resource specification specifies a single resource in a library.
+ """
+ implements(interfaces.IResourceSpec)
+
+ def __init__(self, library, relpath, part_of=None, **kw):
+ """Create a resource specification
+
+ library - the library this resource is in
+ relpath - the relative path from the root of the library indicating
+ the actual resource
+ part_of - optionally, a resource is also part of a larger
+ consolidated resource. This can be a path to the
+ larger resource in the same library, or a fully
+ specified ResourceSpec.
+ key word arguments - different paths that represent the same
+ resource in different modes (debug, minified, etc),
+ or alternatively a fully specified ResourceSpec.
+ """
+ self.library = library
+ self.relpath = relpath
+ self.part_of = part_of
+ self.modes = kw
+
+ def ext(self):
+ name, ext = os.path.splitext(self.relpath)
+ return ext
+
+ def mode(self, mode):
+ if mode is None:
+ return self
+ # try getting the alternative
+ try:
+ mode_info = self.modes[mode]
+ if isinstance(mode_info, ResourceSpec):
+ return mode_info
+ return ResourceSpec(self.library, mode_info)
+ except KeyError:
+ # fall back on the default mode if mode not found
+ return self
+
+ def consolidated(self):
+ """Get the resource spec in consolidated form.
+
+ A resource can be part of a larger resource. Returns the
+ resource spec of the consolidated resource, or None if no
+ such larger resource exists.
+ """
+ if self.part_of is None:
+ return None
+ if isinstance(self.part_of, ResourceSpec):
+ return self.part_of
+ return ResourceSpec(self.library, self.part_of)
+
+ def key(self):
+ # XXX has to be a tuple right now as consolidation code depends
+ # on this
+ return self.library.name, self.relpath
+
+ def __repr__(self):
+ return "<Resource '%s' in library '%s'>" % (
+ self.relpath, self.library.name)
+
+class Inclusion(object):
+ implements(interfaces.IInclusion)
+
+ def __init__(self, resources, depends=None):
+ """Create an inclusion
+
+ resources - the list of resource specs that should be on the page
+ when this inclusion is used.
+ depends - one or more inclusions that this inclusion depends on.
+ """
+ self._resources = r = {}
+ for resource in resources:
+ ext_resources = r.setdefault(resource.ext(), [])
+ ext_resources.append(resource)
+ self.depends = depends or []
+
+ def depth(self):
+ depth = 0
+ for depend in self.depends:
+ depend_depth = depend.depth()
+ if depend_depth > depth:
+ depth = depend_depth
+ return depth + 1
+
+ def resources_of_ext(self, ext):
+ resources = []
+ for depend in self.depends:
+ resources.extend(depend.resources_of_ext(ext))
+ resources.extend(self._resources.get(ext, []))
+ return resources
+
+ def need(self):
+ needed = component.getUtility(
+ interfaces.ICurrentNeededInclusions)()
+ needed.need(self)
+
+class NeededInclusions(object):
+ def __init__(self):
+ self._inclusions = []
+
+ def need(self, inclusion):
+ self._inclusions.append(inclusion)
+
+ def _sorted_inclusions(self):
+ return reversed(sorted(self._inclusions, key=lambda i: i.depth()))
+
+ def resources(self, mode=None):
+ resources_of_ext = {}
+ for inclusion in self._sorted_inclusions():
+ for ext in EXTENSIONS:
+ r = resources_of_ext.setdefault(ext, [])
+ r.extend(inclusion.resources_of_ext(ext))
+ resources = []
+ for ext in EXTENSIONS:
+ resources.extend(resources_of_ext.get(ext, []))
+ resources = apply_mode(resources, mode)
+ return remove_duplicates(consolidate(resources))
+
+ def render(self):
+ result = []
+ for resource in self.resources():
+ url = ''
+ result.append(render_resource(resource, url))
+ return '\n'.join(result)
+
+def apply_mode(resources, mode):
+ result = []
+ for resource in resources:
+ result.append(resource.mode(mode))
+ return result
+
+def remove_duplicates(resources):
+ """Given a set of resources, consolidate them so resource only occurs once.
+ """
+ seen = set()
+ result = []
+ for resource in resources:
+ if resource.key() in seen:
+ continue
+ seen.add(resource.key())
+ result.append(resource)
+ return result
+
+def consolidate(resources):
+ consolidated = {}
+ processed = []
+ for resource in resources:
+ c = resource.consolidated()
+ if c is None:
+ processed.append(resource)
+ else:
+ processed.append(c.key())
+ r = consolidated.setdefault(c.key(), [])
+ r.append(resource)
+ result = []
+ for resource in processed:
+ if type(resource) is TupleType:
+ key = resource
+ r = consolidated[key]
+ if len(r) == 1:
+ result.append(r[0])
+ else:
+ result.append(r[0].consolidated())
+ else:
+ result.append(resource)
+ return result
+
+def render_css(url):
+ return ('<link rel="stylesheet" type="text/css" href="%s" />' %
+ url)
+
+def render_kss(url):
+ raise NotImplementedError
+
+def render_js(url):
+ return ('<script type="text/javascript" src="%s"></script>' %
+ url)
+
+resource_renderers = {
+ '.css': render_css,
+ '.kss': render_kss,
+ '.js': render_js,
+ }
+
+def render_resource(resource, url):
+ renderer = resource_renderers.get(resource.ext(), None)
+ if renderer is None:
+ raise UnknownResourceExtension(
+ "Unknown resource extension %s for resource: %s" %
+ (resource.ext(), repr(resource)))
+ return renderer(url)
Added: hurry.resource/trunk/src/hurry/resource/interfaces.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/interfaces.py (rev 0)
+++ hurry.resource/trunk/src/hurry/resource/interfaces.py 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,108 @@
+from zope.interface import Interface, Attribute
+
+class ILibrary(Interface):
+ """A library contains one or more resources.
+
+ A library has a unique name. It is expected to have multiple
+ subclasses of the library class for particular kinds of resource
+ libraries.
+ """
+ name = Attribute("The unique name of the library")
+
+class IResourceSpec(Interface):
+ """Resource specification
+
+ A resource specification specifies a single resource in a library.
+ """
+
+ def ext():
+ """Get the filesystem extension of this resource.
+
+ This is used to determine what kind of resource we are dealing
+ with.
+ """
+
+ def mode(mode):
+ """Get the resource in a different mode.
+
+ mode - the mode (minified, debug, etc) that we want this
+ resource to be in. None is the default mode, and is
+ this resource spec itself.
+
+ An IResourceSpec for that mode is returned.
+
+ If we cannot find a particular mode for a resource, the
+ resource spec is also used.
+ """
+
+ def consolidated():
+ """Get the resource spec in consolidated form.
+
+ A resource can be part of a larger resource, for instance
+ multiple CSS files or .js files concatenated to each
+ other. This is done for performance reasons to cut down on the
+ amount of requests.
+
+ Returns the resource spec of the consolidated resource, or
+ None if no such larger resource is known.
+ """
+
+ def key():
+ """Returns a unique, hashable identifier for the resource.
+ """
+
+class IInclusion(Interface):
+ """A resource inclusion.
+
+ This represents one or more resources that are included on a page
+ together (in the HTML head section). An inclusion may have
+ dependencies on other inclusions.
+ """
+ def depth():
+ """The depth of the inclusion tree.
+
+ This is used to sort the inclusions. If multiple inclusions are
+ required on a page, the ones with the deepest inclusion trees
+ are sorted first.
+ """
+
+ def resources_of_ext(ext):
+ """Retrieve all resources with a certain extension in this inclusion.
+
+ This also goes up to all inclusions that this inclusion depends on.
+ """
+
+ def need():
+ """Express need directly for the current INeededInclusions.
+
+ This is a convenience method to help express inclusions more
+ easily, just do myinclusion.need() to have it be included in
+ the HTML that is currently being rendered.
+ """
+
+class INeededInclusions(Interface):
+ """A collection of inclusions that are needed for page display.
+ """
+
+ def need(inclusion):
+ """Add the inclusion to the list of needed inclusions.
+
+ See also IInclusion.need() for a convenience method.
+ """
+
+ def resources(mode=None):
+ """Give all resources needed.
+
+ mode - optional argument that tries to give inclusions in
+ a particular mode (such as debug, minified, etc)
+ Has no effect if none of the included resources know
+ about that mode.
+ """
+
+class ICurrentNeededInclusions(Interface):
+ def __call__():
+ """Return the current needed inclusions object.
+
+ These can for instance be retrieved from the current request.
+ """
+
Added: hurry.resource/trunk/src/hurry/resource/tests.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/tests.py (rev 0)
+++ hurry.resource/trunk/src/hurry/resource/tests.py 2008-09-22 11:37:31 UTC (rev 91321)
@@ -0,0 +1,14 @@
+import unittest, doctest
+
+def test_suite():
+ globs = {}
+ optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+
+ suite = unittest.TestSuite()
+
+ suite.addTest(doctest.DocFileSuite(
+ 'README.txt',
+ globs=globs,
+ optionflags=optionflags))
+ return suite
+
More information about the Checkins
mailing list