Skip to content

Commit

Permalink
First extension interface documentation and updates in that interface
Browse files Browse the repository at this point in the history
--HG--
branch : trunk
  • Loading branch information
mitsuhiko committed May 8, 2008
1 parent 612b3a8 commit 023b5e9
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 134 deletions.
61 changes: 61 additions & 0 deletions docs/cache_extension.py
@@ -0,0 +1,61 @@
from jinja2 import nodes
from jinja2.ext import Extension


class CacheExtension(Extension):
"""Adds support for fragment caching to Jinja2."""
tags = set(['cache'])

def __init__(self, environment):
Extension.__init__(self, 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

def parse(self, parser):
# the first token is the token that started the tag. In our case
# we only listen to ``'cache'`` so this will be a name token with
# `cache` as value. We get the line number so that we can give
# that line number to the nodes we create by hand.
lineno = parser.stream.next().lineno

# 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()
args.append(parser.parse_expression())

# otherwise set the timeout to `None`
else:
args.append(nodes.Const(None))

# now we parse the body of the cache block up to `endcache` and
# drop the needle (which would always be `endcache` in that case)
body = parser.parse_statements(['name:endcache'], drop_needle=True)

# now return a `CallBlock` node that calls our _cache_support
# helper method on this extension.
return nodes.CallBlock(
nodes.Call(self.attr('_cache_support'), args, [], None, None),
[], [], body
).set_lineno(lineno)

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

# 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)
return rv
134 changes: 125 additions & 9 deletions docs/extensions.rst
Expand Up @@ -22,17 +22,15 @@ example creates a Jinja2 environment with the i18n extension loaded::
jinja_env = Environment(extensions=['jinja.ext.i18n'])


Built-in Extensions
-------------------

.. _i18n-extension:

i18n
~~~~
i18n Extension
--------------

The i18n extension can be 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`.
Jinja2 currently comes with one extension, the i18n extension. It can be
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
Expand Down Expand Up @@ -80,9 +78,127 @@ The usage of the `i18n` extension for template designers is covered as part
.. _gettext: http://docs.python.org/dev/library/gettext
.. _babel: http://babel.edgewall.org/


.. _writing-extensions:

Writing Extensions
------------------

TODO
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
useful, another one would be fragment caching.

Example Extension
~~~~~~~~~~~~~~~~~

The following example implements a `cache` tag for Jinja2:

.. 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::

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)

.. _Werkzeug: http://werkzeug.pocoo.org/

Extension API
~~~~~~~~~~~~~

Extensions always have to extend the :class:`jinja2.ext.Extension` class:

.. autoclass:: Extension
:members: parse, attr

.. attribute:: identifier

The identifier of the extension. This is always the true import name
of the extension class and must not be changed.

.. attribute:: tags

If the extension implements custom tags this is a set of tag names
the extension is listening for.

Parser API
~~~~~~~~~~

The parser passed to :meth:`Extension.parse` provides ways to parse
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

.. attribute:: filename

The filename of the template the parser processes. This is **not**
the load name of the template which is unavailable at parsing time.
For templates that were not loaded form the file system this is
`None`.

.. attribute:: stream

The current :class:`~jinja2.lexer.TokenStream`

.. autoclass:: jinja2.lexer.TokenStream
:members: push, look, eos, skip, next, expect

.. attribute:: current

The current :class:`~jinja2.lexer.Token`.

.. autoclass:: jinja2.lexer.Token
:members: test, test_any

.. attribute:: lineno

The line number of the token

.. attribute:: type

The type of the token. This string is interned so you may compare
it with arbitrary strings using the `is` operator.

.. attribute:: value

The value of the token.

AST
~~~

The AST (Abstract Syntax Tree) is used to represent a template after parsing.
It's build of nodes that the compiler then converts into executable Python
code objects. Extensions that provide custom statements can return nodes to
execute custom Python code.

The list below describes all nodes that are currently available. The AST may
change between Jinja2 versions but will stay backwards compatible.

For more information have a look at the repr of :meth:`jinja2.Environment.parse`.

.. module:: jinja2.nodes

.. jinjanodes::

.. autoexception:: Impossible
53 changes: 43 additions & 10 deletions docs/jinjaext.py
Expand Up @@ -24,6 +24,19 @@
from jinja2 import Environment, FileSystemLoader


def parse_rst(state, content_offset, doc):
node = nodes.section()
# hack around title style bookkeeping
surrounding_title_styles = state.memo.title_styles
surrounding_section_level = state.memo.section_level
state.memo.title_styles = []
state.memo.section_level = 0
state.nested_parse(doc, content_offset, node, match_titles=1)
state.memo.title_styles = surrounding_title_styles
state.memo.section_level = surrounding_section_level
return node.children


class JinjaStyle(Style):
title = 'Jinja Style'
default_style = ""
Expand Down Expand Up @@ -136,24 +149,44 @@ def jinja_changelog(dirname, arguments, options, content, lineno,
doc.append(line.rstrip(), '<jinjaext>')
finally:
changelog.close()
node = nodes.section()
# hack around title style bookkeeping
surrounding_title_styles = state.memo.title_styles
surrounding_section_level = state.memo.section_level
state.memo.title_styles = []
state.memo.section_level = 0
state.nested_parse(doc, content_offset, node, match_titles=1)
state.memo.title_styles = surrounding_title_styles
state.memo.section_level = surrounding_section_level
return node.children
return parse_rst(state, content_offset, doc)


from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS
jinja_filters = dump_functions(DEFAULT_FILTERS)
jinja_tests = dump_functions(DEFAULT_TESTS)


def jinja_nodes(dirname, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
from jinja2.nodes import Node
doc = ViewList()
def walk(node, indent):
p = ' ' * indent
sig = ', '.join(node.fields)
doc.append(p + '.. autoclass:: %s(%s)' % (node.__name__, sig), '')
if node.abstract:
members = []
for key, name in node.__dict__.iteritems():
if not key.startswith('_') and callable(name):
members.append(key)
if members:
members.sort()
doc.append('%s :members: %s' % (p, ', '.join(members)), '')
else:
doc.append('', '')
doc.append(p + ' :Node type: :class:`%s`' % node.__base__.__name__, '')
doc.append('', '')
children = node.__subclasses__()
children.sort(key=lambda x: x.__name__.lower())
for child in children:
walk(child, indent)
walk(Node, 0)
return parse_rst(state, content_offset, doc)


def setup(app):
app.add_directive('jinjafilters', jinja_filters, 0, (0, 0, 0))
app.add_directive('jinjatests', jinja_tests, 0, (0, 0, 0))
app.add_directive('jinjachangelog', jinja_changelog, 0, (0, 0, 0))
app.add_directive('jinjanodes', jinja_nodes, 0, (0, 0, 0))
33 changes: 32 additions & 1 deletion jinja2/compiler.py
Expand Up @@ -41,6 +41,8 @@

def generate(node, environment, name, filename, stream=None):
"""Generate the python source for a node tree."""
if not isinstance(node, nodes.Template):
raise TypeError('Can\'t compile non template nodes')
generator = CodeGenerator(environment, name, filename, stream)
generator.visit(node)
if stream is None:
Expand Down Expand Up @@ -305,6 +307,9 @@ def __init__(self, environment, name, filename, stream=None):
self.filename = filename
self.stream = stream

# aliases for imports
self.import_aliases = {}

# a registry for all blocks. Because blocks are moved out
# into the global python scope they are registered here
self.blocks = {}
Expand Down Expand Up @@ -558,7 +563,6 @@ def visit_Template(self, node, frame=None):
from jinja2.runtime import __all__ as exported
self.writeline('from __future__ import division')
self.writeline('from jinja2.runtime import ' + ', '.join(exported))
self.writeline('name = %r' % self.name)

# do we have an extends tag at all? If not, we can save some
# overhead by just not processing any inheritance code.
Expand All @@ -572,6 +576,21 @@ def visit_Template(self, node, frame=None):
self.name)
self.blocks[block.name] = block

# find all imports and import them
for import_ in node.find_all(nodes.ImportedName):
if import_.importname not in self.import_aliases:
imp = import_.importname
self.import_aliases[imp] = alias = self.temporary_identifier()
if '.' in imp:
module, obj = imp.rsplit('.', 1)
self.writeline('from %s import %s as %s' %
(module, obj, alias))
else:
self.writeline('import %s as %s' % (imp, alias))

# add the load name
self.writeline('name = %r' % self.name)

# generate the root render function.
self.writeline('def root(context, environment=environment):', extra=1)

Expand Down Expand Up @@ -1070,6 +1089,18 @@ def visit_MarkSafe(self, node, frame):
self.visit(node.expr, frame)
self.write(')')

def visit_EnvironmentAttribute(self, node, frame):
self.write('environment.' + node.name)

def visit_ExtensionAttribute(self, node, frame):
self.write('environment.extensions[%r].%s' % (node.identifier, node.attr))

def visit_ImportedName(self, node, frame):
self.write(self.import_aliases[node.importname])

def visit_InternalName(self, node, frame):
self.write(node.name)

def visit_Const(self, node, frame):
val = node.value
if isinstance(val, float):
Expand Down
14 changes: 7 additions & 7 deletions jinja2/environment.py
Expand Up @@ -51,13 +51,13 @@ def create_cache(size):

def load_extensions(environment, extensions):
"""Load the extensions from the list and bind it to the environment.
Returns a new list of instanciated environments.
Returns a dict of instanciated environments.
"""
result = []
result = {}
for extension in extensions:
if isinstance(extension, basestring):
extension = import_string(extension)
result.append(extension(environment))
result[extension.identifier] = extension(environment)
return result


Expand Down Expand Up @@ -255,11 +255,11 @@ def overlay(self, block_start_string=missing, block_end_string=missing,
if cache_size is not missing:
rv.cache = create_cache(cache_size)

rv.extensions = []
for extension in self.extensions:
rv.extensions.append(extension.bind(self))
rv.extensions = {}
for key, value in self.extensions.iteritems():
rv.extensions[key] = value.bind(rv)
if extensions is not missing:
rv.extensions.extend(load_extensions(extensions))
rv.extensions.update(load_extensions(extensions))

return _environment_sanity_check(rv)

Expand Down

0 comments on commit 023b5e9

Please sign in to comment.