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'