Browse files

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
  • Loading branch information...
1 parent f3d3d7b commit 6dc6f291eb46352de4df7dfaf10fb14369dc7fb1 @mitsuhiko mitsuhiko committed Jun 12, 2008
Showing with 131 additions and 35 deletions.
  1. +13 −1 CHANGES
  2. +4 −0 docs/_static/style.css
  3. +23 −0 docs/templates.rst
  4. +7 −2 jinja2/compiler.py
  5. +15 −2 jinja2/environment.py
  6. +6 −9 jinja2/filters.py
  7. +19 −3 jinja2/nodes.py
  8. +1 −1 jinja2/optimizer.py
  9. +10 −6 jinja2/parser.py
  10. +27 −6 jinja2/sandbox.py
  11. +6 −5 tests/test_various.py
View
14 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
View
4 docs/_static/style.css
@@ -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;
}
View
23 docs/templates.rst
@@ -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
View
9 jinja2/compiler.py
@@ -1268,7 +1268,12 @@ def visit_Operand(self, node, frame):
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 @@ def visit_Subscript(self, node, frame):
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)
View
17 jinja2/environment.py
@@ -286,8 +286,8 @@ def lexer(self):
"""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 @@ def subscribe(self, obj, argument):
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
View
15 jinja2/filters.py
@@ -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,21 +624,18 @@ 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)
except AttributeError:
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
View
22 jinja2/nodes.py
@@ -582,7 +582,7 @@ def as_const(self):
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 @@ def as_const(self):
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()
View
2 jinja2/optimizer.py
@@ -63,6 +63,6 @@ def fold(self, node):
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
View
16 jinja2/parser.py
@@ -565,11 +565,16 @@ def parse_subscript(self, node):
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 @@ def parse_subscript(self, node):
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
View
33 jinja2/sandbox.py
@@ -183,7 +183,7 @@ def is_safe_callable(self, obj):
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 @@ def subscribe(self, obj, argument):
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
View
11 tests/test_various.py
@@ -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'

0 comments on commit 6dc6f29

Please sign in to comment.