added support for dotted names in tests and filters
authorArmin Ronacher <armin.ronacher@active-4.com>
Sat, 10 May 2008 21:36:28 +0000 (23:36 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sat, 10 May 2008 21:36:28 +0000 (23:36 +0200)
--HG--
branch : trunk

docs/api.rst
docs/extensions.rst
jinja2/compiler.py
jinja2/nodes.py
jinja2/parser.py

index 4ce79ff686dd0998bb3cfee64112a2e35be4f163..5a131ed3b62aa69ef6ea898d4efd03efead04b19 100644 (file)
@@ -68,13 +68,15 @@ High Level API
 
         A dict of filters for this environment.  As long as no template was
         loaded it's safe to add new filters or remove old.  For custom filters
-        see :ref:`writing-filters`.
+        see :ref:`writing-filters`.  For valid filter names have a look at
+        :ref:`identifier-naming`.
 
     .. attribute:: tests
 
         A dict of test functions for this environment.  As long as no
         template was loaded it's safe to modify this dict.  For custom tests
-        see :ref:`writing-tests`.
+        see :ref:`writing-tests`.  For valid test names have a look at
+        :ref:`identifier-naming`.
 
     .. attribute:: globals
 
@@ -82,6 +84,7 @@ High Level API
         in a template and (if the optimizer is enabled) may not be
         overridden by templates.  As long as no template was loaded it's safe
         to modify this dict.  For more details see :ref:`global-namespace`.
+        For valid object names have a look at :ref:`identifier-naming`.
 
     .. automethod:: overlay([options])
 
@@ -111,6 +114,24 @@ High Level API
     :members: disable_buffering, enable_buffering
 
 
+.. _identifier-naming:
+
+Notes on Identifiers
+~~~~~~~~~~~~~~~~~~~~
+
+Jinja2 uses the regular Python 2.x naming rules.  Valid identifiers have to
+match ``[a-zA-Z_][a-zA-Z0-9_]*``.  As a matter of fact non ASCII characters
+are currently not allowed.  This limitation will probably go away as soon as
+unicode identifiers are fully specified for Python 3.
+
+Filters and tests are looked up in separate namespaces and have slightly
+modified identifier syntax.  Filters and tests may contain dots to group
+filters and tests by topic.  For example it's perfectly valid to add a
+function into the filter dict and call it `to.unicode`.  The regular
+expression for filter and test identifiers is
+``[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*```.
+
+
 Undefined Types
 ---------------
 
index 8bd80f5bac010047f88ef8ab216d07b87ed24a1a..13fe639ebcd29615475288ee1824e0ba66af808e 100644 (file)
@@ -102,6 +102,13 @@ task and usually not needed as the default tags and expressions cover all
 common use cases.  The i18n extension is a good example of why extensions are
 useful, another one would be fragment caching.
 
+When writing extensions you have to keep in mind that you are working with the
+Jinja2 template compiler which does not validate the node tree you are possing
+to it.  If the AST is malformed you will get all kinds of compiler or runtime
+errors that are horrible to debug.  Always make sure you are using the nodes
+you create correctly.  The API documentation below shows which nodes exist and
+how to use them.
+
 Example Extension
 ~~~~~~~~~~~~~~~~~
 
index 9ee323d625e9146ff9f412ed8c118054ba13deba..6518427d2fddcdf1142d0cba7e92a21720f3a4d6 100644 (file)
@@ -325,6 +325,10 @@ class CodeGenerator(NodeVisitor):
         # the current line number
         self.code_lineno = 1
 
+        # registry of all filters and tests (global, not block local)
+        self.tests = {}
+        self.filters = {}
+
         # the debug information
         self.debug_info = []
         self._write_debug_info = None
@@ -473,10 +477,13 @@ class CodeGenerator(NodeVisitor):
         visitor = DependencyFinderVisitor()
         for node in nodes:
             visitor.visit(node)
-        for name in visitor.filters:
-            self.writeline('f_%s = environment.filters[%r]' % (name, name))
-        for name in visitor.tests:
-            self.writeline('t_%s = environment.tests[%r]' % (name, name))
+        for dependency in 'filters', 'tests':
+            mapping = getattr(self, dependency)
+            for name in getattr(visitor, dependency):
+                if name not in mapping:
+                    mapping[name] = self.temporary_identifier()
+                self.writeline('%s = environment.%s[%r]' %
+                               (mapping[name], dependency, name))
 
     def collect_shadowed(self, frame):
         """This function returns all the shadowed variables in a dict
@@ -1215,7 +1222,7 @@ class CodeGenerator(NodeVisitor):
             self.visit(node.step, frame)
 
     def visit_Filter(self, node, frame, initial=None):
-        self.write('f_%s(' % node.name)
+        self.write(self.filters[node.name] + '(')
         func = self.environment.filters.get(node.name)
         if func is None:
             raise TemplateAssertionError('no filter named %r' % node.name,
@@ -1234,7 +1241,7 @@ class CodeGenerator(NodeVisitor):
         self.write(')')
 
     def visit_Test(self, node, frame):
-        self.write('t_%s(' % node.name)
+        self.write(self.tests[node.name] + '(')
         if node.name not in self.environment.tests:
             raise TemplateAssertionError('no test named %r' % node.name,
                                          node.lineno, self.filename)
index 25196826d3f6e7972cc91a6fd30f546ee5ec6695..180478d749b02f3be852002e4a83c26b0daca556 100644 (file)
@@ -727,6 +727,9 @@ class EnvironmentAttribute(Expr):
 class ExtensionAttribute(Expr):
     """Returns the attribute of an extension bound to the environment.
     The identifier is the identifier of the :class:`Extension`.
+
+    This node is usually constructed by calling the
+    :meth:`~jinja2.ext.Extension.attr` method on an extension.
     """
     fields = ('identifier', 'attr')
 
index dae1a6b6910344580276f752213daac31dabc44b..8d23b5f75caa6f527c4a689931716d75f7becddf 100644 (file)
@@ -676,18 +676,21 @@ class Parser(object):
                           lineno=token.lineno)
 
     def parse_filter(self, node, start_inline=False):
-        lineno = self.stream.current.type
         while self.stream.current.type == 'pipe' or start_inline:
             if not start_inline:
                 self.stream.next()
             token = self.stream.expect('name')
+            name = token.value
+            while self.stream.current.type is 'dot':
+                self.stream.next()
+                name += '.' + self.stream.expect('name').value
             if self.stream.current.type is 'lparen':
                 args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None)
             else:
                 args = []
                 kwargs = []
                 dyn_args = dyn_kwargs = None
-            node = nodes.Filter(node, token.value, args, kwargs, dyn_args,
+            node = nodes.Filter(node, name, args, kwargs, dyn_args,
                                 dyn_kwargs, lineno=token.lineno)
             start_inline = False
         return node
@@ -700,6 +703,9 @@ class Parser(object):
         else:
             negated = False
         name = self.stream.expect('name').value
+        while self.stream.current.type is 'dot':
+            self.stream.next()
+            name += '.' + self.stream.expect('name').value
         dyn_args = dyn_kwargs = None
         kwargs = []
         if self.stream.current.type is 'lparen':