From 7259c7664c2bd40b0c8b098bfefa7a1367f19e8d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Apr 2008 13:03:59 +0200 Subject: [PATCH] moved caching from loaders to environment and added environment overlays --HG-- branch : trunk --- .hgignore | 5 +- docs/api.rst | 189 ++++++++++++++++++++++++++++++++++----- docs/conf.py | 2 +- docs/jinjaext.py | 2 +- docs/templates.rst | 21 +++++ jinja2/__init__.py | 10 +++ jinja2/defaults.py | 13 +++ jinja2/environment.py | 201 +++++++++++++++++++++++++++++++----------- jinja2/ext.py | 21 ++++- jinja2/loaders.py | 56 ++++-------- jinja2/runtime.py | 53 ++++++----- jinja2/utils.py | 4 + tests/conftest.py | 4 +- 13 files changed, 429 insertions(+), 152 deletions(-) diff --git a/.hgignore b/.hgignore index bfd0c2a..55d124f 100644 --- a/.hgignore +++ b/.hgignore @@ -1,6 +1,7 @@ ^instance$ ^instance/ -^jinja/.*\.so$ -^(build|dist|Jinja\.egg-info)/ +^jinja2/.*\.so$ +^docs/_.* +^(build|dist|Jinja2\.egg-info)/ \.py[co]$ \.DS_Store$ diff --git a/docs/api.rst b/docs/api.rst index 18ef9a2..9562263 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -48,7 +48,7 @@ High Level API -------------- .. autoclass:: jinja2.environment.Environment - :members: from_string, get_template, join_path + :members: from_string, get_template, join_path, overlay .. attribute:: shared @@ -67,24 +67,34 @@ High Level API .. attribute:: filters A dict of filters for this environment. As long as no template was - loaded it's safe to add new filters or remove old. + loaded it's safe to add new filters or remove old. For custom filters + see :ref:`writing-filters`. .. attribute:: tests A dict of test funcitons for this environment. As long as no - template way loaded it's safe to modify this dict. + template way loaded it's safe to modify this dict. For custom tests + see :ref:`writing-tests`. .. attribute:: globals A dict of global variables. These variables are always available in a template and (if the optimizer is enabled) may not be override by templates. As long as no template was loaded it's safe - to modify this dict. + to modify this dict. For more details see :ref:`global-namespace`. .. autoclass:: jinja2.Template :members: render, stream, generate, module + .. attribute:: globals + + foo + + .. attribute:: name + + foo + .. autoclass:: jinja2.environment.TemplateStream :members: disable_buffering, enable_buffering @@ -110,29 +120,56 @@ disallows all operations beside testing if it's an undefined object. .. autoclass:: jinja2.runtime.StrictUndefined +The Context +----------- + +.. autoclass:: jinja2.runtime.TemplateContext + :members: super, get, get_exported, get_all + + .. attribute:: parent + + A dict of read only, global variables the template looks up. These + can either come from another :class:`TemplateContext`, from the + :attr:`Environment.globals` or :attr:`Template.globals`. It must not + be altered. + + .. attribute:: vars + + The template local variables. This list contains environment and + context functions from the :attr:`parent` scope as well as local + modifications and exported variables from the template. The template + will modify this dict during template evaluation but filters and + context functions are not allowed to modify it. + + .. attribute:: environment + + The environment that loaded the template. + + .. attribute:: exported_vars + + This set contains all the names the template exports. The values for + the names are in the :attr:`vars` dict. In order to get a copy of the + exported variables as dict, :meth:`get_exported` can be used. + + .. attribute:: name + + The load name of the template owning this context. + + .. attribute:: blocks + + A dict with the current mapping of blocks in the template. The keys + in this dict are the names of the blocks, and the values a list of + blocks registered. The last item in each list is the current active + block (latest in the inheritance chain). + + Loaders ------- Loaders are responsible for loading templates from a resource such as the -file system and for keeping the compiled modules in memory. These work like -Python's `sys.modules` which keeps the imported templates in memory. Unlike -`sys.modules` however this cache is limited in size by default and templates -are automatically reloaded. Each loader that extends :class:`BaseLoader` -supports this caching and accepts two parameters to configure it: - -`cache_size` - The size of the cache. Per default this is ``50`` which means that if - more than 50 templates are loaded the loader will clean out the least - recently used template. If the cache size is set to ``0`` templates are - recompiled all the time, if the cache size is ``-1`` the cache will not - be cleaned. - -`auto_reload` - Some loaders load templates from locations where the template sources - may change (ie: file system or database). If `auto_reload` is set to - `True` (default) every time a template is requested the loader checks - if the source changed and if yes, it will reload the template. For - higher performance it's possible to disable that. +file system. The environment will keep the compiled modules in memory like +Python's `sys.modules`. Unlike `sys.modules` however this cache is limited in +size by default and templates are automatically reloaded. .. autoclass:: jinja2.loaders.FileSystemLoader @@ -189,3 +226,109 @@ Exceptions .. autoclass:: jinja2.exceptions.TemplateSyntaxError .. autoclass:: jinja2.exceptions.TemplateAssertionError + + +.. _writing-filters: + +Custom Filters +-------------- + +Custom filters are just regular Python functions that take the left side of +the filter as first argument and the the arguments passed to the filter as +extra arguments or keyword arguments. + +For example in the filter ``{{ 42|myfilter(23) }}`` the function would be +called with ``myfilter(42, 23)``. Here for example a simple filter that can +be applied to datetime objects to format them:: + + def datetimeformat(value, format='%H:%M / %d-%m-%Y'): + return value.strftime(format) + +You can register it on the template environment by updating the +:attr:`~Environment.filters` dict on the environment:: + + environment.filters['datetimeformat'] = datetimeformat + +Inside the template it can then be used as follows: + +.. sourcecode:: jinja + + written on: {{ article.pub_date|datetimeformat }} + publication date: {{ article.pub_date|datetimeformat('%d-%m-%Y') }} + +Filters can also be passed the current template context or environment. This +is useful if a filters wants to return an undefined value or check the current +:attr:`~Environment.autoescape` setting. For this purpose two decorators +exist: :func:`environmentfilter` and :func:`contextfilter`. + +Here a small example filter that breaks a text into HTML line breaks and +paragraphs and marks the return value as safe HTML string if autoescaping is +enabled:: + + import re + from jinja2 import environmentfilter, Markup, escape + + _paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}') + + @environmentfilter + def nl2br(environment, value): + result = u'\n\n'.join(u'

%s

' % p.replace('\n', '
\n') + for p in _paragraph_re.split(escape(value))) + if environment.autoescape: + result = Markup(result) + return result + +Context filters work the same just that the first argument is the current +active :class:`TemplateContext` rather then the environment. + + +.. _writing-tests: + +Custom Tests +------------ + +Tests work like filters just that there is no way for a filter to get access +to the environment or context and that they can't be chained. The return +value of a filter should be `True` or `False`. The purpose of a filter is to +give the template designers the possibility to perform type and conformability +checks. + +Here a simple filter that checks if a variable is a prime number:: + + import math + + def is_prime(n): + if n == 2: + return True + for i in xrange(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + +You can register it on the template environment by updating the +:attr:`~Environment.tests` dict on the environment:: + + environment.tests['prime'] = is_prime + +A template designer can then use the test like this: + +.. sourcecode:: jinja + + {% if 42 is prime %} + 42 is a prime number + {% else %} + 42 is not a prime number + {% endif %} + + +.. _global-namespace: + +The Global Namespace +-------------------- + +Variables stored in the :attr:`Environment.globals` or :attr:`Template.globals` +dicts are special as they are available for imported templates too and will be +used by the optimizer in future releases to evaluates parts of the template at +compile time. This is the place where you can put variables and functions +that should be available all the time. diff --git a/docs/conf.py b/docs/conf.py index 237b61b..54a1ad4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -137,7 +137,7 @@ latex_preamble = ''' \definecolor{TitleColor}{rgb}{0.7,0,0} \definecolor{InnerLinkColor}{rgb}{0.7,0,0} \definecolor{OuterLinkColor}{rgb}{0.8,0,0} -\definecolor{VerbatimColor}{rgb}{0.98,0.98,0.98} +\definecolor{VerbatimColor}{rgb}{0.985,0.985,0.985} \definecolor{VerbatimBorderColor}{rgb}{0.8,0.8,0.8} ''' diff --git a/docs/jinjaext.py b/docs/jinjaext.py index 8485a62..57c7097 100644 --- a/docs/jinjaext.py +++ b/docs/jinjaext.py @@ -43,7 +43,7 @@ class JinjaStyle(Style): Name.Tag: 'bold #686868', Name.Decorator: '#686868', - String: '#BE9B5D', + String: '#AA891C', Number: '#444444', Generic.Heading: 'bold #000080', diff --git a/docs/templates.rst b/docs/templates.rst index 384d27e..85171d6 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -782,3 +782,24 @@ List of Builtin Tests --------------------- .. jinjatests:: + + +List of Global Functions +------------------------ + +The following functions are available in the global scope by default: + +.. function:: range([start,] stop[, step]) + + Return a list containing an arithmetic progression of integers. + range(i, j) returns [i, i+1, i+2, ..., j-1]; start (!) defaults to 0. + When step is given, it specifies the increment (or decrement). + For example, range(4) returns [0, 1, 2, 3]. The end point is omitted! + These are exactly the valid indices for a list of 4 elements. + +.. function:: lipsum(n=5, html=True, min=20, max=100) + + Generates some lorem ipsum for the template. Per default five paragraphs + with HTML are generated each paragraph between 20 and 100 words. If html + is disabled regular text is returned. This is useful to generate simple + contents for layout testing. diff --git a/jinja2/__init__.py b/jinja2/__init__.py index 4310d34..0ab7423 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -50,3 +50,13 @@ from jinja2.exceptions import TemplateError, UndefinedError, \ # decorators and public utilities from jinja2.filters import environmentfilter, contextfilter from jinja2.utils import Markup, escape, environmentfunction, contextfunction + +__all__ = [ + 'Environment', 'Template', 'BaseLoader', 'FileSystemLoader', + 'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader', + 'ChoiceLoader', 'Undefined', 'DebugUndefined', 'StrictUndefined', + 'TemplateError', 'UndefinedError', 'TemplateNotFound', + 'TemplateSyntaxError', 'TemplateAssertionError', 'environmentfilter', + 'contextfilter', 'Markup', 'escape', 'environmentfunction', + 'contextfunction' +] diff --git a/jinja2/defaults.py b/jinja2/defaults.py index 773ad80..848cb8f 100644 --- a/jinja2/defaults.py +++ b/jinja2/defaults.py @@ -13,7 +13,20 @@ from jinja2.tests import TESTS as DEFAULT_TESTS from jinja2.utils import generate_lorem_ipsum +BLOCK_START_STRING = '{%' +BLOCK_END_STRING = '%}' +VARIABLE_START_STRING = '{{' +VARIABLE_END_STRING = '}}' +COMMENT_START_STRING = '{#' +COMMENT_END_STRING = '#}' +LINE_STATEMENT_PREFIX = None + + DEFAULT_NAMESPACE = { 'range': xrange, 'lipsum': generate_lorem_ipsum } + + +# export all constants +__all__ = tuple(x for x in locals() if x.isupper()) diff --git a/jinja2/environment.py b/jinja2/environment.py index 919ee9a..09a0ae2 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -9,14 +9,14 @@ :license: BSD, see LICENSE for more details. """ import sys +from jinja2.defaults import * 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, TemplateContext, concat from jinja2.debug import translate_exception -from jinja2.utils import import_string, LRUCache, Markup -from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE +from jinja2.utils import import_string, LRUCache, Markup, missing # for direct template usage we have up to ten living environments @@ -39,29 +39,35 @@ def get_spontaneous_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. +def create_cache(size): + """Return the cache class for the given size.""" + if size == 0: + return None + if size < 0: + return {} + return LRUCache(size) + + +def load_extensions(environment, extensions): + """Load the extensions from the list and bind it to the environment. + Returns a new list of instanciated environments. """ - 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 + result = [] + for extension in extensions: + if isinstance(extension, basestring): + extension = import_string(extension) + result.append(extension(environment)) + return result + + +def _environment_sanity_check(environment): + """Perform a sanity check on the environment.""" + assert issubclass(environment.undefined, Undefined), 'undefined must ' \ + 'be a subclass of undefined because filters depend on it.' + assert environment.block_start_string != \ + environment.variable_start_string != \ + environment.comment_start_string, 'block, variable and comment ' \ + 'start strings must be different' class Environment(object): @@ -118,6 +124,20 @@ class Environment(object): `loader` The template loader for this environment. + + `cache_size` + The size of the cache. Per default this is ``50`` which means that if + more than 50 templates are loaded the loader will clean out the least + recently used template. If the cache size is set to ``0`` templates are + recompiled all the time, if the cache size is ``-1`` the cache will not + be cleaned. + + `auto_reload` + Some loaders load templates from locations where the template sources + may change (ie: file system or database). If `auto_reload` is set to + `True` (default) every time a template is requested the loader checks + if the source changed and if yes, it will reload the template. For + higher performance it's possible to disable that. """ #: if this environment is sandboxed. Modifying this variable won't make @@ -125,25 +145,30 @@ class Environment(object): #: have a look at jinja2.sandbox sandboxed = False + #: True if the environment is just an overlay + overlay = False + #: shared environments have this set to `True`. A shared environment #: must not be modified shared = False def __init__(self, - block_start_string='{%', - block_end_string='%}', - variable_start_string='{{', - variable_end_string='}}', - comment_start_string='{#', - comment_end_string='#}', - line_statement_prefix=None, + block_start_string=BLOCK_START_STRING, + block_end_string=BLOCK_END_STRING, + variable_start_string=VARIABLE_START_STRING, + variable_end_string=VARIABLE_END_STRING, + comment_start_string=COMMENT_START_STRING, + comment_end_string=COMMENT_END_STRING, + line_statement_prefix=LINE_STATEMENT_PREFIX, trim_blocks=False, extensions=(), optimized=True, undefined=Undefined, finalize=None, autoescape=False, - loader=None): + loader=None, + cache_size=50, + auto_reload=True): # !!Important notice!! # The constructor accepts quite a few arguments that should be # passed by keyword rather than position. However it's important to @@ -153,14 +178,7 @@ class Environment(object): # - unittests # 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. - - # santity checks - assert issubclass(undefined, Undefined), 'undefined must be ' \ - 'a subclass of undefined because filters depend on it.' - assert block_start_string != variable_start_string != \ - comment_start_string, 'block, variable and comment ' \ - 'start strings must be different' + # existing already. # lexer / parser information self.block_start_string = block_start_string @@ -185,16 +203,60 @@ class Environment(object): # set the loader provided self.loader = loader - - # create lexer - self.lexer = Lexer(self) + self.cache = create_cache(cache_size) + self.auto_reload = auto_reload # load extensions - self.extensions = [] - for extension in extensions: - if isinstance(extension, basestring): - extension = import_string(extension) - self.extensions.append(extension(self)) + self.extensions = load_extensions(self, extensions) + + _environment_sanity_check(self) + + def overlay(self, block_start_string=missing, block_end_string=missing, + variable_start_string=missing, variable_end_string=missing, + comment_start_string=missing, comment_end_string=missing, + line_statement_prefix=missing, trim_blocks=missing, + extensions=missing, optimized=missing, undefined=missing, + finalize=missing, autoescape=missing, loader=missing, + cache_size=missing, auto_reload=missing): + """Create a new overlay environment that shares all the data with the + current environment except of cache and the overriden attributes. + Extensions cannot be removed for a overlayed environment. A overlayed + environment automatically gets all the extensions of the environment it + is linked to plus optional extra extensions. + + Creating overlays should happen after the initial environment was set + up completely. Not all attributes are truly linked, some are just + copied over so modifications on the original environment may not shine + through. + """ + args = dict(locals()) + del args['self'], args['cache_size'], args['extensions'] + + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.overlay = True + rv.linked_to = self + + for key, value in args.iteritems(): + if value is not missing: + setattr(rv, key, value) + + if cache_size is not missing: + rv.cache = create_cache(cache_size) + + rv.extensions = [] + for extension in self.extensions: + rv.extensions.append(extension.bind(self)) + if extensions is not missing: + rv.extensions.extend(load_extensions(extensions)) + + _environment_sanity_check(rv) + return rv + + @property + def lexer(self): + """Return a fresh lexer for the environment.""" + return Lexer(self) def subscribe(self, obj, argument): """Get an item or attribute of an object.""" @@ -279,15 +341,26 @@ class Environment(object): raise TypeError('no loader for this environment specified') if parent is not None: name = self.join_path(name, parent) - return self.loader.load(self, name, self.make_globals(globals)) + + if self.cache is not None: + template = self.cache.get(name) + if template is not None and (not self.auto_reload or \ + template.is_up_to_date): + return template + + template = self.loader.load(self, name, self.make_globals(globals)) + if self.cache is not None: + self.cache[name] = template + return template def from_string(self, source, globals=None, template_class=None): """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) + cls = template_class or self.template_class + return cls.from_code(self, self.compile(source, globals=globals), + globals, None) def make_globals(self, d): """Return a dict for the globals.""" @@ -345,9 +418,33 @@ class Template(object): 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, autoescape) + undefined, finalize, autoescape, None, 0, False) return env.from_string(source, template_class=cls) + @classmethod + def from_code(cls, environment, code, globals, uptodate=None): + """Creates a template object from compiled code and the globals. This + is used by the loaders and environment to create a template object. + """ + t = object.__new__(cls) + 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 + def render(self, *args, **kwargs): """This method accepts the same arguments as the `dict` constructor: A dict, a dict subclass or some keyword arguments. If no arguments diff --git a/jinja2/ext.py b/jinja2/ext.py index bc04a69..84d656d 100644 --- a/jinja2/ext.py +++ b/jinja2/ext.py @@ -26,7 +26,13 @@ GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext') class Extension(object): - """Instances of this class store parser extensions.""" + """Extensions can be used to add extra functionality to the Jinja template + system at the parser level. This is a supported but currently + undocumented interface. Custom extensions are bound to an environment but + may not store environment specific data on `self`. The reason for this is + that an extension can be bound to another environment (for overlays) by + creating a copy and reassigning the `environment` attribute. + """ #: if this extension parses this is the list of tags it's listening to. tags = set() @@ -34,6 +40,13 @@ class Extension(object): def __init__(self, environment): self.environment = environment + def bind(self, environment): + """Create a copy of this extension bound to another environment.""" + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.environment = environment + return rv + def parse(self, parser): """Called if one of the tags matched.""" @@ -289,8 +302,10 @@ def babel_extract(fileobj, keywords, comment_tags, options): options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true'), tuple(extensions), # fill with defaults so that environments are shared - # with other spontaneus environments. - True, Undefined, None, False + # with other spontaneus environments. The rest of the + # arguments are optimizer, undefined, finalize, autoescape, + # loader, cache size and auto reloading setting + True, Undefined, None, False, None, 0, False ) node = environment.parse(fileobj.read().decode(encoding)) diff --git a/jinja2/loaders.py b/jinja2/loaders.py index 9744ef4..cdda94a 100644 --- a/jinja2/loaders.py +++ b/jinja2/loaders.py @@ -5,12 +5,15 @@ Jinja loader classes. + XXX: move caching from the loaders to environment.get_template and add + environment overlays that allow to redefine escaping and other things but + shared the globals and filter mappings. + :copyright: 2008 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ from os import path from jinja2.exceptions import TemplateNotFound -from jinja2.environment import template_from_code from jinja2.utils import LRUCache @@ -57,15 +60,6 @@ class BaseLoader(object): return source, path, lambda: mtime != getmtime(path) """ - def __init__(self, cache_size=50, auto_reload=True): - if cache_size == 0: - self.cache = None - elif cache_size < 0: - self.cache = {} - else: - self.cache = LRUCache(cache_size) - self.auto_reload = auto_reload - def get_source(self, environment, template): """Get the template source, filename and reload helper for a template. It's passed the environment and template name and has to return a @@ -95,19 +89,10 @@ class BaseLoader(object): """ if globals is None: globals = {} - - if self.cache is not None: - template = self.cache.get(name) - if template is not None and (not self.auto_reload or \ - template.is_up_to_date): - return template - source, filename, uptodate = self.get_source(environment, name) code = environment.compile(source, name, filename, globals) - template = template_from_code(environment, code, globals, uptodate) - if self.cache is not None: - self.cache[name] = template - return template + return environment.template_class.from_code(environment, code, + globals, uptodate) class FileSystemLoader(BaseLoader): @@ -125,9 +110,7 @@ class FileSystemLoader(BaseLoader): 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) + def __init__(self, searchpath, encoding='utf-8'): if isinstance(searchpath, basestring): searchpath = [searchpath] self.searchpath = list(searchpath) @@ -165,8 +148,7 @@ class PackageLoader(BaseLoader): """ def __init__(self, package_name, package_path='templates', - encoding='utf-8', cache_size=50, auto_reload=True): - BaseLoader.__init__(self, cache_size, auto_reload) + encoding='utf-8'): from pkg_resources import DefaultProvider, ResourceManager, get_provider provider = get_provider(package_name) self.encoding = encoding @@ -201,8 +183,7 @@ class DictLoader(BaseLoader): Because auto reloading is rarely useful this is disabled per default. """ - def __init__(self, mapping, cache_size=50, auto_reload=False): - BaseLoader.__init__(self, cache_size, auto_reload) + def __init__(self, mapping): self.mapping = mapping def get_source(self, environment, template): @@ -230,8 +211,7 @@ class FunctionLoader(BaseLoader): return value. """ - def __init__(self, load_func, cache_size=50, auto_reload=True): - BaseLoader.__init__(self, cache_size, auto_reload) + def __init__(self, load_func): self.load_func = load_func def get_source(self, environment, template): @@ -245,9 +225,9 @@ class FunctionLoader(BaseLoader): 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: + to a prefix. The prefix is delimited from the template by a slash per + default, which can be changed by setting the `delimiter` argument to + something else. >>> loader = PrefixLoader({ ... 'app1': PackageLoader('mypackage.app1'), @@ -258,9 +238,7 @@ class PrefixLoader(BaseLoader): by loading ``'app2/index.html'`` the file from the second. """ - def __init__(self, mapping, delimiter='/', cache_size=50, - auto_reload=True): - BaseLoader.__init__(self, cache_size, auto_reload) + def __init__(self, mapping, delimiter='/'): self.mapping = mapping self.delimiter = delimiter @@ -276,8 +254,7 @@ class PrefixLoader(BaseLoader): 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. + is tried. >>> loader = ChoiceLoader([ ... FileSystemLoader('/path/to/user/templates'), @@ -288,8 +265,7 @@ class ChoiceLoader(BaseLoader): from a different location. """ - def __init__(self, loaders, cache_size=50, auto_reload=True): - BaseLoader.__init__(self, cache_size, auto_reload) + def __init__(self, loaders): self.loaders = loaders def get_source(self, environment, template): diff --git a/jinja2/runtime.py b/jinja2/runtime.py index f4a2f1a..5233210 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -11,7 +11,7 @@ import sys from types import FunctionType from itertools import chain, imap -from jinja2.utils import Markup, partial, soft_unicode, escape +from jinja2.utils import Markup, partial, soft_unicode, escape, missing from jinja2.exceptions import UndefinedError, TemplateRuntimeError @@ -21,10 +21,6 @@ __all__ = ['LoopContext', 'TemplateContext', 'TemplateReference', 'Macro', 'markup_join', 'unicode_join'] -# special singleton representing missing values for the runtime -missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})() - - # concatenate a list of strings and convert them to unicode. # unfortunately there is a bug in python 2.4 and lower that causes # unicode.join trash the traceback. @@ -33,8 +29,8 @@ try: raise TypeError(_test_gen_bug) yield None u''.join(_test_gen_bug()) -except TypeError, e: - if e.args and e.args[0] is _test_gen_bug: +except TypeError, _error: + if _error.args and _error.args[0] is _test_gen_bug: concat = u''.join else: def concat(gen): @@ -43,7 +39,7 @@ except TypeError, e: except: exc_type, exc_value, tb = sys.exc_info() raise exc_type, exc_value, tb.tb_next -del _test_gen_bug + del _test_gen_bug, _error def markup_join(*args): @@ -63,15 +59,21 @@ def unicode_join(*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 - update and other methods will not work as they seem (they don't update - the exported variables for example). - - The context is immutable. Modifications on `parent` must not happen and - modifications on `vars` are allowed from generated template code. However - functions that are passed the template context may not modify the context - in any way. + """The template context holds the variables of a template. It stores the + values passed to the template and also the names the template exports. + Creating instances is neither supported nor useful as it's created + automatically at various stages of the template evaluation and should not + be created by hand. + + The context is immutable. Modifications on :attr:`parent` **must not** + happen and modifications on :attr:`vars` are allowed from generated + template code only. Template filters and global functions marked as + :func:`contextfunction`\s get the active context passed as first argument + and are allowed to access the context read-only. + + The template context supports read only dict operations (`get`, + `__getitem__`, `__contains__`) however `__getitem__` doesn't fail with + a `KeyError` but returns an :attr:`Undefined` object. """ def __init__(self, environment, parent, name, blocks): @@ -110,7 +112,9 @@ class TemplateContext(object): return render def get(self, key, default=None): - """For dict compatibility""" + """Returns an item from the template context, if it doesn't exist + `default` is returned. + """ if key in self.vars: return self.vars[key] if key in self.parent: @@ -121,19 +125,12 @@ class TemplateContext(object): """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 a copy of the complete context as dict including the + global variables. + """ return dict(self.parent, **self.vars) - def clone(self): - """Return a copy of the context without the locals.""" - return self.__class__(self.environment, self.parent, - self.name, self.blocks) - def __contains__(self, name): return name in self.vars or name in self.parent diff --git a/jinja2/utils.py b/jinja2/utils.py index 5d8ca48..6a5c9a0 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -25,6 +25,10 @@ _punctuation_re = re.compile( _simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') +# special singleton representing missing values for the runtime +missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})() + + def contextfunction(f): """This decorator can be used to mark a callable as context callable. A context callable is passed the active context as first argument if it diff --git a/tests/conftest.py b/tests/conftest.py index e38299f..8a24673 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,8 +63,8 @@ class GlobalLoader(BaseLoader): raise TemplateNotFound(name) -loader = GlobalLoader(cache_size=0) -simple_env = Environment(trim_blocks=True, loader=loader) +loader = GlobalLoader() +simple_env = Environment(trim_blocks=True, loader=loader, cache_size=0) class Module(py.test.collect.Module): -- 2.26.2