Permalink
Browse files

First extension interface documentation and updates in that interface

--HG--
branch : trunk
  • Loading branch information...
1 parent 612b3a8 commit 023b5e9212155169d5ee5f979e3168b4196268f9 @mitsuhiko mitsuhiko committed May 8, 2008
Showing with 565 additions and 134 deletions.
  1. +61 −0 docs/cache_extension.py
  2. +125 −9 docs/extensions.rst
  3. +43 −10 docs/jinjaext.py
  4. +32 −1 jinja2/compiler.py
  5. +7 −7 jinja2/environment.py
  6. +45 −12 jinja2/ext.py
  7. +9 −11 jinja2/lexer.py
  8. +195 −59 jinja2/nodes.py
  9. +48 −25 jinja2/parser.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
View
@@ -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
@@ -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
View
@@ -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 = ""
@@ -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))
View
@@ -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:
@@ -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 = {}
@@ -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.
@@ -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)
@@ -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):
View
@@ -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
@@ -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)
Oops, something went wrong. Retry.

0 comments on commit 023b5e9

Please sign in to comment.