Improved attribute and item lookup by allowing template designers to express the priority.  foo.bar checks foo.bar first and then foo['bar'] and the other way round.

--HG--
branch : trunk
diff --git a/CHANGES b/CHANGES
index 0c8e30e..393ba0c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -5,4 +5,16 @@
 -----------
 (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
diff --git a/docs/_static/style.css b/docs/_static/style.css
index a52436d..e6238d5 100644
--- a/docs/_static/style.css
+++ b/docs/_static/style.css
@@ -227,6 +227,10 @@
     font-size: 15px;
 }
 
+div.admonition p.admonition-title a {
+    color: white!important;
+}
+
 div.admonition-note {
     background: url(note.png) no-repeat 10px 40px;
 }
diff --git a/docs/templates.rst b/docs/templates.rst
index 11d9978..ac9c9a1 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -74,6 +74,29 @@
 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
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index 9d68e4c..75869cf 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -1268,7 +1268,12 @@
         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 @@
             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)
diff --git a/jinja2/environment.py b/jinja2/environment.py
index acb5c02..689bc92 100644
--- a/jinja2/environment.py
+++ b/jinja2/environment.py
@@ -286,8 +286,8 @@
         """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 @@
                         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
diff --git a/jinja2/filters.py b/jinja2/filters.py
index 959b4da..2bcce43 100644
--- a/jinja2/filters.py
+++ b/jinja2/filters.py
@@ -572,7 +572,7 @@
     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 @@
 @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 @@
         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
 
 
diff --git a/jinja2/nodes.py b/jinja2/nodes.py
index f4b1f32..5950920 100644
--- a/jinja2/nodes.py
+++ b/jinja2/nodes.py
@@ -582,7 +582,7 @@
             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 @@
         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()
 
diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py
index 8f92e38..4838971 100644
--- a/jinja2/optimizer.py
+++ b/jinja2/optimizer.py
@@ -63,6 +63,6 @@
 
     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
diff --git a/jinja2/parser.py b/jinja2/parser.py
index 7efe79c..e73d820 100644
--- a/jinja2/parser.py
+++ b/jinja2/parser.py
@@ -565,11 +565,16 @@
         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 @@
                 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
diff --git a/jinja2/sandbox.py b/jinja2/sandbox.py
index 7135ff0..20de369 100644
--- a/jinja2/sandbox.py
+++ b/jinja2/sandbox.py
@@ -183,7 +183,7 @@
         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 @@
                     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
diff --git a/tests/test_various.py b/tests/test_various.py
index cbde4db..535b97c 100644
--- a/tests/test_various.py
+++ b/tests/test_various.py
@@ -60,13 +60,14 @@
     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'