# HG changeset patch # User cmlenz # Date 1174035534 0 # Node ID c478a6fa9e77cbba7371adfafe0f069503d8b6c6 # Parent 359451249ce09ee25cd4e979032923d2df5cfa62 Make expression error handling more strict. Closes #88. diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -42,6 +42,11 @@ `Attrs` objects (for example, stream filters and generators). * Python code blocks are now supported using the `` processing instruction (ticket #84). + * Runtime error handling in template expressions has become more strict + (ticket #88). Where you previously could access undefined variables or + members, you now get an exception. If a variable is not necessarily defined + at the top level of the template data, the new built-in functions + `defined(name)` and `value_of(name, default)` need to be used. Version 0.3.6 diff --git a/genshi/template/eval.py b/genshi/template/eval.py --- a/genshi/template/eval.py +++ b/genshi/template/eval.py @@ -24,9 +24,10 @@ import sys from genshi.core import Markup +from genshi.template.base import TemplateRuntimeError from genshi.util import flatten -__all__ = ['Expression', 'Suite', 'Undefined'] +__all__ = ['Expression', 'Suite'] class Code(object): @@ -119,10 +120,13 @@ @param data: a mapping containing the data to evaluate against @return: the result of the evaluation """ + __traceback_hide__ = 'before_and_this' return eval(self.code, {'data': data, '_lookup_name': _lookup_name, '_lookup_attr': _lookup_attr, - '_lookup_item': _lookup_item}, + '_lookup_item': _lookup_item, + 'defined': _defined(data), + 'value_of': _value_of(data)}, {'data': data}) @@ -142,68 +146,30 @@ @param data: a mapping containing the data to execute in """ + __traceback_hide__ = 'before_and_this' exec self.code in {'data': data, '_lookup_name': _lookup_name, '_lookup_attr': _lookup_attr, - '_lookup_item': _lookup_item}, data + '_lookup_item': _lookup_item, + 'defined': _defined(data), + 'value_of': _value_of(data)}, data -class Undefined(object): - """Represents a reference to an undefined variable. - - Unlike the Python runtime, template expressions can refer to an undefined - variable without causing a `NameError` to be raised. The result will be an - instance of the `Undefined´ class, which is treated the same as `False` in - conditions, and acts as an empty collection in iterations: - - >>> foo = Undefined('foo') - >>> bool(foo) - False - >>> list(foo) - [] - >>> print foo - undefined - - However, calling an undefined variable, or trying to access an attribute - of that variable, will raise an exception that includes the name used to - reference that undefined variable. - - >>> foo('bar') - Traceback (most recent call last): - ... - NameError: Variable "foo" is not defined +def _defined(data): + def defined(name): + """Return whether a variable with the specified name exists in the + expression scope. + """ + return name in data + return defined - >>> foo.bar - Traceback (most recent call last): - ... - NameError: Variable "foo" is not defined - """ - __slots__ = ['_name'] - - def __init__(self, name): - self._name = name - - def __call__(self, *args, **kwargs): - __traceback_hide__ = True - self.throw() - - def __getattr__(self, name): - __traceback_hide__ = True - self.throw() - - def __iter__(self): - return iter([]) - - def __nonzero__(self): - return False - - def __repr__(self): - return 'undefined' - - def throw(self): - __traceback_hide__ = True - raise NameError('Variable "%s" is not defined' % self._name) - +def _value_of(data): + def value_of(name, default=None): + """If a variable of the specified name is defined, return its value. + Otherwise, return the provided default value, or `None`. + """ + return data.get(name, default) + return value_of def _parse(source, mode='eval'): if isinstance(source, unicode): @@ -238,42 +204,56 @@ code.co_lnotab, (), ()) BUILTINS = __builtin__.__dict__.copy() -BUILTINS.update({'Markup': Markup, 'Undefined': Undefined}) -_UNDEF = Undefined(None) +BUILTINS.update({'Markup': Markup}) +UNDEFINED = object() + + +class UndefinedError(TemplateRuntimeError): + """Exception thrown when a template expression attempts to access a variable + not defined in the context. + """ + def __init__(self, name, owner=UNDEFINED): + if owner is not UNDEFINED: + orepr = repr(owner) + if len(orepr) > 60: + orepr = orepr[:60] + '...' + message = '%s (%s) has no member named "%s"' % ( + type(owner).__name__, orepr, name + ) + else: + message = '"%s" not defined' % name + TemplateRuntimeError.__init__(self, message) + def _lookup_name(data, name): __traceback_hide__ = True - val = data.get(name, _UNDEF) - if val is _UNDEF: + val = data.get(name, UNDEFINED) + if val is UNDEFINED: val = BUILTINS.get(name, val) - if val is _UNDEF: - return Undefined(name) + if val is UNDEFINED: + raise UndefinedError(name) return val def _lookup_attr(data, obj, key): __traceback_hide__ = True - if type(obj) is Undefined: - obj.throw() if hasattr(obj, key): return getattr(obj, key) try: return obj[key] except (KeyError, TypeError): - return Undefined(key) + raise UndefinedError(key, owner=obj) def _lookup_item(data, obj, key): __traceback_hide__ = True - if type(obj) is Undefined: - obj.throw() if len(key) == 1: key = key[0] try: return obj[key] except (AttributeError, KeyError, IndexError, TypeError), e: if isinstance(key, basestring): - val = getattr(obj, key, _UNDEF) - if val is _UNDEF: - val = Undefined(key) + val = getattr(obj, key, UNDEFINED) + if val is UNDEFINED: + raise UndefinedError(key, owner=obj) return val raise @@ -523,7 +503,7 @@ """ def __init__(self): - self.locals = [] + self.locals = [set(['defined', 'value_of'])] def visitConst(self, node): if isinstance(node.value, str): diff --git a/genshi/template/tests/eval.py b/genshi/template/tests/eval.py --- a/genshi/template/tests/eval.py +++ b/genshi/template/tests/eval.py @@ -16,7 +16,7 @@ import unittest from genshi.core import Markup -from genshi.template.eval import Expression, Suite, Undefined +from genshi.template.eval import Expression, Suite, UndefinedError class ExpressionTestCase(unittest.TestCase): @@ -40,8 +40,6 @@ def test_builtins(self): expr = Expression('Markup') self.assertEqual(expr.evaluate({}), Markup) - expr = Expression('Undefined') - self.assertEqual(expr.evaluate({}), Undefined) def test_str_literal(self): self.assertEqual('foo', Expression('"foo"').evaluate({})) @@ -323,80 +321,68 @@ def test_error_access_undefined(self): expr = Expression("nothing", filename='index.html', lineno=50) - self.assertEqual(Undefined, type(expr.evaluate({}))) - - def test_error_call_undefined(self): - expr = Expression("nothing()", filename='index.html', lineno=50) try: expr.evaluate({}) - self.fail('Expected NameError') - except NameError, e: - exc_type, exc_value, exc_traceback = sys.exc_info() - frame = exc_traceback.tb_next - frames = [] - while frame.tb_next: - frame = frame.tb_next - frames.append(frame) - self.assertEqual('Variable "nothing" is not defined', str(e)) - self.assertEqual("", - frames[-3].tb_frame.f_code.co_name) - self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) - - def test_error_getattr_undefined(self): - expr = Expression("nothing.nil", filename='index.html', lineno=50) - try: - expr.evaluate({}) - self.fail('Expected NameError') - except NameError, e: + self.fail('Expected UndefinedError') + except UndefinedError, e: exc_type, exc_value, exc_traceback = sys.exc_info() frame = exc_traceback.tb_next frames = [] while frame.tb_next: frame = frame.tb_next frames.append(frame) - self.assertEqual('Variable "nothing" is not defined', str(e)) - self.assertEqual("", - frames[-3].tb_frame.f_code.co_name) + self.assertEqual('"nothing" not defined', str(e)) + self.assertEqual("", + frames[-2].tb_frame.f_code.co_name) self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) + frames[-2].tb_frame.f_code.co_filename) + self.assertEqual(50, frames[-2].tb_lineno) - def test_error_getitem_undefined(self): - expr = Expression("nothing[0]", filename='index.html', lineno=50) + def test_error_getattr_undefined(self): + class Something(object): + def __repr__(self): + return '' + expr = Expression('something.nil', filename='index.html', lineno=50) try: - expr.evaluate({}) - self.fail('Expected NameError') - except NameError, e: + expr.evaluate({'something': Something()}) + self.fail('Expected UndefinedError') + except UndefinedError, e: exc_type, exc_value, exc_traceback = sys.exc_info() frame = exc_traceback.tb_next frames = [] while frame.tb_next: frame = frame.tb_next frames.append(frame) - self.assertEqual('Variable "nothing" is not defined', str(e)) - self.assertEqual("", - frames[-3].tb_frame.f_code.co_name) + self.assertEqual('Something () has no member named "nil"', + str(e)) + self.assertEqual("", + frames[-2].tb_frame.f_code.co_name) self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) + frames[-2].tb_frame.f_code.co_filename) + self.assertEqual(50, frames[-2].tb_lineno) - def test_error_getattr_nested_undefined(self): - expr = Expression("nothing.nil", filename='index.html', lineno=50) - val = expr.evaluate({'nothing': object()}) - assert isinstance(val, Undefined) - self.assertEqual("nil", val._name) - - def test_error_getitem_nested_undefined_string(self): - expr = Expression("nothing['bla']", filename='index.html', lineno=50) - val = expr.evaluate({'nothing': object()}) - assert isinstance(val, Undefined) - self.assertEqual("bla", val._name) - - def test_error_getitem_nested_undefined_int(self): - expr = Expression("nothing[0]", filename='index.html', lineno=50) - self.assertRaises(TypeError, expr.evaluate, {'nothing': object()}) + def test_error_getitem_undefined_string(self): + class Something(object): + def __repr__(self): + return '' + expr = Expression('something["nil"]', filename='index.html', lineno=50) + try: + expr.evaluate({'something': Something()}) + self.fail('Expected UndefinedError') + except UndefinedError, e: + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = exc_traceback.tb_next + frames = [] + while frame.tb_next: + frame = frame.tb_next + frames.append(frame) + self.assertEqual('Something () has no member named "nil"', + str(e)) + self.assertEqual('''''', + frames[-2].tb_frame.f_code.co_name) + self.assertEqual('index.html', + frames[-2].tb_frame.f_code.co_filename) + self.assertEqual(50, frames[-2].tb_lineno) class SuiteTestCase(unittest.TestCase): diff --git a/genshi/template/tests/text.py b/genshi/template/tests/text.py --- a/genshi/template/tests/text.py +++ b/genshi/template/tests/text.py @@ -37,7 +37,7 @@ #if foo bar #end 'if foo'""") - self.assertEqual('\n', str(tmpl.generate())) + self.assertEqual('\n', str(tmpl.generate(foo=False))) def test_latin1_encoded(self): text = u'$foo\xf6$bar'.encode('iso-8859-1')