From d134231de2fcfadd1e413cfec8061fa87d036694 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Apr 2008 12:20:12 +0200 Subject: [PATCH] autoescaping is separate from finalize now and Markup is completely ignored if the environment is not in autoescape mode --HG-- branch : trunk --- docs/conf.py | 8 +- docs/index.rst | 9 +- jinja2/__init__.py | 4 + jinja2/compiler.py | 47 ++++++--- jinja2/environment.py | 218 +++++++++++++++++++++++++++++------------- jinja2/exceptions.py | 8 +- jinja2/ext.py | 4 +- jinja2/filters.py | 34 ++++--- jinja2/loaders.py | 137 +++++++++++++++++++++----- jinja2/runtime.py | 73 ++++++++++++-- 10 files changed, 396 insertions(+), 146 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b81ae0a..501c094 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -#extensions = [] +extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -67,7 +67,7 @@ today_fmt = '%B %d, %Y' #show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'autumn' # Options for HTML output @@ -120,7 +120,7 @@ htmlhelp_basename = 'Jinja2doc' # ------------------------ # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +latex_paper_size = 'a4' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' @@ -132,7 +132,7 @@ latex_documents = [ ] # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] diff --git a/docs/index.rst b/docs/index.rst index 34458f2..c548b91 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,3 @@ -.. Jinja2 documentation master file, created by sphinx-quickstart on Sun Apr 27 21:42:41 2008. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Welcome to Jinja2's documentation! ================================== @@ -10,10 +6,13 @@ Contents: .. toctree:: :maxdepth: 2 + intro + api + templates + Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/jinja2/__init__.py b/jinja2/__init__.py index 8141aad..4310d34 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -43,6 +43,10 @@ from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \ # undefined types from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined +# exceptions +from jinja2.exceptions import TemplateError, UndefinedError, \ + TemplateNotFound, TemplateSyntaxError, TemplateAssertionError + # decorators and public utilities from jinja2.filters import environmentfilter, contextfilter from jinja2.utils import Markup, escape, environmentfunction, contextfunction diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 8c699f5..2631c45 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -843,7 +843,10 @@ class CodeGenerator(NodeVisitor): self.pull_locals(macro_frame) self.writeline('%s = []' % buf) self.blockvisit(node.body, macro_frame) - self.writeline("return Markup(concat(%s))" % buf) + if self.environment.autoescape: + self.writeline('return Markup(concat(%s))' % buf) + else: + self.writeline("return concat(%s)" % buf) self.outdent() self.newline() if frame.toplevel: @@ -874,7 +877,10 @@ class CodeGenerator(NodeVisitor): self.pull_locals(call_frame) self.writeline('%s = []' % buf) self.blockvisit(node.body, call_frame) - self.writeline("return Markup(concat(%s))" % buf) + if self.environment.autoescape: + self.writeline("return Markup(concat(%s))" % buf) + else: + self.writeline('return concat(%s)' % buf) self.outdent() arg_tuple = ', '.join(repr(x.name) for x in node.args) if len(node.args) == 1: @@ -927,12 +933,6 @@ class CodeGenerator(NodeVisitor): return self.newline(node) - if self.environment.finalize is unicode: - finalizer = 'unicode' - have_finalizer = False - else: - finalizer = 'environment.finalize' - have_finalizer = True # if we are in the toplevel scope and there was already an extends # statement we have to add a check that disables our yield(s) here @@ -972,9 +972,16 @@ class CodeGenerator(NodeVisitor): else: if frame.buffer is None: self.writeline('yield ') - self.write(finalizer + '(') + close = 1 + if self.environment.autoescape: + self.write('escape(') + else: + self.write('unicode(') + if self.environment.finalize is not None: + self.write('environment.finalize(') + close += 1 self.visit(item, frame) - self.write(')') + self.write(')' * close) if frame.buffer is not None: self.write(', ') if frame.buffer is not None: @@ -999,12 +1006,15 @@ class CodeGenerator(NodeVisitor): self.indent() for argument in arguments: self.newline(argument) - if have_finalizer: - self.write(finalizer + '(') + close = 0 + if self.environment.autoescape: + self.write('escape(') + close += 1 + if self.environment.finalize is not None: + self.write('environment.finalize(') + close += 1 self.visit(argument, frame) - if have_finalizer: - self.write(')') - self.write(',') + self.write(')' * close + ',') self.outdent() self.writeline(')') if frame.buffer is not None: @@ -1105,6 +1115,13 @@ class CodeGenerator(NodeVisitor): visit_Not = uaop('not ') del binop, uaop + def visit_Concat(self, node, frame): + self.write('join((') + for arg in node.nodes: + self.visit(arg, frame) + self.write(', ') + self.write('))') + def visit_Compare(self, node, frame): self.visit(node.expr, frame) for op in node.ops: diff --git a/jinja2/environment.py b/jinja2/environment.py index 6a00fda..7f4144b 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -65,11 +65,59 @@ def template_from_code(environment, code, globals, uptodate=None, class Environment(object): - """The Jinja environment. - - The core component of Jinja is the `Environment`. It contains + """The core component of Jinja is the `Environment`. It contains important shared variables like configuration, filters, tests, - globals and others. + globals and others. Instances of this class may be modified if + they are not shared and if no template was loaded so far. + Modifications on environments after the first template was loaded + will lead to surprising effects and undefined behavior. + + Here the possible initialization parameters: + + `block_start_string` + The string marking the begin of a block. Defaults to ``'{%'``. + + `block_end_string` + The string marking the end of a block. Defaults to ``'%}'``. + + `variable_start_string` + The string marking the begin of a print statement. + Defaults to ``'{{'``. + + `comment_start_string` + The string marking the begin of a comment. Defaults to ``'{#'``. + + `comment_end_string` + The string marking the end of a comment. Defaults to ``'#}'``. + + `line_statement_prefix` + If given and a string, this will be used as prefix for line based + statements. + + `trim_blocks` + If this is set to ``True`` the first newline after a block is + removed (block, not variable tag!). Defaults to `False`. + + `extensions` + List of Jinja extensions to use. This can either be import paths + as strings or extension classes. + + `optimized` + should the optimizer be enabled? Default is `True`. + + `undefined` + :class:`Undefined` or a subclass of it that is used to represent + undefined values in the template. + + `finalize` + A callable that finalizes the variable. Per default no finalizing + is applied. + + `autoescape` + If set to true the XML/HTML autoescaping feature is enabled. + + `loader` + The template loader for this environment. """ #: if this environment is sandboxed. Modifying this variable won't make @@ -93,7 +141,8 @@ class Environment(object): extensions=(), optimized=True, undefined=Undefined, - finalize=unicode, + finalize=None, + autoescape=False, loader=None): # !!Important notice!! # The constructor accepts quite a few arguments that should be @@ -105,36 +154,6 @@ class Environment(object): # If parameter changes are required only add parameters at the end # and don't change the arguments (or the defaults!) of the arguments # up to (but excluding) loader. - """Here the possible initialization parameters: - - ========================= ============================================ - `block_start_string` the string marking the begin of a block. - this defaults to ``'{%'``. - `block_end_string` the string marking the end of a block. - defaults to ``'%}'``. - `variable_start_string` the string marking the begin of a print - statement. defaults to ``'{{'``. - `comment_start_string` the string marking the begin of a - comment. defaults to ``'{#'``. - `comment_end_string` the string marking the end of a comment. - defaults to ``'#}'``. - `line_statement_prefix` If given and a string, this will be used as - prefix for line based statements. See the - documentation for more details. - `trim_blocks` If this is set to ``True`` the first newline - after a block is removed (block, not - variable tag!). Defaults to ``False``. - `extensions` List of Jinja extensions to use. - `optimized` should the optimizer be enabled? Default is - ``True``. - `undefined` a subclass of `Undefined` that is used to - represent undefined variables. - `finalize` A callable that finalizes the variable. Per - default this is `unicode`, other useful - builtin finalizers are `escape`. - `loader` the loader which should be used. - ========================= ============================================ - """ # santity checks assert issubclass(undefined, Undefined), 'undefined must be ' \ @@ -157,6 +176,7 @@ class Environment(object): self.undefined = undefined self.optimized = optimized self.finalize = finalize + self.autoescape = autoescape # defaults self.filters = DEFAULT_FILTERS.copy() @@ -187,29 +207,35 @@ class Environment(object): return self.undefined(obj=obj, name=argument) def parse(self, source, name=None): - """Parse the sourcecode and return the abstract syntax tree. This tree - of nodes is used by the compiler to convert the template into - executable source- or bytecode. + """Parse the sourcecode and return the abstract syntax tree. This + tree of nodes is used by the compiler to convert the template into + executable source- or bytecode. This is useful for debugging or to + extract information from templates. """ return Parser(self, source, name).parse() def lex(self, source, name=None): - """Lex the given sourcecode and return a generator that yields tokens. - The stream returned is not usable for Jinja but can be used if - Jinja templates should be processed by other tools (for example - syntax highlighting etc) - - The tuples are returned in the form ``(lineno, token, value)``. + """Lex the given sourcecode and return a generator that yields + tokens as tuples in the form ``(lineno, token_type, value)``. """ return self.lexer.tokeniter(source, name) def compile(self, source, name=None, filename=None, globals=None, raw=False): - """Compile a node or source. The name is the load name of the - template after it was joined using `join_path` if necessary, - filename is the estimated filename of the template on the file - system. If the template came from a database or memory this - can be omitted. + """Compile a node or template source code. The `name` parameter is + the load name of the template after it was joined using + :meth:`join_path` if necessary, not the filename on the file system. + the `filename` parameter is the estimated filename of the template on + the file system. If the template came from a database or memory this + can be omitted. The `globals` parameter can be used to provide extra + variables at compile time for the template. In the future the + optimizer will be able to evaluate parts of the template at compile + time based on those variables. + + The return value of this method is a python code object. If the `raw` + parameter is `True` the return value will be a string with python + code equivalent to the bytecode returned otherwise. This method is + mainly used internally. """ if isinstance(source, basestring): source = self.parse(source, name) @@ -226,12 +252,29 @@ class Environment(object): def join_path(self, template, parent): """Join a template with the parent. By default all the lookups are - relative to the loader root, but if the paths should be relative this - function can be used to calculate the real filename.""" + relative to the loader root so this method returns the `template` + parameter unchanged, but if the paths should be relative to the + parent template, this function can be used to calculate the real + template name. + + Subclasses may override this method and implement template path + joining here. + """ return template def get_template(self, name, parent=None, globals=None): - """Load a template.""" + """Load a template from the loader. If a loader is configured this + method ask the loader for the template and returns a :class:`Template`. + If the `parent` parameter is not `None`, :meth:`join_path` is called + to get the real template name before loading. + + The `globals` parameter can be used to provide compile-time globals. + In the future this will allow the optimizer to render parts of the + templates at compile-time. + + If the template does not exist a :exc:`TemplateNotFound` exception is + raised. + """ if self.loader is None: raise TypeError('no loader for this environment specified') if parent is not None: @@ -239,7 +282,9 @@ class Environment(object): return self.loader.load(self, name, self.make_globals(globals)) def from_string(self, source, globals=None, template_class=None): - """Load a template from a string.""" + """Load a template from a string. This parses the source given and + returns a :class:`Template` object. + """ globals = self.make_globals(globals) return template_from_code(self, self.compile(source, globals=globals), globals, None, template_class) @@ -255,8 +300,8 @@ class Template(object): """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 + Normally the template object is generated from an :class:`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. @@ -294,16 +339,25 @@ class Template(object): extensions=(), optimized=True, undefined=Undefined, - finalize=unicode): + finalize=None, + autoescape=False): 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, tuple(extensions), optimized, - undefined, finalize) + undefined, finalize, autoescape) return env.from_string(source, template_class=cls) def render(self, *args, **kwargs): - """Render the template into a string.""" + """This method accepts the same arguments as the `dict` constructor: + A dict, a dict subclass or some keyword arguments. If no arguments + are given the context will be empty. These two calls do the same:: + + template.render(knights='that say nih') + template.render({'knights': 'that say nih'}) + + This will return the rendered template as unicode string. + """ try: return concat(self.generate(*args, **kwargs)) except: @@ -312,7 +366,9 @@ class Template(object): raise exc_type, exc_value, tb.tb_next def stream(self, *args, **kwargs): - """Return a `TemplateStream` that generates the template.""" + """Works exactly like :meth:`generate` but returns a + :class:`TemplateStream`. + """ try: return TemplateStream(self.generate(*args, **kwargs)) except: @@ -321,7 +377,13 @@ class Template(object): raise exc_type, exc_value, tb.tb_next def generate(self, *args, **kwargs): - """Return a generator that generates the template.""" + """For very large templates it can be useful to not render the whole + template at once but evaluate each statement after another and yield + piece for piece. This method basically does exactly that and returns + a generator that yields one item after another as unicode strings. + + It accepts the same arguments as :meth:`render`. + """ # assemble the context context = dict(*args, **kwargs) @@ -364,11 +426,24 @@ class Template(object): self.blocks) def include(self, vars=None): - """Include this template. When passed a template context or dict - the template is evaluated in that context and an `IncludedTemplate` - object is returned. This object then exposes all the exported - variables as attributes and renders the contents of the template - when converted to unicode. + """Some templates may export macros or other variables. It's possible + to access those variables by "including" the template. This is mainly + used internally but may also be useful on the Python layer. If passed + a context, the template is evaluated in it, otherwise an empty context + with just the globals is used. + + The return value is an included template object. Converting it to + unicode returns the rendered contents of the template, the exported + variables are accessable via the attribute syntax. + + This example shows how it can be used: + + >>> t = Template('{% say_hello(name) %}Hello {{ name }}!{% endmacro %}42') + >>> i = t.include() + >>> unicode(i) + u'42' + >>> i.say_hello('John') + u'Hello John!' """ if isinstance(vars, TemplateContext): context = TemplateContext(self.environment, vars.parent, @@ -433,9 +508,14 @@ class IncludedTemplate(object): class TemplateStream(object): - """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. + """A template stream works pretty much like an ordinary python generator + but it can buffer multiple items to reduce the number of total iterations. + Per default the output is unbuffered which means that for every unbuffered + instruction in the template one unicode string is yielded. + + If buffering is enabled with a buffer size of 5, five items are combined + into a new unicode string. This is mainly useful if you are streaming + big templates to a client via WSGI which flushes after each iteration. """ def __init__(self, gen): @@ -449,7 +529,7 @@ class TemplateStream(object): self.buffered = False def enable_buffering(self, size=5): - """Enable buffering. Buffer `size` items before yielding them.""" + """Enable buffering. Buffer `size` items before yielding them.""" if size <= 1: raise ValueError('buffer size too small') diff --git a/jinja2/exceptions.py b/jinja2/exceptions.py index b742959..0bbe33e 100644 --- a/jinja2/exceptions.py +++ b/jinja2/exceptions.py @@ -5,7 +5,7 @@ Jinja exceptions. - :copyright: 2007 by Armin Ronacher. + :copyright: 2008 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -15,7 +15,7 @@ class TemplateError(Exception): class UndefinedError(TemplateError): - """Raised if a template tries to operate on `Undefined`.""" + """Raised if a template tries to operate on :class:`Undefined`.""" class TemplateNotFound(IOError, LookupError, TemplateError): @@ -44,6 +44,4 @@ class TemplateAssertionError(TemplateSyntaxError): class TemplateRuntimeError(TemplateError): - """Raised by the template engine if a tag encountered an error when - rendering. - """ + """A runtime error.""" diff --git a/jinja2/ext.py b/jinja2/ext.py index e480b00..5ae317d 100644 --- a/jinja2/ext.py +++ b/jinja2/ext.py @@ -16,7 +16,7 @@ from jinja2.environment import get_spontaneous_environment from jinja2.runtime import Undefined, concat from jinja2.parser import statement_end_tokens from jinja2.exceptions import TemplateAssertionError -from jinja2.utils import import_string +from jinja2.utils import import_string, Markup # the only real useful gettext functions for a Jinja template. Note @@ -286,7 +286,7 @@ def babel_extract(fileobj, keywords, comment_tags, options): tuple(extensions), # fill with defaults so that environments are shared # with other spontaneus environments. - True, Undefined, unicode + True, Undefined, None, False ) node = environment.parse(fileobj.read().decode(encoding)) diff --git a/jinja2/filters.py b/jinja2/filters.py index c4c108e..15323ad 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -51,7 +51,8 @@ def do_forceescape(value): return escape(unicode(value)) -def do_replace(s, old, new, count=None): +@environmentfilter +def do_replace(environment, s, old, new, count=None): """Return a copy of the value with all occurrences of a substring replaced with a new one. The first argument is the substring that should be replaced, the second is the replacement string. @@ -68,6 +69,8 @@ def do_replace(s, old, new, count=None): """ if count is None: count = -1 + if not environment.autoescape: + return unicode(s).replace(unicode(old), unicode(new), count) if hasattr(old, '__html__') or hasattr(new, '__html__') and \ not hasattr(s, '__html__'): s = escape(s) @@ -86,7 +89,8 @@ def do_lower(s): return soft_unicode(s).lower() -def do_xmlattr(d, autospace=False): +@environmentfilter +def do_xmlattr(_environment, *args, **kwargs): """Create an SGML/XML attribute string based on the items in a dict. All values that are neither `none` nor `undefined` are automatically escaped: @@ -107,23 +111,18 @@ def do_xmlattr(d, autospace=False): As you can see it automatically prepends a space in front of the item - if the filter returned something. You can disable this by passing - `false` as only argument to the filter. + if the filter returned something. """ - if not hasattr(d, 'iteritems'): - raise TypeError('a dict is required') - result = [] - for key, value in d.iteritems(): - if value is not None and not isinstance(value, Undefined): - result.append(u'%s="%s"' % (escape(key), escape(value))) rv = u' '.join( u'%s="%s"' % (escape(key), escape(value)) - for key, value in d.iteritems() + for key, value in dict(*args, **kwargs).iteritems() if value is not None and not isinstance(value, Undefined) ) - if autospace: - rv = ' ' + rv - return Markup(rv) + if rv: + rv = u' ' + rv + if _environment.autoescape: + rv = Markup(rv) + return rv def do_capitalize(s): @@ -197,7 +196,8 @@ def do_default(value, default_value=u'', boolean=False): return value -def do_join(value, d=u''): +@environmentfilter +def do_join(environment, value, d=u''): """Return a string which is the concatenation of the strings in the sequence. The separator between elements is an empty string per default, you can define ith with the optional parameter: @@ -210,6 +210,10 @@ def do_join(value, d=u''): {{ [1, 2, 3]|join }} -> 123 """ + # no automatic escaping? joining is a lot eaiser then + if not environment.autoescape: + return unicode(d).join(imap(unicode, value)) + # if the delimiter doesn't have an html representation we check # if any of the items has. If yes we do a coercion to Markup if not hasattr(d, '__html__'): diff --git a/jinja2/loaders.py b/jinja2/loaders.py index 2bcb30d..9744ef4 100644 --- a/jinja2/loaders.py +++ b/jinja2/loaders.py @@ -31,10 +31,30 @@ def split_template_path(template): class BaseLoader(object): """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 - call the loader bound to an environment. + implement a custom loading mechanism. The environment provides a + `get_template` method that calls the loader's `load` method to get the + :class:`Template` object. + + A very basic example for a loader that looks up templates on the file + system could look like this:: + + from jinja2 import BaseLoader, TemplateNotFound + from os.path import join, exists, getmtime + + class MyLoader(BaseLoader): + + def __init__(self, path, cache_size=50, auto_reload=True): + BaseLoader.__init__(self, cache_size, auto_reload) + self.path = path + + def get_source(self, environment, template): + path = join(self.path, template) + if not exists(path): + raise TemplateNotFound(template) + mtime = getmtime(path) + with file(path) as f: + source = f.read().decode('utf-8') + return source, path, lambda: mtime != getmtime(path) """ def __init__(self, cache_size=50, auto_reload=True): @@ -67,8 +87,11 @@ class BaseLoader(object): raise TemplateNotFound(template) def load(self, environment, name, globals=None): - """Loads a template. This method should not be overriden by - subclasses unless `get_source` doesn't provide enough flexibility. + """Loads a template. This method looks up the template in the cache + or loads one by calling :meth:`get_source`. Subclasses should not + override this method as loaders working on collections of other + loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) + will not call this method but `get_source` directly. """ if globals is None: globals = {} @@ -88,14 +111,26 @@ class BaseLoader(object): class FileSystemLoader(BaseLoader): - """Loads templates from the file system.""" + """Loads templates from the file system. This loader can find templates + in folders on the file system and is the preferred way to load them. + + The loader takes the path to the templates as string, or if multiple + locations are wanted a list of them which is then looked up in the + given order: + + >>> loader = FileSystemLoader('/path/to/templates') + >>> loader = FileSystemLoader(['/path/to/templates', '/other/path']) + + Per default the template encoding is ``'utf-8'`` which can be changed + by setting the `encoding` parameter to something else. + """ def __init__(self, searchpath, encoding='utf-8', cache_size=50, auto_reload=True): BaseLoader.__init__(self, cache_size, auto_reload) if isinstance(searchpath, basestring): searchpath = [searchpath] - self.searchpath = searchpath + self.searchpath = list(searchpath) self.encoding = encoding def get_source(self, environment, template): @@ -115,34 +150,65 @@ class FileSystemLoader(BaseLoader): class PackageLoader(BaseLoader): - """Load templates from python eggs.""" + """Load templates from python eggs or packages. It is constructed with + the name of the python package and the path to the templates in that + package: + + >>> loader = PackageLoader('mypackage', 'views') - def __init__(self, package_name, package_path, charset='utf-8', - cache_size=50, auto_reload=True): + If the package path is not given, ``'templates'`` is assumed. + + Per default the template encoding is ``'utf-8'`` which can be changed + by setting the `encoding` parameter to something else. Due to the nature + of eggs it's only possible to reload templates if the package was loaded + from the file system and not a zip file. + """ + + def __init__(self, package_name, package_path='templates', + encoding='utf-8', cache_size=50, auto_reload=True): BaseLoader.__init__(self, cache_size, auto_reload) - import pkg_resources - self._pkg = pkg_resources - self.package_name = package_name + from pkg_resources import DefaultProvider, ResourceManager, get_provider + provider = get_provider(package_name) + self.encoding = encoding + self.manager = ResourceManager() + self.filesystem_bound = isinstance(provider, DefaultProvider) + self.provider = provider self.package_path = package_path def get_source(self, environment, template): pieces = split_template_path(template) - path = '/'.join((self.package_path,) + tuple(pieces)) - if not self._pkg.resource_exists(self.package_name, path): + p = '/'.join((self.package_path,) + tuple(pieces)) + if not self.provider.has_resource(p): raise TemplateNotFound(template) - return self._pkg.resource_string(self.package_name, path), None, None + + filename = uptodate = None + if self.filesystem_bound: + filename = self.provider.get_resource_filename(self.manager, p) + mtime = path.getmtime(filename) + def uptodate(): + return path.getmtime(filename) != mtime + + source = self.provider.get_resource_string(self.manager, p) + return source.decode(self.encoding), filename, uptodate class DictLoader(BaseLoader): - """Loads a template from a python dict. Used for unittests mostly.""" + """Loads a template from a python dict. It's passed a dict of unicode + strings bound to template names. This loader is useful for unittesting: + + >>> loader = DictLoader({'index.html': 'source here'}) + + Because auto reloading is rarely useful this is disabled per default. + """ - def __init__(self, mapping, cache_size=50): - BaseLoader.__init__(self, cache_size, False) + def __init__(self, mapping, cache_size=50, auto_reload=False): + BaseLoader.__init__(self, cache_size, auto_reload) self.mapping = mapping def get_source(self, environment, template): if template in self.mapping: - return self.mapping[template], None, None + source = self.mapping[template] + return source, None, lambda: source != self.mapping[template] raise TemplateNotFound(template) @@ -151,6 +217,17 @@ class FunctionLoader(BaseLoader): function becomes the name of the template passed and has to return either an unicode string with the template source, a tuple in the form ``(source, filename, uptodatefunc)`` or `None` if the template does not exist. + + >>> def load_template(name): + ... if name == 'index.html' + ... return '...' + ... + >>> loader = FunctionLoader(load_template) + + The `uptodatefunc` is a function that is called if autoreload is enabled + and has to return `True` if the template is still up to date. For more + details have a look at :meth:`BaseLoader.get_source` which has the same + return value. """ def __init__(self, load_func, cache_size=50, auto_reload=True): @@ -170,7 +247,15 @@ 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. + template by a slash: + + >>> loader = PrefixLoader({ + ... 'app1': PackageLoader('mypackage.app1'), + ... 'app2': PackageLoader('mypackage.app2') + ... }) + + By loading ``'app1/index.html'`` the file from the app1 package is loaded, + by loading ``'app2/index.html'`` the file from the second. """ def __init__(self, mapping, delimiter='/', cache_size=50, @@ -193,6 +278,14 @@ class ChoiceLoader(BaseLoader): 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. + + >>> loader = ChoiceLoader([ + ... FileSystemLoader('/path/to/user/templates'), + ... PackageLoader('myapplication') + ]) + + This is useful if you want to allow users to override builtin templates + from a different location. """ def __init__(self, loaders, cache_size=50, auto_reload=True): diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 5f8de1f..b289500 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -9,13 +9,15 @@ :license: GNU GPL. """ from types import FunctionType -from jinja2.utils import Markup, partial -from jinja2.exceptions import UndefinedError +from itertools import chain, imap +from jinja2.utils import Markup, partial, soft_unicode, escape +from jinja2.exceptions import UndefinedError, TemplateRuntimeError # these variables are exported to the template runtime __all__ = ['LoopContext', 'TemplateContext', 'TemplateReference', 'Macro', - 'Markup', 'missing', 'concat'] + 'TemplateRuntimeError', 'Markup', 'missing', 'concat', 'escape', + 'markup_join', 'unicode_join'] # special singleton representing missing values for the runtime @@ -26,6 +28,22 @@ missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})() concat = u''.join +def markup_join(*args): + """Concatenation that escapes if necessary and converts to unicode.""" + buf = [] + iterator = imap(soft_unicode, args) + for arg in iterator: + buf.append(arg) + if hasattr(arg, '__html__'): + return Markup(u'').join(chain(buf, iterator)) + return concat(buf) + + +def unicode_join(*args): + """Simple args to unicode conversion and concatenation.""" + return concat(imap(unicode, args)) + + 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 @@ -68,7 +86,8 @@ class TemplateContext(object): except LookupError: return self.environment.undefined('there is no parent block ' 'called %r.' % name) - render = lambda: Markup(concat(blocks[pos](self))) + wrap = self.environment.autoescape and Markup or (lambda x: x) + render = lambda: wrap(concat(blocks[pos](self))) render.__name__ = render.name = name return render @@ -123,7 +142,9 @@ class TemplateReference(object): def __getitem__(self, name): func = self.__context.blocks[name][-1] - render = lambda: Markup(concat(func(self.__context))) + wrap = self.__context.environment.autoescape and \ + Markup or (lambda x: x) + render = lambda: wrap(concat(func(self.__context))) render.__name__ = render.name = name return render @@ -269,9 +290,18 @@ def fail_with_undefined_error(self, *args, **kwargs): class Undefined(object): - """The default undefined implementation. This undefined implementation - can be printed and iterated over, but every other access will raise a - `NameError`. Custom undefined classes must subclass this. + """The default undefined type. This undefined type can be printed and + iterated over, but every other access will raise an :exc:`UndefinedError`: + + >>> foo = Undefined(name='foo') + >>> str(foo) + '' + >>> not foo + True + >>> foo + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined """ __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name') @@ -307,7 +337,18 @@ class Undefined(object): class DebugUndefined(Undefined): - """An undefined that returns the debug info when printed.""" + """An undefined that returns the debug info when printed. + + >>> foo = DebugUndefined(name='foo') + >>> str(foo) + '{{ foo }}' + >>> not foo + True + >>> foo + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + """ __slots__ = () def __unicode__(self): @@ -325,6 +366,20 @@ class StrictUndefined(Undefined): """An undefined that barks on print and iteration as well as boolean tests and all kinds of comparisons. In other words: you can do nothing with it except checking if it's defined using the `defined` test. + + >>> foo = StrictUndefined(name='foo') + >>> str(foo) + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + >>> not foo + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + >>> foo + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined """ __slots__ = () __iter__ = __unicode__ = __len__ = __nonzero__ = __eq__ = __ne__ = \ -- 2.26.2