improved variable exporting, added TODO file
authorArmin Ronacher <armin.ronacher@active-4.com>
Mon, 12 May 2008 07:00:03 +0000 (09:00 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Mon, 12 May 2008 07:00:03 +0000 (09:00 +0200)
--HG--
branch : trunk

TODO [new file with mode: 0644]
docs/jinjaext.py
docs/tricks.rst
jinja2/compiler.py
jinja2/ext.py
jinja2/nodes.py

diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..6881b76
--- /dev/null
+++ b/TODO
@@ -0,0 +1,57 @@
+Todo Before Release
+===================
+
+This has to be implemented before the release:
+
+Drop special casing template globals
+------------------------------------
+
+The idea some time ago was to partially evaluate templates at compile time.
+For that we decided to not allow globals being overridden by locals as the
+static compiler could have changed semantics.  Turns out that the static
+compiler blows up the complexity to something nobody wants to support which
+means that this restriction is kinda pointless.
+
+That should go for good.
+
+Pull Attributes Onces
+---------------------
+
+Imagine the following template::
+
+    {% if foo.bar %}
+        {{ baz(foo.bar) }}
+    {% endif %}
+
+Problem with that is that it compiles to this::
+
+    if environment.subscribe(l_foo, 'bar'):
+        if 0: yield None
+        yield u'\n    %s\n' % (
+            l_baz(environment.subscribe(l_foo, 'bar')),
+        )
+
+As `environment.subscribe` is more expensive then regular attribute lookups
+(it tries getitem/getattr and in sandbox mode also permissions) multiple
+lookups with the same parameters in the same scope should get local aliases.
+The result we have is that one::
+
+    t1 = environment.subscribe(l_foo, 'bar')
+    if t1:
+        if 0: yield None
+        yield u'\n    %s\n' % (
+            l_baz(t1),
+        )
+
+However that should only happen if the attribute is accessed multiple times
+unlike locals and filters/tests which are always pulled.  We're not doing that
+for filters/tests/locals as nested scopes may access it and testing is too
+complicated for the tiny performance improvement but easy for attribute
+lookups, keeping the complexity of the whole thing in mind.
+
+Use `set` for Assignments
+-------------------------
+
+The keyword-less way to assign variabeles is a left-over from the days when
+it was possible to use Jinja2 like a regular programming language without
+necessary template end/start delimiters to group statements.
index 78a45ecbc5185094c4447c5e824f06ff289ecddf..1ed6d352d87a7e5587b4d153210e46bd744d0307 100644 (file)
@@ -168,14 +168,17 @@ def jinja_nodes(dirname, arguments, options, content, lineno,
         if node.abstract:
             members = []
             for key, name in node.__dict__.iteritems():
-                if not key.startswith('_') and callable(name):
+                if not key.startswith('_') and \
+                   not hasattr(node.__base__, key) and callable(name):
                     members.append(key)
             if members:
                 members.sort()
                 doc.append('%s :members: %s' % (p, ', '.join(members)), '')
-        else:
+        if node.__base__ != object:
             doc.append('', '')
-            doc.append(p + ' :Node type: :class:`%s`' % node.__base__.__name__, '')
+            doc.append('%s :Node type: :class:`%s`' %
+                       (p, node.__base__.__name__), '')
+            # XXX: sphinx bug?  Expr gives a rst warning
         doc.append('', '')
         children = node.__subclasses__()
         children.sort(key=lambda x: x.__name__.lower())
index 6029ec56be261a2d62cf6e18dc715d1eaef6a40c..1cfdaa883ec90dedc1f9e5783fc8e7e656563de5 100644 (file)
@@ -65,12 +65,12 @@ child template::
 The layout template can then access `active_page`.  Additionally it makes
 sense to defined a default for that variable::
 
-    {% navigation_bar = [
+    {% set navigation_bar = [
         ('/', 'index', 'Index'),
         ('/downloads/', 'downloads', 'Downloads'),
         ('/about/', 'about', 'About')
     ] -%}
-    {% active_page = active_page|default('index') -%}
+    {% set active_page = active_page|default('index') -%}
     ...
     <ul id="navigation">
     {% for href, id, caption in navigation_bar %}
index cf0f4a545063d797554d4dad0a6018f44e6985b4..cb0da5a1f030408fa4329a3097516a2dfd04a087 100644 (file)
@@ -43,6 +43,7 @@ def generate(node, environment, name, filename, stream=None):
     """Generate the python source for a node tree."""
     if not isinstance(node, nodes.Template):
         raise TypeError('Can\'t compile non template nodes')
+    node.freeze()
     generator = CodeGenerator(environment, name, filename, stream)
     generator.visit(node)
     if stream is None:
@@ -1138,10 +1139,25 @@ class CodeGenerator(NodeVisitor):
 
         # make sure toplevel assignments are added to the context.
         if frame.toplevel:
-            for name in assignment_frame.assigned_names:
+            public_names = [x for x in assignment_frame.assigned_names
+                            if not x.startswith('__')]
+            if len(assignment_frame.assigned_names) == 1:
+                name = iter(assignment_frame.assigned_names).next()
                 self.writeline('context.vars[%r] = l_%s' % (name, name))
-                if not name.startswith('__'):
-                    self.writeline('context.exported_vars.add(%r)' % name)
+            else:
+                self.writeline('context.vars.update({')
+                for idx, name in enumerate(assignment_frame.assigned_names):
+                    if idx:
+                        self.write(', ')
+                    self.write('%r: l_%s' % (name, name))
+                self.write('})')
+            if public_names:
+                if len(public_names) == 1:
+                    self.writeline('context.exported_vars.add(%r)' %
+                                   public_names[0])
+                else:
+                    self.writeline('context.exported_vars.update((%s))' %
+                                   ', '.join(map(repr, public_names)))
 
     def visit_Name(self, node, frame):
         if node.ctx == 'store' and frame.toplevel:
index aac752870b0c36cf22bafb9f1b1bf878536f389b..117c2f3b0dc0ce388170e4d9506f40b5acd6554b 100644 (file)
@@ -77,17 +77,17 @@ class Extension(object):
 
     def attr(self, name, lineno=None):
         """Return an attribute node for the current extension.  This is useful
-        to pass callbacks to template code::
+        to pass constants on extensions to generated template code::
 
-            nodes.Call(self.attr('_my_callback'), args, kwargs, None, None)
-
-        That would call `self._my_callback` when the template is evaluated.
+            self.attr('_my_attribute', lineno=lineno)
         """
         return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
 
     def call_method(self, name, args=None, kwargs=None, dyn_args=None,
                     dyn_kwargs=None, lineno=None):
-        """Call a method of the extension."""
+        """Call a method of the extension.  This is a shortcut for
+        :meth:`attr` + :class:`jinja2.nodes.Call`.
+        """
         if args is None:
             args = []
         if kwargs is None:
index 67f7559b3b37bebbade6e6b6026a8c2ec8a1f7ea..0db061a3f22bd69df9b0e40aba2ecea3b90b1eae 100644 (file)
@@ -90,13 +90,20 @@ class Node(object):
     two attributes: `lineno` (the line number of the node) and `environment`.
     The `environment` attribute is set at the end of the parsing process for
     all nodes automatically.
+
+    Nodes can be frozen which makes them hashable.  The compiler freezes the
+    nodes automatically.  Modifications on frozen nodes are possible but not
+    allowed.
     """
     __metaclass__ = NodeType
     fields = ()
     attributes = ('lineno', 'environment')
     abstract = True
+    frozen = False
 
     def __init__(self, *fields, **attributes):
+        if self.abstract:
+            raise TypeError('abstract nodes are not instanciable')
         if fields:
             if len(fields) != len(self.fields):
                 if not self.fields:
@@ -213,6 +220,30 @@ class Node(object):
             todo.extend(node.iter_child_nodes())
         return self
 
+    def freeze(self):
+        """Freeze the complete node tree which makes them hashable.
+        This happens automatically on compilation.  Frozen nodes must not be
+        modified any further.  Extensions may not freeze nodes that appear
+        in the final node tree (ie: nodes that are returned from the extension
+        parse method).
+        """
+        todo = deque([self])
+        while todo:
+            node = todo.popleft()
+            node.frozen = True
+            todo.extend(node.iter_child_nodes())
+
+    def __eq__(self, other):
+        return type(self) is type(other) and self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __hash__(self):
+        if not self.frozen:
+            raise TypeError('unfrozen nodes are unhashable')
+        return hash(tuple(self.__dict__.items()))
+
     def __repr__(self):
         return '%s(%s)' % (
             self.__class__.__name__,
@@ -374,6 +405,7 @@ class BinExpr(Expr):
     """Baseclass for all binary expressions."""
     fields = ('left', 'right')
     operator = None
+    abstract = True
 
     def as_const(self):
         f = _binop_to_func[self.operator]
@@ -387,6 +419,7 @@ class UnaryExpr(Expr):
     """Baseclass for all unary expressions."""
     fields = ('node',)
     operator = None
+    abstract = True
 
     def as_const(self):
         f = _uaop_to_func[self.operator]
@@ -412,6 +445,7 @@ class Name(Expr):
 
 class Literal(Expr):
     """Baseclass for literals."""
+    abstract = True
 
 
 class Const(Literal):