some documentation improvements, jinja escapes " and ' now, both into charpoints...
[jinja2.git] / jinja2 / environment.py
index 7f4144be014ef403fc5d37c782b0daac2dcb2af6..35bbb15fe6daf1d68ad4c12bbab56bc47f16201e 100644 (file)
@@ -5,18 +5,19 @@
 
     Provides a class that holds runtime and parsing time options.
 
-    :copyright: 2007 by Armin Ronacher.
+    :copyright: 2008 by Armin Ronacher.
     :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.runtime import Undefined, Context
+from jinja2.debug import translate_exception, translate_syntax_error
+from jinja2.exceptions import TemplateSyntaxError
+from jinja2.utils import import_string, LRUCache, Markup, missing, concat
 
 
 # for direct template usage we have up to ten living environments
@@ -39,29 +40,36 @@ 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'
+    return environment
 
 
 class Environment(object):
@@ -84,6 +92,9 @@ class Environment(object):
         The string marking the begin of a print statement.
         Defaults to ``'{{'``.
 
+    `variable_stop_string`
+        The string marking the end of a print statement. Defaults to ``'}}'``.
+
     `comment_start_string`
         The string marking the begin of a comment.  Defaults to ``'{#'``.
 
@@ -92,7 +103,7 @@ class Environment(object):
 
     `line_statement_prefix`
         If given and a string, this will be used as prefix for line based
-        statements.
+        statements.  See also :ref:`line-statements`.
 
     `trim_blocks`
         If this is set to ``True`` the first newline after a block is
@@ -118,6 +129,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 +150,33 @@ class Environment(object):
     #: have a look at jinja2.sandbox
     sandboxed = False
 
+    #: True if the environment is just an overlay
+    overlay = False
+
+    #: the environment this environment is linked to if it is an overlay
+    linked_to = None
+
     #: 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 +186,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 +211,59 @@ 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))
+
+        return _environment_sanity_check(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."""
@@ -206,13 +275,17 @@ class Environment(object):
             except (TypeError, LookupError):
                 return self.undefined(obj=obj, name=argument)
 
-    def parse(self, source, name=None):
+    def parse(self, source, filename=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.  This is useful for debugging or to
         extract information from templates.
         """
-        return Parser(self, source, name).parse()
+        try:
+            return Parser(self, source, filename).parse()
+        except TemplateSyntaxError, e:
+            exc_type, exc_value, tb = translate_syntax_error(e)
+            raise exc_type, exc_value, tb
 
     def lex(self, source, name=None):
         """Lex the given sourcecode and return a generator that yields
@@ -238,7 +311,7 @@ class Environment(object):
         mainly used internally.
         """
         if isinstance(source, basestring):
-            source = self.parse(source, name)
+            source = self.parse(source, filename)
         if self.optimized:
             node = optimize(source, self, globals or {})
         source = generate(node, self, name, filename)
@@ -279,15 +352,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 +429,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
@@ -359,22 +467,16 @@ class Template(object):
         This will return the rendered template as unicode string.
         """
         try:
-            return concat(self.generate(*args, **kwargs))
+            return concat(self._generate(*args, **kwargs))
         except:
-            # hide the `generate` frame
-            exc_type, exc_value, tb = sys.exc_info()
-            raise exc_type, exc_value, tb.tb_next
+            exc_type, exc_value, tb = translate_exception(sys.exc_info())
+            raise exc_type, exc_value, tb
 
     def stream(self, *args, **kwargs):
         """Works exactly like :meth:`generate` but returns a
         :class:`TemplateStream`.
         """
-        try:
-            return TemplateStream(self.generate(*args, **kwargs))
-        except:
-            # hide the `generate` frame
-            exc_type, exc_value, tb = sys.exc_info()
-            raise exc_type, exc_value, tb.tb_next
+        return TemplateStream(self.generate(*args, **kwargs))
 
     def generate(self, *args, **kwargs):
         """For very large templates it can be useful to not render the whole
@@ -384,6 +486,14 @@ class Template(object):
 
         It accepts the same arguments as :meth:`render`.
         """
+        try:
+            for item in self._generate(*args, **kwargs):
+                yield item
+        except:
+            exc_type, exc_value, tb = translate_exception(sys.exc_info())
+            raise exc_type, exc_value, tb
+
+    def _generate(self, *args, **kwargs):
         # assemble the context
         context = dict(*args, **kwargs)
 
@@ -401,12 +511,7 @@ class Template(object):
                                      'will lead to unexpected results.' %
                     (plural, ', '.join(overrides), plural or ' a', plural))
 
-        try:
-            for event in self.root_render_func(self.new_context(context)):
-                yield event
-        except:
-            exc_type, exc_value, tb = translate_exception(sys.exc_info())
-            raise exc_type, exc_value, tb
+        return self.root_render_func(self.new_context(context))
 
     def new_context(self, vars=None, shared=False):
         """Create a new template context for this template.  The vars
@@ -422,35 +527,33 @@ class Template(object):
             parent = vars
         else:
             parent = dict(self.globals, **vars)
-        return TemplateContext(self.environment, parent, self.name,
-                               self.blocks)
-
-    def include(self, vars=None):
-        """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:
+        return Context(self.environment, parent, self.name, self.blocks)
+
+    def make_module(self, vars=None, shared=False):
+        """This method works like the :attr:`module` attribute when called
+        without arguments but it will evaluate the template every call
+        rather then caching the template.  It's also possible to provide
+        a dict which is then used as context.  The arguments are the same
+        as fo the :meth:`new_context` method.
+        """
+        return TemplateModule(self, self.new_context(vars, shared))
 
-        >>> t = Template('{% say_hello(name) %}Hello {{ name }}!{% endmacro %}42')
-        >>> i = t.include()
-        >>> unicode(i)
+    @property
+    def module(self):
+        """The template as module.  This is used for imports in the
+        template runtime but is also useful if one wants to access
+        exported template variables from the Python layer:
+
+        >>> t = Template('{% macro foo() %}42{% endmacro %}23')
+        >>> unicode(t.module)
+        u'23'
+        >>> t.module.foo()
         u'42'
-        >>> i.say_hello('John')
-        u'Hello John!'
         """
-        if isinstance(vars, TemplateContext):
-            context = TemplateContext(self.environment, vars.parent,
-                                      self.name, self.blocks)
-        else:
-            context = self.new_context(vars)
-        return IncludedTemplate(self, context)
+        if hasattr(self, '_module'):
+            return self._module
+        self._module = rv = self.make_module()
+        return rv
 
     def get_corresponding_lineno(self, lineno):
         """Return the source line number of a line number in the
@@ -482,14 +585,18 @@ class Template(object):
         return '<%s %s>' % (self.__class__.__name__, name)
 
 
-class IncludedTemplate(object):
-    """Represents an included template.  All the exported names of the
+class TemplateModule(object):
+    """Represents an imported template.  All the exported names of the
     template are available as attributes on this object.  Additionally
     converting it into an unicode- or bytestrings renders the contents.
     """
 
     def __init__(self, template, context):
-        self.__body_stream = tuple(template.root_render_func(context))
+        # don't alter this attribute unless you change it in the
+        # compiler too.  The Include without context passing directly
+        # uses the mangled name.  The reason why we use a mangled one
+        # is to avoid name clashes with macros with those names.
+        self.__body_stream = list(template.root_render_func(context))
         self.__dict__.update(context.get_exported())
         self.__name__ = template.name
 
@@ -546,7 +653,7 @@ class TemplateStream(object):
                         c_size += 1
                 except StopIteration:
                     if not c_size:
-                        raise
+                        return
                 yield concat(buf)
                 del buf[:]
                 c_size = 0