# HG changeset patch # User cmlenz # Date 1156355354 0 # Node ID cda3bdfc19ed22724fbd37d2cc9e6d4c6b4d4d5b # Parent 929ef2913b875af082bbd63c587eec74b8df30ce Expression evaluation now differentiates between undefined variables and variables that are defined but set to `None`. diff --git a/ChangeLog b/ChangeLog --- 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) diff --git a/markup/eval.py b/markup/eval.py --- 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 @@ '' % (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: diff --git a/markup/plugin.py b/markup/plugin.py --- 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) diff --git a/markup/template.py b/markup/template.py --- 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): diff --git a/markup/tests/eval.py b/markup/tests/eval.py --- 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('', - 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('', + 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():