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/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