Improved attribute and item lookup by allowing template designers to express the...
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 12 Jun 2008 06:50:07 +0000 (08:50 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 12 Jun 2008 06:50:07 +0000 (08:50 +0200)
--HG--
branch : trunk

CHANGES
docs/_static/style.css
docs/templates.rst
jinja2/compiler.py
jinja2/environment.py
jinja2/filters.py
jinja2/nodes.py
jinja2/optimizer.py
jinja2/parser.py
jinja2/sandbox.py
tests/test_various.py

diff --git a/CHANGES b/CHANGES
index 0c8e30ee8fad9ff979fd07b0e90d8e28b864b12a..393ba0cd5feb17e5b5e56eec036f2af2669751d3 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -5,4 +5,16 @@ Version 2.0
 -----------
 (codename to be selected, release around July 2008)
 
-- initial release of Jinja2
+- the subscribing of objects (looking up attributes and items) changed from
+  slightly.  It's now possible to give attributes or items a higher priority
+  by either using dot-notation lookup or the bracket syntax.  This also
+  changed the AST slightly.  `Subscript` is gone and was replaced with
+  :class:`~jinja2.nodes.Getitem` and :class:`~jinja2.nodes.Getattr`.
+
+  For more information see :ref:`the implementation details <notes-on-subscribing>`.
+
+Version 2.0rc1
+--------------
+(no codename, released on July 9th 2008)
+
+- first release of Jinja2
index a52436d3fec16a04336a017ca7aa78caa66095cd..e6238d5e24d33507a4a03987237b8af0b02c43f3 100644 (file)
@@ -227,6 +227,10 @@ div.admonition p.admonition-title {
     font-size: 15px;
 }
 
+div.admonition p.admonition-title a {
+    color: white!important;
+}
+
 div.admonition-note {
     background: url(note.png) no-repeat 10px 40px;
 }
index 11d9978f87e3bf5871091f8fefd200ab86a2f99f..ac9c9a14089a913ea55bc3c02af1f150cf184d1f 100644 (file)
@@ -74,6 +74,29 @@ value.  What you can do with that kind of value depends on the application
 configuration, the default behavior is that it evaluates to an empty string
 if printed and that you can iterate over it, but every other operation fails.
 
+.. _notes-on-subscribing:
+
+.. admonition:: Implementation
+
+    The process of looking up attributes and items of objects is called
+    "subscribing" an object.  For convenience sake ``foo.bar`` in Jinja2
+    does the following things on the Python layer:
+
+    -   check if there is an attribute called `bar` on `foo`.
+    -   if there is not, check if there is an item ``'bar'`` in `foo`.
+    -   if there is not, return an undefined object.
+
+    ``foo['bar']`` on the other hand works mostly the same with the a small
+    difference in the order:
+
+    -   check if there is an item ``'bar'`` in `foo`.
+    -   if there is not, check if there is an attribute called `bar` on `foo`.
+    -   if there is not, return an undefined object.
+
+    This is important if an object has an item or attribute with the same
+    name.  Additionally there is the :func:`attr` filter that just looks up
+    attributes.
+
 .. _filters:
 
 Filters
index 9d68e4c551a4313a5274644e30c96d9ce36b49e6..75869cfdda7a72d309bb409e74f8d51c7fccf461 100644 (file)
@@ -1268,7 +1268,12 @@ class CodeGenerator(NodeVisitor):
         self.write(' %s ' % operators[node.op])
         self.visit(node.expr, frame)
 
-    def visit_Subscript(self, node, frame):
+    def visit_Getattr(self, node, frame):
+        self.write('environment.getattr(')
+        self.visit(node.node, frame)
+        self.write(', %r)' % node.attr)
+
+    def visit_Getitem(self, node, frame):
         # slices or integer subscriptions bypass the subscribe
         # method if we can determine that at compile time.
         if isinstance(node.arg, nodes.Slice) or \
@@ -1279,7 +1284,7 @@ class CodeGenerator(NodeVisitor):
             self.visit(node.arg, frame)
             self.write(']')
         else:
-            self.write('environment.subscribe(')
+            self.write('environment.getitem(')
             self.visit(node.node, frame)
             self.write(', ')
             self.visit(node.arg, frame)
index acb5c0272d6b211b6d97198a219c33e787a36989..689bc92f12ea506a374f71b02f7932fdb2a73973 100644 (file)
@@ -286,8 +286,8 @@ class Environment(object):
         """Return a fresh lexer for the environment."""
         return Lexer(self)
 
-    def subscribe(self, obj, argument):
-        """Get an item or attribute of an object."""
+    def getitem(self, obj, argument):
+        """Get an item or attribute of an object but prefer the item."""
         try:
             return obj[argument]
         except (TypeError, LookupError):
@@ -303,6 +303,19 @@ class Environment(object):
                         pass
             return self.undefined(obj=obj, name=argument)
 
+    def getattr(self, obj, attribute):
+        """Get an item or attribute of an object but prefer the attribute.
+        Unlike :meth:`getitem` the attribute *must* be a bytestring.
+        """
+        try:
+            return getattr(obj, attribute)
+        except AttributeError:
+            pass
+        try:
+            return obj[attribute]
+        except (TypeError, LookupError):
+            return self.undefined(obj=obj, name=attribute)
+
     def parse(self, source, name=None, 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
index 959b4daee480eb7cd3272aabaa5b5fe321a83c7b..2bcce4356abf9122dc96491764095055c684c3fb 100644 (file)
@@ -572,7 +572,7 @@ def do_groupby(environment, value, attribute):
     attribute and the `list` contains all the objects that have this grouper
     in common.
     """
-    expr = lambda x: environment.subscribe(x, attribute)
+    expr = lambda x: environment.getitem(x, attribute)
     return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
 
 
@@ -624,10 +624,10 @@ def do_reverse(value):
 @environmentfilter
 def do_attr(environment, obj, name):
     """Get an attribute of an object.  ``foo|attr("bar")`` works like
-    ``foo["bar"]`` just that always an attribute is returned.  This is useful
-    if data structures are passed to the template that have an item that hides
-    an attribute with the same name.  For example a dict ``{'items': []}``
-    that obviously hides the item method of a dict.
+    ``foo["bar"]`` just that always an attribute is returned and items are not
+    looked up.
+
+    See :ref:`Notes on subscribing <notes-on-subscribing>` for more details.
     """
     try:
         value = getattr(obj, name)
@@ -635,10 +635,7 @@ def do_attr(environment, obj, name):
         return environment.undefined(obj=obj, name=name)
     if environment.sandboxed and not \
        environment.is_safe_attribute(obj, name, value):
-        return environment.undefined('access to attribute %r of %r '
-                                     'object is unsafe.' % (
-            name, obj.__class__.__name__
-        ), name=name, obj=obj, exc=SecurityError)
+        return environment.unsafe_undefined(obj, name)
     return value
 
 
index f4b1f3232330d43aa85447a759b0a849dd6de6ee..59509209691516daaa40080f00d0ec903e87c020 100644 (file)
@@ -582,7 +582,7 @@ class Call(Expr):
             raise Impossible()
 
 
-class Subscript(Expr):
+class Getitem(Expr):
     """Subscribe an expression by an argument.  This node performs a dict
     and an attribute lookup on the object whatever succeeds.
     """
@@ -592,8 +592,24 @@ class Subscript(Expr):
         if self.ctx != 'load':
             raise Impossible()
         try:
-            return self.environment.subscribe(self.node.as_const(),
-                                              self.arg.as_const())
+            return self.environment.getitem(self.node.as_const(),
+                                            self.arg.as_const())
+        except:
+            raise Impossible()
+
+    def can_assign(self):
+        return False
+
+
+class Getattr(Expr):
+    """Subscribe an attribute."""
+    fields = ('node', 'attr', 'ctx')
+
+    def as_const(self):
+        if self.ctx != 'load':
+            raise Impossible()
+        try:
+            return self.environment.getattr(self.node.as_const(), arg)
         except:
             raise Impossible()
 
index 8f92e38952a6ff4e8f271927b0509ddcd87c1c56..4838971cee2b2b2a383ae89df46ac0ecf43c13eb 100644 (file)
@@ -63,6 +63,6 @@ class Optimizer(NodeTransformer):
 
     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_Not = visit_Compare = visit_Getitem = visit_Getattr = visit_Call = \
     visit_Filter = visit_Test = visit_CondExpr = fold
     del fold
index 7efe79cdd767c109eeee26c8b9a366a1c7329192..e73d820bf15b3aaf9965d7ec464e9654fc2a2943 100644 (file)
@@ -565,11 +565,16 @@ class Parser(object):
         token = self.stream.next()
         if token.type is 'dot':
             attr_token = self.stream.current
-            if attr_token.type not in ('name', 'integer'):
+            self.stream.next()
+            if attr_token.type is 'name':
+                return nodes.Getattr(node, attr_token.value, 'load',
+                                     lineno=token.lineno)
+            elif attr_token.type is not 'integer':
                 self.fail('expected name or number', attr_token.lineno)
             arg = nodes.Const(attr_token.value, lineno=attr_token.lineno)
-            self.stream.next()
-        elif token.type is 'lbracket':
+            return nodes.Getitem(node, arg, 'load', lineno=token.lineno)
+        if token.type is 'lbracket':
+            priority_on_attribute = False
             args = []
             while self.stream.current.type is not 'rbracket':
                 if args:
@@ -580,9 +585,8 @@ class Parser(object):
                 arg = args[0]
             else:
                 arg = nodes.Tuple(args, self.lineno, self.filename)
-        else:
-            self.fail('expected subscript expression', self.lineno)
-        return nodes.Subscript(node, arg, 'load', lineno=token.lineno)
+            return nodes.Getitem(node, arg, 'load', lineno=token.lineno)
+        self.fail('expected subscript expression', self.lineno)
 
     def parse_subscribed(self):
         lineno = self.stream.current.lineno
index 7135ff0987825854a8bb3f93e7e03a8e7b480ed5..20de3695b7faef5a60381c03b2594defa6a7cc62 100644 (file)
@@ -183,7 +183,7 @@ class SandboxedEnvironment(Environment):
         return not (getattr(obj, 'unsafe_callable', False) or \
                     getattr(obj, 'alters_data', False))
 
-    def subscribe(self, obj, argument):
+    def getitem(self, obj, argument):
         """Subscribe an object from sandboxed code."""
         try:
             return obj[argument]
@@ -201,13 +201,34 @@ class SandboxedEnvironment(Environment):
                     else:
                         if self.is_safe_attribute(obj, argument, value):
                             return value
-                        return self.undefined('access to attribute %r of %r '
-                                              'object is unsafe.' % (
-                            argument,
-                            obj.__class__.__name__
-                        ), name=argument, obj=obj, exc=SecurityError)
+                        return self.unsafe_undefined(obj, argument)
         return self.undefined(obj=obj, name=argument)
 
+    def getattr(self, obj, attribute):
+        """Subscribe an object from sandboxed code and prefer the
+        attribute.  The attribute passed *must* be a bytestring.
+        """
+        try:
+            value = getattr(obj, attribute)
+        except AttributeError:
+            try:
+                return obj[argument]
+            except (TypeError, LookupError):
+                pass
+        else:
+            if self.is_safe_attribute(obj, attribute, value):
+                return value
+            return self.unsafe_undefined(obj, attribute)
+        return self.undefined(obj=obj, name=argument)
+
+    def unsafe_undefined(self, obj, attribute):
+        """Return an undefined object for unsafe attributes."""
+        return self.undefined('access to attribute %r of %r '
+                              'object is unsafe.' % (
+            attribute,
+            obj.__class__.__name__
+        ), name=attribute, obj=obj, exc=SecurityError)
+
     def call(__self, __context, __obj, *args, **kwargs):
         """Call an object from sandboxed code."""
         # the double prefixes are to avoid double keyword argument
index cbde4db49ba96cde0f4950b713d0ba4f4b3bf198..535b97c80455cb03746d9219a95c22349d27024a 100644 (file)
@@ -60,13 +60,14 @@ def test_markup_leaks():
     assert len(counts) == 1, 'ouch, c extension seems to leak objects'
 
 
-def test_item_before_attribute():
+def test_item_and_attribute():
     from jinja2 import Environment
     from jinja2.sandbox import SandboxedEnvironment
 
     for env in Environment(), SandboxedEnvironment():
         tmpl = env.from_string('{{ foo.items() }}')
-        assert tmpl.render(foo={'items': lambda: 42}) == '42'
-        assert tmpl.render(foo={}) == '[]'
-        tmpl = env.from_string('{{ foo|attr("items")() }}')
-        assert tmpl.render(foo={'items': None}) == "[('items', None)]"
+        assert tmpl.render(foo={'items': 42}) == "[('items', 42)]"
+        tmpl = env.from_string('{{ foo|attr("items") }}')
+        assert tmpl.render(foo={'items': 42}) == "[('items', 42)]"
+        tmpl = env.from_string('{{ foo["items"] }}')
+        assert tmpl.render(foo={'items': 42}) == '42'