more updates on the extension API
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 8 May 2008 21:57:56 +0000 (23:57 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 8 May 2008 21:57:56 +0000 (23:57 +0200)
--HG--
branch : trunk

docs/_static/metal.png [new file with mode: 0644]
docs/_static/style.css
docs/api.rst
docs/cache_extension.py
docs/extensions.rst
jinja2/environment.py
jinja2/ext.py
jinja2/nodes.py
jinja2/parser.py

diff --git a/docs/_static/metal.png b/docs/_static/metal.png
new file mode 100644 (file)
index 0000000..97166f1
Binary files /dev/null and b/docs/_static/metal.png differ
index 46748d052355907dfd6628e2a19d83bd3c2715c1..097b229355ac0a53e293d0354a498384b560162b 100644 (file)
@@ -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;
 }
 
index 82bb531076aa6015e921e70887644c73729e854b..4ce79ff686dd0998bb3cfee64112a2e35be4f163 100644 (file)
@@ -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
 
index 19839fe932f0fe78809eb98ab5b9783ff1619e3f..c9ed92c837dfeaafd0d3b3e15e37180a1109921c 100644 (file)
@@ -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
index 094afa9e8e7c1fca240dff5c9c05dc7512ac8118..7be9f128dbee207c1c6ccedcadd09b0f5331bec6 100644 (file)
@@ -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 <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 <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 <i18n-in-templates>`.
 
-
 .. _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
 
index e771dd6d0efab4b2a6f5a5c4770971fd76670f7e..2fbe217b8dd844adc4fea2039e6c7448f899034e 100644 (file)
@@ -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 <writing-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,
index 22adf8289652e61821af543e833d8cb038aa1fbf..f60b85a17d28f69e54276ee1ea0d0cee6c20e8df 100644 (file)
@@ -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')
index 0d921a7b5ea507d7bcd5ba866be19208c6e998cf..25196826d3f6e7972cc91a6fd30f546ee5ec6695 100644 (file)
@@ -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.
     """
index 91a848b6d9338087808bd4dfb18df6bcecb1a6d4..427cb058dac323e082fd9c0efaa5c1bb834ffd1f 100644 (file)
@@ -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.