changeset 442:97544725bb7f trunk

Back out [510] and instead implement configurable error handling modes. The default is the old 0.3.x behaviour, but more strict error handling is available as an option.
author cmlenz
date Thu, 12 Apr 2007 22:40:49 +0000
parents 2755b06148b3
children 84c6a7522abb
files ChangeLog UPGRADE.txt doc/index.txt doc/templates.txt doc/text-templates.txt doc/xml-templates.txt genshi/template/base.py genshi/template/directives.py genshi/template/eval.py genshi/template/interpolation.py genshi/template/loader.py genshi/template/markup.py genshi/template/plugin.py genshi/template/tests/eval.py genshi/template/text.py
diffstat 15 files changed, 698 insertions(+), 404 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -42,11 +42,16 @@
    `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.
+ * The way errors in template expressions are handled can now be configured. The
+   option `LenientLookup` provides the same forgiving mode used in previous
+   Genshi versions, while `StrictLookup` raises exceptions when undefined
+   variables or members are accessed. The lenient mode is still the default in
+   this version, but that may change in the future. (ticket #88)
+ * If a variable is not necessarily defined at the top level of the template
+   data, the new built-in functions `defined(key)` and `value_of(key, default)`
+   can be used so that the template also works in strict lookup mode. These
+   functions were previously only available when using Genshi via the template
+   engine plugin (for compatibility with Kid).
  * `style` attributes are no longer allowed by the `HTMLSanitizer` by default.
    If it is explicitly added to the set of safe attributes, and unicode escapes
    in the attribute value are handled correctly.
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -10,16 +10,6 @@
 leftover traces of the `template.py` file on the installation path.
 This is not necessary when Genshi was installed as a Python egg.
 
-Handling of errors in template expressions is now more strict. In
-particular, it is no longer possible to reference an undefined
-variable without an exception being raised. The previous error
-handling let expressions get away with minor typos, which would
-result in subtle bugs that were hard to find. The functions
-`defined()` and `value_of()` are now available in all template
-code; they can be used to test whether a specific variable is
-defined at the top-level scope. Please refer to the template
-language documentation for details.
-
 Results of evaluating template expressions are no longer implicitly
 called if they are callable. If you have been using that feature, you
 will need to add the parenthesis to actually call the function.
--- a/doc/index.txt
+++ b/doc/index.txt
@@ -21,8 +21,9 @@
 which is heavily inspired by Kid.
 
 * `Markup Streams <streams.html>`_
-* `Genshi XML Template Language <xml-templates.html>`_
-* `Genshi Text Template Language <text-templates.html>`_
+* `Templating Basics <templates.html>`_
+* `XML Template Language <xml-templates.html>`_
+* `Text Template Language <text-templates.html>`_
 * `Using Stream Filters <filters.html>`_
-* `Using XPath in Genshi <xpath.html>`_
+* `Using XPath <xpath.html>`_
 * `Generated API Documentation <api/index.html>`_
new file mode 100644
--- /dev/null
+++ b/doc/templates.txt
@@ -0,0 +1,325 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+========================
+Genshi Templating Basics
+========================
+
+Genshi provides a template engine that can be used for generating either
+markup (such as HTML_ or XML_) or plain text. While both share some of the
+syntax (and much of the underlying implementation) they are essentially
+separate languages.
+
+.. _html: http://www.w3.org/html/
+.. _xml: http://www.w3.org/XML/
+
+This document describes the common parts of the template engine and will be most
+useful as reference to those developing Genshi templates. Templates are XML or
+plain text files that include processing directives_ that affect how the
+template is rendered, and template expressions_ that are dynamically substituted
+by variable data.
+
+
+.. contents:: Contents
+   :depth: 3
+.. sectnum::
+
+--------
+Synopsis
+--------
+
+A Genshi *markup template* is a well-formed XML document with embedded Python
+used for control flow and variable substitution. Markup templates should be
+used to generate any kind of HTML or XML output, as they provide many advantages
+over simple text-based templates (such as automatic escaping of strings).
+
+The following illustrates a very basic Genshi markup template::
+
+  <?python
+    title = "A Genshi Template"
+    fruits = ["apple", "orange", "kiwi"]
+  ?>
+  <html xmlns:py="http://genshi.edgewall.org/">
+    <head>
+      <title py:content="title">This is replaced.</title>
+    </head>
+
+    <body>
+      <p>These are some of my favorite fruits:</p>
+      <ul>
+        <li py:for="fruit in fruits">
+          I like ${fruit}s
+        </li>
+      </ul>
+    </body>
+  </html>
+
+This example shows:
+
+(a) a Python code block, using a processing instruction
+(b) the Genshi namespace declaration
+(c) usage of templates directives (``py:content`` and ``py:for``)
+(d) an inline Python expression (``${fruit}``).
+
+A *text template* is a simple plain text document that can also contain embedded
+Python code. Text templates can be used to generate simple *non-markup* text
+formats, such as the body of an plain text email. For example::
+
+  Dear $name,
+  
+  These are some of my favorite fruits:
+  #for fruit in fruits
+   * $fruit
+  #end
+
+
+----------
+Python API
+----------
+
+The Python code required for templating with Genshi is generally based on the
+following pattern:
+
+* Attain a ``MarkupTemplate`` or ``TextTemplate`` object from a string or
+  file-like object containing the template source. This can either be done
+  directly, or through a ``TemplateLoader`` instance.
+* Call the ``generate()`` method of the template, passing any data that should
+  be made available to the template as keyword arguments.
+* Serialize the resulting stream using its ``render()`` method.
+
+For example::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<h1>Hello, $name!</h1>')
+  >>> stream = tmpl.generate(name='world')
+  >>> print stream.render()
+  <h1>Hello, world!</h1>
+
+Using a text template is similar::
+
+  >>> from genshi.template import TextTemplate
+  >>> tmpl = TextTemplate('Hello, $name!')
+  >>> stream = tmpl.generate(name='world')
+  >>> print stream.render()
+  Hello, world!
+
+.. note:: See the Serialization_ section of the `Markup Streams`_ page for
+          information on configuring template output options.
+
+.. _serialization: streams.html#serialization
+.. _`Markup Streams`: streams.html
+
+Using a template loader provides the advantage that “compiled” templates are
+automatically cached, and only parsed again when the template file changes. In
+addition, it enables the use of a *template search path*, allowing template
+directories to be spread across different file-system locations. Using a
+template loader would generally look as follows::
+
+  from genshi.template import TemplateLoader
+  loader = TemplateLoader([templates_dir1, templates_dir2])
+  tmpl = loader.load('test.html')
+  stream = tmpl.generate(title='Hello, world!')
+  print stream.render()
+
+See the `API documentation <api/index.html>`_ for details on using Genshi via
+the Python API.
+
+
+.. _`expressions`:
+
+------------------------------------
+Template Expressions and Code Blocks
+------------------------------------
+
+Python_ expressions can be used in text and directive arguments. An expression
+is substituted with the result of its evaluation against the template data.
+Expressions in text (which includes the values of non-directive attributes) need
+to prefixed with a dollar sign (``$``) and usually enclosed in curly braces
+(``{…}``).
+
+.. _python: http://www.python.org/
+
+If the expression starts with a letter and contains only letters, digits, dots,
+and underscores, the curly braces may be omitted. In all other cases, the
+braces are required so that the template processor knows where the expression
+ends::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<em>${items[0].capitalize()} item</em>')
+  >>> print tmpl.generate(items=['first', 'second'])
+  <em>First item</em>
+
+Expressions support the full power of Python. In addition, it is possible to
+access items in a dictionary using “dotted notation” (i.e. as if they were
+attributes), and vice-versa (i.e. access attributes as if they were items in a
+dictionary)::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<em>${dict.foo}</em>')
+  >>> print tmpl.generate(dict={'foo': 'bar'})
+  <em>bar</em>
+
+Because there are two ways to access either attributes or items, expressions
+do not raise the standard ``AttributeError`` or ``IndexError`` exceptions, but
+rather an exception of the type ``UndefinedError``. The same kind of error is
+raised when you try to access a top-level variable that is not in the context
+data.
+
+
+.. _`code blocks`:
+
+Code Blocks
+===========
+
+XML templates also support full Python code blocks using the ``<?python ?>``
+processing instruction::
+
+  <div>
+    <?python
+        from genshi.builder import tag
+        def greeting(name):
+            return tag.b('Hello, %s!' % name') ?>
+    ${greeting('world')}
+  </div>
+
+This will produce the following output::
+
+  <div>
+    <b>Hello, world!</b>
+  </div>
+
+Code blocks can import modules, define classes and functions, and basically do
+anything you can do in normal Python code. What code blocks can *not* do is to
+produce content that is included directly in the generated page.
+
+.. note:: Using the ``print`` statement will print to the standard output
+          stream, just as it does for other Python code in your application.
+
+This feature is not supposed to encourage mixing application code into
+templates, which is generally considered bad design. If you're using many code
+blocks, that me be a sign that you should move such code into separate Python
+modules.
+
+.. note:: Code blocks are not currently supported in text templates.
+
+
+.. _`error handling`:
+
+Error Handling
+==============
+
+By default, Genshi allows you to access variables that are not defined, without
+raising a ``NameError`` exception as regular Python code would::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${doh}</p>')
+  >>> print tmpl.generate().render('xhtml')
+  <p></p>
+
+You *will* however get an exception if you try to call an undefined variable, or
+do anything else with it, such as accessing its attributes::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${doh.oops}</p>')
+  >>> print tmpl.generate().render('xhtml')
+  Traceback (most recent call last):
+    ...
+  UndefinedError: "doh" not defined
+
+If you need to know whether a variable is defined, you can check its type
+against the ``Undefined`` class, for example in a conditional directive::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${type(doh) is not Undefined}</p>')
+  >>> print tmpl.generate().render('xhtml')
+  <p>False</p>
+
+Alternatively, the built-in functions defined_ or value_of_ can be used in this
+case.
+
+Strict Mode
+-----------
+
+In addition to the default "lenient" error handling, Genshi lets you use a less
+forgiving mode if you prefer errors blowing up loudly instead of being ignored
+silently.
+
+This mode can be chosen by passing the ``lookup='strict'`` keyword argument to
+the template initializer, or by passing the ``variable_lookup='strict'`` keyword
+argument to the ``TemplateLoader`` initializer::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${doh}</p>', lookup='strict')
+  >>> print tmpl.generate().render('xhtml')
+  Traceback (most recent call last):
+    ...
+  UndefinedError: "doh" not defined
+
+When using strict mode, any reference to an undefined variable, as well as
+trying to access an non-existing item or attribute of an object, will cause an
+``UndefinedError`` to be raised immediately.
+
+.. note:: While this mode is currently not the default, it may be promoted to
+          the default in future versions of Genshi. In general, the default
+          lenient error handling mode can be considered dangerous as it silently
+          ignores typos.
+
+Custom Modes
+------------
+
+In addition to the built-in "lenient" and "strict" modes, it is also possible to
+use a custom error handling mode. For example, you could use lenient error
+handling in a production environment, while also logging a warning when an
+undefined variable is referenced.
+
+See the API documentation of the ``genshi.template.eval`` module for details.
+
+
+Built-in Functions & Types
+==========================
+
+The following functions and types are available by default in template code, in
+addition to the standard built-ins that are available to all Python code.
+
+.. _`defined`:
+
+``defined(name)``
+-----------------
+This function determines whether a variable of the specified name exists in
+the context data, and returns ``True`` if it does.
+ 
+.. _`value_of`:
+
+``value_of(name, default=None)``
+--------------------------------
+This function returns the value of the variable with the specified name if
+such a variable is defined, and returns the value of the ``default``
+parameter if no such variable is defined.
+
+.. _`Markup`:
+
+``Markup(text)``
+----------------
+The ``Markup`` type marks a given string as being safe for inclusion in markup,
+meaning it will *not* be escaped in the serialization stage. Use this with care,
+as not escaping a user-provided string may allow malicious users to open your
+web site to cross-site scripting attacks.
+
+.. _`Undefined`:
+
+``Undefined``
+----------------
+The ``Undefined`` type can be used to check whether a reference variable is
+defined, as explained in `error handling`_.
+
+
+.. _`directives`:
+
+-------------------
+Template Directives
+-------------------
+
+Directives provide control flow functionality for templates, such as conditions
+or iteration. As the syntax for directives depends on whether you're using
+markup or text templates, refer to the
+`XML Template Language <xml-templates.html>`_ or
+`Text Template Language <text-templates.html>`_ pages for information.
--- a/doc/text-templates.txt
+++ b/doc/text-templates.txt
@@ -12,119 +12,19 @@
 .. _velocity: http://jakarta.apache.org/velocity/
 
 This document describes the template language and will be most useful as
-reference to those developing Genshi text templates. Templates are XML files of some
-kind (such as XHTML) that include processing directives_ (elements or
-attributes identified by a separate namespace) that affect how the template is
-rendered, and template expressions_ that are dynamically substituted by
+reference to those developing Genshi text templates. Templates are text files of
+some kind that include processing directives_ that affect how the template is
+rendered, and template expressions that are dynamically substituted by
 variable data.
 
+See `Genshi Templating Basics <templates.html>`_ for general information on
+embedding Python code in templates.
+
 
 .. contents:: Contents
    :depth: 3
 .. sectnum::
 
-----------
-Python API
-----------
-
-The Python code required for templating with Genshi is generally based on the
-following pattern:
-
-* Attain a ``TextTemplate`` object from a string or file object containing the
-  template source. This can either be done directly, or through a
-  ``TemplateLoader`` instance.
-* Call the ``generate()`` method of the template, passing any data that should
-  be made available to the template as keyword arguments.
-* Serialize the resulting stream using its ``render()`` method.
-
-For example::
-
-  from genshi.template import TextTemplate
-
-  tmpl = TextTemplate('$title')
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('text')
-
-That code would produce the following output::
-
-  Hello, world!
-
-Using a template loader provides the advantage that “compiled” templates are
-automatically cached, and only parsed again when the template file changes::
-
-  from genshi.template import TemplateLoader
-
-  loader = TemplateLoader([templates_dir])
-  tmpl = loader.load('test.txt' cls=TextTemplate)
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('text')
-
-
-.. _`expressions`:
-
---------------------
-Template Expressions
---------------------
-
-Python_ expressions can be used in text and as arguments to directives_. An expression is substituted with the result of its evaluation against the
-template data. Expressions need to prefixed with a dollar sign (``$``) and 
-usually enclosed in curly braces (``{…}``).
-
-.. _python: http://www.python.org/
-
-If the expression starts with a letter and contains only letters, digits, dots,
-and underscores, the curly braces may be omitted. In all other cases, the
-braces are required so that the template processor knows where the expression
-ends::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${items[0].capitalize()} item')
-  >>> print tmpl.generate(items=['first', 'second'])
-  First item
-
-Expressions support the full power of Python. In addition, it is possible to
-access items in a dictionary using “dotted notation” (i.e. as if they were
-attributes), and vice-versa (i.e. access attributes as if they were items in
-a dictionary)::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${dict.foo}')
-  >>> print tmpl.generate(dict={'foo': 'bar'})
-  bar
-
-Because there are two ways to access either attributes or items, expressions
-do not raise the standard ``AttributeError`` or ``IndexError`` exceptions, but
-rather an exception of the type ``UndefinedError``. The same kind of error is
-raised when you try to access a top-level variable that is not in the context
-data.
-
-Built-in Functions & Types
-==========================
-
-The following functions and types are available by default in template code, in
-addition to the standard built-ins that are available to all Python code.
-
-``defined(name)``
------------------
- 
-This function determines whether a variable of the specified name exists in
-the context data, and returns ``True`` if it does.
- 
-``value_of(name, default=None)``
---------------------------------
-
-This function returns the value of the variable with the specified name if
-such a variable is defined, and returns the value of the ``default``
-parameter if no such variable is defined.
-
-``Markup(text)``
-----------------
-
-The ``Markup`` type marks a given string as being safe for inclusion in markup,
-meaning it will *not* be escaped in the serialization stage. Use this with care,
-as not escaping a user-provided string may allow malicious users to open your
-web site to cross-site scripting attacks.
-
 
 .. _`directives`:
 
--- a/doc/xml-templates.txt
+++ b/doc/xml-templates.txt
@@ -18,151 +18,17 @@
 reference to those developing Genshi XML templates. Templates are XML files of
 some kind (such as XHTML) that include processing directives_ (elements or
 attributes identified by a separate namespace) that affect how the template is
-rendered, and template expressions_ that are dynamically substituted by
+rendered, and template expressions that are dynamically substituted by
 variable data.
 
+See `Genshi Templating Basics <templates.html>`_ for general information on
+embedding Python code in templates.
+
 
 .. contents:: Contents
    :depth: 3
 .. sectnum::
 
-----------
-Python API
-----------
-
-The Python code required for templating with Genshi is generally based on the
-following pattern:
-
-* Attain a ``MarkupTemplate`` object from a string or file object containing
-  the template XML source. This can either be done directly, or through a
-  ``TemplateLoader`` instance.
-* Call the ``generate()`` method of the template, passing any data that should
-  be made available to the template as keyword arguments.
-* Serialize the resulting stream using its ``render()`` method.
-
-For example::
-
-  from genshi.template import MarkupTemplate
-
-  tmpl = MarkupTemplate('<h1>$title</h1>')
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('xhtml')
-
-That code would produce the following output::
-
-  <h1>Hello, world!</h1>
-
-However, if you want includes_ to work, you should attain the template instance
-through a ``TemplateLoader``, and load the template from a file::
-
-  from genshi.template import TemplateLoader
-
-  loader = TemplateLoader([templates_dir])
-  tmpl = loader.load('test.html')
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('xhtml')
-
-
-.. _`expressions`:
-
-------------------------------------
-Template Expressions and Code Blocks
-------------------------------------
-
-Python_ expressions can be used in text and attribute values. An expression is
-substituted with the result of its evaluation against the template data.
-Expressions need to prefixed with a dollar sign (``$``) and usually enclosed in
-curly braces (``{…}``).
-
-If the expression starts with a letter and contains only letters, digits, dots,
-and underscores, the curly braces may be omitted. In all other cases, the
-braces are required so that the template processor knows where the expression
-ends::
-
-  >>> from genshi.template import MarkupTemplate
-  >>> tmpl = MarkupTemplate('<em>${items[0].capitalize()} item</em>')
-  >>> print tmpl.generate(items=['first', 'second'])
-  <em>First item</em>
-
-Expressions support the full power of Python. In addition, it is possible to
-access items in a dictionary using “dotted notation” (i.e. as if they were
-attributes), and vice-versa (i.e. access attributes as if they were items in a
-dictionary)::
-
-  >>> from genshi.template import MarkupTemplate
-  >>> tmpl = MarkupTemplate('<em>${dict.foo}</em>')
-  >>> print tmpl.generate(dict={'foo': 'bar'})
-  <em>bar</em>
-
-Because there are two ways to access either attributes or items, expressions
-do not raise the standard ``AttributeError`` or ``IndexError`` exceptions, but
-rather an exception of the type ``UndefinedError``. The same kind of error is
-raised when you try to access a top-level variable that is not in the context
-data.
-
-
-.. _`code blocks`:
-
-Code Blocks
-===========
-
-XML templates also support full Python code blocks using the ``<?python ?>``
-processing instruction::
-
-  <div>
-    <?python
-        from genshi.builder import tag
-        def greeting(name):
-            return tag.b('Hello, %s!' % name') ?>
-    ${greeting('world')}
-  </div>
-
-This will produce the following output::
-
-  <div>
-    <b>Hello, world!</b>
-  </div>
-
-Code blocks can import modules, define classes and functions, and basically do
-anything you can do in normal Python code. What code blocks can *not* do is to
-produce content that is included directly in the generated page.
-
-.. note:: Using the ``print`` statement will print to the standard output
-          stream, just as it does for other Python code in your application.
-
-This feature is not supposed to encourage mixing application code into
-templates, which is generally considered bad design. If you're using many code
-blocks, that me be a sign that you should move such code into separate Python
-modules.
-
-
-Built-in Functions & Types
-==========================
-
-The following functions and types are available by default in template code, in
-addition to the standard built-ins that are available to all Python code.
-
-``defined(name)``
------------------
- 
-This function determines whether a variable of the specified name exists in
-the context data, and returns ``True`` if it does.
- 
-``value_of(name, default=None)``
---------------------------------
-
-This function returns the value of the variable with the specified name if
-such a variable is defined, and returns the value of the ``default``
-parameter if no such variable is defined.
-
-``Markup(text)``
-----------------
-
-The ``Markup`` type marks a given string as being safe for inclusion in markup,
-meaning it will *not* be escaped in the serialization stage. Use this with care,
-as not escaping a user-provided string may allow malicious users to open your
-web site to cross-site scripting attacks.
-
 
 .. _`directives`:
 
@@ -627,7 +493,7 @@
 .. _`xinclude specification`: http://www.w3.org/TR/xinclude/
 
 Incudes in Genshi are fully dynamic: Just like normal attributes, the `href`
-attribute accepts expressions_, and directives_ can be used on the
+attribute accepts expressions, and directives_ can be used on the
 ``<xi:include />`` element just as on any other element, meaning you can do
 things like conditional includes::
 
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -130,6 +130,18 @@
         self.push = self.frames.appendleft
         self._match_templates = []
 
+        # Helper functions for use in expressions
+        def defined(name):
+            """Return whether a variable with the specified name exists in the
+            expression scope."""
+            return name in self
+        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 self.get(name, default)
+        data.setdefault('defined', defined)
+        data.setdefault('value_of', value_of)
+
     def __repr__(self):
         return repr(list(self.frames))
 
@@ -273,7 +285,7 @@
     """
 
     def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None):
+                 encoding=None, lookup='lenient'):
         """Initialize a template from either a string, a file-like object, or
         an already parsed markup stream.
         
@@ -287,6 +299,8 @@
                          base directory
         :param loader: the `TemplateLoader` to use for load included templates
         :param encoding: the encoding of the `source`
+        :param lookup: the variable lookup mechanism; either "lenient" (the
+                       default), "strict", or a custom lookup class
         """
         self.basedir = basedir
         self.filename = filename
@@ -295,6 +309,7 @@
         else:
             self.filepath = filename
         self.loader = loader
+        self.lookup = lookup
 
         if isinstance(source, basestring):
             source = StringIO(source)
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -57,9 +57,9 @@
     __metaclass__ = DirectiveMeta
     __slots__ = ['expr']
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+    def __init__(self, value, template=None, namespaces=None, lineno=-1,
                  offset=-1):
-        self.expr = self._parse_expr(value, filename, lineno, offset)
+        self.expr = self._parse_expr(value, template, lineno, offset)
 
     def attach(cls, template, stream, value, namespaces, pos):
         """Called after the template stream has been completely parsed.
@@ -77,7 +77,7 @@
         at runtime. `stream` is an event stream that replaces the original
         stream associated with the directive.
         """
-        return cls(value, namespaces, template.filepath, *pos[1:]), stream
+        return cls(value, template, namespaces, *pos[1:]), stream
     attach = classmethod(attach)
 
     def __call__(self, stream, ctxt, directives):
@@ -96,16 +96,17 @@
             expr = ' "%s"' % self.expr.source
         return '<%s%s>' % (self.__class__.__name__, expr)
 
-    def _parse_expr(cls, expr, filename=None, lineno=-1, offset=-1):
+    def _parse_expr(cls, expr, template, lineno=-1, offset=-1):
         """Parses the given expression, raising a useful error message when a
         syntax error is encountered.
         """
         try:
-            return expr and Expression(expr, filename, lineno) or None
+            return expr and Expression(expr, template.filepath, lineno,
+                                       lookup=template.lookup) or None
         except SyntaxError, err:
             err.msg += ' in expression "%s" of "%s" directive' % (expr,
                                                                   cls.tagname)
-            raise TemplateSyntaxError(err, filename, lineno,
+            raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
     _parse_expr = classmethod(_parse_expr)
 
@@ -198,7 +199,7 @@
     __slots__ = []
 
     def attach(cls, template, stream, value, namespaces, pos):
-        expr = cls._parse_expr(value, template.filepath, *pos[1:])
+        expr = cls._parse_expr(value, template, *pos[1:])
         return None, [stream[0], (EXPR, expr, pos),  stream[-1]]
     attach = classmethod(attach)
 
@@ -248,9 +249,8 @@
 
     ATTRIBUTE = 'function'
 
-    def __init__(self, args, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+    def __init__(self, args, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
         ast = _parse(args).node
         self.args = []
         self.defaults = {}
@@ -259,8 +259,10 @@
             for arg in ast.args:
                 if isinstance(arg, compiler.ast.Keyword):
                     self.args.append(arg.name)
-                    self.defaults[arg.name] = Expression(arg.expr, filename,
-                                                         lineno)
+                    self.defaults[arg.name] = Expression(arg.expr,
+                                                         template.filepath,
+                                                         lineno,
+                                                         lookup=template.lookup)
                 else:
                     self.args.append(arg.name)
         else:
@@ -319,16 +321,15 @@
 
     ATTRIBUTE = 'each'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
         if ' in ' not in value:
             raise TemplateSyntaxError('"in" keyword missing in "for" directive',
-                                      filename, lineno, offset)
+                                      template.filepath, lineno, offset)
         assign, value = value.split(' in ', 1)
         ast = _parse(assign, 'exec')
         self.assign = _assignment(ast.node.nodes[0].expr)
-        self.filename = filename
-        Directive.__init__(self, value.strip(), namespaces, filename, lineno,
+        self.filename = template.filepath
+        Directive.__init__(self, value.strip(), template, namespaces, lineno,
                            offset)
 
     def __call__(self, stream, ctxt, directives):
@@ -398,10 +399,9 @@
 
     ATTRIBUTE = 'path'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.path = Path(value, filename, lineno)
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
+        self.path = Path(value, template.filepath, lineno)
         self.namespaces = namespaces or {}
 
     def __call__(self, stream, ctxt, directives):
@@ -446,7 +446,7 @@
         if not value:
             raise TemplateSyntaxError('missing value for "replace" directive',
                                       template.filepath, *pos[1:])
-        expr = cls._parse_expr(value, template.filepath, *pos[1:])
+        expr = cls._parse_expr(value, template, *pos[1:])
         return None, [(EXPR, expr, pos)]
     attach = classmethod(attach)
 
@@ -568,10 +568,9 @@
 
     ATTRIBUTE = 'test'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, value, namespaces, filename, lineno, offset)
-        self.filename = filename
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, value, template, namespaces, lineno, offset)
+        self.filename = template.filepath
 
     def __call__(self, stream, ctxt, directives):
         matched, frame = ctxt._find('_choose.matched')
@@ -608,10 +607,9 @@
     """
     __slots__ = ['filename']
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.filename = filename
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
+        self.filename = template.filepath
 
     def __call__(self, stream, ctxt, directives):
         matched, frame = ctxt._find('_choose.matched')
@@ -643,9 +641,8 @@
 
     ATTRIBUTE = 'vars'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.vars = []
         value = value.strip()
         try:
@@ -656,13 +653,14 @@
                 elif not isinstance(node, compiler.ast.Assign):
                     raise TemplateSyntaxError('only assignment allowed in '
                                               'value of the "with" directive',
-                                              filename, lineno, offset)
+                                              template.filepath, lineno, offset)
                 self.vars.append(([_assignment(n) for n in node.nodes],
-                                  Expression(node.expr, filename, lineno)))
+                                  Expression(node.expr, template.filepath,
+                                             lineno, lookup=template.lookup)))
         except SyntaxError, err:
             err.msg += ' in expression "%s" of "%s" directive' % (value,
                                                                   self.tagname)
-            raise TemplateSyntaxError(err, filename, lineno,
+            raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
 
     def __call__(self, stream, ctxt, directives):
--- a/genshi/template/eval.py
+++ b/genshi/template/eval.py
@@ -27,15 +27,16 @@
 from genshi.template.base import TemplateRuntimeError
 from genshi.util import flatten
 
-__all__ = ['Code', 'Expression', 'Suite', 'UndefinedError']
+__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
+           'Undefined', 'UndefinedError']
 __docformat__ = 'restructuredtext en'
 
 
 class Code(object):
     """Abstract base class for the `Expression` and `Suite` classes."""
-    __slots__ = ['source', 'code']
+    __slots__ = ['source', 'code', '_globals']
 
-    def __init__(self, source, filename=None, lineno=-1):
+    def __init__(self, source, filename=None, lineno=-1, lookup='lenient'):
         """Create the code object, either from a string, or from an AST node.
         
         :param source: either a string containing the source code, or an AST
@@ -43,6 +44,9 @@
         :param filename: the (preferably absolute) name of the file containing
                          the code
         :param lineno: the number of the line on which the code was found
+        :param lookup: the lookup class that defines how variables are looked
+                       up in the context. Can be either `LenientLookup` (the
+                       default), `StrictLookup`, or a custom lookup class
         """
         if isinstance(source, basestring):
             self.source = source
@@ -57,6 +61,11 @@
 
         self.code = _compile(node, self.source, mode=self.mode,
                              filename=filename, lineno=lineno)
+        if lookup is None:
+            lookup = LenientLookup
+        elif isinstance(lookup, basestring):
+            lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
+        self._globals = lookup.globals()
 
     def __eq__(self, other):
         return (type(other) == type(self)) and (self.code == other.code)
@@ -122,13 +131,9 @@
         :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,
-                                'defined': _defined(data),
-                                'value_of': _value_of(data)},
-                               {'data': data})
+        _globals = self._globals
+        _globals['data'] = data
+        return eval(self.code, _globals, {'data': data})
 
 
 class Suite(Code):
@@ -148,29 +153,207 @@
         :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,
-                           'defined': _defined(data),
-                           'value_of': _value_of(data)}, data
+        _globals = self._globals
+        _globals['data'] = data
+        exec self.code in _globals, data
 
 
-def _defined(data):
-    def defined(name):
-        """Return whether a variable with the specified name exists in the
-        expression scope.
+UNDEFINED = object()
+
+
+class UndefinedError(TemplateRuntimeError):
+    """Exception thrown when a template expression attempts to access a variable
+    not defined in the context.
+    
+    :see: `LenientLookup`, `StrictLookup`
+    """
+    def __init__(self, name, owner=UNDEFINED):
+        if owner is not UNDEFINED:
+            message = '%s has no member named "%s"' % (repr(owner), name)
+        else:
+            message = '"%s" not defined' % name
+        TemplateRuntimeError.__init__(self, message)
+
+
+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, but raise an exception on any other operation:
+    
+    >>> 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):
+        ...
+    UndefinedError: "foo" not defined
+
+    >>> foo.bar
+    Traceback (most recent call last):
+        ...
+    UndefinedError: "foo" not defined
+    
+    :see: `LenientLookup`
+    """
+    __slots__ = ['_name', '_owner']
+
+    def __init__(self, name, owner=UNDEFINED):
+        """Initialize the object.
+        
+        :param name: the name of the reference
+        :param owner: the owning object, if the variable is accessed as a member
         """
-        return name in data
-    return defined
+        self._name = name
+        self._owner = owner
 
-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``.
+    def __iter__(self):
+        return iter([])
+
+    def __nonzero__(self):
+        return False
+
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self._name)
+
+    def __str__(self):
+        return 'undefined'
+
+    def _die(self, *args, **kwargs):
+        """Raise an `UndefinedError`."""
+        __traceback_hide__ = True
+        raise UndefinedError(self._name, self._owner)
+    __call__ = __getattr__ = __getitem__ = _die
+
+
+class LookupBase(object):
+    """Abstract base class for variable lookup implementations."""
+
+    def globals(cls):
+        """Construct the globals dictionary to use as the execution context for
+        the expression or suite.
         """
-        return data.get(name, default)
-    return value_of
+        return {
+            '_lookup_name': cls.lookup_name,
+            '_lookup_attr': cls.lookup_attr,
+            '_lookup_item': cls.lookup_item
+        }
+    globals = classmethod(globals)
+
+    def lookup_name(cls, data, name):
+        __traceback_hide__ = True
+        val = data.get(name, UNDEFINED)
+        if val is UNDEFINED:
+            val = BUILTINS.get(name, val)
+            if val is UNDEFINED:
+                return cls.undefined(name)
+        return val
+    lookup_name = classmethod(lookup_name)
+
+    def lookup_attr(cls, data, obj, key):
+        __traceback_hide__ = True
+        if hasattr(obj, key):
+            return getattr(obj, key)
+        try:
+            return obj[key]
+        except (KeyError, TypeError):
+            return cls.undefined(key, owner=obj)
+    lookup_attr = classmethod(lookup_attr)
+
+    def lookup_item(cls, data, obj, key):
+        __traceback_hide__ = True
+        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, UNDEFINED)
+                if val is UNDEFINED:
+                    return cls.undefined(key, owner=obj)
+                return val
+            raise
+    lookup_item = classmethod(lookup_item)
+
+    def undefined(cls, key, owner=UNDEFINED):
+        """Can be overridden by subclasses to specify behavior when undefined
+        variables are accessed.
+        
+        :param key: the name of the variable
+        :param owner: the owning object, if the variable is accessed as a member
+        """
+        raise NotImplementedError
+    undefined = classmethod(undefined)
+
+
+class LenientLookup(LookupBase):
+    """Default variable lookup mechanism for expressions.
+    
+    When an undefined variable is referenced using this lookup style, the
+    reference evaluates to an instance of the `Undefined` class:
+    
+    >>> expr = Expression('nothing', lookup='lenient')
+    >>> undef = expr.evaluate({})
+    >>> undef
+    <Undefined 'nothing'>
+    
+    The same will happen when a non-existing attribute or item is accessed on
+    an existing object:
+    
+    >>> expr = Expression('something.nil', lookup='lenient')
+    >>> expr.evaluate({'something': dict()})
+    <Undefined 'nil'>
+    
+    See the documentation of the `Undefined` class for details on the behavior
+    of such objects.
+    
+    :see: `StrictLookup`
+    """
+    def undefined(cls, key, owner=UNDEFINED):
+        """Return an ``Undefined`` object."""
+        __traceback_hide__ = True
+        return Undefined(key, owner=owner)
+    undefined = classmethod(undefined)
+
+
+class StrictLookup(LookupBase):
+    """Strict variable lookup mechanism for expressions.
+    
+    Referencing an undefined variable using this lookup style will immediately
+    raise an ``UndefinedError``:
+    
+    >>> expr = Expression('nothing', lookup='strict')
+    >>> expr.evaluate({})
+    Traceback (most recent call last):
+        ...
+    UndefinedError: "nothing" not defined
+    
+    The same happens when a non-existing attribute or item is accessed on an
+    existing object:
+    
+    >>> expr = Expression('something.nil', lookup='strict')
+    >>> expr.evaluate({'something': dict()})
+    Traceback (most recent call last):
+        ...
+    UndefinedError: {} has no member named "nil"
+    """
+    def undefined(cls, key, owner=UNDEFINED):
+        """Raise an ``UndefinedError`` immediately."""
+        __traceback_hide__ = True
+        raise UndefinedError(key, owner=owner)
+    undefined = classmethod(undefined)
+
 
 def _parse(source, mode='eval'):
     if isinstance(source, unicode):
@@ -205,53 +388,7 @@
                     code.co_lnotab, (), ())
 
 BUILTINS = __builtin__.__dict__.copy()
-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:
-            message = '%s has no member named "%s"' % (repr(owner), name)
-        else:
-            message = '"%s" not defined' % name
-        TemplateRuntimeError.__init__(self, message)
-
-
-def _lookup_name(data, name):
-    __traceback_hide__ = True
-    val = data.get(name, UNDEFINED)
-    if val is UNDEFINED:
-        val = BUILTINS.get(name, val)
-        if val is UNDEFINED:
-            raise UndefinedError(name)
-    return val
-
-def _lookup_attr(data, obj, key):
-    __traceback_hide__ = True
-    if hasattr(obj, key):
-        return getattr(obj, key)
-    try:
-        return obj[key]
-    except (KeyError, TypeError):
-        raise UndefinedError(key, owner=obj)
-
-def _lookup_item(data, obj, key):
-    __traceback_hide__ = True
-    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, UNDEFINED)
-            if val is UNDEFINED:
-                raise UndefinedError(key, owner=obj)
-            return val
-        raise
+BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
 
 
 class ASTTransformer(object):
@@ -499,7 +636,7 @@
     """
 
     def __init__(self):
-        self.locals = [set(['defined', 'value_of'])]
+        self.locals = []
 
     def visitConst(self, node):
         if isinstance(node.value, str):
--- a/genshi/template/interpolation.py
+++ b/genshi/template/interpolation.py
@@ -30,17 +30,19 @@
 NAMECHARS = NAMESTART + '.0123456789'
 PREFIX = '$'
 
-def interpolate(text, basedir=None, filename=None, lineno=-1, offset=0):
+def interpolate(text, basedir=None, filename=None, lineno=-1, offset=0,
+                lookup='lenient'):
     """Parse the given string and extract expressions.
     
     This function is a generator that yields `TEXT` events for literal strings,
     and `EXPR` events for expressions, depending on the results of parsing the
     string.
     
-    >>> for kind, data, pos in interpolate("$foo bar"):
+    >>> for kind, data, pos in interpolate("hey ${foo}bar"):
     ...     print kind, `data`
+    TEXT u'hey '
     EXPR Expression('foo')
-    TEXT u' bar'
+    TEXT u'bar'
     
     :param text: the text to parse
     :param basedir: base directory of the file in which the text was found
@@ -49,6 +51,8 @@
     :param lineno: the line number at which the text was found (optional)
     :param offset: the column number at which the text starts in the source
                    (optional)
+    :param lookup: the variable lookup mechanism; either "lenient" (the
+                   default), "strict", or a custom lookup class
     :return: a list of `TEXT` and `EXPR` events
     :raise TemplateSyntaxError: when a syntax error in an expression is
                                 encountered
@@ -68,7 +72,8 @@
                 textpos = None
             if chunk:
                 try:
-                    expr = Expression(chunk.strip(), pos[0], pos[1])
+                    expr = Expression(chunk.strip(), pos[0], pos[1],
+                                     lookup=lookup)
                     yield EXPR, expr, tuple(pos)
                 except SyntaxError, err:
                     raise TemplateSyntaxError(err, filepath, pos[1],
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -73,7 +73,7 @@
     """
     def __init__(self, search_path=None, auto_reload=False,
                  default_encoding=None, max_cache_size=25, default_class=None,
-                 callback=None):
+                 variable_lookup='lenient', callback=None):
         """Create the template laoder.
         
         :param search_path: a list of absolute path names that should be
@@ -87,11 +87,15 @@
                                cache
         :param default_class: the default `Template` subclass to use when
                               instantiating templates
+        :param variable_lookup: the variable lookup mechanism; either "lenient"
+                                (the default), "strict", or a custom lookup
+                                class
         :param callback: (optional) a callback function that is invoked after a
                          template was initialized by this loader; the function
                          is passed the template object as only argument. This
                          callback can be used for example to add any desired
                          filters to the template
+        :see: `LenientLookup`, `StrictLookup`
         """
         from genshi.template.markup import MarkupTemplate
 
@@ -103,6 +107,7 @@
         self.auto_reload = auto_reload
         self.default_encoding = default_encoding
         self.default_class = default_class or MarkupTemplate
+        self.variable_lookup = variable_lookup
         if callback is not None and not callable(callback):
             raise TypeError('The "callback" parameter needs to be callable')
         self.callback = callback
@@ -194,7 +199,8 @@
                             filename = os.path.join(dirname, filename)
                             dirname = ''
                         tmpl = cls(fileobj, basedir=dirname, filename=filename,
-                                   loader=self, encoding=encoding)
+                                   loader=self, lookup=self.variable_lookup,
+                                   encoding=encoding)
                         if self.callback:
                             self.callback(tmpl)
                         self._cache[filename] = tmpl
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -70,9 +70,9 @@
                   ('strip', StripDirective)]
 
     def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None):
+                 encoding=None, lookup='lenient'):
         Template.__init__(self, source, basedir=basedir, filename=filename,
-                          loader=loader, encoding=encoding)
+                          loader=loader, encoding=encoding, lookup=lookup)
 
         self.filters += [self._exec, self._match]
         if loader:
@@ -132,7 +132,9 @@
                         directives.append((cls, value, ns_prefix.copy(), pos))
                     else:
                         if value:
-                            value = list(interpolate(value, self.basedir, *pos))
+                            value = list(interpolate(value, self.basedir,
+                                                     pos[0], pos[1], pos[2],
+                                                     lookup=self.lookup))
                             if len(value) == 1 and value[0][0] is TEXT:
                                 value = value[0][1]
                         else:
@@ -197,7 +199,8 @@
                         rest = '\n'.join(['    ' + line for line
                                           in rest.splitlines()])
                     source = '\n'.join([first, rest])
-                    suite = Suite(source, self.filepath, pos[1])
+                    suite = Suite(source, self.filepath, pos[1],
+                                  lookup=self.lookup)
                 except SyntaxError, err:
                     raise TemplateSyntaxError(err, self.filepath,
                                               pos[1] + (err.lineno or 1) - 1,
@@ -205,7 +208,9 @@
                 stream.append((EXEC, suite, pos))
 
             elif kind is TEXT:
-                for kind, data, pos in interpolate(data, self.basedir, *pos):
+                for kind, data, pos in interpolate(data, self.basedir, pos[0],
+                                                   pos[1], pos[2],
+                                                   lookup=self.lookup):
                     stream.append((kind, data, pos))
 
             elif kind is COMMENT:
--- a/genshi/template/plugin.py
+++ b/genshi/template/plugin.py
@@ -20,7 +20,7 @@
 
 from genshi.input import ET, HTML, XML
 from genshi.output import DocType
-from genshi.template.base import Context, Template
+from genshi.template.base import Template
 from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
 from genshi.template.text import TextTemplate
@@ -58,10 +58,16 @@
             raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
                                      options.get('genshi.max_cache_size'))
 
+        lookup_errors = options.get('genshi.lookup_errors', 'lenient')
+        if lookup_errors not in ('lenient', 'strict'):
+            raise ConfigurationError('Unknown lookup errors mode "%s"' %
+                                     lookup_errors)
+
         self.loader = TemplateLoader(filter(None, search_path),
                                      auto_reload=auto_reload,
                                      max_cache_size=max_cache_size,
-                                     default_class=self.template_class)
+                                     default_class=self.template_class,
+                                     variable_lookup=lookup_errors)
 
     def load_template(self, templatename, template_string=None):
         """Find a template specified in python 'dot' notation, or load one from
--- a/genshi/template/tests/eval.py
+++ b/genshi/template/tests/eval.py
@@ -16,7 +16,8 @@
 import unittest
 
 from genshi.core import Markup
-from genshi.template.eval import Expression, Suite, UndefinedError
+from genshi.template.eval import Expression, Suite, Undefined, UndefinedError, \
+                                 UNDEFINED
 
 
 class ExpressionTestCase(unittest.TestCase):
@@ -319,8 +320,38 @@
         expr = Expression("numbers[:-1]")
         self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)}))
 
+    def test_access_undefined(self):
+        expr = Expression("nothing", filename='index.html', lineno=50)
+        retval = expr.evaluate({})
+        assert isinstance(retval, Undefined)
+        self.assertEqual('nothing', retval._name)
+        assert retval._owner is UNDEFINED
+
+    def test_getattr_undefined(self):
+        class Something(object):
+            def __repr__(self):
+                return '<Something>'
+        something = Something()
+        expr = Expression('something.nil', filename='index.html', lineno=50)
+        retval = expr.evaluate({'something': something})
+        assert isinstance(retval, Undefined)
+        self.assertEqual('nil', retval._name)
+        assert retval._owner is something
+
+    def test_getitem_undefined_string(self):
+        class Something(object):
+            def __repr__(self):
+                return '<Something>'
+        something = Something()
+        expr = Expression('something["nil"]', filename='index.html', lineno=50)
+        retval = expr.evaluate({'something': something})
+        assert isinstance(retval, Undefined)
+        self.assertEqual('nil', retval._name)
+        assert retval._owner is something
+
     def test_error_access_undefined(self):
-        expr = Expression("nothing", filename='index.html', lineno=50)
+        expr = Expression("nothing", filename='index.html', lineno=50,
+                          lookup='strict')
         try:
             expr.evaluate({})
             self.fail('Expected UndefinedError')
@@ -333,16 +364,17 @@
                 frames.append(frame)
             self.assertEqual('"nothing" not defined', str(e))
             self.assertEqual("<Expression 'nothing'>",
-                             frames[-2].tb_frame.f_code.co_name)
+                             frames[-3].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)
+                             frames[-3].tb_frame.f_code.co_filename)
+            self.assertEqual(50, frames[-3].tb_lineno)
 
     def test_error_getattr_undefined(self):
         class Something(object):
             def __repr__(self):
                 return '<Something>'
-        expr = Expression('something.nil', filename='index.html', lineno=50)
+        expr = Expression('something.nil', filename='index.html', lineno=50,
+                          lookup='strict')
         try:
             expr.evaluate({'something': Something()})
             self.fail('Expected UndefinedError')
@@ -355,16 +387,17 @@
                 frames.append(frame)
             self.assertEqual('<Something> has no member named "nil"', str(e))
             self.assertEqual("<Expression 'something.nil'>",
-                             frames[-2].tb_frame.f_code.co_name)
+                             frames[-3].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)
+                             frames[-3].tb_frame.f_code.co_filename)
+            self.assertEqual(50, frames[-3].tb_lineno)
 
     def test_error_getitem_undefined_string(self):
         class Something(object):
             def __repr__(self):
                 return '<Something>'
-        expr = Expression('something["nil"]', filename='index.html', lineno=50)
+        expr = Expression('something["nil"]', filename='index.html', lineno=50,
+                          lookup='strict')
         try:
             expr.evaluate({'something': Something()})
             self.fail('Expected UndefinedError')
@@ -377,10 +410,10 @@
                 frames.append(frame)
             self.assertEqual('<Something> has no member named "nil"', str(e))
             self.assertEqual('''<Expression 'something["nil"]'>''',
-                             frames[-2].tb_frame.f_code.co_name)
+                             frames[-3].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)
+                             frames[-3].tb_frame.f_code.co_filename)
+            self.assertEqual(50, frames[-3].tb_lineno)
 
 
 class SuiteTestCase(unittest.TestCase):
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -75,7 +75,8 @@
             if start > offset:
                 text = source[offset:start]
                 for kind, data, pos in interpolate(text, self.basedir,
-                                                   self.filename, lineno):
+                                                   self.filename, lineno,
+                                                   lookup=self.lookup):
                     stream.append((kind, data, pos))
                 lineno += len(text.splitlines())
 
@@ -107,7 +108,8 @@
         if offset < len(source):
             text = source[offset:].replace('\\#', '#')
             for kind, data, pos in interpolate(text, self.basedir,
-                                               self.filename, lineno):
+                                               self.filename, lineno,
+                                               lookup=self.lookup):
                 stream.append((kind, data, pos))
 
         return stream
Copyright (C) 2012-2017 Edgewall Software