[Checkins] SVN: hurry.custom/ Initial import.

Martijn Faassen faassen at startifact.com
Fri May 1 09:48:46 EDT 2009


Log message for revision 99648:
  Initial import.
  

Changed:
  A   hurry.custom/
  A   hurry.custom/trunk/
  A   hurry.custom/trunk/CHANGES.txt
  A   hurry.custom/trunk/buildout.cfg
  A   hurry.custom/trunk/ideas.txt
  A   hurry.custom/trunk/setup.py
  A   hurry.custom/trunk/src/
  A   hurry.custom/trunk/src/hurry/
  A   hurry.custom/trunk/src/hurry/__init__.py
  A   hurry.custom/trunk/src/hurry/custom/
  A   hurry.custom/trunk/src/hurry/custom/README.txt
  A   hurry.custom/trunk/src/hurry/custom/__init__.py
  A   hurry.custom/trunk/src/hurry/custom/core.py
  A   hurry.custom/trunk/src/hurry/custom/interfaces.py
  A   hurry.custom/trunk/src/hurry/custom/testing.py
  A   hurry.custom/trunk/src/hurry/custom/tests.py

-=-
Added: hurry.custom/trunk/CHANGES.txt
===================================================================
--- hurry.custom/trunk/CHANGES.txt	                        (rev 0)
+++ hurry.custom/trunk/CHANGES.txt	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,7 @@
+Changes
+-------
+
+0.5 (unreleased)
+~~~~~~~~~~~~~~~~
+
+* Initial public release.

Added: hurry.custom/trunk/buildout.cfg
===================================================================
--- hurry.custom/trunk/buildout.cfg	                        (rev 0)
+++ hurry.custom/trunk/buildout.cfg	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,9 @@
+[buildout]
+develop = . 
+parts = test
+newest = false
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = hurry.custom
+defaults = ['--tests-pattern', '^f?tests$', '-v']

Added: hurry.custom/trunk/ideas.txt
===================================================================
--- hurry.custom/trunk/ideas.txt	                        (rev 0)
+++ hurry.custom/trunk/ideas.txt	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,15 @@
+Some ideas
+==========
+
+* each collection is registered under an id, not equal to the path.
+
+* a collection could also have a display title
+
+* templates are identified using collection id + template path
+
+* a collection id could be based on a dotted name + template dir name
+  (up to the system, but provide utility functionality for this?)
+
+* a Grok CustomizableView would automatically register the collection if
+  necessary, and hooks into the template lookup system.
+  (megrok.custom) (megrok.custom.Collection?)

Added: hurry.custom/trunk/setup.py
===================================================================
--- hurry.custom/trunk/setup.py	                        (rev 0)
+++ hurry.custom/trunk/setup.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,41 @@
+from setuptools import setup, find_packages
+import os
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+    read('src', 'hurry', 'custom', 'README.txt')
+    + '\n' +
+    read('CHANGES.txt')
+    + '\n' +
+    'Download\n'
+    '========\n'
+    )
+
+setup(
+    name="hurry.custom",
+    version="0.5dev",
+    description="A framework for allowing customizing templates",
+    long_description=long_description,    
+    classifiers=[
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+    keywords='file size bytes',
+    author='Martijn Faassen, Startifact',
+    author_email='faassen at startifact.com',
+    url='',
+    license='ZPL 2.1',
+    packages=find_packages('src'),
+    package_dir= {'':'src'},
+    namespace_packages=['hurry'],
+    include_package_data=True,
+    zip_safe=False,
+    install_requires=[
+       'setuptools',
+       'zope.component',
+       'zope.interface',
+       'zope.hookable',
+       ],
+    )

Added: hurry.custom/trunk/src/hurry/__init__.py
===================================================================
--- hurry.custom/trunk/src/hurry/__init__.py	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/__init__.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -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.custom/trunk/src/hurry/custom/README.txt
===================================================================
--- hurry.custom/trunk/src/hurry/custom/README.txt	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/custom/README.txt	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,410 @@
+hurry.custom
+============
+
+Introduction
+------------
+
+This package contains an infrastructure and API for the customization
+of templates. The only template languages supported by this system are
+"pure-push" languages which do not call into arbitrary Python code
+while executing. Examples of such languages are json-template
+(supported out of the box) and XSLT. The advantage of such languages
+is that they are reasonably secure to expose through-the-web
+customization without an elaborate security infrastructure.
+
+Let's go through the use cases that this system must support:
+
+* templates exist on the filesystem, and those are used by default.
+
+* templates can be customized. 
+
+* this customization can be stored in another database (ZODB,
+  filesystem, a relational database, etc); this is up to the person
+  integrating ``hurry.custom``.
+
+* update template automatically if it is changed in the database.
+
+* it is possible to retrieve the template source (for display in a UI
+  or for later use within for instance a web-browser for client-side
+  rendering).
+
+* support server-side rendering of templates (producing HTML or an
+  email message or whatever). Input is particular to template language
+  (but should be considered immutable).
+
+* provide (static) input samples (such as JSON or XML files) to make
+  it easier to edit and test templates. These input samples can be
+  added both to the filesystem as well as to the database.
+
+* round-trip support. The customized templates and samples can be
+  retrieved from the database and exported back to the
+  filesystem. This is useful when templates need to be taken back
+  under version control after a period of customization by end users.
+
+The package is agnostic about (these things are pluggable):
+
+* the database used for storing customizations of templates or their
+  samples.
+
+* the particular push-only template language used.
+
+What this package does not do is provide a user interface. It only
+provides the API that lets you construct such user interfaces.
+
+Registering a template language
+-------------------------------
+
+In order to register a new push-only template we need to provide a
+factory that takes the template text (which could be compiled down
+further). Instantiating the factory should result in a callable that
+takes the input data (in whatever format is native to the template
+language). The ``ITemplate`` interface defines such an object::
+
+  >>> from hurry.custom.interfaces import ITemplate
+
+For the purposes of demonstrating the functionality in this package,
+we supply a very simplistic push-only templating language, based on
+template strings as provided by the Python ``string`` module::
+
+  >>> import string
+  >>> from zope.interface import implements
+  >>> class StringTemplate(object):
+  ...    implements(ITemplate)
+  ...    def __init__(self, text):
+  ...        self.source = text
+  ...        self.template = string.Template(text)
+  ...    def __call__(self, input):
+  ...        return self.template.substitute(input)
+
+Let's demonstrate it. To render the template, simply call it with the
+data as an argument::
+
+  >>> template = StringTemplate('Hello $thing')
+  >>> template({'thing': 'world'})
+  'Hello world'
+
+The template class defines a template language. Let's register the
+template language so the system is aware of it and treats ``.st`` files
+on the filesystem as a string template::
+
+  >>> from hurry import custom
+  >>> custom.register_language(StringTemplate, extension='.st')
+
+Loading a template from the filesystem
+--------------------------------------
+
+``hurry.custom`` assumes that any templates that can be customized
+reside on the filesystem primarily and are shipped along with an
+application's source code. They form *collections*. A collection is
+simply a directory (with possible sub-directories) that contains
+templates.
+
+Let's create a collection of templates on the filesystem::
+
+  >>> import tempfile, os
+  >>> templates_path = tempfile.mkdtemp(prefix='hurry.custom')
+
+We create a single template, ``test1.st`` for now::
+
+  >>> test1_path = os.path.join(templates_path, 'test1.st')
+  >>> f = open(test1_path, 'w')
+  >>> f.write('Hello $thing')
+  >>> f.close()
+
+In order for the system to work, we need to register this collection
+of templates on the filesystem. We need to supply a globally unique
+collection id, the templates path, and (optionally) a title::
+
+  >>> custom.register_collection(id='templates', path=templates_path)
+
+We can now look up the template in this collection::
+
+  >>> template = custom.lookup('templates', 'test1.st')
+
+We got our proper template::
+
+  >>> template.source
+  'Hello $thing'
+
+  >>> template({'thing': 'world'})
+  'Hello world'
+
+The underlying template will not be reloaded unless it is changed on
+the filesystem::
+
+  >>> orig = template.template
+
+When we trigger a potential reload nothing happens - the template did
+not change on the filesystem::
+
+  >>> template.source
+  'Hello $thing'
+  >>> template.template is orig
+  True
+  
+It will however automatically reload the template when it has changed
+on the filesystem. We will demonstrate that by modifying the file::
+
+  >>> f = open(test1_path, 'w')
+  >>> f.write('Bye $thing')
+  >>> f.close()
+
+Unfortunately this won't work in the tests as the modification time of
+files has a second-granularity on some platforms, way too long to
+delay the tests for. We will therefore manually update the last updated
+time as a hack::
+
+  >>> template._last_updated -= 1
+
+Now the template will have changed::
+
+  >>> template.source
+  'Bye $thing'
+  
+  >>> template({'thing': 'world'})
+  'Bye world'
+
+Customization database
+----------------------
+
+Let's now register a customization database for our collection, in a
+particular site. This means in such a site, the new customized
+template database will be used (with a fallback on the original one if
+no customization can be found).
+
+Let's create a site first::
+
+  >>> site1 = DummySite(id=1)
+
+We register a customization database for our collection named
+``templates``. For the purposes of testing we will use an in-memory
+database::
+
+  >>> from hurry.custom.interfaces import ITemplateDatabase
+  >>> mem_db = custom.InMemoryTemplateDatabase('templates', 'Templates')
+  >>> sm1 = site1.getSiteManager()
+  >>> sm1.registerUtility(mem_db, provided=ITemplateDatabase, 
+  ...   name='templates')
+
+We go into this site::
+
+  >>> setSite(site1)
+
+We haven't placed any customization in the customization database
+yet, so we'll see the same thing as before when we look up the
+template::
+
+  >>> template = custom.lookup('templates', 'test1.st')
+  >>> template({'thing': "universe"})
+  'Bye universe'
+
+Customization of a template
+---------------------------
+
+Now that we have a locally set up customization database, we can
+customize the ``test1.st`` template. 
+
+In this customization we change 'Bye' to 'Goodbye'. For now, ``hurry.custom``
+does not yet specify a database-agnostic update mechanism, so 
+we will use the update mechanism that is particular to the in-memory
+database::
+
+  >>> source = template.source
+  >>> source = source.replace('Bye', 'Goodbye')
+  >>> mem_db.update('test1.st', source)
+
+Another database might have an entirely different storage and update
+mechanism; this is just an example. All you need to do to hook in your
+own database is to implement the ``ITemplateDatabase`` interface and
+register it (either globally or locally in a site).
+
+Let's see whether we get the customized template now::
+
+  >>> template = custom.lookup('templates', 'test1.st')
+  >>> template({'thing': 'planet'})
+  'Goodbye planet'
+
+It is sometimes useful to be able to retrieve the original version of
+the template, before customization::
+
+  >>> template.original_source
+  'Bye $thing'
+
+This could be used to implement a "revert" functionality in a
+customization UI, for instance.
+
+Checking which template languages are recognized
+------------------------------------------------
+
+We can check which template languages are recognized::
+
+  >>> languages = custom.recognized_languages()
+  >>> sorted(languages)
+  [(u'.st', <class 'StringTemplate'>)]
+
+When we register another language::
+
+  >>> class StringTemplate2(StringTemplate):
+  ...   pass
+  >>> custom.register_language(StringTemplate2, extension='.st2')
+
+It will show up too::
+
+  >>> languages = custom.recognized_languages()
+  >>> sorted(languages)
+  [(u'.st', <class 'StringTemplate'>), (u'.st2', <class 'StringTemplate2'>)]
+
+Retrieving which templates can be customized
+--------------------------------------------
+
+For the filesystem-level templates it is possible to get a data
+structure that indicates which templates can be customized. This is
+useful when constructing a UI. This data structure is designed to be
+easily useful as JSON so that a client-side UI can be constructed.
+
+Let's retrieve the customization database for our collection::
+
+  >>> l = custom.structure('templates')
+  >>> from pprint import pprint
+  >>> pprint(l)
+  [{'extension': '.st',
+    'name': 'test1',
+    'path': 'test1.st',
+    'template': 'test1.st'}]
+
+Samples
+-------
+
+In a customization user interface it is useful to be able to test the
+template. Sometimes this can be done with live data coming from the
+software, but in other cases it is more convenient to try it on some
+representative sample data. This sample data needs to be in the format
+as expected as the argument when calling the template.
+
+Just like a template language is stored as plain text on the
+filesystem, sample data can also be stored as plain text on the file
+system. The format of this plain text is its data language. Examples
+of data languages are JSON and XML.
+
+For the purposes of demonstration, we'll define a simle data language
+that can turn into a dictionary a data file with key value pairs like
+this::
+
+  >>> data = """\
+  ... a: b
+  ... c: d
+  ... e: f
+  ... """
+
+Now we define a function that can parse this data into a dictionary::
+
+  >>> def parse_dict_data(data):
+  ...    result = {}
+  ...    for line in data.splitlines():
+  ...        key, value = line.split(':')
+  ...        key = key.strip()
+  ...        value = value.strip()
+  ...        result[key] = value
+  ...    return result
+  >>> d = parse_dict_data(data)
+  >>> sorted(d.items())
+  [('a', 'b'), ('c', 'd'), ('e', 'f')]
+
+The idea is that we can ask a particular template for those sample inputs
+that are available for it. Let's for instance check for sample inputs 
+available for ``test1.st``::
+
+  >>> template.samples()
+  {}
+
+There's nothing yet.
+
+In order to get samples to work, we first need to register the data
+language::
+
+  >>> custom.register_data_language(parse_dict_data, '.d')
+
+Files with the extension ``.d`` can now be recognized as containing
+sample data.
+
+We still need to tell the system that StringTemplate templates in
+particular can be expected to find sample data with this extension. In
+order to express this, we need to register the StringTemplate language
+again with an extra argument that indicates this (``sample_extension``)::
+
+  >>> custom.register_language(StringTemplate,
+  ...    extension='.st', sample_extension='.d')
+
+Now we can actually look for samples. Of course there still aren't
+any as we haven't created any ``.d`` files yet::
+
+  >>> template.samples()
+  {}
+
+We need a pattern to associate a sample data file with a template
+file.  The convention used is that a sample data file is in the same
+directory as the template file, and starts with the name of the
+template followed by a dash (``-``). Following the dash should be the
+name of the sample itself. Finally, the extension should be the sample
+extension. Here we create a sample file for the ``test1.st``
+template::
+
+  >>> test1_path = os.path.join(templates_path, 'test1-sample1.d')
+  >>> f = open(test1_path, 'w')
+  >>> f.write('thing: galaxy')
+  >>> f.close()
+
+Now when we ask for the samples available for our ``test1`` template,
+we should see ``sample1``::
+
+  >>> r = template.samples()
+  >>> r
+  {'sample1': {'thing': 'galaxy'}}
+
+By definition, we can use the sample data for a template and pass it
+to the template itself::
+
+  >>> template(r['sample1'])
+  'Goodbye galaxy'
+
+Error handling
+--------------
+
+Let's try to look up a template in a collection that doesn't exist. We
+get a message that the template database could not be found::
+
+  >>> custom.lookup('nonexistent', 'dummy.st')
+  Traceback (most recent call last):
+    ...
+  ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplateDatabase>, 'nonexistent')
+
+Let's look up a non-existent template in an existing database. We get
+the lookup error of the deepest database, which is assumed to be the
+filesystem::
+
+  >>> template = custom.lookup('templates', 'nonexisting.st')
+  Traceback (most recent call last):
+    ...
+  IOError: [Errno 2] No such file or directory: '.../nonexisting.st'
+
+Let's look up a template with an unrecognized extension::
+
+  >>> template = custom.lookup('templates', 'dummy.unrecognized')
+  Traceback (most recent call last):
+    ...
+  IOError: [Errno 2] No such file or directory: '.../dummy.unrecognized'
+
+This of course happens because ``dummy.unrecognized`` doesn't exist. Let's
+make it exist::
+
+  >>> unrecognized = os.path.join(templates_path, 'dummy.unrecognized')
+  >>> f = open(unrecognized, 'w')
+  >>> f.write('Some weird template language')
+  >>> f.close()
+
+Now let's look at it again::
+
+  >>> template = custom.lookup('templates', 'dummy.unrecognized')
+  Traceback (most recent call last):
+    ...
+  ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplate>, '.unrecognized')

Added: hurry.custom/trunk/src/hurry/custom/__init__.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/__init__.py	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/custom/__init__.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,16 @@
+from zope.interface import moduleProvides
+
+from hurry.custom.core import (lookup,
+                               structure,
+                               register_language,
+                               register_data_language,
+                               register_collection,
+                               recognized_languages)
+
+from hurry.custom.core import (FilesystemTemplateDatabase,
+                               InMemoryTemplateDatabase)
+
+from hurry.custom.interfaces import IHurryCustomAPI
+
+moduleProvides(IHurryCustomAPI)
+__all__ = list(IHurryCustomAPI)

Added: hurry.custom/trunk/src/hurry/custom/core.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/core.py	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/custom/core.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,251 @@
+import os, time, glob
+from datetime import datetime
+from zope.interface import implements
+from zope import component
+from hurry.custom.interfaces import (
+    ITemplate, IManagedTemplate, ITemplateDatabase, IDataLanguage,
+    ISampleExtension)
+
+def register_language(template_class, extension, sample_extension=None):
+    component.provideUtility(template_class,
+                             provides=ITemplate,
+                             name=extension)
+    if sample_extension is not None:
+        component.provideUtility(sample_extension,
+                                 provides=ISampleExtension,
+                                 name=extension)
+
+def register_data_language(parse_func, extension):
+    component.provideUtility(parse_func,
+                             provides=IDataLanguage,
+                             name=extension)
+
+def recognized_languages():
+    return component.getUtilitiesFor(ITemplate)
+
+def register_collection(id, path, title=None):
+    if title is None:
+        title = id
+    db = FilesystemTemplateDatabase(id=id, path=path, title=title)
+    component.provideUtility(db,
+                             provides=ITemplateDatabase,
+                             name=id)
+
+def lookup(id, template_path):
+    db = component.getUtility(ITemplateDatabase, name=id)
+    while db.get_source(template_path) is None:
+        db = getNextUtility(db, ITemplateDatabase, name=id) 
+    dummy, ext = os.path.splitext(template_path)
+    template_class = component.getUtility(ITemplate, name=ext)
+    return ManagedTemplate(template_class, db, template_path)
+
+def sample_datas(id, template_path):
+    db = get_filesystem_database(id)
+
+def structure(id):
+    extensions = set([extension for
+                      (extension, language) in recognized_languages()])
+    db = _get_root_database(id)
+    return _get_structure_helper(db.path, db.path, extensions)
+
+class ManagedTemplate(object):
+    implements(IManagedTemplate)
+    
+    def __init__(self, template_class, db, template_path):
+        self.template_class = template_class
+        self.db = db
+        self.template_path = template_path
+        self.load()
+        self._last_updated = 0
+        
+    def load(self):
+        self.template = self.template_class(
+            self.db.get_source(self.template_path))
+
+    def check(self):
+        mtime = self.db.get_modification_time(self.template_path)
+        if mtime > self._last_updated:
+            self._last_updated = mtime
+            self.load()
+    
+    @property
+    def source(self):
+        self.check()
+        return self.template.source
+
+    @property
+    def original_source(self):
+        db = queryNextUtility(self.db, ITemplateDatabase,
+                              name=self.db.id,
+                              default=self.db)
+        return db.get_source(self.template_path)
+
+    def __call__(self, input):
+        self.check()
+        return self.template(input)
+
+    def samples(self):
+        db = _get_root_database(self.db.id)
+        return db.get_samples(self.template_path)
+        
+class FilesystemTemplateDatabase(object):
+    implements(ITemplateDatabase)
+    
+    def __init__(self, id, path, title):
+        self.id = id
+        self.path = path
+        self.title = title
+        
+    def get_source(self, template_id):
+        template_path = os.path.join(self.path, template_id)
+        f = open(template_path, 'r')
+        result = f.read()
+        f.close()
+        return result
+    
+    def get_modification_time(self, template_id):
+        template_path = os.path.join(self.path, template_id)
+        return os.path.getmtime(template_path)
+
+    def get_samples(self, template_id):
+        template_path = os.path.join(self.path, template_id)
+        template_dir = os.path.dirname(template_path)
+        template_name, extension = os.path.splitext(template_id)
+        result = {}
+        sample_extension = component.queryUtility(ISampleExtension,
+                                                  name=extension,
+                                                  default=None)
+        if sample_extension is None:
+            return result
+        parse = component.getUtility(IDataLanguage, name=sample_extension)
+        for path in glob.glob(
+            os.path.join(template_dir,
+                         template_name + '-*' + sample_extension)):
+            filename = os.path.basename(path)
+            name, dummy = os.path.splitext(filename)
+            # +1 to adjust for -
+            name = name[len(template_name) + 1:]        
+            f = open(path, 'rb')
+            data = f.read()
+            f.close()
+            result[name] = parse(data)
+        return result
+
+class InMemoryTemplateSource(object):
+    def __init__(self, source):
+        self.source = source
+        self.last_updated = time.time()
+
+class InMemoryTemplateDatabase(object):
+    implements(ITemplateDatabase)
+    
+    def __init__(self, id, title):
+        self.id = id
+        self.title = title
+        self._templates = {}
+
+    def get_source(self, template_id):
+        try:
+            return self._templates[template_id].source
+        except KeyError:
+            return None
+        
+    def get_modification_time(self, template_id):
+        try:
+            return self._templates[template_id].last_updated
+        except KeyError:
+            return None
+
+    def get_samples(self, template_id):
+        return {}
+
+    def update(self, template_id, source):
+        self._templates[template_id] = InMemoryTemplateSource(source)
+
+def _get_structure_helper(path, collection_path, extensions):
+    entries = os.listdir(path)
+    result = []
+    for entry in entries:
+        entry_path = os.path.join(path, entry)
+        if os.path.isdir(entry_path):
+            info = {
+                'directory': entry,
+                'entries': _get_structure_helper(entry_path,
+                                                 collection_path, extensions),
+                'path': relpath(entry_path, collection_path),
+                }
+            result.append(info)
+        else:
+            name, ext = os.path.splitext(entry)
+            if ext not in extensions:
+                continue
+            info = {
+                'template': entry,
+                'name': name,
+                'extension': ext,
+                'path': relpath(entry_path, collection_path),
+                }
+            result.append(info)
+    return result
+
+def _get_root_database(id):
+    # assume root database is always a FilesystemTemplateDatabase
+    db = component.getUtility(ITemplateDatabase, name=id)
+    while not isinstance(db, FilesystemTemplateDatabase):
+        db = getNextUtility(db, ITemplateDatabase, name=id)
+    return db
+
+# XXX copied from zope.app.component to avoid dependency on it
+# note that newer versions of zope.component have this, so
+# when the target app depends on that we can switch and
+# eliminate this code
+
+from zope.component import getSiteManager
+
+_marker = object()
+
+def queryNextUtility(context, interface, name='', default=None):
+    """Query for the next available utility.
+
+    Find the next available utility providing `interface` and having the
+    specified name. If no utility was found, return the specified `default`
+    value.
+    """
+    sm = getSiteManager(context)
+    bases = sm.__bases__
+    for base in bases:
+        util = base.queryUtility(interface, name, _marker)
+        if util is not _marker:
+            return util
+    return default
+
+def getNextUtility(context, interface, name=''):
+    """Get the next available utility.
+
+    If no utility was found, a `ComponentLookupError` is raised.
+    """
+    util = queryNextUtility(context, interface, name, _marker)
+    if util is _marker:
+        raise zope.component.interfaces.ComponentLookupError(
+              "No more utilities for %s, '%s' have been found." % (
+                  interface, name))
+    return util
+
+# XXX this code comes from Python 2.6 - when switching to this
+# python version we can import it from os.path and get rid of this code
+from os.path import commonprefix, abspath, join, sep, pardir
+
+def relpath(path, start):
+    """Return a relative version of a path"""
+
+    if not path:
+        raise ValueError("no path specified")
+
+    start_list = abspath(start).split(sep)
+    path_list = abspath(path).split(sep)
+
+    # Work out how much of the filepath is shared by start and path.
+    i = len(commonprefix([start_list, path_list]))
+
+    rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
+    return join(*rel_list)

Added: hurry.custom/trunk/src/hurry/custom/interfaces.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/interfaces.py	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/custom/interfaces.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,141 @@
+from zope.interface import Interface, Attribute
+
+class IHurryCustomAPI(Interface):
+    """API for hurry.custom.
+    """
+    def register_language(template_class):
+        """Register a template language with the system.
+
+        The template language is a class which implements ITemplate
+        """
+
+    def register_collection(id, path, title=None):
+        """Register a collection of templates on the filesystem with the system.
+
+        id - globally unique collection id (used to look up the collection)
+        path - the path of the collection on the filesystem
+        title - optionally, human-readable title for collection.
+                By default the 'id' will be used.
+        """
+    
+    def lookup(id, template_path):
+        """Look up template.
+        
+        id: the id for the collection
+        template_path: the relative path (or filename) of the template
+                       itself, under the path of the collection
+        """
+
+    def structure(id):
+        """Get a list with all the templates in this collection.
+
+        id - the collection id
+        
+        All recognized template extensions are reported; unrecognized
+        extensions are ignored. Subdirectories are also reported.
+    
+        Returned is a list of all entries.
+    
+        List entries for templates look like this:
+        
+        { 'template': 'template1.st',
+        'name': 'template1',
+        'extension': '.st'
+        'path': 'template1.st',
+        }
+
+        template: the name of the template as it is in its immediate
+                  directory.
+        name: the name of the template without extension
+        extension: the template extension
+        path: the relative path to the extension from the collection_path
+    
+        List entries for sub directories look like this:
+    
+        { 'directory': 'subdirname',
+        'entries': [ ... ],
+        'path': 'subdirname',
+        }
+
+        directory: the name of the directory
+        entries: the entries of the subdirectory, in a list
+        path: the relative path to this directory from the collection_path
+        """
+
+    def recognized_languages():
+        """Get an iterable with the recognized languages.
+        
+        The items are name-value pairs (language extension, template class).
+        """
+    
+class ITemplate(Interface):
+    source = Attribute("The source text of the template.")
+
+    def __call__(input):
+        """Render the template given input.
+
+        input - opaque template-language native data structure.
+        """
+        
+class IDataLanguage(Interface):
+    def __call__(data):
+        """Parse data into data structure that can be passed to ITemplate()"""
+
+class ISampleExtension(Interface):
+    """Marker interface used to register the extension of the sample language.
+    """
+
+class IManagedTemplate(ITemplate):
+
+    template = Attribute("The real template object being managed.")
+
+    original_source = Attribute("The original source of the template, "
+                                "before customization.")
+
+    def check():
+        """Update the template if it has changed.
+        """
+
+    def load():
+        """Load the template from the filesystem.
+        """
+
+    def samples():
+        """Get samples.
+
+        Returns a dictionary with sample inputs.
+
+        keys are the unique ids for the sample inputs.
+        values are the actual template-language native data structures.
+        """
+
+class ITemplateDatabase(Interface):
+    """A per-collection template database.
+    """
+    id = Attribute("The id of the collection")
+    title = Attribute("The title of the collection")
+    
+    def get_source(template_id):
+        """Get the source of a given template.
+
+        Returns None if the source cannot be loaded.
+        """
+
+    def get_modification_time(template_id):
+        """Get the time at which a template was last updated.
+
+        Time must be in number of seconds since epoch (preferably with
+        sub-second accuracy, but this is database dependent).
+
+        Returns None if the time cannot be retrieved.
+        """
+        
+    def get_samples(template_id):
+        """Get samples for a given template.
+
+        Returns a dictionary with sample inputs.
+
+        keys are the unique ids for the sample inputs.
+        values are the actual template-language native data structures.
+        """
+

Added: hurry.custom/trunk/src/hurry/custom/testing.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/testing.py	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/custom/testing.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,50 @@
+from zope.testing.cleanup import addCleanUp
+from zope import component
+from zope.component import registry
+
+# a very simple implementation of setSite and getSite so we don't have
+# to rely on zope.app.component just for our tests
+_site = None
+
+class DummySite(object):
+    def __init__(self, id):
+        self.id = id
+        self._sm = SiteManager()
+        
+    def getSiteManager(self):
+        return self._sm
+
+class SiteManager(registry.Components):
+    def __init__(self):
+        super(SiteManager, self).__init__()
+        self.__bases__ = (component.getGlobalSiteManager(),)
+
+def setSite(site=None):
+    global _site
+    _site = site
+
+def getSite():
+    return _site
+
+def adapter_hook(interface, object, name='', default=None):
+    try:
+        return getSiteManager().adapters.adapter_hook(
+            interface, object, name, default)
+    except component.interfaces.ComponentLookupError:
+        return default
+
+def getSiteManager(context=None):
+    if _site is not None:
+        return _site.getSiteManager()
+    return component.getGlobalSiteManager()
+
+def setHooks():
+    component.adapter_hook.sethook(adapter_hook)
+    component.getSiteManager.sethook(getSiteManager)
+
+def resetHooks():
+    component.adapter_hook.reset()
+    component.getSiteManager.reset()
+
+# make sure hooks get cleaned up after tests are run
+addCleanUp(resetHooks)

Added: hurry.custom/trunk/src/hurry/custom/tests.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/tests.py	                        (rev 0)
+++ hurry.custom/trunk/src/hurry/custom/tests.py	2009-05-01 13:48:46 UTC (rev 99648)
@@ -0,0 +1,35 @@
+import unittest
+import doctest
+
+from zope.testing import cleanup
+import zope.component.eventtesting
+
+from hurry.custom.testing import setHooks, setSite, getSite, DummySite
+
+def setUpReadMe(test):
+    # set up special local component architecture
+    setHooks()
+    # set up event handling
+    zope.component.eventtesting.setUp(test)
+
+def tearDownReadMe(test):
+    # clean up Zope
+    cleanup.cleanUp()
+
+def test_suite():
+    optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+    globs = {
+        'DummySite': DummySite,
+        'setSite': setSite,
+        'getSite': getSite,
+        }
+    
+    suite = unittest.TestSuite()
+    
+    suite.addTest(doctest.DocFileSuite(
+        'README.txt',
+        optionflags=optionflags,
+        setUp=setUpReadMe,
+        tearDown=tearDownReadMe,
+        globs=globs))
+    return suite



More information about the Checkins mailing list