[Checkins] SVN: hurry.custom/trunk/ Quite a few improvements:
Martijn Faassen
faassen at startifact.com
Tue Jun 9 13:46:50 EDT 2009
Log message for revision 100761:
Quite a few improvements:
* notion of CompileError and RenderError
* top-level render function in the API that falls back on
original templates in case of errors.
* fixes in interface documentation
* remove ``original_source`` and ``samples`` method from
the IManagedTemplate interface; these are better accessed using
the ``ITemplateDatabase`` API.
Changed:
U hurry.custom/trunk/CHANGES.txt
U hurry.custom/trunk/src/hurry/custom/README.txt
U hurry.custom/trunk/src/hurry/custom/__init__.py
U hurry.custom/trunk/src/hurry/custom/core.py
U hurry.custom/trunk/src/hurry/custom/interfaces.py
U hurry.custom/trunk/src/hurry/custom/jsont.py
U hurry.custom/trunk/src/hurry/custom/jsontemplate.txt
-=-
Modified: hurry.custom/trunk/CHANGES.txt
===================================================================
--- hurry.custom/trunk/CHANGES.txt 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/CHANGES.txt 2009-06-09 17:46:50 UTC (rev 100761)
@@ -4,12 +4,27 @@
0.6 (unreleased)
~~~~~~~~~~~~~~~~
-* Introduce the notion of a ``BrokenTemplate``. If a customized
- template is broken, the system falls back to using the original
- version of the template. A template is broken if it raises
- the ``hurry.custom.interfaces.BrokenTemplate`` exception when it is
- created.
+* Introduce the notion of ``CompileError`` and ``RenderError``. A
+ ``CompileError`` should be raised by a template if the template
+ cannot be parsed or compiled. A ``RenderError`` should be raised if
+ there is any run-time error during template rendering.
+* Introduce ``render`` in the API and de-emphasize the use of ``lookup``.
+ Normally templates are rendered by calling ``render``.
+
+* When a template is looked up and there is a ``CompileError`` during
+ its creation, fall back on original template.
+
+* When a template is rendered using the top-level ``render`` function
+ and there is a ``RenderError`` during the rendering process, fall
+ back on the original template.
+
+* Remove ``original_source`` and ``samples`` methods from
+ ``IManagedTemplate`` interface. These are better handled by directly
+ using the ``ITemplateDatabase`` API.
+
+* Some fixes in the interfaces, bringing them more inline with the code.
+
0.5 (2009-05-22)
~~~~~~~~~~~~~~~~
Modified: hurry.custom/trunk/src/hurry/custom/README.txt
===================================================================
--- hurry.custom/trunk/src/hurry/custom/README.txt 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/src/hurry/custom/README.txt 2009-06-09 17:46:50 UTC (rev 100761)
@@ -51,8 +51,8 @@
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
--------------------------------
+Creating and 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
@@ -60,7 +60,7 @@
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, BrokenTemplate
+ >>> from hurry.custom.interfaces import ITemplate, CompileError, RenderError
For the purposes of demonstrating the functionality in this package,
we supply a very simplistic push-only templating language, based on
@@ -72,11 +72,14 @@
... implements(ITemplate)
... def __init__(self, text):
... if '&' in text:
- ... raise BrokenTemplate("& in template!")
+ ... raise CompileError("& in template!")
... self.source = text
... self.template = string.Template(text)
... def __call__(self, input):
- ... return self.template.substitute(input)
+ ... try:
+ ... return self.template.substitute(input)
+ ... except KeyError, e:
+ ... raise RenderError(unicode(e))
Let's demonstrate it. To render the template, simply call it with the
data as an argument::
@@ -86,7 +89,7 @@
'Hello world'
Note we have put some special logic in the ``__init__`` that triggers a
-``BrokenTemplate`` error if the string ``&`` is found in the
+``CompileError`` error if the string ``&`` is found in the
template. This is so we can easily demonstrate templates that are
broken - treat a template with ``&`` as a template with a syntax
(compilation) error. Let's try it::
@@ -94,8 +97,18 @@
>>> template = StringTemplate('Hello & bye')
Traceback (most recent call last):
...
- BrokenTemplate: & in template!
+ CompileError: & in template!
+We have also made sure we catch a possible runtime error (a
+``KeyError`` when a key is missing in the input dictionary in this
+case) and raise this as a ``RenderError``::
+
+ >>> template = StringTemplate('Hello $thing')
+ >>> template({'thang': 'world'})
+ Traceback (most recent call last):
+ ...
+ RenderError: 'thing'
+
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::
@@ -137,27 +150,34 @@
>>> custom.register_collection(id='templates', path=templates_path)
-We can now look up the template in this collection::
+We can now render the template::
+ >>> custom.render('templates', 'test1.st', {'thing': 'world'})
+ u'Hello world'
+
+We'll try another template::
+
+ >>> custom.render('templates', 'test2.st', {'thing': 'stars'})
+ u"It's full of stars"
+
+We can also look up the template object::
+
>>> template = custom.lookup('templates', 'test1.st')
We got our proper template::
- >>> template.source
- u'Hello $thing'
-
-As we can see the source text of the template was interpreted as a
-UTF-8 string. The template source should always be in unicode format
-(or in plain ASCII).
-
>>> template({'thing': 'world'})
u'Hello world'
-We'll try another template::
+The templat also has a ``source`` attribute::
- >>> custom.lookup('templates', 'test2.st')({'thing': 'stars'})
- u"It's full of stars"
+ >>> template.source
+ u'Hello $thing'
+The source text of the template was interpreted as a UTF-8 string. The
+template source should always be in unicode format (or in plain
+ASCII).
+
The underlying template will not be reloaded unless it is changed on
the filesystem::
@@ -229,8 +249,7 @@
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"})
+ >>> custom.render('templates', 'test1.st', {'thing': "universe"})
u'Bye universe'
Customization of a template
@@ -241,7 +260,7 @@
In this customization we change 'Bye' to 'Goodbye'::
- >>> source = template.source
+ >>> source = root_db.get_source('test1.st')
>>> source = source.replace('Bye', 'Goodbye')
We now need to update the database so that it has this customized
@@ -267,38 +286,39 @@
Let's see whether we get the customized template now::
- >>> template = custom.lookup('templates', 'test1.st')
- >>> template({'thing': 'planet'})
+ >>> custom.render('templates', 'test1.st', {'thing': 'planet'})
u'Goodbye planet'
-It is sometimes useful to be able to retrieve the original version of
-the template, before customization::
-
- >>> template.original_source
- u'Bye $thing'
-
-This could be used to implement a "revert" functionality in a
-customization UI, for instance.
-
Broken custom template
----------------------
-If a custom template is broken, the system falls back on the
+If a custom template cannot be compiled, the system falls back on the
filesystem template instead. We construct a broken custom template by
adding ``&`` to it::
- >>> template2 = custom.lookup('templates', 'test2.st')
- >>> source = template2.source
- >>> source = source.replace('full of', 'filled with &')
+ >>> original_source = root_db.get_source('test2.st')
+ >>> source = original_source.replace('full of', 'filled with &')
>>> mem_db.update('test2.st', source)
We try to render this template, but instead we'll see the original
template::
- >>> template2 = custom.lookup('templates', 'test2.st')
- >>> template2({'thing': 'planets'})
+ >>> custom.render('templates', 'test2.st', {'thing': 'planets'})
u"It's full of planets"
+It could also be the case that the custom template can be compiled but
+instead cannot be rendered. Let's construct one that expects ``thang``
+instead of ``thing``::
+
+ >>> source = original_source.replace('$thing', '$thang')
+ >>> mem_db.update('test2.st', source)
+
+When rendering the system will notice the RenderError and fall back on
+the original uncustomized template for rendering::
+
+ >>> custom.render('templates', 'test2.st', {'thing': 'planets'})
+ u"It's full of planets"
+
Checking which template languages are recognized
------------------------------------------------
@@ -384,7 +404,7 @@
that are available for it. Let's for instance check for sample inputs
available for ``test1.st``::
- >>> template.samples()
+ >>> root_db.get_samples('test1.st')
{}
There's nothing yet.
@@ -408,7 +428,7 @@
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()
+ >>> root_db.get_samples('test1.st')
{}
We need a pattern to associate a sample data file with a template
@@ -427,45 +447,70 @@
Now when we ask for the samples available for our ``test1`` template,
we should see ``sample1``::
- >>> r = template.samples()
+ >>> r = root_db.get_samples('test1.st')
>>> r
{'sample1': {'thing': 'galaxy'}}
By definition, we can use the sample data for a template and pass it
to the template itself::
+ >>> template = custom.lookup('templates', 'test1.st')
>>> template(r['sample1'])
u'Goodbye galaxy'
+Testing a template
+------------------
+
+In a user interface it can be useful to be able to test whether the
+template compiles and renders. ``hurry.custom`` therefore implements a
+``check`` function that does so. This function raises an error
+(``CompileError`` or ``RenderError``), and passes silently if there is no
+problem.
+
+Let's first try it with a broken template::
+
+ >>> custom.check('templates', 'test1.st', 'foo & bar')
+ Traceback (most recent call last):
+ ...
+ CompileError: & in template!
+
+We'll now try it with a template that does compile but doesn't work
+with ``sample1``, as no ``something`` is supplied::
+
+ >>> custom.check('templates', 'test1.st', 'hello $something')
+ Traceback (most recent call last):
+ ...
+ RenderError: 'something'
+
Error handling
--------------
-Let's try to look up a template in a collection that doesn't exist. We
+Let's try to render 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')
+ >>> custom.render('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
+Let's render 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')
+ >>> custom.render('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::
+Let's render a template with an unrecognized extension::
- >>> template = custom.lookup('templates', 'dummy.unrecognized')
+ >>> custom.render('templates', 'dummy.unrecognized', {})
Traceback (most recent call last):
...
- IOError: [Errno 2] No such file or directory: '.../dummy.unrecognized'
+ ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplate>, '.unrecognized')
-This of course happens because ``dummy.unrecognized`` doesn't exist. Let's
-make it exist::
+The template language ``.unrecognized`` could not be found. Let's make the
+file exist; we should get the same result::
>>> unrecognized = os.path.join(templates_path, 'dummy.unrecognized')
>>> f = open(unrecognized, 'w')
@@ -474,7 +519,7 @@
Now let's look at it again::
- >>> template = custom.lookup('templates', 'dummy.unrecognized')
+ >>> template = custom.render('templates', 'dummy.unrecognized', {})
Traceback (most recent call last):
...
ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplate>, '.unrecognized')
Modified: hurry.custom/trunk/src/hurry/custom/__init__.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/__init__.py 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/src/hurry/custom/__init__.py 2009-06-09 17:46:50 UTC (rev 100761)
@@ -1,6 +1,8 @@
from zope.interface import moduleProvides
-from hurry.custom.core import (lookup,
+from hurry.custom.core import (render,
+ lookup,
+ check,
structure,
register_language,
register_data_language,
Modified: hurry.custom/trunk/src/hurry/custom/core.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/core.py 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/src/hurry/custom/core.py 2009-06-09 17:46:50 UTC (rev 100761)
@@ -2,10 +2,9 @@
from datetime import datetime
from zope.interface import implements
from zope import component
-from hurry.custom.interfaces import NotSupported
from hurry.custom.interfaces import (
ITemplate, IManagedTemplate, ITemplateDatabase, IDataLanguage,
- ISampleExtension, BrokenTemplate)
+ ISampleExtension, CompileError, RenderError, NotSupported)
def register_language(template_class, extension, sample_extension=None):
component.provideUtility(template_class,
@@ -32,23 +31,45 @@
provides=ITemplateDatabase,
name=id)
-def lookup(id, template_path):
- db = component.getUtility(ITemplateDatabase, name=id)
+def render(id, template_path, input):
+ template = lookup(id, template_path)
while True:
+ try:
+ return template(input)
+ except RenderError:
+ template = lookup(
+ id, template_path,
+ db=getNextUtility(template.db, ITemplateDatabase, name=id))
+
+def lookup(id, template_path, db=None):
+ dummy, ext = os.path.splitext(template_path)
+ template_class = component.getUtility(ITemplate, name=ext)
+
+ db = db or component.getUtility(ITemplateDatabase, name=id)
+
+ while True:
source = db.get_source(template_path)
- if source is None:
- db = getNextUtility(db, ITemplateDatabase, name=id)
- continue
- dummy, ext = os.path.splitext(template_path)
- template_class = component.getUtility(ITemplate, name=ext)
+ if source is not None:
+ try:
+ return ManagedTemplate(template_class, db, template_path)
+ except CompileError:
+ pass
+ db = getNextUtility(db, ITemplateDatabase, name=id)
+
+def check(id, template_path, source):
+ dummy, ext = os.path.splitext(template_path)
+ template_class = component.getUtility(ITemplate, name=ext)
+ # can raise CompileError
+ template = template_class(source)
+ db = _get_root_database(id)
+ samples = db.get_samples(template_path)
+ for key, value in samples.items():
try:
- return ManagedTemplate(template_class, db, template_path)
- except BrokenTemplate:
- db = getNextUtility(db, ITemplateDatabase, name=id)
- continue
-
-def sample_datas(id, template_path):
- db = get_filesystem_database(id)
+ template(value)
+ except RenderError, e:
+ # add data_id and re-raise
+ e.data_id = key
+ raise e
def structure(id):
extensions = set([extension for
@@ -81,20 +102,9 @@
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)
@@ -105,7 +115,11 @@
self.id = id
self.path = path
self.title = title
-
+
+ def update(self, template_id, source):
+ raise NotSupported(
+ "Cannot update templates in FilesystemTemplateDatabase.")
+
def get_source(self, template_id):
template_path = os.path.join(self.path, template_id)
f = open(template_path, 'r')
@@ -141,10 +155,6 @@
result[name] = parse(data)
return result
- def update(self, template_id, source):
- raise NotSupported(
- "Cannot update templates in FilesystemTemplateDatabase.")
-
class InMemoryTemplateSource(object):
def __init__(self, source):
self.source = source
@@ -158,6 +168,9 @@
self.title = title
self._templates = {}
+ def update(self, template_id, source):
+ self._templates[template_id] = InMemoryTemplateSource(source)
+
def get_source(self, template_id):
try:
return self._templates[template_id].source
@@ -173,9 +186,6 @@
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 = []
Modified: hurry.custom/trunk/src/hurry/custom/interfaces.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/interfaces.py 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/src/hurry/custom/interfaces.py 2009-06-09 17:46:50 UTC (rev 100761)
@@ -3,12 +3,25 @@
class IHurryCustomAPI(Interface):
"""API for hurry.custom.
"""
- def register_language(template_class):
+ def register_language(template_class, extension, sample_extension=None):
"""Register a template language with the system.
- The template language is a class which implements ITemplate
+ template_class - a class which implements ITemplate
+ extension - the filename extension to register this
+ language. (example: .jsont)
+ sample_extension - the filename extension of sample data files
+ for an extension. (example: .json)
"""
+ def register_data_language(parse_func, extension):
+ """Register a data language for template input.
+
+ parse_func - a function that takes a text and parses it into
+ a data structure.
+ extension - the extension to register the data language under.
+ (example: .json).
+ """
+
def register_collection(id, path, title=None):
"""Register a collection of templates on the filesystem with the system.
@@ -17,15 +30,46 @@
title - optionally, human-readable title for collection.
By default the 'id' will be used.
"""
-
+
+ def render(id, template_path, input):
+ """Render a template.
+
+ id - the id for the collection
+ template_path - the relative path (or filename) of the template
+ itself, under the path of the collection
+ input - input data for the template
+
+ If the template raises a CompileError or RenderError, the
+ system will automatically fall back on the original
+ non-customized template.
+ """
+
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
+ id - the id for the collection
+ template_path - the relative path (or filename) of the template
+ itself, under the path of the collection
"""
+ def check(id, template_path, source):
+ """Test a template (before customization).
+
+ id - the id for the collection
+ template_path - the template path of the template being customized
+ source - the source of the customized template
+
+ This tries a test-compile of the template. If the
+ compilation cannot proceed, a CompileError is raised.
+
+ Then tries to render the template with any sample inputs.
+ If a rendering fails, a RenderError is raised. In a special
+ 'data_id' attribute of the error the failing input data is
+ indicated.
+
+ If the check succeeds, no exception is raised.
+ """
+
def structure(id):
"""Get a list with all the templates in this collection.
@@ -69,18 +113,30 @@
"""
class ITemplate(Interface):
+ """Uses for classes implementing a template language.
+
+ When creating an object that provides ITemplate, raise
+ a CompileError if the template text cannot be processed.
+ """
source = Attribute("The source text of the template.")
def __call__(input):
"""Render the template given input.
input - opaque template-language native data structure.
+
+ Raise a RenderError if the template cannot be rendered.
"""
-class BrokenTemplate(Exception):
- """Error when a template is broken.
+class CompileError(Exception):
+ """Error when a template is broken (cannot be parsed/compiled).
"""
-
+
+class RenderError(Exception):
+ """Error when an error cannot be rendered (incorrect input data or
+ other run-time error.
+ """
+
class IDataLanguage(Interface):
def __call__(data):
"""Parse data into data structure that can be passed to ITemplate()"""
@@ -90,12 +146,8 @@
"""
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.
"""
@@ -103,16 +155,7 @@
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 NotSupported(Exception):
pass
@@ -132,16 +175,6 @@
raised.
"""
- def test(template_id, source):
- """Test a template.
-
- This tries a test-compile of the template, and if sample
- inputs are known, test-renders of the template.
-
- Return False if the compilation or any of the test renderings
- fails. Returns True if there was no error.
- """
-
def get_source(template_id):
"""Get the source of a given template.
Modified: hurry.custom/trunk/src/hurry/custom/jsont.py
===================================================================
--- hurry.custom/trunk/src/hurry/custom/jsont.py 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/src/hurry/custom/jsont.py 2009-06-09 17:46:50 UTC (rev 100761)
@@ -1,15 +1,21 @@
import jsontemplate
from zope.interface import implements
-from hurry.custom.interfaces import ITemplate
+from hurry.custom.interfaces import ITemplate, CompileError, RenderError
class JsonTemplate(object):
implements(ITemplate)
def __init__(self, source):
- self.json_template = jsontemplate.Template(source)
+ try:
+ self.json_template = jsontemplate.Template(source)
+ except jsontemplate.CompilationError, e:
+ raise CompileError(unicode(e))
self.source = source
def __call__(self, input):
- return self.json_template.expand(input)
+ try:
+ return self.json_template.expand(input)
+ except jsontemplate.EvaluationError, e:
+ raise RenderError(unicode(e))
Modified: hurry.custom/trunk/src/hurry/custom/jsontemplate.txt
===================================================================
--- hurry.custom/trunk/src/hurry/custom/jsontemplate.txt 2009-06-09 14:23:51 UTC (rev 100760)
+++ hurry.custom/trunk/src/hurry/custom/jsontemplate.txt 2009-06-09 17:46:50 UTC (rev 100761)
@@ -43,3 +43,25 @@
>>> template({'target': 'universe'})
u'Hello universe!'
+
+Errors
+------
+
+Let's demonstrate that a compilation error is raised when the template
+cannot be compiled::
+
+ >>> r = JsonTemplate('Foo {.section foo}')
+ Traceback (most recent call last):
+ ...
+ CompileError: Got too few {end} statements
+
+If we have a template that can be compiled but not rendered, we should
+get a render error::
+
+ >>> r = JsonTemplate('Hello {thing}')
+ >>> r({'thang': 'world'})
+ Traceback (most recent call last):
+ ...
+ RenderError: 'thing' is not defined
+ ...
+
More information about the Checkins
mailing list