Skip to content

Commit

Permalink
Merge pull request #195 from malthe/issue-194-document-error-modes
Browse files Browse the repository at this point in the history
Add render error exception base class
  • Loading branch information
malthe committed Feb 4, 2015
2 parents eb9b485 + 207a9b2 commit 0012814
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 131 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Expand Up @@ -3,6 +3,11 @@ Changes

In next release ...

- Added ``RenderError`` exception which indicates that an error
occurred during the evaluation of an expression.

- Clean up ``TemplateError`` exception implementation.


2.20 (2015-01-12)
-----------------
Expand Down
109 changes: 38 additions & 71 deletions docs/library.rst
Expand Up @@ -108,78 +108,14 @@ It's probably best to stick with a Python expression::
<div tal:content="'hello, world'.upper()" />


.. _whats-new:

Changes between 1.x and 2.x
---------------------------

This sections describes new features, improvements and changes from
1.x to 2.x.

New parser
~~~~~~~~~~

This series features a new, custom-built parser, implemented in pure
Python. It parses both HTML and XML inputs (the previous parser relied
on the expat system library and was more strict about its input).

The main benefit of the new parser is that the compiler is now able to
point to the source location of parse- and compilation errors much
more accurately. This should be a great aid in debugging these errors.

Compatible output
~~~~~~~~~~~~~~~~~

The 2.x engine matches the output of the reference implementation more
closely (usually exactly). There are less differences altogether; for
instance, the method of escaping TALES expression (usually a
semicolon) has been changed to match that of the reference
implementation.

New language features
~~~~~~~~~~~~~~~~~~~~~

This series also introduces a number of new language features:

1. Support for the ``tal:on-error`` from the reference specification
has been added.

2. Two new attributes ``tal:switch`` and ``tal:case`` have been added
to make element conditions more flexible.


Code improvements
~~~~~~~~~~~~~~~~~

The template classes have been refactored and simplified allowing
better reuse of code and more intuitive APIs on the lower levels.

Expression engine
~~~~~~~~~~~~~~~~~

The expression engine has been redesigned to make it easier to
understand and extend. The new engine is based on the ``ast`` module
(available since Python 2.6; backports included for Python 2.5). This
means that expression compilers now need to return a valid list of AST
statements that include an assignment to the target node.

Compiler
~~~~~~~~

The new compiler has been optimized for complex templates. As a
result, in the benchmark suite included with the package, this
compiler scores about half of the 1.x series. For most real world
applications, the engine should still perform as well as the 1.x
series.


API reference
-------------

This section describes the documented API of the library.

Template classes
~~~~~~~~~~~~~~~~

Templates
~~~~~~~~~

Use the ``PageTemplate*`` template classes to define a template from a
string or file input:
Expand All @@ -199,8 +135,8 @@ string or file input:

.. autoclass:: chameleon.PageTextTemplateFile

Template loader
~~~~~~~~~~~~~~~
Loader
~~~~~~

Some systems have framework support for loading templates from
files. The following loader class is directly compatible with the
Expand All @@ -227,8 +163,39 @@ Pylons framework and may be adapted to other frameworks:

.. automethod:: load

Expression engine
~~~~~~~~~~~~~~~~~

Exceptions
~~~~~~~~~~

Chameleon may raise exceptions during both the cooking and the
rendering phase, but those raised during the cooking phase (parse and
compile) all inherit from a single base class:

.. class:: chameleon.TemplateError(msg, token)

This exception is the base class of all exceptions raised by the
template engine in the case where a template has an error.

It may be raised during rendering since templates are processed
lazily (unless eager loading is enabled).


An error that occurs during the rendering of a template is wrapped in
an exception class to disambiguate the two cases:

.. class:: chameleon.RenderError(*args)

Indicates an exception that resulted from the evaluation of an
expression in a template.

A complete traceback is attached to the exception beginning with
the expression that resulted in the error. The traceback includes
a string representation of the template variable scope for further
reference.


Expressions
~~~~~~~~~~~

For advanced integration, the compiler module provides support for
dynamic expression evaluation:
Expand Down
39 changes: 2 additions & 37 deletions docs/reference.rst
Expand Up @@ -185,7 +185,7 @@ statement, the replacement is made on each repetition of the element,
and the replacement expression is evaluated fresh for each repetition.

.. note:: If you want to include a semicolon (";") in an expression, it
must be escaped by doubling it (";;") [1]_.
must be escaped by doubling it (";;").

Examples
~~~~~~~~
Expand Down Expand Up @@ -359,7 +359,7 @@ or ``global`` in front of the assignment. The default setting is
``local``; thus, in practice, only the ``global`` keyword is used.

.. note:: If you want to include a semicolon (";") in an expression, it
must be escaped by doubling it (";;") [1]_.
must be escaped by doubling it (";;").

Examples
~~~~~~~~
Expand Down Expand Up @@ -1691,38 +1691,3 @@ implementation (ZPT):
The `z3c.pt <http://pypi.python.org/pypi/z3c.pt>`_ package works as a
compatibility layer. The template classes in this package provide a
implementation which is fully compatible with ZPT.

Notes
#####

.. [1] This has been changed in 2.x. Previously, it was up to the
expression engine to parse the expression values including any
semicolons and since for instance Python-expressions can never
end in a semicolon, it was possible to clearly distinguish
between the different uses of the symbol, e.g.
::
tal:define="text 'Hello world; goodbye world'"
The semicolon appearing in the definition above is part of the
Python-expression simply because it makes the expression
valid. Meanwhile:
::
tal:define="text1 'Hello world'; text2 'goodbye world'"
The semicolon here must denote a second variable definition
because there is no valid Python-expression that includes it.
While this behavior works well in practice, it is incompatible
with the reference specification, and also blurs the interface
between the compiler and the expression engine. In 2.x we
therefore have to escape the semicolon by doubling it (as
defined by the specification):
::
tal:define="text 'Hello world;; goodbye world'"
1 change: 1 addition & 0 deletions src/chameleon/__init__.py
Expand Up @@ -3,3 +3,4 @@
from .zpt.template import PageTextTemplate
from .zpt.template import PageTextTemplateFile
from .zpt.loader import TemplateLoader as PageTemplateLoader
from .exc import TemplateError
2 changes: 1 addition & 1 deletion src/chameleon/compiler.py
Expand Up @@ -741,7 +741,7 @@ def __call__(self, expression, target):
raise

exc = sys.exc_info()[1]
p = pickle.dumps(exc)
p = pickle.dumps(exc, -1)

stmts = template(
"__exc = loads(p)", loads=self.loads_symbol, p=ast.Str(s=p)
Expand Down
45 changes: 28 additions & 17 deletions src/chameleon/exc.py
Expand Up @@ -99,10 +99,12 @@ def ellipsify(string, limit):
return string


def reconstruct_exc(cls, state):
exc = Exception.__new__(cls)
exc.__dict__ = state
return exc
class RenderError(Exception):
"""An error raised during rendering.
This class is used as a mixin which is added to the original
exception.
"""


class TemplateError(Exception):
Expand All @@ -121,7 +123,7 @@ class TemplateError(Exception):
And pickle/unpickled:
>>> from pickle import dumps, loads
>>> loads(dumps(TemplateError(message, token)))
>>> loads(dumps(TemplateError(message, token), -1))
TemplateError('message', 'token')
"""
Expand All @@ -130,23 +132,16 @@ def __init__(self, msg, token):
if not isinstance(token, Token):
token = Token(token, 0)

self.msg = msg
self.token = safe_native(token)
self.offset = getattr(token, "pos", 0)
self.filename = token.filename
self.location = token.location
Exception.__init__(self, msg, token)

def __copy__(self):
inst = Exception.__new__(type(self))
inst.__dict__ = self.__dict__.copy()
inst.args = self.args
return inst

def __reduce__(self):
return reconstruct_exc, (type(self), self.__dict__)

def __str__(self):
text = "%s\n\n" % self.msg
text += " - String: \"%s\"" % self.token
text = "%s\n\n" % self.args[0]
text += " - String: \"%s\"" % safe_native(self.token)

if self.filename:
text += "\n"
Expand All @@ -161,11 +156,27 @@ def __str__(self):
def __repr__(self):
try:
return "%s('%s', '%s')" % (
self.__class__.__name__, self.msg, self.token
self.__class__.__name__, self.args[0], safe_native(self.token)
)
except AttributeError:
return object.__repr__(self)

@property
def token(self):
return self.args[1]

@property
def filename(self):
return self.token.filename

@property
def location(self):
return self.token.location

@property
def offset(self):
return getattr(self.token, "pos", 0)


class ParseError(TemplateError):
"""An error occurred during parsing.
Expand Down
7 changes: 5 additions & 2 deletions src/chameleon/template.py
Expand Up @@ -21,6 +21,7 @@
pkg_digest.update(version)


from .exc import RenderError
from .exc import TemplateError
from .exc import ExceptionFormatter
from .compiler import Compiler
Expand Down Expand Up @@ -181,7 +182,9 @@ def render(self, **__kw):
formatter = ExceptionFormatter(errors, econtext, rcontext)

try:
exc = create_formatted_exception(exc, cls, formatter)
exc = create_formatted_exception(
exc, cls, formatter, RenderError
)
except TypeError:
pass

Expand Down Expand Up @@ -222,7 +225,7 @@ def _cook(self, body, name, builtins):
cooked = self.loader.build(source, filename)
except TemplateError:
exc = sys.exc_info()[1]
exc.filename = self.filename
exc.token.filename = self.filename
raise
elif self.keep_source:
module = sys.modules.get(cooked.get('__name__'))
Expand Down
4 changes: 3 additions & 1 deletion src/chameleon/tests/test_templates.py
Expand Up @@ -18,6 +18,7 @@


from chameleon.utils import byte_string
from chameleon.exc import RenderError


class Message(object):
Expand Down Expand Up @@ -369,7 +370,8 @@ def test_exception(self):
)
try:
template()
except:
except Exception as exc:
self.assertIn(RenderError, type(exc).__bases__)
exc = sys.exc_info()[1]
formatted = str(exc)
self.assertFalse('NameError:' in formatted)
Expand Down
4 changes: 2 additions & 2 deletions src/chameleon/utils.py
Expand Up @@ -207,10 +207,10 @@ def substitute_entity(match, n2cp=htmlentitydefs.name2codepoint):
return match.group()


def create_formatted_exception(exc, cls, formatter):
def create_formatted_exception(exc, cls, formatter, base=Exception):
try:
try:
new = type(cls.__name__, (cls, Exception), {
new = type(cls.__name__, (cls, base), {
'__str__': formatter,
'__new__': BaseException.__new__,
'__module__': cls.__module__,
Expand Down

0 comments on commit 0012814

Please sign in to comment.