Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
more updates on the extension API
--HG--
branch : trunk
  • Loading branch information
mitsuhiko committed May 8, 2008
1 parent 023b5e9 commit 762079c
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 115 deletions.
Binary file added docs/_static/metal.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions docs/_static/style.css
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Expand Up @@ -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

Expand Down
39 changes: 18 additions & 21 deletions docs/cache_extension.py
Expand Up @@ -2,19 +2,18 @@
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
Expand All @@ -26,13 +25,10 @@ def parse(self, parser):
# 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))

Expand All @@ -49,13 +45,14 @@ def parse(self, parser):

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
104 changes: 52 additions & 52 deletions docs/extensions.rst
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -43,47 +78,25 @@ 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:

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
Expand All @@ -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/

Expand Down Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions jinja2/environment.py
Expand Up @@ -222,6 +222,15 @@ def __init__(self,

_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,
Expand Down
74 changes: 38 additions & 36 deletions jinja2/ext.py
Expand Up @@ -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

Expand Down Expand Up @@ -74,49 +85,40 @@ def attr(self, name, lineno=None):
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."""
Expand All @@ -132,7 +134,7 @@ def parse(self, parser):
parser.stream.expect('comma')

# skip colon for python compatibility
if parser.ignore_colon():
if parser.skip_colon():
break

name = parser.stream.expect('name')
Expand Down
3 changes: 2 additions & 1 deletion jinja2/nodes.py
Expand Up @@ -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.
"""
Expand Down

0 comments on commit 762079c

Please sign in to comment.