[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