From b3a1fcfa3dd261de54b3b669a1d1fb1a5c859a9b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 May 2008 11:04:14 +0200 Subject: [PATCH] added support for local aliasing of some attributes. the technique used is not very good but covers at least some of the more common use cases --HG-- branch : trunk --- docs/api.rst | 29 ++++++++++++++++++++ jinja2/compiler.py | 68 +++++++++++++++++++++++++++++++++++++++++++++- jinja2/nodes.py | 5 ++-- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5b9e5ad..10e497f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 %} +

{{ post.user.username }}

+

Location: {{ post.user.location }}

+ {% endif %} + +That said, the objects passed to the template may not generate different +values every time a property is accessed. diff --git a/jinja2/compiler.py b/jinja2/compiler.py index f835835..cbac003 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -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 \ diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 0db061a..1f742fa 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -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)' % ( -- 2.26.2