From: Armin Ronacher Date: Wed, 21 Mar 2007 17:05:32 +0000 (+0100) Subject: [svn] again huge jinja update. this time regarding keywords X-Git-Tag: 2.0rc1~408 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=9baa5bae6aff648957ab6c31de3f3fb7f7f99664;p=jinja2.git [svn] again huge jinja update. this time regarding keywords --HG-- branch : trunk --- diff --git a/Makefile b/Makefile index 64f448a..1c08093 100644 --- a/Makefile +++ b/Makefile @@ -10,3 +10,9 @@ test: @(cd tests; py.test $(TESTS)) + +documentation: + @(cd docs; ./generate.py) + +webpage: + @(cd www; ./generate.py) diff --git a/docs/src/designerdoc.txt b/docs/src/designerdoc.txt index 77700c0..b4b6c44 100644 --- a/docs/src/designerdoc.txt +++ b/docs/src/designerdoc.txt @@ -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 ==================== diff --git a/jinja/lexer.py b/jinja/lexer.py index 7a442d1..ed5e341 100644 --- a/jinja/lexer.py +++ b/jinja/lexer.py @@ -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()) diff --git a/jinja/loaders.py b/jinja/loaders.py index 0c5168e..ed3c6c3 100644 --- a/jinja/loaders.py +++ b/jinja/loaders.py @@ -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, diff --git a/jinja/nodes.py b/jinja/nodes.py index b3ef5f5..b19994e 100644 --- a/jinja/nodes.py +++ b/jinja/nodes.py @@ -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 ) diff --git a/jinja/parser.py b/jinja/parser.py index cafa45c..479ccab 100644 --- a/jinja/parser.py +++ b/jinja/parser.py @@ -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) diff --git a/jinja/plugin.py b/jinja/plugin.py index 5eb70bf..6ee2f10 100644 --- a/jinja/plugin.py +++ b/jinja/plugin.py @@ -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 diff --git a/jinja/translators/python.py b/jinja/translators/python.py index 6c8ba94..3c9902c 100644 --- a/jinja/translators/python.py +++ b/jinja/translators/python.py @@ -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) diff --git a/setup.py b/setup.py index c296c1f..5c1748e 100644 --- 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']}, ) diff --git a/tests/test_loaders.py b/tests/test_loaders.py index fffec03..e52458c 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -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')