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