added globals
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 13 Apr 2008 17:42:53 +0000 (19:42 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 13 Apr 2008 17:42:53 +0000 (19:42 +0200)
--HG--
branch : trunk

jinja2/compiler.py
jinja2/environment.py
jinja2/loaders.py
jinja2/optimizer.py

index 3c2347d65c5dda2ba8dccb1630d5359e4a47778a..bf7296389ad93fff6091f6c997574256a970ea11 100644 (file)
@@ -39,8 +39,7 @@ else:
 
 def generate(node, environment, filename, stream=None):
     """Generate the python source for a node tree."""
-    is_child = node.find(nodes.Extends) is not None
-    generator = CodeGenerator(environment, is_child, filename, stream)
+    generator = CodeGenerator(environment, filename, stream)
     generator.visit(node)
     if stream is None:
         return generator.stream.getvalue()
@@ -114,16 +113,31 @@ class Frame(object):
 
     def __init__(self, parent=None):
         self.identifiers = Identifiers()
+
         # a toplevel frame is the root + soft frames such as if conditions.
         self.toplevel = False
+
         # the root frame is basically just the outermost frame, so no if
         # conditions.  This information is used to optimize inheritance
         # situations.
         self.rootlevel = False
-        self.parent = parent
+
+        # inside some tags we are using a buffer rather than yield statements.
+        # this for example affects {% filter %} or {% macro %}.  If a frame
+        # is buffered this variable points to the name of the list used as
+        # buffer.
         self.buffer = None
+
+        # if a frame has name_overrides, all read access to a name in this
+        # dict is redirected to a string expression.
         self.name_overrides = {}
+
+        # the name of the block we're in, otherwise None.
         self.block = parent and parent.block or None
+
+        # the parent of this frame
+        self.parent = parent
+
         if parent is not None:
             self.identifiers.declared.update(
                 parent.identifiers.declared |
@@ -214,33 +228,61 @@ class CompilerExit(Exception):
 
 class CodeGenerator(NodeVisitor):
 
-    def __init__(self, environment, is_child, filename, stream=None):
+    def __init__(self, environment, filename, stream=None):
         if stream is None:
             stream = StringIO()
         self.environment = environment
-        self.is_child = is_child
         self.filename = filename
         self.stream = stream
+
+        # a registry for all blocks.  Because blocks are moved out
+        # into the global python scope they are registered here
         self.blocks = {}
-        self.indentation = 0
-        self.new_lines = 0
-        self.last_identifier = 0
+
+        # the number of extends statements so far
         self.extends_so_far = 0
+
+        # some templates have a rootlevel extends.  In this case we
+        # can safely assume that we're a child template and do some
+        # more optimizations.
         self.has_known_extends = False
+
+        # the number of new lines before the next write()
+        self._new_lines = 0
+
+        # the line number of the last written statement
         self._last_line = 0
+
+        # true if nothing was written so far.
         self._first_write = True
 
+        # used by the `temporary_identifier` method to get new
+        # unique, temporary identifier
+        self._last_identifier = 0
+
+        # the current indentation
+        self._indentation = 0
+
     def temporary_identifier(self):
-        self.last_identifier += 1
-        return 't%d' % self.last_identifier
+        """Get a new unique identifier."""
+        self._last_identifier += 1
+        return 't%d' % self._last_identifier
 
     def indent(self):
-        self.indentation += 1
+        """Indent by one."""
+        self._indentation += 1
 
     def outdent(self, step=1):
-        self.indentation -= step
+        """Outdent by step."""
+        self._indentation -= step
 
     def blockvisit(self, nodes, frame, indent=True, force_generator=True):
+        """Visit a list of nodes as block in a frame.  Per default the
+        code is indented, but this can be disabled by setting the indent
+        parameter to False.  If the current frame is no buffer a dummy
+        ``if 0: yield None`` is written automatically unless the
+        force_generator parameter is set to False.
+        """
         if indent:
             self.indent()
         if frame.buffer is None and force_generator:
@@ -254,26 +296,36 @@ class CodeGenerator(NodeVisitor):
             self.outdent()
 
     def write(self, x):
-        if self.new_lines:
+        """Write a string into the output stream."""
+        if self._new_lines:
             if not self._first_write:
-                self.stream.write('\n' * self.new_lines)
+                self.stream.write('\n' * self._new_lines)
             self._first_write = False
-            self.stream.write('    ' * self.indentation)
-            self.new_lines = 0
+            self.stream.write('    ' * self._indentation)
+            self._new_lines = 0
         self.stream.write(x)
 
     def writeline(self, x, node=None, extra=0):
+        """Combination of newline and write."""
         self.newline(node, extra)
         self.write(x)
 
     def newline(self, node=None, extra=0):
-        self.new_lines = max(self.new_lines, 1 + extra)
+        """Add one or more newlines before the next write."""
+        self._new_lines = max(self._new_lines, 1 + extra)
         if node is not None and node.lineno != self._last_line:
             self.write('# line: %s' % node.lineno)
-            self.new_lines = 1
+            self._new_lines = 1
             self._last_line = node.lineno
 
     def signature(self, node, frame, have_comma=True, extra_kwargs=None):
+        """Writes a function call to the stream for the current node.
+        Per default it will write a leading comma but this can be
+        disabled by setting have_comma to False.  If extra_kwargs is
+        given it must be a string that represents a single keyword
+        argument call that is inserted at the end of the regular
+        keyword argument calls.
+        """
         have_comma = have_comma and [True] or []
         def touch_comma():
             if have_comma:
@@ -300,6 +352,10 @@ class CodeGenerator(NodeVisitor):
             self.visit(node.dyn_kwargs, frame)
 
     def pull_locals(self, frame, indent=True):
+        """Pull all the references identifiers into the local scope.
+        This affects regular names, filters and tests.  If indent is
+        set to False, no automatic indentation will take place.
+        """
         if indent:
             self.indent()
         for name in frame.identifiers.undeclared:
@@ -312,6 +368,10 @@ class CodeGenerator(NodeVisitor):
             self.outdent()
 
     def collect_shadowed(self, frame):
+        """This function returns all the shadowed variables in a dict
+        in the form name: alias and will write the required assignments
+        into the current scope.  No indentation takes place.
+        """
         # make sure we "backup" overridden, local identifiers
         # TODO: we should probably optimize this and check if the
         # identifier is in use afterwards.
@@ -322,6 +382,17 @@ class CodeGenerator(NodeVisitor):
         return aliases
 
     def function_scoping(self, node, frame):
+        """In Jinja a few statements require the help of anonymous
+        functions.  Those are currently macros and call blocks and in
+        the future also recursive loops.  As there is currently
+        technical limitation that doesn't allow reading and writing a
+        variable in a scope where the initial value is coming from an
+        outer scope, this function tries to fall back with a common
+        error message.  Additionally the frame passed is modified so
+        that the argumetns are collected and callers are looked up.
+
+        This will return the modified frame.
+        """
         func_frame = frame.inner()
         func_frame.inspect(node.iter_child_nodes(), hard_scope=True)
 
index 8458a2381dbebe13cd096386a46cf4c8e7d90e06..7c1962caf180155f153d96bf093e5a1469219a2c 100644 (file)
@@ -32,6 +32,7 @@ class Environment(object):
                  comment_end_string='#}',
                  line_statement_prefix=None,
                  trim_blocks=False,
+                 optimized=True,
                  loader=None):
         """Here the possible initialization parameters:
 
@@ -52,6 +53,8 @@ class Environment(object):
         `trim_blocks`             If this is set to ``True`` the first newline
                                   after a block is removed (block, not
                                   variable tag!). Defaults to ``False``.
+        `optimized`               should the optimizer be enabled?  Default is
+                                  ``True``.
         `loader`                  the loader which should be used.
         ========================= ============================================
         """
@@ -65,6 +68,7 @@ class Environment(object):
         self.comment_end_string = comment_end_string
         self.line_statement_prefix = line_statement_prefix
         self.trim_blocks = trim_blocks
+        self.optimized = optimized
 
         # defaults
         self.filters = DEFAULT_FILTERS.copy()
@@ -101,11 +105,12 @@ class Environment(object):
         """
         return self.lexer.tokeniter(source, filename)
 
-    def compile(self, source, filename=None, raw=False):
+    def compile(self, source, filename=None, raw=False, globals=None):
         """Compile a node or source."""
         if isinstance(source, basestring):
             source = self.parse(source, filename)
-        node = optimize(source, self)
+        if self.optimized:
+            node = optimize(source, self, globals or {})
         source = generate(node, self, filename)
         if raw:
             return source
@@ -119,35 +124,64 @@ class Environment(object):
         function can be used to calculate the real filename."""
         return template
 
-    def get_template(self, name, parent=None):
+    def get_template(self, name, parent=None, globals=None):
         """Load a template."""
         if self.loader is None:
             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)
+        globals = self.make_globals(globals)
+        return self.loader.load(self, name, globals)
 
-    def from_string(self, source, filename='<string>'):
+    def from_string(self, source, filename='<string>', globals=None):
         """Load a template from a string."""
-        return Template(self, self.compile(source, filename))
+        globals = self.make_globals(globals)
+        return Template(self, self.compile(source, filename), globals)
+
+    def make_globals(self, d):
+        """Return a dict for the globals."""
+        if d is None:
+            return self.globals
+        return dict(self.globals, **d)
 
 
 class Template(object):
     """Represents a template."""
 
-    def __init__(self, environment, code):
+    def __init__(self, environment, code, globals):
         namespace = {'environment': environment}
         exec code in namespace
         self.environment = environment
         self.name = namespace['filename']
         self.root_render_func = namespace['root']
         self.blocks = namespace['blocks']
+        self.globals = globals
 
     def render(self, *args, **kwargs):
-        return u''.join(self.stream(*args, **kwargs))
+        return u''.join(self.generate(*args, **kwargs))
 
     def stream(self, *args, **kwargs):
-        gen = self.root_render_func(dict(*args, **kwargs))
+        return TemplateStream(self.generate(*args, **kwargs))
+
+    def generate(self, *args, **kwargs):
+        # assemble the context
+        context = self.globals.copy()
+        context.update(*args, **kwargs)
+
+        # if the environment is using the optimizer locals may never
+        # override globals as optimizations might have happened
+        # depending on values of certain globals.  This assertion goes
+        # away if the python interpreter is started with -O
+        if __debug__ and self.environment.optimized:
+            overrides = set(context) & set(self.globals)
+            if overrides:
+                plural = len(overrides) != 1 and 's' or ''
+                raise AssertionError('the per template variable%s %s '
+                                     'override%s global variable%s. '
+                                     'With an enabled optimizer this '
+                                     'will lead to unexpected results.' %
+                    (plural, ', '.join(overrides), plural or ' a', plural))
+        gen = self.root_render_func(context)
         # skip the first item which is a reference to the stream
         gen.next()
         return gen
index 97e51d6a2c9d2d9ad7a1c280d2f9342cef00c3c6..4d1c335e47eee9fa46927890aab97ec84653be4c 100644 (file)
@@ -19,10 +19,10 @@ class BaseLoader(object):
     def get_source(self, environment, template):
         raise TemplateNotFound()
 
-    def load(self, environment, template):
+    def load(self, environment, template, globals=None):
         source, filename = self.get_source(environment, template)
-        code = environment.compile(source, filename)
-        return Template(environment, code)
+        code = environment.compile(source, filename, globals=globals)
+        return Template(environment, code, globals or {})
 
 
 class FileSystemLoader(BaseLoader):
index 4dbd5d9d27f399d262d728bcc53c1dc04151a988..48155c5dddb261054b1ce8a0ad757cc6d90813b9 100644 (file)
@@ -249,8 +249,9 @@ class Optimizer(NodeTransformer):
                                               environment=self.environment)
         except nodes.Impossible:
             return node
+
     visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \
     visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \
     visit_Not = visit_Compare = visit_Subscript = visit_Call = \
-    visit_Filter = visit_Test = fold
+    visit_Filter = visit_Test = visit_CondExpr = fold
     del fold