changeset 192:b64e36bc1100 trunk

Expression evaluation now differentiates between undefined variables and variables that are defined but set to `None`.
author cmlenz
date Wed, 23 Aug 2006 17:49:14 +0000
parents 3289055a8c32
children 404c02f3156b
files ChangeLog markup/eval.py markup/plugin.py markup/template.py markup/tests/eval.py
diffstat 5 files changed, 141 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+Version 0.3
+http://svn.edgewall.org/repos/markup/tags/0.3.0/
+(?, from branches/stable/0.3.x)
+
+ * Expression evaluation now differentiates between undefined variables and
+   variables that are defined but set to `None`. This also means that local
+   variables can override built-ins even if the local variable are set to
+   `None` (ticket #36).
+ * The parsing of `py:with` directives has been improved: you can now assign
+   to multiple names, and semicolons inside string literals are treated as
+   expected.
+
+
 Version 0.2
 http://svn.edgewall.org/repos/markup/tags/0.2.0/
 (Aug 22 2006, from branches/stable/0.2.x)
--- a/markup/eval.py
+++ b/markup/eval.py
@@ -18,7 +18,7 @@
 from compiler.pycodegen import ExpressionCodeGenerator
 import new
 
-__all__ = ['Expression']
+__all__ = ['Expression', 'Undefined']
 
 
 class Expression(object):
@@ -97,6 +97,60 @@
         return retval
 
 
+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
+
+    >>> 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):
+        self.throw()
+
+    def __getattr__(self, name):
+        self.throw()
+
+    def __iter__(self):
+        return iter([])
+
+    def __nonzero__(self):
+        return False
+
+    def __repr__(self):
+        return 'undefined'
+
+    def throw(self):
+        raise NameError('Variable "%s" is not defined' % self.name)
+
+
 def _compile(node, source=None, filename=None, lineno=-1):
     tree = ExpressionASTTransformer().visit(node)
     if isinstance(filename, unicode):
@@ -120,17 +174,28 @@
                     '<Expression %s>' % (repr(source).replace("'", '"') or '?'),
                     lineno, code.co_lnotab, (), ())
 
+BUILTINS = __builtin__.__dict__.copy()
+BUILTINS['Undefined'] = Undefined
+
 def _lookup_name(data, name, locals_=None):
-    val = None
+    val = Undefined
     if locals_:
-        val = locals_.get(name)
-    if val is None:
-        val = data.get(name)
-    if val is None:
-        val = getattr(__builtin__, name, None)
-    return val
+        val = locals_.get(name, val)
+    if val is Undefined:
+        val = data.get(name, val)
+        if val is Undefined:
+            val = BUILTINS.get(name, val)
+            if val is not Undefined or name == 'Undefined':
+                return val
+        else:
+            return val
+    else:
+        return val
+    return val(name)
 
 def _lookup_attr(data, obj, key):
+    if type(obj) is Undefined:
+        obj.throw()
     if hasattr(obj, key):
         return getattr(obj, key)
     try:
@@ -139,6 +204,8 @@
         return None
 
 def _lookup_item(data, obj, key):
+    if type(obj) is Undefined:
+        obj.throw()
     if len(key) == 1:
         key = key[0]
     try:
--- a/markup/plugin.py
+++ b/markup/plugin.py
@@ -19,9 +19,11 @@
 from pkg_resources import resource_filename
 
 from markup.core import Attrs, Stream, QName
+from markup.eval import Undefined
+from markup.input import HTML, XML
 from markup.template import Context, Template, TemplateLoader
 
-def et_to_stream(element):
+def ET(element):
     """Converts the given ElementTree element to a markup stream."""
     tag_name = element.tag
     if tag_name.startswith('{'):
@@ -71,9 +73,21 @@
         if not isinstance(template, Template):
             template = self.load_template(template)
 
-        data = {'ET': et_to_stream}
+        data = {'ET': ET, 'HTML': HTML, 'XML': XML}
         if self.get_extra_vars:
             data.update(self.get_extra_vars())
         data.update(info)
+        ctxt = Context(**data)
 
-        return template.generate(**data)
+        # Some functions for Kid compatibility
+        def defined(name):
+            return ctxt.get(name, Undefined) is not Undefined
+        ctxt['defined'] = defined
+        def value_of(name, default=None):
+            val = ctxt.get(name, Undefined)
+            if val is not Undefined:
+                return val
+            return default
+        ctxt['value_of'] = value_of
+
+        return template.generate(ctxt)
--- a/markup/template.py
+++ b/markup/template.py
@@ -111,13 +111,14 @@
         """Set a variable in the current scope."""
         self.frames[0][key] = value
 
-    def get(self, key):
+    def get(self, key, default=None):
         """Get a variable's value, starting at the current scope and going
         upward.
         """
         for frame in self.frames:
             if key in frame:
                 return frame[key]
+        return default
     __getitem__ = get
 
     def push(self, data):
--- a/markup/tests/eval.py
+++ b/markup/tests/eval.py
@@ -20,6 +20,13 @@
 
 class ExpressionTestCase(unittest.TestCase):
 
+    def test_name_lookup(self):
+        self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'}))
+        self.assertEqual(id, Expression('id').evaluate({}, nocall=True))
+        self.assertEqual('bar', Expression('id').evaluate({'id': 'bar'}))
+        self.assertEqual(None, Expression('id').evaluate({'id': None},
+                                                         nocall=True))
+
     def test_str_literal(self):
         self.assertEqual('foo', Expression('"foo"').evaluate({}))
         self.assertEqual('foo', Expression('"""foo"""').evaluate({}))
@@ -233,20 +240,41 @@
         expr = Expression("[i['name'] for i in items if i['value'] > 1]")
         self.assertEqual(['b'], expr.evaluate({'items': items}))
 
-    def test_error_position(self):
+    def test_error_call_undefined(self):
         expr = Expression("nothing()", filename='index.html', lineno=50)
         try:
             expr.evaluate({})
-            self.fail('Expected TypeError')
-        except TypeError, e:
+            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('<Expression "nothing()">',
-                             frame.tb_frame.f_code.co_name)
-            self.assertEqual('index.html', frame.tb_frame.f_code.co_filename)
-            self.assertEqual(50, frame.tb_lineno)
+                             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:
+            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('<Expression "nothing.nil">',
+                             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 suite():
Copyright (C) 2012-2017 Edgewall Software