From: Armin Ronacher Date: Thu, 8 May 2008 21:57:56 +0000 (+0200) Subject: more updates on the extension API X-Git-Tag: 2.0rc1~82 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=762079cd25921596b9e1227e6c3bad6d465c430f;p=jinja2.git more updates on the extension API --HG-- branch : trunk --- diff --git a/docs/_static/metal.png b/docs/_static/metal.png new file mode 100644 index 0000000..97166f1 Binary files /dev/null and b/docs/_static/metal.png differ diff --git a/docs/_static/style.css b/docs/_static/style.css index 46748d0..097b229 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -179,7 +179,7 @@ a:hover { } pre { - background-color: #f9f9f9; + background: #ededed url(metal.png); border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 5px; @@ -296,7 +296,8 @@ table.indextable dl dd a { dl.function dt, dl.class dt, -dl.exception dt { +dl.exception dt, +dl.method dt { font-weight: normal; } diff --git a/docs/api.rst b/docs/api.rst index 82bb531..4ce79ff 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -48,7 +48,7 @@ High Level API -------------- .. autoclass:: jinja2.environment.Environment([options]) - :members: from_string, get_template, join_path, parse, lex + :members: from_string, get_template, join_path, parse, lex, extend .. attribute:: shared diff --git a/docs/cache_extension.py b/docs/cache_extension.py index 19839fe..c9ed92c 100644 --- a/docs/cache_extension.py +++ b/docs/cache_extension.py @@ -2,19 +2,18 @@ from jinja2 import nodes from jinja2.ext import Extension -class CacheExtension(Extension): - """Adds support for fragment caching to Jinja2.""" +class FragmentCacheExtension(Extension): + # a set of names that trigger the extension. tags = set(['cache']) def __init__(self, environment): - Extension.__init__(self, environment) + super(FragmentCacheExtension, self).__init__(environment) - # default dummy implementations. If the class does not implement - # those methods we add some noop defaults. - if not hasattr(environment, 'add_fragment_to_cache'): - environment.add_fragment_to_cache = lambda n, v, t: None - if not hasattr(environment, 'load_fragment_from_cache'): - environment.load_fragment_from_cache = lambda n: None + # add the defaults to the environment + environment.extend( + fragment_cache_prefix='', + fragment_cache=None + ) def parse(self, parser): # the first token is the token that started the tag. In our case @@ -26,13 +25,10 @@ class CacheExtension(Extension): # now we parse a single expression that is used as cache key. args = [parser.parse_expression()] - # if there is a comma, someone provided the timeout. parse the - # timeout then - if parser.stream.current.type is 'comma': - parser.stream.next() + # if there is a comma, the user provided a timeout. If not use + # None as second parameter. + if parser.skip_comma(): args.append(parser.parse_expression()) - - # otherwise set the timeout to `None` else: args.append(nodes.Const(None)) @@ -49,13 +45,14 @@ class CacheExtension(Extension): def _cache_support(self, name, timeout, caller): """Helper callback.""" - # try to load the block from the cache - rv = self.environment.load_fragment_from_cache(name) - if rv is not None: - return rv + key = self.environment.fragment_cache_prefix + name + # try to load the block from the cache # if there is no fragment in the cache, render it and store # it in the cache. - rv = caller() - self.environment.add_fragment_to_cache(name, rv, timeout) + rv = self.environment.fragment_cache.get(key) + if rv is None: + return rv + rv = caller() + self.environment.fragment_cache.add(key, rv, timeout) return rv diff --git a/docs/extensions.rst b/docs/extensions.rst index 094afa9..7be9f12 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -3,8 +3,6 @@ Extensions ========== -.. module:: jinja2.ext - Jinja2 supports extensions that can add extra filters, tests, globals or even extend the parser. The main motivation of extensions is it to move often used code into a reusable class like adding support for internationalization. @@ -32,9 +30,46 @@ used in combination with `gettext`_ or `babel`_. If the i18n extension is enabled Jinja2 provides a `trans` statement that marks the wrapped string as translatable and calls `gettext`. -After enabling dummy `_`, `gettext` and `ngettext` functions are added to -the template globals. A internationalized application has to override those -methods with more useful versions. +After enabling dummy `_` function that forwards calls to `gettext` is added +to the environment globals. An internationalized application then has to +provide at least an `gettext` and optoinally a `ngettext` function into the +namespace. Either globally or for each rendering. + +After enabling of the extension the environment provides the following +additional methods: + +.. method:: jinja2.Environment.install_gettext_translations(translations) + + Installs a translation globally for that environment. The tranlations + object provided must implement at least `ugettext` and `ungettext`. + The `gettext.NullTranslations` and `gettext.GNUTranslations` classes + as well as `Babel`_\s `Translations` class are supported. + +.. method:: jinja2.Environment.install_null_translations() + + Install dummy gettext functions. This is useful if you want to prepare + the application for internationalization but don't want to implement the + full internationalization system yet. + +.. method:: jinja2.Environment.uninstall_gettext_translations() + + Uninstall the translations again. + +.. method:: jinja2.Environment.extract_translations(source) + + Extract localizable strings from the given template node or source. + + For every string found this function yields a ``(lineno, function, + message)`` tuple, where: + + * `lineno` is the number of the line on which the string was found, + * `function` is the name of the `gettext` function used (if the + string was extracted from embedded Python code), and + * `message` is the string itself (a `unicode` object, or a tuple + of `unicode` objects for functions with multiple string arguments). + + If `Babel`_ is installed :ref:`the babel integration ` + can be used to extract strings for babel. For a web application that is available in multiple languages but gives all the users the same language (for example a multilingual forum software @@ -43,40 +78,16 @@ translation methods to the environment at environment generation time:: translations = get_gettext_translations() env = Environment(extensions=['jinja.ext.i18n']) - env.globals.update( - gettext=translations.ugettext, - ngettext=translations.ungettext - ) + env.install_gettext_translations(translations) The `get_gettext_translations` function would return the translator for the -current configuration. Keep in mind that Jinja2 uses unicode internally so -you must pass the `ugettext` and `ungettext` functions to the template. - -The default `_` function injected by the extension calls `gettext` -automatically. - -If you want to pass the gettext function into the context at render time -because you don't know the language/translations earlier and the optimizer -is enabled (which it is per default), you have to unregister the `gettext` -and `ugettext` functions first:: - - del env.globals['gettext'], env.globals['ugettext'] - -Jinja2 also provides a way to extract recognized strings. For one the -`jinja.ext` module provides a function that can return all the occurences -of gettext calls in a node (as returned by :meth:`Environment.parse`): - -.. autofunction:: extract_from_ast - -If `babel`_ is installed :ref:`the babel integration ` -can be used to. +current configuration. (For example by using `gettext.find`) The usage of the `i18n` extension for template designers is covered as part :ref:`of the template documentation `. - .. _gettext: http://docs.python.org/dev/library/gettext -.. _babel: http://babel.edgewall.org/ +.. _Babel: http://babel.edgewall.org/ .. _writing-extensions: @@ -84,6 +95,8 @@ The usage of the `i18n` extension for template designers is covered as part Writing Extensions ------------------ +.. module:: jinja2.ext + By writing extensions you can add custom tags to Jinja2. This is a non trival 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 @@ -92,32 +105,19 @@ useful, another one would be fragment caching. Example Extension ~~~~~~~~~~~~~~~~~ -The following example implements a `cache` tag for Jinja2: +The following example implements a `cache` tag for Jinja2 by using the +`Werkzeug`_ caching contrib module: .. literalinclude:: cache_extension.py :language: python -In order to use the cache extension it makes sense to subclass the environment -to implement the `add_fragment_to_cache` and `load_fragment_from_cache` -methods. The following example shows how to use the `Werkzeug`_ caching -with the extension from above:: +And here is how you use it in an environment:: from jinja2 import Environment from werkzeug.contrib.cache import SimpleCache - cache = SimpleCache() - cache_prefix = 'tempalte_fragment/' - - class MyEnvironment(Environment): - - def __init__(self): - Environment.__init__(self, extensions=[CacheExtension]) - - def add_fragment_to_cache(self, key, value, timeout): - cache.add(cache_prefix + key, value, timeout) - - def load_fragment_from_cache(self, key): - return cache.get(cache_prefix + key) + env = Environment(extensions=[FragmentCacheExtension]) + env.fragment_cache = SimpleCache() .. _Werkzeug: http://werkzeug.pocoo.org/ @@ -147,8 +147,8 @@ expressions of different types. The following methods may be used by extensions: .. autoclass:: jinja2.parser.Parser - :members: parse_expression, parse_tuple, parse_statements, ignore_colon, - free_identifier + :members: parse_expression, parse_tuple, parse_statements, skip_colon, + skip_comma, free_identifier .. attribute:: filename diff --git a/jinja2/environment.py b/jinja2/environment.py index e771dd6..2fbe217 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -222,6 +222,15 @@ class Environment(object): _environment_sanity_check(self) + def extend(self, **attributes): + """Add the items to the instance of the environment if they do not exist + yet. This is used by :ref:`extensions ` to register + callbacks and configuration values without breaking inheritance. + """ + for key, value in attributes.iteritems(): + if not hasattr(self, key): + setattr(self, key, value) + def overlay(self, block_start_string=missing, block_end_string=missing, variable_start_string=missing, variable_end_string=missing, comment_start_string=missing, comment_end_string=missing, diff --git a/jinja2/ext.py b/jinja2/ext.py index 22adf82..f60b85a 100644 --- a/jinja2/ext.py +++ b/jinja2/ext.py @@ -40,6 +40,17 @@ class Extension(object): may not store environment specific data on `self`. The reason for this is that an extension can be bound to another environment (for overlays) by creating a copy and reassigning the `environment` attribute. + + As extensions are created by the environment they cannot accept any + arguments for configuration. One may want to work around that by using + a factory function, but that is not possible as extensions are identified + by their import name. The correct way to configure the extension is + storing the configuration values on the environment. Because this way the + environment ends up acting as central configuration storage the + attributes may clash which is why extensions have to ensure that the names + they choose for configuration are not too generic. ``prefix`` for example + is a terrible name, ``fragment_cache_prefix`` on the other hand is a good + name as includes the name of the extension (fragment cache). """ __metaclass__ = ExtensionRegistry @@ -74,49 +85,40 @@ class Extension(object): return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno) -class CacheExtension(Extension): - """An example extension that adds cacheable blocks.""" - tags = set(['cache']) +class InternationalizationExtension(Extension): + """This extension adds gettext support to Jinja2.""" + tags = set(['trans']) def __init__(self, environment): Extension.__init__(self, environment) - environment.globals['__cache_ext_support'] = self.cache_support - - def cache_support(self, name, timeout, caller): - """Helper for the cache_fragment function.""" - if not hasattr(environment, 'cache_support'): - return caller() - args = [name] - if timeout is not None: - args.append(timeout) - return self.environment.cache_support(generate=caller, *args) + environment.globals['_'] = contextfunction(lambda c, x: c['gettext'](x)) + environment.extend( + install_gettext_translations=self._install, + install_null_translations=self._install_null, + uninstall_gettext_translations=self._uninstall, + extract_translations=self._extract + ) - def parse(self, parser): - lineno = parser.stream.next().lineno - args = [parser.parse_expression()] - if parser.stream.current.type is 'comma': - parser.stream.next() - args.append(parser.parse_expression()) - else: - args.append(nodes.Const(None, lineno=lineno)) - body = parser.parse_statements(('name:endcache',), drop_needle=True) - return nodes.CallBlock( - nodes.Call(nodes.Name('__cache_ext_support', 'load', lineno=lineno), - args, [], None, None), [], [], body, lineno=lineno + def _install(self, translations): + self.environment.globals.update( + gettext=translations.ugettext, + ngettext=translations.ungettext ) + def _install_null(self): + self.environment.globals.update( + gettext=lambda x: x, + ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0] + ) -class InternationalizationExtension(Extension): - """This extension adds gettext support to Jinja.""" - tags = set(['trans']) + def _uninstall(self, translations): + for key in 'gettext', 'ngettext': + self.environment.globals.pop(key, None) - def __init__(self, environment): - Extension.__init__(self, environment) - environment.globals.update({ - '_': contextfunction(lambda c, x: c['gettext'](x)), - 'gettext': lambda x: x, - 'ngettext': lambda s, p, n: (s, p)[n != 1] - }) + def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS): + if isinstance(source, basestring): + source = self.environment.parse(source) + return extract_from_ast(source, gettext_functions) def parse(self, parser): """Parse a translatable tag.""" @@ -132,7 +134,7 @@ class InternationalizationExtension(Extension): parser.stream.expect('comma') # skip colon for python compatibility - if parser.ignore_colon(): + if parser.skip_colon(): break name = parser.stream.expect('name') diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 0d921a7..2519682 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -742,7 +742,8 @@ class ImportedName(Expr): class InternalName(Expr): """An internal name in the compiler. You cannot create these nodes - yourself but the parser provides a `free_identifier` method that creates + yourself but the parser provides a + :meth:`~jinja2.parser.Parser.free_identifier` method that creates a new identifier for you. This identifier is not available from the template and is not threated specially by the compiler. """ diff --git a/jinja2/parser.py b/jinja2/parser.py index 91a848b..427cb05 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -42,13 +42,20 @@ class Parser(object): 'rparen') or \ self.stream.current.test('name:in') - def ignore_colon(self): + def skip_colon(self): """If there is a colon, skip it and return `True`, else `False`.""" if self.stream.current.type is 'colon': self.stream.next() return True return False + def skip_comma(self): + """If there is a comma, skip it and return `True`, else `False`.""" + if self.stream.current.type is 'comma': + self.stream.next() + return True + return False + def free_identifier(self, lineno=None): """Return a new free identifier as :class:`~jinja2.nodes.InternalName`.""" self._last_identifier += 1 @@ -100,7 +107,7 @@ class Parser(object): can be set to `True` and the end token is removed. """ # the first token may be a colon for python compatibility - self.ignore_colon() + self.skip_colon() # in the future it would be possible to add whole code sections # by adding some sort of end of statement token and parsing those here.