inheritance uses a less awkward hack for contexts now and subclassing templates is...
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 24 Apr 2008 19:54:44 +0000 (21:54 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 24 Apr 2008 19:54:44 +0000 (21:54 +0200)
--HG--
branch : trunk

13 files changed:
jinja2/__init__.py
jinja2/compiler.py
jinja2/debug.py
jinja2/environment.py
jinja2/ext.py
jinja2/i18n.py
jinja2/lexer.py
jinja2/loaders.py
jinja2/nodes.py
jinja2/runtime.py
jinja2/utils.py
setup.py
tests/test_syntax.py

index 7dbe329c6342922c0974fef8446cd3a691dfa05c..8141aadfa7ccccb925cba32ba206f2a6ffb6e825 100644 (file)
@@ -3,62 +3,46 @@
     jinja2
     ~~~~~~
 
-    Jinja is a `sandboxed`_ template engine written in pure Python. It
-    provides a `Django`_ like non-XML syntax and compiles templates into
-    executable python code. It's basically a combination of Django templates
-    and python code.
+    Jinja2 is a template engine written in pure Python.  It provides a
+    Django inspired non-XML syntax but supports inline expressions and
+    an optional sandboxed environment.
 
     Nutshell
     --------
 
-    Here a small example of a Jinja template::
+    Here a small example of a Jinja2 template::
 
         {% extends 'base.html' %}
         {% block title %}Memberlist{% endblock %}
         {% block content %}
           <ul>
           {% for user in users %}
-            <li><a href="{{ user.url|e }}">{{ user.username|e }}</a></li>
+            <li><a href="{{ user.url }}">{{ user.username }}</a></li>
           {% endfor %}
           </ul>
         {% endblock %}
 
-    Philosophy
-    ----------
 
-    Application logic is for the controller but don't try to make the life
-    for the template designer too hard by giving him too few functionality.
-
-    For more informations visit the new `jinja webpage`_ and `documentation`_.
-
-    Note
-    ----
-
-    This is the Jinja 1.0 release which is completely incompatible with the
-    old "pre 1.0" branch. The old branch will still receive security updates
-    and bugfixes but the 1.0 branch will be the only version that receives
-    support.
-
-    If you have an application that uses Jinja 0.9 and won't be updated in
-    the near future the best idea is to ship a Jinja 0.9 checkout together
-    with the application.
-
-    The `Jinja tip`_ is installable via `easy_install` with ``easy_install
-    Jinja==dev``.
-
-    .. _sandboxed: http://en.wikipedia.org/wiki/Sandbox_(computer_security)
-    .. _Django: http://www.djangoproject.com/
-    .. _jinja webpage: http://jinja.pocoo.org/
-    .. _documentation: http://jinja.pocoo.org/documentation/index.html
-    .. _Jinja tip: http://dev.pocoo.org/hg/jinja-main/archive/tip.tar.gz#egg=Jinja-dev
-
-
-    :copyright: 2008 by Armin Ronacher.
+    :copyright: 2008 by Armin Ronacher, Christoph Hack.
     :license: BSD, see LICENSE for more details.
 """
-from jinja2.environment import Environment
+__docformat__ = 'restructuredtext en'
+try:
+    __version__ = __import__('pkg_resources') \
+        .get_distribution('Jinja2').version
+except:
+    __version__ = 'unknown'
+
+# high level interface
+from jinja2.environment import Environment, Template
+
+# loaders
 from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \
-     DictLoader
+     DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader
+
+# undefined types
 from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined
+
+# decorators and public utilities
 from jinja2.filters import environmentfilter, contextfilter
-from jinja2.utils import Markup, escape, contextfunction
+from jinja2.utils import Markup, escape, environmentfunction, contextfunction
index 871728f48d1a82b2b0d844ed111c60ed29c2f575..9bf1e4d61a69984a64b5b7cbfd83f7cf9e5fec7b 100644 (file)
@@ -475,14 +475,12 @@ class CodeGenerator(NodeVisitor):
             self.blocks[block.name] = block
 
         # generate the root render function.
-        self.writeline('def root(globals, environment=environment'
-                       ', standalone=False):', extra=1)
-        self.indent()
-        self.writeline('context = TemplateContext(environment, globals, %r, '
-                       'blocks, standalone)' % self.name)
+        self.writeline('def root(context, environment=environment'
+                       '):', extra=1)
         if have_extends:
-            self.writeline('parent_root = None')
-        self.outdent()
+            self.indent()
+            self.writeline('parent_template = None')
+            self.outdent()
 
         # process the root
         frame = Frame()
@@ -490,7 +488,6 @@ class CodeGenerator(NodeVisitor):
         frame.toplevel = frame.rootlevel = True
         self.indent()
         self.pull_locals(frame, indent=False)
-        self.writeline('yield context')
         self.blockvisit(node.body, frame, indent=False)
         self.outdent()
 
@@ -498,14 +495,13 @@ class CodeGenerator(NodeVisitor):
         if have_extends:
             if not self.has_known_extends:
                 self.indent()
-                self.writeline('if parent_root is not None:')
+                self.writeline('if parent_template is not None:')
             self.indent()
-            self.writeline('stream = parent_root(context)')
-            self.writeline('stream.next()')
-            self.writeline('for event in stream:')
+            self.writeline('for event in parent_template.'
+                           'root_render_func(context):')
             self.indent()
             self.writeline('yield event')
-            self.outdent(1 + self.has_known_extends)
+            self.outdent(2 + (not self.has_known_extends))
 
         # at this point we now have the blocks collected and can visit them too.
         for name, block in self.blocks.iteritems():
@@ -513,7 +509,8 @@ class CodeGenerator(NodeVisitor):
             block_frame.inspect(block.body)
             block_frame.block = name
             block_frame.identifiers.add_special('super')
-            block_frame.name_overrides['super'] = 'context.super(%r)' % name
+            block_frame.name_overrides['super'] = 'context.super(%r, ' \
+                'block_%s)' % (name, name)
             self.writeline('def block_%s(context, environment=environment):'
                            % name, block, 1)
             self.pull_locals(block_frame)
@@ -524,9 +521,8 @@ class CodeGenerator(NodeVisitor):
                        extra=1)
 
         # add a function that returns the debug info
-        self.writeline('def get_debug_info():', extra=1)
-        self.indent()
-        self.writeline('return %r' % self.debug_info)
+        self.writeline('debug_info = %r' % '&'.join('%s=%s' % x for x
+                                                    in self.debug_info))
 
     def visit_Block(self, node, frame):
         """Call a block and register it for the template."""
@@ -537,7 +533,7 @@ class CodeGenerator(NodeVisitor):
             if self.has_known_extends:
                 return
             if self.extends_so_far > 0:
-                self.writeline('if parent_root is None:')
+                self.writeline('if parent_template is None:')
                 self.indent()
                 level += 1
         self.writeline('for event in context.blocks[%r][-1](context):' % node.name)
@@ -565,7 +561,7 @@ class CodeGenerator(NodeVisitor):
             # time too, but i welcome it not to confuse users by throwing the
             # same error at different times just "because we can".
             if not self.has_known_extends:
-                self.writeline('if parent_root is not None:')
+                self.writeline('if parent_template is not None:')
                 self.indent()
             self.writeline('raise TemplateRuntimeError(%r)' %
                            'extended multiple times')
@@ -576,9 +572,15 @@ class CodeGenerator(NodeVisitor):
                 raise CompilerExit()
             self.outdent()
 
-        self.writeline('parent_root = environment.get_template(', node, 1)
+        self.writeline('parent_template = environment.get_template(', node, 1)
         self.visit(node.template, frame)
-        self.write(', %r).root_render_func' % self.name)
+        self.write(', %r)' % self.name)
+        self.writeline('for name, parent_block in parent_template.'
+                       'blocks.iteritems():')
+        self.indent()
+        self.writeline('context.blocks.setdefault(name, []).'
+                       'insert(0, parent_block)')
+        self.outdent()
 
         # if this extends statement was in the root level we can take
         # advantage of that information and simplify the generated code
@@ -601,11 +603,17 @@ class CodeGenerator(NodeVisitor):
             self.write(')')
             return
 
-        self.writeline('included_stream = environment.get_template(', node)
+        self.writeline('included_template = environment.get_template(', node)
         self.visit(node.template, frame)
-        self.write(').root_render_func(context, standalone=True)')
-        self.writeline('included_context = included_stream.next()')
-        self.writeline('for event in included_stream:')
+        self.write(')')
+        if frame.toplevel:
+            self.writeline('included_context = included_template.new_context('
+                           'context.get_root())')
+            self.writeline('for event in included_template.root_render_func('
+                           'included_context):')
+        else:
+            self.writeline('for event in included_template.root_render_func('
+                           'included_template.new_context(context.get_root())):')
         self.indent()
         if frame.buffer is None:
             self.writeline('yield event')
@@ -796,7 +804,7 @@ class CodeGenerator(NodeVisitor):
         # so that they don't appear in the output.
         outdent_later = False
         if frame.toplevel and self.extends_so_far != 0:
-            self.writeline('if parent_root is None:')
+            self.writeline('if parent_template is None:')
             self.indent()
             outdent_later = True
 
index 75d70b2d3043c58c1c460b11d3f5e5ed6f4a42b7..d0157bb975fb2985178f9e24ed88fccbbc9ddfbf 100644 (file)
@@ -38,7 +38,11 @@ def fake_exc_info(exc_info, filename, lineno, tb_back=None):
 
     # figure the real context out
     real_locals = tb.tb_frame.f_locals.copy()
-    locals = dict(real_locals.get('context', {}))
+    ctx = real_locals.get('context')
+    if ctx:
+        locals = ctx.get_all()
+    else:
+        locals = {}
     for name, value in real_locals.iteritems():
         if name.startswith('l_'):
             locals[name[2:]] = value
index 5b705e40c65af30213334fb4778c3d7387975cf1..9807184b735fb1199bfdcd0bff26368d5e520caa 100644 (file)
@@ -13,12 +13,56 @@ from jinja2.lexer import Lexer
 from jinja2.parser import Parser
 from jinja2.optimizer import optimize
 from jinja2.compiler import generate
-from jinja2.runtime import Undefined
+from jinja2.runtime import Undefined, TemplateContext
 from jinja2.debug import translate_exception
-from jinja2.utils import import_string
+from jinja2.utils import import_string, LRUCache
 from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE
 
 
+# for direct template usage we have up to ten living environments
+_spontaneous_environments = LRUCache(10)
+
+
+def _get_spontaneous_environment(*args):
+    """Return a new spontaneus environment.  A spontaneus environment is an
+    unnamed and unaccessable (in theory) environment that is used for
+    template generated from a string and not from the file system.
+    """
+    try:
+        env = _spontaneous_environments.get(args)
+    except TypeError:
+        return Environment(*args)
+    if env is not None:
+        return env
+    _spontaneous_environments[args] = env = Environment(*args)
+    return env
+
+
+def template_from_code(environment, code, globals, uptodate=None,
+                       template_class=None):
+    """Generate a new template object from code.  It's used in the
+    template constructor and the loader `load` implementation.
+    """
+    t = object.__new__(template_class or environment.template_class)
+    namespace = {
+        'environment':          environment,
+        '__jinja_template__':   t
+    }
+    exec code in namespace
+    t.environment = environment
+    t.name = namespace['name']
+    t.filename = code.co_filename
+    t.root_render_func = namespace['root']
+    t.blocks = namespace['blocks']
+    t.globals = globals
+
+    # debug and loader helpers
+    t._debug_info = namespace['debug_info']
+    t._uptodate = uptodate
+
+    return t
+
+
 class Environment(object):
     """The Jinja environment.
 
@@ -93,11 +137,14 @@ class Environment(object):
         self.comment_end_string = comment_end_string
         self.line_statement_prefix = line_statement_prefix
         self.trim_blocks = trim_blocks
+
+        # load extensions
         self.extensions = []
         for extension in extensions:
             if isinstance(extension, basestring):
                 extension = import_string(extension)
-            self.extensions.append(extension(self))
+            # extensions are instanciated early but initalized later.
+            self.extensions.append(object.__new__(extension))
 
         # runtime information
         self.undefined = undefined
@@ -108,8 +155,6 @@ class Environment(object):
         self.filters = DEFAULT_FILTERS.copy()
         self.tests = DEFAULT_TESTS.copy()
         self.globals = DEFAULT_NAMESPACE.copy()
-        for extension in self.extensions:
-            extension.update_globals(self.globals)
 
         # set the loader provided
         self.loader = loader
@@ -117,6 +162,10 @@ class Environment(object):
         # create lexer
         self.lexer = Lexer(self)
 
+        # initialize extensions
+        for extension in self.extensions:
+            extension.__init__(self)
+
     def subscribe(self, obj, argument):
         """Get an item or attribute of an object."""
         try:
@@ -180,11 +229,11 @@ class Environment(object):
         globals = self.make_globals(globals)
         return self.loader.load(self, name, globals)
 
-    def from_string(self, source, globals=None):
+    def from_string(self, source, globals=None, template_class=None):
         """Load a template from a string."""
         globals = self.make_globals(globals)
-        return Template(self, self.compile(source, globals=globals),
-                        globals)
+        return template_from_code(self, self.compile(source, globals=globals),
+                                  globals, template_class)
 
     def make_globals(self, d):
         """Return a dict for the globals."""
@@ -194,24 +243,57 @@ class Environment(object):
 
 
 class Template(object):
-    """Represents a template."""
-
-    def __init__(self, environment, code, globals, uptodate=None):
-        namespace = {
-            'environment':          environment,
-            '__jinja_template__':   self
-        }
-        exec code in namespace
-        self.environment = environment
-        self.name = namespace['name']
-        self.filename = code.co_filename
-        self.root_render_func = namespace['root']
-        self.blocks = namespace['blocks']
-        self.globals = globals
-
-        # debug and loader helpers
-        self._get_debug_info = namespace['get_debug_info']
-        self._uptodate = uptodate
+    """The central template object.  This class represents a compiled template
+    and is used to evaluate it.
+
+    Normally the template object is generated from an `Environment` but it
+    also has a constructor that makes it possible to create a template
+    instance directly using the constructor.  It takes the same arguments as
+    the environment constructor but it's not possible to specify a loader.
+
+    Every template object has a few methods and members that are guaranteed
+    to exist.  However it's important that a template object should be
+    considered immutable.  Modifications on the object are not supported.
+
+    Template objects created from the constructor rather than an environment
+    do have an `environment` attribute that points to a temporary environment
+    that is probably shared with other templates created with the constructor
+    and compatible settings.
+
+    >>> template = Template('Hello {{ name }}!')
+    >>> template.render(name='John Doe')
+    u'Hello John Doe!'
+
+    >>> stream = template.stream(name='John Doe')
+    >>> stream.next()
+    u'Hello John Doe!'
+    >>> stream.next()
+    Traceback (most recent call last):
+        ...
+    StopIteration
+    """
+
+    def __new__(cls, source,
+                block_start_string='{%',
+                block_end_string='%}',
+                variable_start_string='{{',
+                variable_end_string='}}',
+                comment_start_string='{#',
+                comment_end_string='#}',
+                line_statement_prefix=None,
+                trim_blocks=False,
+                optimized=True,
+                undefined=Undefined,
+                extensions=(),
+                finalize=unicode):
+        # make sure extensions are hashable
+        extensions = tuple(extensions)
+        env = _get_spontaneous_environment(
+            block_start_string, block_end_string, variable_start_string,
+            variable_end_string, comment_start_string, comment_end_string,
+            line_statement_prefix, trim_blocks, optimized, undefined,
+            None, extensions, finalize)
+        return env.from_string(source, template_class=cls)
 
     def render(self, *args, **kwargs):
         """Render the template into a string."""
@@ -249,42 +331,53 @@ class Template(object):
                                      'With an enabled optimizer this '
                                      'will lead to unexpected results.' %
                     (plural, ', '.join(overrides), plural or ' a', plural))
-        gen = self.root_render_func(dict(self.globals, **context))
-        # skip the first item which is a reference to the context
-        gen.next()
 
         try:
-            for event in gen:
+            for event in self.root_render_func(self.new_context(context)):
                 yield event
         except:
-            exc_info = translate_exception(sys.exc_info())
-            raise exc_info[0], exc_info[1], exc_info[2]
+            exc_type, exc_value, tb = translate_exception(sys.exc_info())
+            raise exc_type, exc_value, tb
+
+    def new_context(self, vars):
+        """Create a new template context for this template."""
+        return TemplateContext(self.environment, dict(self.globals, **vars),
+                               self.name, self.blocks)
 
     def get_corresponding_lineno(self, lineno):
         """Return the source line number of a line number in the
         generated bytecode as they are not in sync.
         """
-        for template_line, code_line in reversed(self._get_debug_info()):
+        for template_line, code_line in reversed(self.debug_info):
             if code_line <= lineno:
                 return template_line
         return 1
 
     @property
     def is_up_to_date(self):
-        """Check if the template is still up to date."""
+        """If this variable is `False` there is a newer version available."""
         if self._uptodate is None:
             return True
         return self._uptodate()
 
+    @property
+    def debug_info(self):
+        """The debug info mapping."""
+        return [tuple(map(int, x.split('='))) for x in
+                self._debug_info.split('&')]
+
     def __repr__(self):
         return '<%s %r>' % (
             self.__class__.__name__,
-            self.name
+            self.name or '<from string>'
         )
 
 
 class TemplateStream(object):
-    """Wraps a genererator for outputing template streams."""
+    """This class wraps a generator returned from `Template.generate` so that
+    it's possible to buffer multiple elements so that it's possible to return
+    them from a WSGI application which flushes after each iteration.
+    """
 
     def __init__(self, gen):
         self._gen = gen
@@ -300,31 +393,37 @@ class TemplateStream(object):
         """Enable buffering. Buffer `size` items before yielding them."""
         if size <= 1:
             raise ValueError('buffer size too small')
-        self.buffered = True
 
-        def buffering_next():
+        def generator():
             buf = []
             c_size = 0
             push = buf.append
             next = self._gen.next
 
-            try:
-                while 1:
-                    item = next()
-                    if item:
-                        push(item)
+            while 1:
+                try:
+                    while 1:
+                        push(next())
                         c_size += 1
-                    if c_size >= size:
-                        raise StopIteration()
-            except StopIteration:
-                if not c_size:
-                    raise
-            return u''.join(buf)
+                        if c_size >= size:
+                            raise StopIteration()
+                except StopIteration:
+                    if not c_size:
+                        raise
+                yield u''.join(buf)
+                del buf[:]
+                c_size = 0
 
-        self._next = buffering_next
+        self.buffered = True
+        self._next = generator().next
 
     def __iter__(self):
         return self
 
     def next(self):
         return self._next()
+
+
+# hook in default template class.  if anyone reads this comment: ignore that
+# it's possible to use custom templates ;-)
+Environment.template_class = Template
index 947150d30009c245c01d1a69cec9310edf20b6f7..acce835f480963723a5696f43584d5780a938bd9 100644 (file)
@@ -25,10 +25,6 @@ class Extension(object):
     def __init__(self, environment):
         self.environment = environment
 
-    def update_globals(self, globals):
-        """Called to inject runtime variables into the globals."""
-        pass
-
     def parse(self, parser):
         """Called if one of the tags matched."""
 
@@ -37,6 +33,13 @@ class CacheExtension(Extension):
     """An example extension that adds cacheable blocks."""
     tags = set(['cache'])
 
+    def __init__(self, environment):
+        Extension.__init__(self, environment)
+        def dummy_cache_support(name, timeout=None, caller=None):
+            if caller is not None:
+                return caller()
+        environment.globals['cache_support'] = dummy_cache_support
+
     def parse(self, parser):
         lineno = parser.stream.next().lineno
         args = [parser.parse_expression()]
index 9125ee9b96c08ec1f4e05b68926d0628ee5a4072..6718962cfbe320802f941a7129af6a885470b944 100644 (file)
@@ -97,9 +97,9 @@ def babel_extract(fileobj, keywords, comment_tags, options):
 class TransExtension(Extension):
     tags = set(['trans'])
 
-    def update_globals(self, globals):
-        """Inject noop translation functions."""
-        globals.update({
+    def __init__(self, environment):
+        Extension.__init__(self, environment)
+        environment.globals.update({
             '_':        lambda x: x,
             'gettext':  lambda x: x,
             'ngettext': lambda s, p, n: (s, p)[n != 1]
index beb9866696593d352605359a863a906f0472d40e..5576ac1281e2e094383b66829441f480be58ab04 100644 (file)
@@ -188,14 +188,14 @@ class LexerMeta(type):
     """
 
     def __call__(cls, environment):
-        key = hash((environment.block_start_string,
-                    environment.block_end_string,
-                    environment.variable_start_string,
-                    environment.variable_end_string,
-                    environment.comment_start_string,
-                    environment.comment_end_string,
-                    environment.line_statement_prefix,
-                    environment.trim_blocks))
+        key = (environment.block_start_string,
+               environment.block_end_string,
+               environment.variable_start_string,
+               environment.variable_end_string,
+               environment.comment_start_string,
+               environment.comment_end_string,
+               environment.line_statement_prefix,
+               environment.trim_blocks)
 
         # use the cached lexer if possible
         if key in _lexer_cache:
index dc3ccfb709cf257e1915d5df539a5546a9966251..395816920b90ae1d70284d636961d4d023697126 100644 (file)
@@ -10,7 +10,7 @@
 """
 from os import path
 from jinja2.exceptions import TemplateNotFound
-from jinja2.environment import Template
+from jinja2.environment import template_from_code
 from jinja2.utils import LRUCache
 
 
@@ -30,8 +30,7 @@ def split_template_path(template):
 
 
 class BaseLoader(object):
-    """
-    Baseclass for all loaders.  Subclass this and override `get_source` to
+    """Baseclass for all loaders.  Subclass this and override `get_source` to
     implement a custom loading mechanism.
 
     The environment provides a `get_template` method that will automatically
@@ -82,7 +81,7 @@ class BaseLoader(object):
 
         source, filename, uptodate = self.get_source(environment, name)
         code = environment.compile(source, name, filename, globals)
-        template = Template(environment, code, globals, uptodate)
+        template = template_from_code(environment, code, globals, uptodate)
         if self.cache is not None:
             self.cache[name] = template
         return template
@@ -144,3 +143,63 @@ class DictLoader(BaseLoader):
         if template in self.mapping:
             return self.mapping[template], None, None
         raise TemplateNotFound(template)
+
+
+class FunctionLoader(BaseLoader):
+    """A loader that is passed a function which does the loading.  The
+    function has to work like a `get_source` method but the return value for
+    not existing templates may be `None` instead of a `TemplateNotFound`
+    exception.
+    """
+
+    def __init__(self, load_func, cache_size=50, auto_reload=True):
+        BaseLoader.__init__(self, cache_size, auto_reload)
+        self.load_func = load_func
+
+    def get_source(self, environment, template):
+        rv = self.load_func(environment, template)
+        if rv is None:
+            raise TemplateNotFound(template)
+        return rv
+
+
+class PrefixLoader(BaseLoader):
+    """A loader that is passed a dict of loaders where each loader is bound
+    to a prefix.  The caching is independent of the actual loaders so the
+    per loader cache settings are ignored.  The prefix is delimited from the
+    template by a slash.
+    """
+
+    def __init__(self, mapping, delimiter='/', cache_size=50,
+                 auto_reload=True):
+        BaseLoader.__init__(self, cache_size, auto_reload)
+        self.mapping = mapping
+        self.delimiter = delimiter
+
+    def get_source(self, environment, template):
+        try:
+            prefix, template = template.split(self.delimiter, 1)
+            loader = self.mapping[prefix]
+        except (ValueError, KeyError):
+            raise TemplateNotFound(template)
+        return loader.get_source(environment, template)
+
+
+class ChoiceLoader(BaseLoader):
+    """This loader works like the `PrefixLoader` just that no prefix is
+    specified.  If a template could not be found by one loader the next one
+    is tried.  Like for the `PrefixLoader` the cache settings of the actual
+    loaders don't matter as the choice loader does the caching.
+    """
+
+    def __init__(self, loaders, cache_size=50, auto_reload=True):
+        BaseLoader.__init__(self, cache_size, auto_reload)
+        self.loaders = loaders
+
+    def get_source(self, environment, template):
+        for loader in self.loaders:
+            try:
+                return loader.get_source(environment, template)
+            except TemplateNotFound:
+                pass
+        raise TemplateNotFound(template)
index 7688f6284d1f1f3c931fecd246ccdf23af060bdf..62686c49cfe396f494dd0764fbdb6a27e280bf4b 100644 (file)
@@ -452,11 +452,13 @@ class Call(Expr):
         obj = self.node.as_const()
 
         # don't evaluate context functions
-        if type(obj) is FunctionType and \
-           getattr(obj, 'contextfunction', False):
-            raise Impossible()
-
         args = [x.as_const() for x in self.args]
+        if type(obj) is FunctionType:
+            if getattr(obj, 'contextfunction', False):
+                raise Impossible()
+            elif obj.environmentfunction:
+                args.insert(0, self.environment)
+
         kwargs = dict(x.as_const() for x in self.kwargs)
         if self.dyn_args is not None:
             try:
index 8cc1b2ffa1f8948b3b178be49deb5b5475d8870d..9018d525074a730d6d8cf00440d6da8cf8bf3456 100644 (file)
@@ -8,10 +8,6 @@
     :copyright: Copyright 2008 by Armin Ronacher.
     :license: GNU GPL.
 """
-try:
-    from collections import defaultdict
-except ImportError:
-    defaultdict = None
 from types import FunctionType
 from jinja2.utils import Markup, partial
 from jinja2.exceptions import UndefinedError
@@ -21,62 +17,72 @@ __all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
            'Macro', 'IncludedTemplate', 'Markup']
 
 
-class TemplateContext(dict):
+class TemplateContext(object):
     """Holds the variables of the local template or of the global one.  It's
     not save to use this class outside of the compiled code.  For example
     update and other methods will not work as they seem (they don't update
     the exported variables for example).
     """
 
-    def __init__(self, environment, globals, name, blocks, standalone):
-        dict.__init__(self, globals)
+    def __init__(self, environment, parent, name, blocks):
+        self.parent = parent
+        self.vars = {}
         self.environment = environment
-        self.exported = set()
+        self.exported_vars = set()
         self.name = name
+
+        # bind functions to the context of environment if required
+        for name, obj in self.parent.iteritems():
+            if type(obj) is FunctionType:
+                if getattr(obj, 'contextfunction', 0):
+                    self.vars[key] = partial(obj, self)
+                elif getattr(obj, 'environmentfunction', 0):
+                    self.vars[key] = partial(obj, environment)
+
+        # create the initial mapping of blocks.  Whenever template inheritance
+        # takes place the runtime will update this mapping with the new blocks
+        # from the template.
         self.blocks = dict((k, [v]) for k, v in blocks.iteritems())
 
-        # give all context functions the context as first argument
-        for key, value in self.iteritems():
-            if type(value) is FunctionType and \
-               getattr(value, 'contextfunction', False):
-                dict.__setitem__(self, key, partial(value, self))
-
-        # if the template is in standalone mode we don't copy the blocks over.
-        # this is used for includes for example but otherwise, if the globals
-        # are a template context, this template is participating in a template
-        # inheritance chain and we have to copy the blocks over.
-        if not standalone and isinstance(globals, TemplateContext):
-            for name, parent_blocks in globals.blocks.iteritems():
-                self.blocks.setdefault(name, []).extend(parent_blocks)
-
-    def super(self, block):
+    def super(self, name, current):
         """Render a parent block."""
-        try:
-            func = self.blocks[block][-2]
-        except LookupError:
+        last = None
+        for block in self.blocks[name]:
+            if block is current:
+                break
+            last = block
+        if last is None:
             return self.environment.undefined('there is no parent block '
                                               'called %r.' % block)
-        return SuperBlock(block, self, func)
+        return SuperBlock(block, self, last)
 
-    def __setitem__(self, key, value):
-        """If we set items to the dict we track the variables set so
-        that includes can access the exported variables."""
-        dict.__setitem__(self, key, value)
-        self.exported.add(key)
+    def update(self, mapping):
+        """Update vars from a mapping but don't export them."""
+        self.vars.update(mapping)
 
     def get_exported(self):
-        """Get a dict of all exported variables."""
-        return dict((k, self[k]) for k in self.exported)
-
-    # if there is a default dict, dict has a __missing__ method we can use.
-    if defaultdict is None:
-        def __getitem__(self, name):
-            if name in self:
-                return self[name]
-            return self.environment.undefined(name=name)
-    else:
-        def __missing__(self, name):
-            return self.environment.undefined(name=name)
+        """Get a new dict with the exported variables."""
+        return dict((k, self.vars[k]) for k in self.exported_vars)
+
+    def get_root(self):
+        """Return a new dict with all the non local variables."""
+        return dict(self.parent)
+
+    def get_all(self):
+        """Return a copy of the complete context as dict."""
+        return dict(self.parent, **self.vars)
+
+    def __setitem__(self, key, value):
+        self.vars[key] = value
+        self.exported_vars.add(key)
+
+    def __getitem__(self, key):
+        if key in self.vars:
+            return self.vars[key]
+        try:
+            return self.parent[key]
+        except KeyError:
+            return self.environment.undefined(name=key)
 
     def __repr__(self):
         return '<%s %s of %r>' % (
@@ -109,10 +115,9 @@ class IncludedTemplate(object):
 
     def __init__(self, environment, context, template):
         template = environment.get_template(template)
-        gen = template.root_render_func(context, standalone=True)
-        context = gen.next()
+        context = template.new_context(context.get_root())
         self._name = template.name
-        self._rendered_body = u''.join(gen)
+        self._rendered_body = u''.join(template.root_render_func(context))
         self._context = context.get_exported()
 
     __getitem__ = lambda x, n: x._context[n]
@@ -257,6 +262,28 @@ class Macro(object):
         )
 
 
+def fail_with_undefined_error(self, *args, **kwargs):
+    """Regular callback function for undefined objects that raises an
+    `UndefinedError` on call.
+    """
+    if self._undefined_hint is None:
+        if self._undefined_obj is None:
+            hint = '%r is undefined' % self._undefined_name
+        elif not isinstance(self._undefined_name, basestring):
+            hint = '%r object has no element %r' % (
+                self._undefined_obj.__class__.__name__,
+                self._undefined_name
+            )
+        else:
+            hint = '%r object has no attribute %r' % (
+                self._undefined_obj.__class__.__name__,
+                self._undefined_name
+            )
+    else:
+        hint = self._undefined_hint
+    raise UndefinedError(hint)
+
+
 class Undefined(object):
     """The default undefined implementation.  This undefined implementation
     can be printed and iterated over, but every other access will raise a
@@ -268,30 +295,10 @@ class Undefined(object):
         self._undefined_obj = obj
         self._undefined_name = name
 
-    def _fail_with_error(self, *args, **kwargs):
-        if self._undefined_hint is None:
-            if self._undefined_obj is None:
-                hint = '%r is undefined' % self._undefined_name
-            elif not isinstance(self._undefined_name, basestring):
-                hint = '%r object has no element %r' % (
-                    self._undefined_obj.__class__.__name__,
-                    self._undefined_name
-                )
-            else:
-                hint = '%r object has no attribute %r' % (
-                    self._undefined_obj.__class__.__name__,
-                    self._undefined_name
-                )
-        else:
-            hint = self._undefined_hint
-        raise UndefinedError(hint)
     __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
     __realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \
     __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
-    __getattr__ = __getitem__ = _fail_with_error
-
-    def __unicode__(self):
-        return u''
+    __getattr__ = __getitem__ = fail_with_undefined_error
 
     def __str__(self):
         return self.__unicode__().encode('utf-8')
@@ -299,6 +306,9 @@ class Undefined(object):
     def __repr__(self):
         return 'Undefined'
 
+    def __unicode__(self):
+        return u''
+
     def __len__(self):
         return 0
 
@@ -325,9 +335,9 @@ class DebugUndefined(Undefined):
 
 
 class StrictUndefined(Undefined):
-    """An undefined that barks on print and iteration as well as boolean tests.
-    In other words: you can do nothing with it except checking if it's defined
-    using the `defined` test.
+    """An undefined that barks on print and iteration as well as boolean
+    tests.  In other words: you can do nothing with it except checking if it's
+    defined using the `defined` test.
     """
 
-    __iter__ = __unicode__ = __len__ = __nonzero__ = Undefined._fail_with_error
+    __iter__ = __unicode__ = __len__ = __nonzero__ = fail_with_undefined_error
index 6e9dbc00f0e18b9190b5be444c0d5ffb7fbb2157..639cda6e87631c6a9b81e07c010e3597465a5212 100644 (file)
@@ -33,6 +33,14 @@ def contextfunction(f):
     return f
 
 
+def environmentfunction(f):
+    """Mark a callable as environment callable.  An environment callable is
+    passed the current environment as first argument.
+    """
+    f.environmentfunction = True
+    return f
+
+
 def import_string(import_name, silent=False):
     """Imports an object based on a string.  This use useful if you want to
     use import paths as endpoints or something similar.  An import path can
index 837de51e4c022b33838cfa5d34b2fd53bf7e97c3..7ba463ab0bf2603997127b2f511648c3a9b2daf5 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,11 @@
 # -*- coding: utf-8 -*-
 """
-jinja
-~~~~~
+Jinja2
+~~~~~~
 
-Jinja is a `sandboxed`_ template engine written in pure Python. It
-provides a `Django`_ like non-XML syntax and compiles templates into
-executable python code. It's basically a combination of Django templates
-and python code.
+Jinja2 is a template engine written in pure Python.  It provides a
+`Django`_ inspired non-XML syntax but supports inline expressions and
+an optional `sandboxed`_ environment.
 
 Nutshell
 --------
@@ -18,7 +17,7 @@ Here a small example of a Jinja template::
     {% block content %}
       <ul>
       {% for user in users %}
-        <li><a href="{{ user.url|e }}">{{ user.username|e }}</a></li>
+        <li><a href="{{ user.url }}">{{ user.username }}</a></li>
       {% endfor %}
       </ul>
     {% endblock %}
@@ -29,28 +28,16 @@ Philosophy
 Application logic is for the controller but don't try to make the life
 for the template designer too hard by giving him too few functionality.
 
-For more informations visit the new `jinja webpage`_ and `documentation`_.
+For more informations visit the new `jinja2 webpage`_ and `documentation`_.
 
-Note
-----
-
-This is the Jinja 1.0 release which is completely incompatible with the
-old "pre 1.0" branch. The old branch will still receive security updates
-and bugfixes but the 1.0 branch will be the only version that receives
-support.
-
-If you have an application that uses Jinja 0.9 and won't be updated in
-the near future the best idea is to ship a Jinja 0.9 checkout together
-with the application.
-
-The `Jinja tip`_ is installable via `easy_install` with ``easy_install
-Jinja==dev``.
+The `Jinja2 tip`_ is installable via `easy_install` with ``easy_install
+Jinja2==dev``.
 
 .. _sandboxed: http://en.wikipedia.org/wiki/Sandbox_(computer_security)
 .. _Django: http://www.djangoproject.com/
-.. _jinja webpage: http://jinja.pocoo.org/
-.. _documentation: http://jinja.pocoo.org/documentation/index.html
-.. _Jinja tip: http://dev.pocoo.org/hg/jinja-main/archive/tip.tar.gz#egg=Jinja-dev
+.. _jinja webpage: http://jinja2.pocoo.org/
+.. _documentation: http://jinja2.pocoo.org/documentation/index.html
+.. _Jinja tip: http://dev.pocoo.org/hg/jinja2-main/archive/tip.tar.gz#egg=Jinja2-dev
 """
 import os
 import sys
@@ -71,6 +58,18 @@ def list_files(path):
             yield fn
 
 
+def get_terminal_width():
+    """Return the current terminal dimensions."""
+    try:
+        from struct import pack, unpack
+        from fcntl import ioctl
+        from termios import TIOCGWINSZ
+        s = pack('HHHH', 0, 0, 0, 0)
+        return unpack('HHHH', ioctl(sys.stdout.fileno(), TIOCGWINSZ, s))[1]
+    except:
+        return 80
+
+
 class optional_build_ext(build_ext):
     """This class allows C extension building to fail."""
 
@@ -87,15 +86,16 @@ class optional_build_ext(build_ext):
             self._unavailable()
 
     def _unavailable(self):
-        print '*' * 70
+        width = get_terminal_width()
+        print '*' * width
         print """WARNING:
 An optional C extension could not be compiled, speedups will not be
 available."""
-        print '*' * 70
+        print '*' * width
 
 
 setup(
-    name='Jinja 2',
+    name='Jinja2',
     version='2.0dev',
     url='http://jinja.pocoo.org/',
     license='BSD',
index 81cc53381a367dda76af6c974132514043263474..4cce6a23c38b37f7101f247b27621626b7b8d574 100644 (file)
@@ -22,13 +22,12 @@ UNARY = '''{{ +3 }}|{{ -3 }}'''
 CONCAT = '''{{ [1, 2] ~ 'foo' }}'''
 COMPARE = '''{{ 1 > 0 }}|{{ 1 >= 1 }}|{{ 2 < 3 }}|{{ 2 == 2 }}|{{ 1 <= 1 }}'''
 INOP = '''{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}'''
-LITERALS = '''{{ [] }}|{{ {} }}|{{ () }}|{{ '' }}|{{ @() }}'''
+LITERALS = '''{{ [] }}|{{ {} }}|{{ () }}'''
 BOOL = '''{{ true and false }}|{{ false or true }}|{{ not false }}'''
 GROUPING = '''{{ (true and false) or (false and true) and not false }}'''
 CONDEXPR = '''{{ 0 if true else 1 }}'''
 DJANGOATTR = '''{{ [1, 2, 3].0 }}'''
 FILTERPRIORITY = '''{{ "foo"|upper + "bar"|upper }}'''
-REGEX = r'''{{ @/\S+/.findall('foo bar baz') }}'''
 TUPLETEMPLATES = [
     '{{ () }}',
     '{{ (1, 2) }}',
@@ -42,7 +41,7 @@ TUPLETEMPLATES = [
     '{% for x in foo, bar recursive %}...{% endfor %}',
     '{% for x, in foo, recursive %}...{% endfor %}'
 ]
-TRAILINGCOMMA = '''{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}|{{ @(1, 2,) }}'''
+TRAILINGCOMMA = '''{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}'''
 
 
 def test_call():
@@ -110,7 +109,7 @@ def test_inop(env):
 
 def test_literals(env):
     tmpl = env.from_string(LITERALS)
-    assert tmpl.render().lower() == '[]|{}|()||set([])'
+    assert tmpl.render().lower() == '[]|{}|()'
 
 
 def test_bool(env):
@@ -162,11 +161,6 @@ def test_function_calls(env):
             env.from_string('foo(%s)' % sig)
 
 
-def test_regex(env):
-    tmpl = env.from_string(REGEX)
-    assert tmpl.render() == "['foo', 'bar', 'baz']"
-
-
 def test_tuple_expr(env):
     for tmpl in TUPLETEMPLATES:
         assert env.from_string(tmpl)
@@ -174,25 +168,4 @@ def test_tuple_expr(env):
 
 def test_trailing_comma(env):
     tmpl = env.from_string(TRAILINGCOMMA)
-    assert tmpl.render().lower() == '(1, 2)|[1, 2]|{1: 2}|set([1, 2])'
-
-
-def test_extends_position():
-    env = Environment(loader=DictLoader({
-        'empty': '[{% block empty %}{% endblock %}]'
-    }))
-    tests = [
-        ('{% extends "empty" %}', '[!]'),
-        ('  {% extends "empty" %}', '[!]'),
-        ('  !\n', '  !\n!'),
-        ('{# foo #}  {% extends "empty" %}', '[!]'),
-        ('{% set foo = "blub" %}{% extends "empty" %}', None)
-    ]
-
-    for tmpl, expected_output in tests:
-        try:
-            tmpl = env.from_string(tmpl + '{% block empty %}!{% endblock %}')
-        except TemplateSyntaxError:
-            assert expected_output is None, 'got syntax error'
-        else:
-            assert expected_output == tmpl.render()
+    assert tmpl.render().lower() == '(1, 2)|[1, 2]|{1: 2}'