From 6dc6f291eb46352de4df7dfaf10fb14369dc7fb1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 12 Jun 2008 08:50:07 +0200 Subject: [PATCH] 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 --- CHANGES | 14 +++++++++++++- docs/_static/style.css | 4 ++++ docs/templates.rst | 23 +++++++++++++++++++++++ jinja2/compiler.py | 9 +++++++-- jinja2/environment.py | 17 +++++++++++++++-- jinja2/filters.py | 15 ++++++--------- jinja2/nodes.py | 22 +++++++++++++++++++--- jinja2/optimizer.py | 2 +- jinja2/parser.py | 16 ++++++++++------ jinja2/sandbox.py | 33 +++++++++++++++++++++++++++------ tests/test_various.py | 11 ++++++----- 11 files changed, 131 insertions(+), 35 deletions(-) diff --git a/CHANGES b/CHANGES index 0c8e30e..393ba0c 100644 --- a/CHANGES +++ b/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 `. + +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 @@ 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; } 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 @@ 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 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class Environment(object): """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 @@ class Environment(object): 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 @@ 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,10 +624,10 @@ 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 ` for more details. """ try: value = getattr(obj, name) @@ -635,10 +635,7 @@ def do_attr(environment, obj, name): 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 @@ class Call(Expr): 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 @@ class Subscript(Expr): 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 @@ class Optimizer(NodeTransformer): 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 @@ class Parser(object): 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 @@ class Parser(object): 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 @@ class SandboxedEnvironment(Environment): 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 @@ class SandboxedEnvironment(Environment): 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 @@ 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' -- 2.26.2