From 203bfcb5a6fb2152fe85f659d8aa13bbe754b392 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 24 Apr 2008 21:54:44 +0200 Subject: [PATCH] inheritance uses a less awkward hack for contexts now and subclassing templates is possible --HG-- branch : trunk --- jinja2/__init__.py | 62 +++++-------- jinja2/compiler.py | 60 +++++++------ jinja2/debug.py | 6 +- jinja2/environment.py | 199 +++++++++++++++++++++++++++++++----------- jinja2/ext.py | 11 ++- jinja2/i18n.py | 6 +- jinja2/lexer.py | 16 ++-- jinja2/loaders.py | 67 +++++++++++++- jinja2/nodes.py | 10 ++- jinja2/runtime.py | 154 +++++++++++++++++--------------- jinja2/utils.py | 8 ++ setup.py | 56 ++++++------ tests/test_syntax.py | 35 +------- 13 files changed, 420 insertions(+), 270 deletions(-) diff --git a/jinja2/__init__.py b/jinja2/__init__.py index 7dbe329..8141aad 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -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 %} {% 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 diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 871728f..9bf1e4d 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -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 diff --git a/jinja2/debug.py b/jinja2/debug.py index 75d70b2..d0157bb 100644 --- a/jinja2/debug.py +++ b/jinja2/debug.py @@ -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 diff --git a/jinja2/environment.py b/jinja2/environment.py index 5b705e4..9807184 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -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 '' ) 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 diff --git a/jinja2/ext.py b/jinja2/ext.py index 947150d..acce835 100644 --- a/jinja2/ext.py +++ b/jinja2/ext.py @@ -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()] diff --git a/jinja2/i18n.py b/jinja2/i18n.py index 9125ee9..6718962 100644 --- a/jinja2/i18n.py +++ b/jinja2/i18n.py @@ -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] diff --git a/jinja2/lexer.py b/jinja2/lexer.py index beb9866..5576ac1 100644 --- a/jinja2/lexer.py +++ b/jinja2/lexer.py @@ -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: diff --git a/jinja2/loaders.py b/jinja2/loaders.py index dc3ccfb..3958169 100644 --- a/jinja2/loaders.py +++ b/jinja2/loaders.py @@ -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) diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 7688f62..62686c4 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -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: diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 8cc1b2f..9018d52 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -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 diff --git a/jinja2/utils.py b/jinja2/utils.py index 6e9dbc0..639cda6 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -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 diff --git a/setup.py b/setup.py index 837de51..7ba463a 100644 --- 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 %} {% 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', diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 81cc533..4cce6a2 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -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}' -- 2.26.2