changeset 418:c478a6fa9e77 trunk

Make expression error handling more strict. Closes #88.
author cmlenz
date Fri, 16 Mar 2007 08:58:54 +0000
parents 359451249ce0
children d2dc490aa998
files ChangeLog genshi/template/eval.py genshi/template/tests/eval.py genshi/template/tests/text.py
diffstat 4 files changed, 103 insertions(+), 132 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -42,6 +42,11 @@
    `Attrs` objects (for example, stream filters and generators).
  * Python code blocks are now supported using the `<?python ?>` 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
--- 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):
--- 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("<Expression 'nothing()'>",
-                             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("<Expression 'nothing.nil'>",
-                             frames[-3].tb_frame.f_code.co_name)
+            self.assertEqual('"nothing" not defined', str(e))
+            self.assertEqual("<Expression 'nothing'>",
+                             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 '<Something>'
+        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("<Expression 'nothing[0]'>",
-                             frames[-3].tb_frame.f_code.co_name)
+            self.assertEqual('Something (<Something>) has no member named "nil"',
+                             str(e))
+            self.assertEqual("<Expression 'something.nil'>",
+                             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 '<Something>'
+        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 (<Something>) has no member named "nil"',
+                             str(e))
+            self.assertEqual('''<Expression 'something["nil"]'>''',
+                             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):
--- 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')
Copyright (C) 2012-2017 Edgewall Software