added support for local aliasing of some attributes. the technique used is not very...
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 15 May 2008 09:04:14 +0000 (11:04 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 15 May 2008 09:04:14 +0000 (11:04 +0200)
--HG--
branch : trunk

docs/api.rst
jinja2/compiler.py
jinja2/nodes.py

index 5b9e5aded74a3b07df797a304ffd0844d9580a7b..10e497fd304ba831c8452e8723ee83a7deeae086 100644 (file)
@@ -369,3 +369,32 @@ context.  This is the place where you can put variables and functions
 that should be available all the time.  Additionally :attr:`Template.globals`
 exist that are variables available to a specific template that are available
 to all :meth:`~Template.render` calls.
+
+
+Jinja2 Semantics
+----------------
+
+Jinja2 behaves like regular Python code in the runtime with some additional
+restrictions that allow the template engine to optimize some common idoms.
+The context is semi immutable.  Only Jinja2 itself may modify it, if other
+code (such as :func:`contextfunction`\s) modify it the changes won't show up
+as expected.
+
+Another change to regular Python is that Jinja2 sees attributes as immutable.
+It assumes that every time it accesses `foo.bar`, bar will look the same as
+long as `foo` was not overriden.  In some situations Jinja will take advantage
+of that to internally substitute continuous lookups to the same attributes
+with a copy of the attribute.
+
+For example in the following template the `post.user` attribute is looked up
+once:
+
+.. sourcecode:: html+jinja
+
+    {% if post.user %}
+        <h3>{{ post.user.username }}</h3>
+        <p>Location: {{ post.user.location }}</p>
+    {% endif %}
+
+That said, the objects passed to the template may not generate different
+values every time a property is accessed.
index f8358356adccde48faa8d49a65f886e8220ed625..cbac00317d74141058efe164780ed91b3c492a67 100644 (file)
@@ -107,6 +107,9 @@ class Identifiers(object):
         # names that are declared by parameters
         self.declared_parameter = set()
 
+        # static subscribes
+        self.static_subscribes = {}
+
     def add_special(self, name):
         """Register a special name like `loop`."""
         self.undeclared.discard(name)
@@ -149,6 +152,9 @@ class Frame(object):
         # the name of the block we're in, otherwise None.
         self.block = parent and parent.block or None
 
+        # node overlays.  see `CodeGenerator.overlay` for more details
+        self.overlays = {}
+
         # the parent of this frame
         self.parent = parent
 
@@ -156,7 +162,8 @@ class Frame(object):
             self.identifiers.declared.update(
                 parent.identifiers.declared |
                 parent.identifiers.declared_locally |
-                parent.identifiers.declared_parameter
+                parent.identifiers.declared_parameter |
+                parent.identifiers.undeclared
             )
             self.identifiers.outer_undeclared.update(
                 parent.identifiers.undeclared -
@@ -253,6 +260,25 @@ class FrameIdentifierVisitor(NodeVisitor):
              self.identifiers.is_declared(node.name, self.hard_scope):
             self.identifiers.undeclared.add(node.name)
 
+    def visit_Subscript(self, node):
+        """Under some circumstances subscripts are aliased with local names:
+
+        - the subscript node is either already aliased or a name that was not
+          reassigned.
+        - and the subscription argument is a constant string.
+        """
+        self.generic_visit(node)
+        if isinstance(node.arg, nodes.Const) and \
+           isinstance(node.arg.value, basestring) and \
+           (isinstance(node.node, nodes.Name) and
+            node.node.name not in (self.identifiers.declared_locally |
+                                   self.identifiers.declared_parameter)) or \
+           node.node in self.identifiers.static_subscribes:
+            if node in self.identifiers.static_subscribes:
+                self.identifiers.static_subscribes[node] += 1
+            else:
+                self.identifiers.static_subscribes[node] = 1
+
     def visit_Macro(self, node):
         self.generic_visit(node)
         self.identifiers.declared_locally.add(node.name)
@@ -468,11 +494,47 @@ class CodeGenerator(NodeVisitor):
             self.write('**')
             self.visit(node.dyn_kwargs, frame)
 
+    def overlay(self, node, frame):
+        """Visit an overlay and return `True` or `False` if the overlay
+        does not exist or is exhausted.  An overlay is used to replace a
+        node with another one (or an identifier) N times.  This is for
+        example used to replace static subscribes before reassignments
+        of a name that occour more than one time.  If a node has overlays
+        it's important that this method is called, otherwise the count
+        will be out of sync and the generated code is broken.
+        """
+        if node not in frame.overlays:
+            return False
+        overlay, count = frame.overlays[node]
+        if count is not None and count <= 0:
+            return False
+        if isinstance(overlay, basestring):
+            self.write(overlay)
+        else:
+            self.visit(overlay, frame)
+        frame.overlays[node] = (overlay, count - 1)
+        return True
+
     def pull_locals(self, frame):
         """Pull all the references identifiers into the local scope."""
         for name in frame.identifiers.undeclared:
             self.writeline('l_%s = context.resolve(%r)' % (name, name))
 
+        # find all the static subscribes with a count > 2 and
+        # order them properly so that subscribes depending on other
+        # subscribes are handled later.
+        static_subscribes = [(node, count) for node, count in
+                             frame.identifiers.static_subscribes.items() if
+                             count >= 2]
+        if static_subscribes:
+            static_subscribes.sort(key=lambda x: type(x[0].node)
+                                                 is nodes.Subscript)
+            for node, count in static_subscribes:
+                ident = self.temporary_identifier()
+                self.writeline(ident + ' = ')
+                self.visit(node, frame)
+                frame.overlays[node] = (ident, count)
+
     def pull_dependencies(self, nodes):
         """Pull all the dependencies."""
         visitor = DependencyFinderVisitor()
@@ -1264,6 +1326,10 @@ class CodeGenerator(NodeVisitor):
         self.visit(node.expr, frame)
 
     def visit_Subscript(self, node, frame):
+        # subscripts can have overlays
+        if self.overlay(node, frame):
+            return
+
         # slices or integer subscriptions bypass the subscribe
         # method if we can determine that at compile time.
         if isinstance(node.arg, nodes.Slice) or \
index 0db061a3f22bd69df9b0e40aba2ecea3b90b1eae..1f742fad8a88ac20c81398dda3d6f2534d9a4039 100644 (file)
@@ -234,7 +234,8 @@ class Node(object):
             todo.extend(node.iter_child_nodes())
 
     def __eq__(self, other):
-        return type(self) is type(other) and self.__dict__ == other.__dict__
+        return type(self) is type(other) and \
+               tuple(self.iter_fields()) == tuple(other.iter_fields())
 
     def __ne__(self, other):
         return not self.__eq__(other)
@@ -242,7 +243,7 @@ class Node(object):
     def __hash__(self):
         if not self.frozen:
             raise TypeError('unfrozen nodes are unhashable')
-        return hash(tuple(self.__dict__.items()))
+        return hash(tuple(self.iter_fields()))
 
     def __repr__(self):
         return '%s(%s)' % (