[svn] again huge jinja update. this time regarding keywords
authorArmin Ronacher <armin.ronacher@active-4.com>
Wed, 21 Mar 2007 17:05:32 +0000 (18:05 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Wed, 21 Mar 2007 17:05:32 +0000 (18:05 +0100)
--HG--
branch : trunk

Makefile
docs/src/designerdoc.txt
jinja/lexer.py
jinja/loaders.py
jinja/nodes.py
jinja/parser.py
jinja/plugin.py
jinja/translators/python.py
setup.py
tests/test_loaders.py

index 64f448a9cd73a4248dfe613589bcd6d6d97f69fd..1c080938ef90e5535e563588d0afc34773ed2d74 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -10,3 +10,9 @@
 
 test:
        @(cd tests; py.test $(TESTS))
+
+documentation:
+       @(cd docs; ./generate.py)
+
+webpage:
+       @(cd www; ./generate.py)
index 77700c0fb4a0f8d61e91476191e4dc192ddf42bc..b4b6c44b13a076cfcbab10e568a0572fc0f0aa34 100644 (file)
@@ -622,6 +622,34 @@ When you have multiple elements you can use the ``raw`` block:
         {% endfilter %}
     {% endraw %}
 
+Reserved Keywords
+=================
+
+Jinja has some keywords you cannot use a variable names. This limitation
+exists to make look coherent. Syntax highlighters won't mess things up and
+you will don't have unexpected output.
+
+The following keywords exist and cannot be used as identifiers:
+
+    `and`, `block`, `cycle`, `elif`, `else`, `endblock`, `endfilter`,
+    `endfor`, `endif`, `endmacro`, `endraw`, `endtrans`, `extends`, `filter`,
+    `for`, `if`, `in`, `include`, `is`, `macro`, `not`, `or`, `pluralize`,
+    `raw`, `recursive`, `set`, `trans`
+
+If you want to use such a name you have to prefix or suffix it or use
+alternative names:
+
+.. sourcecode:: jinja
+
+    {% for macro_ in macros %}
+        {{ macro_('foo') }}
+    {% endfor %}
+
+If future Jinja releases add new keywords those will be "light" keywords which
+means that they won't raise an error for several releases but yield warnings
+on the application side. But it's very unlikely that new keywords will be
+added.
+
 Internationalization
 ====================
 
index 7a442d13a4c34856bd4de1f86fdcfbea2a36217e..ed5e341a64de3f7e4183826bc6d1c69091795a5a 100644 (file)
@@ -34,12 +34,12 @@ operator_re = re.compile('(%s)' % '|'.join(
     ur'or\b', ur'and\b', ur'not\b', ur'in\b', ur'is'
 ]))
 
-# set of names that are keywords in python but not in jinja. the lexer
-# appends three trailing underscores, the parser removes them again later
-escaped_names = set(['with', 'as', 'import', 'from', 'class', 'def',
-                     'try', 'except', 'exec', 'global', 'assert',
-                     'break', 'continue', 'lambda', 'return', 'raise',
-                     'yield', 'while', 'pass', 'finally'])
+# set of used keywords
+keywords = set(['and', 'block', 'cycle', 'elif', 'else', 'endblock',
+                'endfilter', 'endfor', 'endif', 'endmacro', 'endraw',
+                'endtrans', 'extends', 'filter', 'for', 'if', 'in',
+                'include', 'is', 'macro', 'not', 'or', 'pluralize', 'raw',
+                'recursive', 'set', 'trans'])
 
 
 class Failure(object):
@@ -121,12 +121,12 @@ class Lexer(object):
         `TokenStream` to get real token instances and be able to push tokens
         back to the stream. That's for example done by the parser.
 
-        Additionally some names like "class" are escaped
+        Additionally non keywords are escaped.
         """
         def filter():
             for lineno, token, value in self.tokeniter(source):
-                if token == 'name' and value in escaped_names:
-                    value += '___'
+                if token == 'name' and value not in keywords:
+                    value += '_'
                 yield lineno, token, value
         return TokenStream(filter())
 
index 0c5168e3abde98f7a81f6ab465a8954d4fa57662..ed3c6c3aa128e520d764fa9cc035ed5bce44a91b 100644 (file)
@@ -175,8 +175,8 @@ class CachedLoaderMixin(object):
                         try:
                             cache_time = path.getmtime(cache_fn)
                         except OSError:
-                            cache_time = 0
-                        if last_change >= cache_time:
+                            cache_time = -1
+                        if cache_time == -1 or last_change >= cache_time:
                             f = file(cache_fn, 'rb')
                             try:
                                 tmpl = Template.load(environment, f)
@@ -265,7 +265,7 @@ class FileSystemLoader(CachedLoaderMixin, BaseLoader):
         filename = get_template_filename(self.searchpath, name)
         if path.exists(filename):
             return path.getmtime(filename)
-        return 0
+        return -1
 
 
 class PackageLoader(CachedLoaderMixin, BaseLoader):
@@ -331,7 +331,7 @@ class PackageLoader(CachedLoaderMixin, BaseLoader):
                                [p for p in name.split('/') if p and p[0] != '.']))
         if resource_exists(self.package_name, fn):
             return path.getmtime(fn)
-        return 0
+        return -1
 
 
 class FunctionLoader(CachedLoaderMixin, BaseLoader):
@@ -363,7 +363,7 @@ class FunctionLoader(CachedLoaderMixin, BaseLoader):
                         value is None it's considered missing.
     ``getmtime_func``   Function used to check if templates requires
                         reloading. Has to return the UNIX timestamp of
-                        the last template change or 0 if this template
+                        the last template change or ``-1`` if this template
                         does not exist or requires updates at any cost.
     ``use_memcache``    Set this to ``True`` to enable memory caching.
                         This is usually a good idea in production mode,
index b3ef5f5790ee6b218908626c9464a8152cfa5792..b19994e211f63f6053ca76f29528d991b4ee6153 100644 (file)
@@ -3,7 +3,7 @@
     jinja.nodes
     ~~~~~~~~~~~
 
-    Additional nodes for jinja. Look like nodes from the ast.
+    Additional nodes for Jinja. Look like nodes from the ast.
 
     :copyright: 2007 by Armin Ronacher.
     :license: BSD, see LICENSE for more details.
@@ -39,7 +39,7 @@ def get_nodes(nodetype, tree):
 
 class Node(ast.Node):
     """
-    jinja node.
+    Jinja node.
     """
 
     def get_items(self):
@@ -96,9 +96,7 @@ class Template(NodeList):
         self.filename = filename
 
     def get_items(self):
-        if self.extends is not None:
-            return [self.extends]
-        return []
+        return self.extends is not None and [self.extends] or []
 
     def __repr__(self):
         return 'Template(%r, %r, %r)' % (
@@ -248,10 +246,10 @@ class Filter(Node):
         self.filters = filters
 
     def get_items(self):
-        return [self.body, self.filters]
+        return [self.body] + list(self.filters)
 
     def __repr__(self):
-        return 'Filter(%r)' % (
+        return 'Filter(%r, %r)' % (
             self.body,
             self.filters
         )
index cafa45c16bcd5df4cfe4a5a5c642e7594e343df8..479ccab7c1c006f88169284dcd131bed47e92148 100644 (file)
@@ -12,7 +12,6 @@ import re
 from compiler import ast, parse
 from compiler.misc import set_filename
 from jinja import nodes
-from jinja.lexer import escaped_names
 from jinja.datastructure import TokenStream
 from jinja.exceptions import TemplateSyntaxError
 try:
@@ -116,7 +115,8 @@ class Parser(object):
             else_ = None
         self.close_remaining_block()
 
-        return nodes.ForLoop(lineno, ast.assign, ast.list, body, else_, bool(recursive))
+        return nodes.ForLoop(lineno, ast.assign, ast.list, body, else_,
+                             bool(recursive))
 
     def handle_if_directive(self, lineno, gen):
         """
@@ -163,7 +163,12 @@ class Parser(object):
         except (StopIteration, ValueError):
             raise TemplateSyntaxError('invalid syntax for set', lineno)
         ast = self.parse_python(lineno, gen, '(%s)')
-        return nodes.Set(lineno, str(name[2]), ast.expr)
+        # disallow keywords
+        if not name[2].endswith('_'):
+            raise TemplateSyntaxError('illegal use of keyword %r '
+                                      'as identifier in set statement.' %
+                                      name[2], lineno)
+        return nodes.Set(lineno, str(name[2][:-1]), ast.expr)
 
     def handle_filter_directive(self, lineno, gen):
         """
@@ -200,7 +205,14 @@ class Parser(object):
         if macro_name[1] != 'name':
             raise TemplateSyntaxError('expected \'name\', got %r' %
                                       macro_name[1], lineno)
-        ast = self.parse_python(lineno, gen, 'def %s(%%s):pass' % str(macro_name[2]))
+        # disallow keywords as identifiers
+        elif not macro_name[2].endswith('_'):
+            raise TemplateSyntaxError('illegal use of keyword %r '
+                                      'as macro name.' % macro_name[2],
+                                      lineno)
+
+        ast = self.parse_python(lineno, gen, 'def %s(%%s):pass' %
+                                str(macro_name[2][:-1]))
         body = self.subparse(end_of_macro, True)
         self.close_remaining_block()
 
@@ -209,7 +221,14 @@ class Parser(object):
                                       'not allowed.', lineno)
         if ast.argnames:
             defaults = [None] * (len(ast.argnames) - len(ast.defaults)) + ast.defaults
-            args = zip(ast.argnames, defaults)
+            args = []
+            for idx, argname in enumerate(ast.argnames):
+                # disallow keywords as argument names
+                if not argname.endswith('_'):
+                    raise TemplateSyntaxError('illegal use of keyword %r '
+                                              'as macro argument.' % argname,
+                                              lineno)
+                args.append((argname[:-1], defaults[idx]))
         else:
             args = None
         return nodes.Macro(lineno, ast.name, args, body)
@@ -225,20 +244,26 @@ class Parser(object):
         if block_name[1] != 'name':
             raise TemplateSyntaxError('expected \'name\', got %r' %
                                       block_name[1], lineno)
+        # disallow keywords
+        if not block_name[2].endswith('_'):
+            raise TemplateSyntaxError('illegal use of keyword %r '
+                                      'as block name.' % block_name[2],
+                                      lineno)
+        name = block_name[2][:-1]
         if tokens:
             raise TemplateSyntaxError('block got too many arguments, '
                                       'requires one.', lineno)
 
         # check if this block does not exist by now.
-        if block_name[2] in self.blocks:
+        if name in self.blocks:
             raise TemplateSyntaxError('block %r defined twice' %
-                                       block_name[2], lineno)
-        self.blocks.add(block_name[2])
+                                       name, lineno)
+        self.blocks.add(name)
 
         # now parse the body and attach it to the block
         body = self.subparse(end_of_block_tag, True)
         self.close_remaining_block()
-        return nodes.Block(lineno, block_name[2], body)
+        return nodes.Block(lineno, name, body)
 
     def handle_extends_directive(self, lineno, gen):
         """
@@ -299,9 +324,15 @@ class Parser(object):
                     if arg.__class__ is not ast.Keyword:
                         raise TemplateSyntaxError('translation tags need explicit '
                                                   'names for values.', lineno)
+                    # argument name doesn't end with "_"? that's a keyword then
+                    if not arg.name.endswith('_'):
+                        raise TemplateSyntaxError('illegal use of keyword %r '
+                                                  'as identifier.' % arg.name,
+                                                  lineno)
+                    # remove the last "_" before writing
                     if first_var is None:
-                        first_var = arg.name
-                    replacements[arg.name] = arg.expr
+                        first_var = arg.name[:-1]
+                    replacements[arg.name[:-1]] = arg.expr
 
             # look for endtrans/pluralize
             buf = singular = []
@@ -315,7 +346,18 @@ class Parser(object):
                 # nested variables
                 elif token == 'variable_begin':
                     _, variable_token, variable_name = self.tokenstream.next()
-                    if variable_token != 'name' or variable_name not in replacements:
+                    if variable_token != 'name':
+                        raise TemplateSyntaxError('can only use variable not '
+                                                  'constants or expressions '
+                                                  'in translation variable '
+                                                  'blocks.', lineno)
+                    # plural name without trailing "_"? that's a keyword
+                    if not variable_name.endswith('_'):
+                        raise TemplateSyntaxError('illegal use of keyword '
+                                                  '%r as identifier in trans '
+                                                  'block.' % variable_name, lineno)
+                    variable_name = variable_name[:-1]
+                    if variable_name not in replacements:
                         raise TemplateSyntaxError('unregistered translation '
                                                   'variable %r.' % variable_name,
                                                   lineno)
@@ -341,6 +383,12 @@ class Parser(object):
                         if plural_token == 'block_end':
                             indicator = first_var
                         elif plural_token == 'name':
+                            # plural name without trailing "_"? that's a keyword
+                            if not plural_name.endswith('_'):
+                                raise TemplateSyntaxError('illegal use of keyword '
+                                                          '%r as identifier.' %
+                                                          plural_name, lineno)
+                            plural_name = plural_name[:-1]
                             if plural_name not in replacements:
                                 raise TemplateSyntaxError('unknown tranlsation '
                                                           'variable %r' %
@@ -405,9 +453,20 @@ class Parser(object):
         todo = [body]
         while todo:
             node = todo.pop()
-            if node.__class__ in (ast.AssName, ast.Name) and \
-               node.name.endswith('___') and node.name[:-3] in escaped_names:
-                node.name = node.name[:-3]
+            # all names excluding keywords have an trailing underline.
+            # if we find a name without trailing underline that's a keyword
+            # and this code raises an error. else strip the underline again
+            if node.__class__ in (ast.AssName, ast.Name):
+                if not node.name.endswith('_'):
+                    raise TemplateSyntaxError('illegal use of keyword %r '
+                                              'as identifier.' % node.name,
+                                              node.lineno)
+                node.name = node.name[:-1]
+            elif node.__class__ is ast.Getattr:
+                if not node.attrname.endswith('_'):
+                    raise TemplateSyntaxError('illegal use of keyword %r '
+                                              'as attribute name.' % node.name)
+                node.attrname = node.attrname[:-1]
             node.filename = self.filename
             todo.extend(node.getChildNodes())
         return nodes.Template(self.filename, body, self.extends)
index 5eb70bfa3783c023791eeece8352818e34edbdaa..6ee2f108f921756a53d629263134791ab69a9ee4 100644 (file)
@@ -11,7 +11,7 @@
     :license: BSD, see LICENSE for more details.
 """
 from jinja.environment import Environment
-from jinja.loaders import FunctionLoader
+from jinja.loaders import FunctionLoader, FileSystemLoader, PackageLoader
 from jinja.exceptions import TemplateNotFound
 
 
@@ -30,6 +30,12 @@ def jinja_plugin_factory(options):
     ``environment``     If this is provided it must be the only
                         configuration value and it's used as jinja
                         environment.
+    ``searchpath``      If provided a new file system loader with this
+                        search path is instanciated.
+    ``package``         Name of the python package containing the
+                        templates. If this and ``package_path`` is
+                        defined a `PackageLoader` is used.
+    ``package_path``    Path to the templates inside of a package.
     ``loader_func``     Function that takes the name of the template to
                         load. If it returns a string or unicode object
                         it's used to load a template. If the return
@@ -68,7 +74,16 @@ def jinja_plugin_factory(options):
         memcache_size = options.pop('memcache_size', 40)
         cache_folder = options.pop('cache_folder', None)
         auto_reload = options.pop('auto_reload', True)
-        if loader_func is not None:
+        if 'searchpath' in options:
+            options['loader'] = FileSystemLoader(options.pop('searchpath'),
+                                                 use_memcache, memcache_size,
+                                                 cache_folder, auto_reload)
+        elif 'package' in options:
+            options['loader'] = PackageLoader(options.pop('package'),
+                                              options.pop('package_path', ''),
+                                              use_memcache, memcache_size,
+                                              cache_folder, auto_reload)
+        elif loader_func is not None:
             options['loader'] = FunctionLoader(loader_func, getmtime_func,
                                                use_memcache, memcache_size,
                                                cache_folder, auto_reload)
@@ -82,6 +97,6 @@ def jinja_plugin_factory(options):
                 tmpl = env.get_template(template)
             except TemplateNotFound:
                 return
-        return (tmpl.render(**values),)
+        return tmpl.render(**values)
 
     return render_function
index 6c8ba9492fd47e1bb029a809c03a382d0b6a56c2..3c9902cfb142341324be0abe2c178c691e78ce96 100644 (file)
@@ -575,7 +575,7 @@ class PythonTranslator(Translator):
         write('ctx_push()')
         nodeinfo = self.nodeinfo(node.body)
         if nodeinfo:
-            write(nodebody)
+            write(nodeinfo)
         buf.append(self.handle_node(node.body))
         write('ctx_pop()')
         return '\n'.join(buf)
index c296c1f56f89c0efce35628a1772bd173b1fd763..5c1748eeb74f1c89de9905b73da8eec0dba78438 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -30,5 +30,5 @@ setup(
     ],
     keywords = ['python.templating.engines'],
     packages = ['jinja', 'jinja.translators'],
-    extras_require = {'plugin': ['setuptools>=0.6a2']}
+    extras_require = {'plugin': ['setuptools>=0.6a2']},
 )
index fffec035bd79f905bb51c1ac67729491975e5889..e52458c7966bb3aa3e1796ab6fd88404795f22c2 100644 (file)
@@ -19,6 +19,8 @@ package_loader = loaders.PackageLoader('loaderres', 'templates')
 
 filesystem_loader = loaders.FileSystemLoader('loaderres/templates')
 
+function_loader = loaders.FunctionLoader({'justfunction.html': 'FOO'}.get)
+
 choice_loader = loaders.ChoiceLoader([dict_loader, package_loader])
 
 
@@ -70,3 +72,15 @@ def test_choice_loader():
         pass
     else:
         raise AssertionError('expected template exception')
+
+
+def test_function_loader():
+    env = Environment(loader=function_loader)
+    tmpl = env.get_template('justfunction.html')
+    assert tmpl.render().strip() == 'FOO'
+    try:
+        env.get_template('missing.html')
+    except TemplateNotFound:
+        pass
+    else:
+        raise AssertionError('expected template exception')