[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