[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