From 0553093657926ceffe3055210acf3198a9d1ed12 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 20 Apr 2008 13:27:49 +0200 Subject: [PATCH] refactored extensions a bit --HG-- branch : trunk --- examples/translate.py | 2 +- jinja2/defaults.py | 6 +- jinja2/environment.py | 15 +-- jinja2/ext.py | 48 ++++++++ jinja2/i18n.py | 266 +++++++++++++++++++++--------------------- jinja2/parser.py | 22 +--- 6 files changed, 198 insertions(+), 161 deletions(-) create mode 100644 jinja2/ext.py diff --git a/examples/translate.py b/examples/translate.py index 0169c88..3358765 100644 --- a/examples/translate.py +++ b/examples/translate.py @@ -1,6 +1,6 @@ from jinja2 import Environment -print Environment(parser_extensions=['jinja2.i18n.trans']).from_string("""\ +print Environment(extensions=['jinja2.i18n.TransExtension']).from_string("""\ {% trans %}Hello {{ user }}!{% endtrans %} {% trans count=users|count %}{{ count }} user{% pluralize %}{{ count }} users{% endtrans %} """).render(user="someone") diff --git a/jinja2/defaults.py b/jinja2/defaults.py index ee698a0..6462f32 100644 --- a/jinja2/defaults.py +++ b/jinja2/defaults.py @@ -13,9 +13,5 @@ from jinja2.tests import TESTS as DEFAULT_TESTS DEFAULT_NAMESPACE = { - 'range': xrange, - # fake translators so that {% trans %} is a noop by default - '_': lambda x: x, - 'gettext': lambda x: x, - 'ngettext': lambda s, p, n: (s, p)[n != 1] + 'range': xrange } diff --git a/jinja2/environment.py b/jinja2/environment.py index 004ef74..5b705e4 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -10,7 +10,7 @@ """ import sys from jinja2.lexer import Lexer -from jinja2.parser import Parser, ParserExtension +from jinja2.parser import Parser from jinja2.optimizer import optimize from jinja2.compiler import generate from jinja2.runtime import Undefined @@ -44,7 +44,7 @@ class Environment(object): optimized=True, undefined=Undefined, loader=None, - parser_extensions=(), + extensions=(), finalize=unicode): """Here the possible initialization parameters: @@ -70,7 +70,7 @@ class Environment(object): `undefined` a subclass of `Undefined` that is used to represent undefined variables. `loader` the loader which should be used. - `parser_extensions` List of parser extensions to use. + `extensions` List of Jinja extensions to use. `finalize` A callable that finalizes the variable. Per default this is `unicode`, other useful builtin finalizers are `escape`. @@ -93,12 +93,11 @@ class Environment(object): self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.trim_blocks = trim_blocks - self.parser_extensions = {} - for extension in parser_extensions: + self.extensions = [] + for extension in extensions: if isinstance(extension, basestring): extension = import_string(extension) - self.parser_extensions[extension.tag] = extension - + self.extensions.append(extension(self)) # runtime information self.undefined = undefined @@ -109,6 +108,8 @@ class Environment(object): self.filters = DEFAULT_FILTERS.copy() self.tests = DEFAULT_TESTS.copy() self.globals = DEFAULT_NAMESPACE.copy() + for extension in self.extensions: + extension.update_globals(self.globals) # set the loader provided self.loader = loader diff --git a/jinja2/ext.py b/jinja2/ext.py new file mode 100644 index 0000000..75952ed --- /dev/null +++ b/jinja2/ext.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" + jinja2.ext + ~~~~~~~~~~ + + Jinja extensions (EXPERIMENAL) + + The plan: i18n and caching becomes a parser extension. cache/endcache + as well as trans/endtrans are not keyword and don't have nodes but + translate into regular jinja nodes so that the person who writes such + custom tags doesn't have to generate python code himself. + + :copyright: Copyright 2008 by Armin Ronacher. + :license: BSD. +""" +from jinja2 import nodes + + +class Extension(object): + """Instances of this class store parser extensions.""" + + #: if this extension parses this is the list of tags it's listening to. + tags = set() + + def __init__(self, environment): + self.environment = environment + + def update_globals(self, globals): + """Called to inject runtime variables into the globals.""" + pass + + def parse(self, parser): + """Called if one of the tags matched.""" + + +class CacheExtension(Extension): + tags = set(['cache']) + + def parse(self, parser): + lineno = parser.stream.next().lineno + args = [parser.parse_expression()] + if self.stream.current.type is 'comma': + args.append(parser.parse_expression()) + body = parser.parse_statements(('name:endcache',), drop_needle=True) + return nodes.CallBlock( + nodes.Call(nodes.Name('cache_support'), args, [], None, None), + [], [], body + ) diff --git a/jinja2/i18n.py b/jinja2/i18n.py index 026750b..ef932d9 100644 --- a/jinja2/i18n.py +++ b/jinja2/i18n.py @@ -11,7 +11,8 @@ from collections import deque from jinja2 import nodes from jinja2.environment import Environment -from jinja2.parser import ParserExtension, statement_end_tokens +from jinja2.parser import statement_end_tokens +from jinja2.ext import Extension from jinja2.exceptions import TemplateAssertionError @@ -81,149 +82,154 @@ def babel_extract(fileobj, keywords, comment_tags, options): yield lineno, func, message, [] -def parse_trans(parser): - """Parse a translatable tag.""" - lineno = parser.stream.next().lineno +class TransExtension(Extension): + tags = set(['trans']) - # skip colon for python compatibility - if parser.stream.current.type is 'colon': - parser.stream.next() + def update_globals(self, globals): + """Inject noop translation functions.""" + globals.update({ + '_': lambda x: x, + 'gettext': lambda x: x, + 'ngettext': lambda s, p, n: (s, p)[n != 1] + }) - # find all the variables referenced. Additionally a variable can be - # defined in the body of the trans block too, but this is checked at - # a later state. - plural_expr = None - variables = {} - while parser.stream.current.type is not 'block_end': - if variables: - parser.stream.expect('comma') - name = parser.stream.expect('name') - if name.value in variables: - raise TemplateAssertionError('translatable variable %r defined ' - 'twice.' % name.value, name.lineno, - parser.filename) - - # expressions - if parser.stream.current.type is 'assign': - parser.stream.next() - variables[name.value] = var = parser.parse_expression() - else: - variables[name.value] = var = nodes.Name(name.value, 'load') - if plural_expr is None: - plural_expr = var - parser.stream.expect('block_end') + def parse(self, parser): + """Parse a translatable tag.""" + lineno = parser.stream.next().lineno - plural = plural_names = None - have_plural = False - referenced = set() + # skip colon for python compatibility + if parser.stream.current.type is 'colon': + parser.stream.next() - # now parse until endtrans or pluralize - singular_names, singular = _parse_block(parser, True) - if singular_names: - referenced.update(singular_names) - if plural_expr is None: - plural_expr = nodes.Name(singular_names[0], 'load') - - # if we have a pluralize block, we parse that too - if parser.stream.current.test('name:pluralize'): - have_plural = True - parser.stream.next() - if parser.stream.current.type is not 'block_end': - plural_expr = parser.parse_expression() - parser.stream.expect('block_end') - plural_names, plural = _parse_block(parser, False) - parser.stream.next() - referenced.update(plural_names) - else: - parser.stream.next() - parser.end_statement() - - # register free names as simple name expressions - for var in referenced: - if var not in variables: - variables[var] = nodes.Name(var, 'load') - - # no variables referenced? no need to escape - if not referenced: - singular = singular.replace('%%', '%') - if plural: - plural = plural.replace('%%', '%') - - if not have_plural: - if plural_expr is None: - raise TemplateAssertionError('pluralize without variables', - lineno, parser.filename) + # find all the variables referenced. Additionally a variable can be + # defined in the body of the trans block too, but this is checked at + # a later state. plural_expr = None + variables = {} + while parser.stream.current.type is not 'block_end': + if variables: + parser.stream.expect('comma') + name = parser.stream.expect('name') + if name.value in variables: + raise TemplateAssertionError('translatable variable %r defined ' + 'twice.' % name.value, name.lineno, + parser.filename) + + # expressions + if parser.stream.current.type is 'assign': + parser.stream.next() + variables[name.value] = var = parser.parse_expression() + else: + variables[name.value] = var = nodes.Name(name.value, 'load') + if plural_expr is None: + plural_expr = var + parser.stream.expect('block_end') - if variables: - variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y) - for x, y in variables.items()]) - else: - vairables = None - - node = _make_node(singular, plural, variables, plural_expr) - node.set_lineno(lineno) - return node + plural = plural_names = None + have_plural = False + referenced = set() + # now parse until endtrans or pluralize + singular_names, singular = self._parse_block(parser, True) + if singular_names: + referenced.update(singular_names) + if plural_expr is None: + plural_expr = nodes.Name(singular_names[0], 'load') -def _parse_block(parser, allow_pluralize): - """Parse until the next block tag with a given name.""" - referenced = [] - buf = [] - while 1: - if parser.stream.current.type is 'data': - buf.append(parser.stream.current.value.replace('%', '%%')) + # if we have a pluralize block, we parse that too + if parser.stream.current.test('name:pluralize'): + have_plural = True parser.stream.next() - elif parser.stream.current.type is 'variable_begin': + if parser.stream.current.type is not 'block_end': + plural_expr = parser.parse_expression() + parser.stream.expect('block_end') + plural_names, plural = self._parse_block(parser, False) parser.stream.next() - name = parser.stream.expect('name').value - referenced.append(name) - buf.append('%%(%s)s' % name) - parser.stream.expect('variable_end') - elif parser.stream.current.type is 'block_begin': - parser.stream.next() - if parser.stream.current.test('name:endtrans'): - break - elif parser.stream.current.test('name:pluralize'): - if allow_pluralize: - break - raise TemplateSyntaxError('a translatable section can have ' - 'only one pluralize section', - parser.stream.current.lineno, - parser.filename) - raise TemplateSyntaxError('control structures in translatable ' - 'sections are not allowed.', - parser.stream.current.lineno, - parser.filename) + referenced.update(plural_names) else: - assert False, 'internal parser error' + parser.stream.next() + parser.end_statement() - return referenced, u''.join(buf) + # register free names as simple name expressions + for var in referenced: + if var not in variables: + variables[var] = nodes.Name(var, 'load') + # no variables referenced? no need to escape + if not referenced: + singular = singular.replace('%%', '%') + if plural: + plural = plural.replace('%%', '%') + + if not have_plural: + if plural_expr is None: + raise TemplateAssertionError('pluralize without variables', + lineno, parser.filename) + plural_expr = None -def _make_node(singular, plural, variables, plural_expr): - """Generates a useful node from the data provided. The nodes won't have - a line number information so the caller must set it via `set_lineno` later. - """ - # singular only: - if plural_expr is None: - gettext = nodes.Name('gettext', 'load') - node = nodes.Call(gettext, [nodes.Const(singular)], - [], None, None) - if variables: - node = nodes.Mod(node, variables) - - # singular and plural - else: - ngettext = nodes.Name('ngettext', 'load') - node = nodes.Call(ngettext, [ - nodes.Const(singular), - nodes.Const(plural), - plural_expr - ], [], None, None) if variables: - node = nodes.Mod(node, variables) - return nodes.Output([node]) + variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y) + for x, y in variables.items()]) + else: + vairables = None + + node = self._make_node(singular, plural, variables, plural_expr) + node.set_lineno(lineno) + return node + + def _parse_block(self, parser, allow_pluralize): + """Parse until the next block tag with a given name.""" + referenced = [] + buf = [] + while 1: + if parser.stream.current.type is 'data': + buf.append(parser.stream.current.value.replace('%', '%%')) + parser.stream.next() + elif parser.stream.current.type is 'variable_begin': + parser.stream.next() + name = parser.stream.expect('name').value + referenced.append(name) + buf.append('%%(%s)s' % name) + parser.stream.expect('variable_end') + elif parser.stream.current.type is 'block_begin': + parser.stream.next() + if parser.stream.current.test('name:endtrans'): + break + elif parser.stream.current.test('name:pluralize'): + if allow_pluralize: + break + raise TemplateSyntaxError('a translatable section can ' + 'have only one pluralize ' + 'section', + parser.stream.current.lineno, + parser.filename) + raise TemplateSyntaxError('control structures in translatable' + ' sections are not allowed.', + parser.stream.current.lineno, + parser.filename) + else: + assert False, 'internal parser error' + return referenced, u''.join(buf) -trans = ParserExtension('trans', parse_trans) + def _make_node(self, singular, plural, variables, plural_expr): + """Generates a useful node from the data provided.""" + # singular only: + if plural_expr is None: + gettext = nodes.Name('gettext', 'load') + node = nodes.Call(gettext, [nodes.Const(singular)], + [], None, None) + if variables: + node = nodes.Mod(node, variables) + + # singular and plural + else: + ngettext = nodes.Name('ngettext', 'load') + node = nodes.Call(ngettext, [ + nodes.Const(singular), + nodes.Const(plural), + plural_expr + ], [], None, None) + if variables: + node = nodes.Mod(node, variables) + return nodes.Output([node]) diff --git a/jinja2/parser.py b/jinja2/parser.py index 4204c6a..27f54ef 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -35,7 +35,10 @@ class Parser(object): self.filename = filename self.closed = False self.stream = environment.lexer.tokenize(source, filename) - self.extensions = environment.parser_extensions + self.extensions = {} + for extension in environment.extensions: + for tag in extension.tags: + self.extensions[tag] = extension.parse def end_statement(self): """Make sure that the statement ends properly.""" @@ -686,20 +689,3 @@ class Parser(object): result = nodes.Template(self.subparse(), lineno=1) result.set_environment(self.environment) return result - - -class ParserExtension(tuple): - """Instances of this class store parser extensions.""" - __slots__ = () - - def __new__(cls, tag, parse_func): - return tuple.__new__(cls, (tag, parse_func)) - - def __call__(self, parser): - return self.parse_func(parser) - - def __repr__(self): - return '<%s %r>' % (self.__class__.__name__, self.tag) - - tag = property(itemgetter(0)) - parse_func = property(itemgetter(1)) -- 2.26.2