##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""TALES
An implementation of a TAL expression engine
"""
import re
try:
from html import escape
except ImportError: # pragma: PY2
from cgi import escape
import six
from zope.interface import Interface
from zope.interface import implementer
from zope.tales.interfaces import ITALESIterator
class ITALExpressionEngine(Interface):
pass
class ITALExpressionCompiler(Interface):
pass
class ITALExpressionErrorInfo(Interface):
pass
try:
# Override with real, if present
from zope.tal.interfaces import ITALExpressionCompiler # noqa: F811
from zope.tal.interfaces import ITALExpressionEngine # noqa: F811
from zope.tal.interfaces import ITALExpressionErrorInfo # noqa: F811
except ImportError:
pass
NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
_parse_expr = re.compile(r"(%s):" % NAME_RE).match
_valid_name = re.compile('%s$' % NAME_RE).match
[docs]class TALESError(Exception):
"""Error during TALES evaluation"""
[docs]class Undefined(TALESError):
"""Exception raised on traversal of an undefined path."""
[docs]class CompilerError(Exception):
"""TALES Compiler Error"""
[docs]class RegistrationError(Exception):
"""Expression type or base name registration Error."""
_default = object()
[docs]@implementer(ITALESIterator)
class Iterator(object):
"""
TALES Iterator.
Default implementation of :class:`zope.tales.interfaces.ITALESIterator`.
"""
def __init__(self, name, seq, context):
"""Construct an iterator
Iterators are defined for a name, a sequence, or an iterator and a
context, where a context simply has a setLocal method:
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
A local variable is not set until the iterator is used:
>>> int("foo" in context.vars)
0
We can create an iterator on an empty sequence:
>>> it = Iterator('foo', (), context)
An iterator works as well:
>>> it = Iterator('foo', {"apple":1, "pear":1, "orange":1}, context)
>>> it.next()
True
>>> it = Iterator('foo', {}, context)
>>> it.next()
False
>>> it = Iterator('foo', iter((1, 2, 3)), context)
>>> it.next()
True
>>> it.next()
True
"""
self._seq = seq
self._iter = i = iter(seq)
self._nextIndex = 0
self._name = name
self._setLocal = context.setLocal
# This is tricky. We want to know if we are on the last item,
# but we can't know that without trying to get it. :(
self._last = False
try:
self._next = next(i)
except StopIteration:
self._done = True
else:
self._done = False
def __next__(self):
"""Advance the iterator, if possible.
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> bool(it.next())
True
>>> context.vars['foo']
'apple'
>>> bool(it.next())
True
>>> context.vars['foo']
'pear'
>>> bool(it.next())
True
>>> context.vars['foo']
'orange'
>>> bool(it.next())
False
>>> it = Iterator('foo', {"apple":1, "pear":1, "orange":1}, context)
>>> bool(it.next())
True
>>> bool(it.next())
True
>>> bool(it.next())
True
>>> bool(it.next())
False
>>> it = Iterator('foo', (), context)
>>> bool(it.next())
False
>>> it = Iterator('foo', {}, context)
>>> bool(it.next())
False
If we can advance, set a local variable to the new value.
"""
# Note that these are *NOT* Python iterators!
if self._done:
return False
self._item = v = self._next
try:
self._next = next(self._iter)
except StopIteration:
self._done = True
self._last = True
self._nextIndex += 1
self._setLocal(self._name, v)
return True
next = __next__ # Python 2 compatibility
[docs] def index(self):
"""Get the iterator index
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.index()
Traceback (most recent call last):
...
TypeError: No iteration position
>>> int(bool(it.next()))
1
>>> it.index()
0
>>> int(bool(it.next()))
1
>>> it.index()
1
>>> int(bool(it.next()))
1
>>> it.index()
2
"""
index = self._nextIndex - 1
if index < 0:
raise TypeError("No iteration position")
return index
[docs] def number(self):
"""Get the iterator position
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> int(bool(it.next()))
1
>>> it.number()
1
>>> int(bool(it.next()))
1
>>> it.number()
2
>>> int(bool(it.next()))
1
>>> it.number()
3
"""
return self._nextIndex
[docs] def even(self):
"""Test whether the position is even
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.even()
True
>>> it.next()
True
>>> it.even()
False
>>> it.next()
True
>>> it.even()
True
"""
return not ((self._nextIndex - 1) % 2)
[docs] def odd(self):
"""Test whether the position is odd
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.odd()
False
>>> it.next()
True
>>> it.odd()
True
>>> it.next()
True
>>> it.odd()
False
"""
return bool((self._nextIndex - 1) % 2)
[docs] def parity(self):
"""Return 'odd' or 'even' depending on the position's parity
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.parity()
'odd'
>>> it.next()
True
>>> it.parity()
'even'
>>> it.next()
True
>>> it.parity()
'odd'
"""
if self._nextIndex % 2:
return 'odd'
return 'even'
[docs] def letter(self, base=ord('a'), radix=26):
"""Get the iterator position as a lower-case letter
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.letter()
Traceback (most recent call last):
...
TypeError: No iteration position
>>> it.next()
True
>>> it.letter()
'a'
>>> it.next()
True
>>> it.letter()
'b'
>>> it.next()
True
>>> it.letter()
'c'
"""
index = self._nextIndex - 1
if index < 0:
raise TypeError("No iteration position")
s = ''
while 1:
index, off = divmod(index, radix)
s = chr(base + off) + s
if not index:
return s
[docs] def Letter(self):
"""Get the iterator position as an upper-case letter
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.Letter()
'A'
>>> it.next()
True
>>> it.Letter()
'B'
>>> it.next()
True
>>> it.Letter()
'C'
"""
return self.letter(base=ord('A'))
[docs] def Roman(self, rnvalues=(
(1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
(100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),
(10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I'))):
"""Get the iterator position as an upper-case roman numeral
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.Roman()
'I'
>>> it.next()
True
>>> it.Roman()
'II'
>>> it.next()
True
>>> it.Roman()
'III'
"""
n = self._nextIndex
s = ''
for v, r in rnvalues:
rct, n = divmod(n, v)
s = s + r * rct
return s
[docs] def roman(self):
"""Get the iterator position as a lower-case roman numeral
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.roman()
'i'
>>> it.next()
True
>>> it.roman()
'ii'
>>> it.next()
True
>>> it.roman()
'iii'
"""
return self.Roman().lower()
[docs] def start(self):
"""Test whether the position is the first position
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.start()
True
>>> it.next()
True
>>> it.start()
False
>>> it.next()
True
>>> it.start()
False
>>> it = Iterator('foo', {}, context)
>>> it.start()
False
>>> it.next()
False
>>> it.start()
False
"""
return self._nextIndex == 1
[docs] def end(self):
"""Test whether the position is the last position
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.next()
True
>>> it.end()
False
>>> it.next()
True
>>> it.end()
False
>>> it.next()
True
>>> it.end()
True
>>> it = Iterator('foo', {}, context)
>>> it.end()
False
>>> it.next()
False
>>> it.end()
False
"""
return self._last
[docs] def item(self):
"""Get the iterator value
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.item()
Traceback (most recent call last):
...
TypeError: No iteration position
>>> it.next()
True
>>> it.item()
'apple'
>>> it.next()
True
>>> it.item()
'pear'
>>> it.next()
True
>>> it.item()
'orange'
>>> it = Iterator('foo', {1:2}, context)
>>> it.next()
True
>>> it.item()
1
"""
if self._nextIndex == 0:
raise TypeError("No iteration position")
return self._item
[docs] def length(self):
"""Get the length of the iterator sequence
>>> context = Context(ExpressionEngine(), {})
>>> it = Iterator('foo', ("apple", "pear", "orange"), context)
>>> it.length()
3
You can even get the length of a mapping:
>>> it = Iterator('foo', {"apple":1, "pear":2, "orange":3}, context)
>>> it.length()
3
But you can't get the length of an iterable which doesn't
support len():
>>> class MyIter(object):
... def __init__(self, seq):
... self._iter = iter(seq)
... def __iter__(self):
... return self
... def __next__(self):
... return next(self._iter)
... next = __next__
>>> it = Iterator('foo', MyIter({"apple":1, "pear":2}), context)
>>> try:
... it.length()
... except TypeError:
... pass
... else:
... print('Expected TypeError')
"""
return len(self._seq)
@implementer(ITALExpressionErrorInfo)
class ErrorInfo(object):
"""Information about an exception passed to an on-error handler."""
# XXX: This is a duplicate of zope.tal.taldefs.ErrorInfo
value = None
def __init__(self, err, position=(None, None)):
self.type = err
if isinstance(err, Exception):
self.type = err.__class__
self.value = err
self.lineno = position[0]
self.offset = position[1]
[docs]@implementer(ITALExpressionCompiler)
class ExpressionEngine(object):
"""
Expression compiler, an implementation of
:class:`zope.tal.interfaces.ITALExpressionCompiler`.
An instance of this class keeps :meth:`a mutable collection of
expression type handlers
<zope.tales.tales.ExpressionEngine.registerType>`. It can compile
expression strings by delegating to these handlers. It can
:meth:`provide an expression engine
<zope.tales.tales.ExpressionEngine.getContext>`, which is capable
of holding state and evaluating compiled expressions.
By default, this object does not know how to compile any
expression types. See :data:`zope.tales.engine.Engine` and
:func:`zope.tales.engine.DefaultEngine` for pre-configured
instances supporting the standard expression types.
"""
def __init__(self):
self.types = {}
self.base_names = {}
self.namespaces = {}
self.iteratorFactory = Iterator
[docs] def registerFunctionNamespace(self, namespacename, namespacecallable):
"""
Register a function namespace
:param str namespace: a string containing the name of the namespace to
be registered
:param callable namespacecallable: a callable object which takes the
following parameter:
:context: the object on which the functions
provided by this namespace will
be called
This callable should return an object which
can be traversed to get the functions provided
by the this namespace.
For example::
class stringFuncs(object):
def __init__(self,context):
self.context = str(context)
def upper(self):
return self.context.upper()
def lower(self):
return self.context.lower()
engine.registerFunctionNamespace('string', stringFuncs)
"""
self.namespaces[namespacename] = namespacecallable
[docs] def getFunctionNamespace(self, namespacename):
""" Returns the function namespace """
return self.namespaces[namespacename]
[docs] def registerType(self, name, handler):
"""
Register an expression of *name* to be handled with *handler*.
:raises RegistrationError: If this is a duplicate registration for
*name*, or if *name* is not a valid expression type name.
"""
if not _valid_name(name):
raise RegistrationError(
'Invalid expression type name "%s".' % name)
types = self.types
if name in types:
raise RegistrationError(
'Multiple registrations for Expression type "%s".' % name)
types[name] = handler
def getTypes(self):
return self.types
def registerBaseName(self, name, object):
if not _valid_name(name):
raise RegistrationError('Invalid base name "%s".' % name)
base_names = self.base_names
if name in base_names:
raise RegistrationError(
'Multiple registrations for base name "%s".' % name)
base_names[name] = object
def getBaseNames(self):
return self.base_names
def compile(self, expression):
m = _parse_expr(expression)
if m:
type = m.group(1)
expr = expression[m.end():]
else:
type = "standard"
expr = expression
try:
handler = self.types[type]
except KeyError:
raise CompilerError('Unrecognized expression type "%s".' % type)
return handler(type, expr, self)
[docs] def getContext(self, contexts=None, **kwcontexts):
"""
Return a new expression engine.
The keyword arguments passed in *kwcantexts* become the default
variable context for the returned engine. If *contexts* is given, it
should be a mapping, and the values it contains will override
the keyword arguments.
:rtype: Context
"""
if contexts is not None:
if kwcontexts:
kwcontexts.update(contexts)
else:
kwcontexts = contexts
return Context(self, kwcontexts)
def getCompilerError(self):
return CompilerError
[docs]@implementer(ITALExpressionEngine)
class Context(object):
"""
Expression engine, an implementation of
:class:`zope.tal.interfaces.ITALExpressionEngine`.
This class is called ``Context`` because an instance of this class
holds context information (namespaces) that it uses when evaluating
compiled expressions.
"""
position = (None, None)
source_file = None
def __init__(self, engine, contexts):
"""
:param engine: A :class:`ExpressionEngine` (a
:class:`zope.tal.interfaces.ITALExpressionCompiler`)
:param contexts: A mapping (namespace) of variables that forms the base
variable scope.
"""
self._engine = engine
self.contexts = contexts
self.setContext('nothing', None)
self.setContext('default', _default)
self.repeat_vars = rv = {}
# Wrap this, as it is visible to restricted code
self.setContext('repeat', rv)
self.setContext('loop', rv) # alias
self.vars = vars = contexts.copy()
self._vars_stack = [vars]
# Keep track of what needs to be popped as each scope ends.
self._scope_stack = []
[docs] def setContext(self, name, value):
"""Hook to allow subclasses to do things like adding security proxies.
"""
self.contexts[name] = value
def beginScope(self):
self.vars = vars = self.vars.copy()
self._vars_stack.append(vars)
self._scope_stack.append([])
def endScope(self):
self._vars_stack.pop()
self.vars = self._vars_stack[-1]
scope = self._scope_stack.pop()
# Pop repeat variables, if any
i = len(scope)
while i:
i = i - 1
name, value = scope[i]
if value is None:
del self.repeat_vars[name]
else:
self.repeat_vars[name] = value
def setLocal(self, name, value):
self.vars[name] = value
def setGlobal(self, name, value):
for vars in self._vars_stack:
vars[name] = value
def getValue(self, name, default=None):
value = default
for vars in self._vars_stack:
value = vars.get(name, default)
if value is not default:
break
return value
def setRepeat(self, name, expr):
expr = self.evaluate(expr)
if not expr:
return self._engine.iteratorFactory(name, (), self)
it = self._engine.iteratorFactory(name, expr, self)
old_value = self.repeat_vars.get(name)
self._scope_stack[-1].append((name, old_value))
self.repeat_vars[name] = it
return it
[docs] def evaluate(self, expression):
"""
Evaluate the *expression* by calling it, passing in this object,
and return the raw results.
If *expression* is a string, it is first compiled.
"""
if isinstance(expression, str):
expression = self._engine.compile(expression)
__traceback_supplement__ = (
TALESTracebackSupplement, self, expression)
return expression(self)
evaluateValue = evaluate
[docs] def evaluateBoolean(self, expr):
"""
Evaluate the expression and return the boolean value of its result.
"""
# "not not", while frowned upon by linters might be faster
# than bool() because it avoids a builtin lookup. Plus it can't be
# reassigned.
return not not self.evaluate(expr)
def evaluateText(self, expr):
text = self.evaluate(expr)
if text is self.getDefault() or text is None:
return text
if isinstance(text, six.string_types):
# text could already be something text-ish, e.g. a Message object
return text
return six.text_type(text)
evaluateStructure = evaluate
# TODO: Should return None or a macro definition
evaluateMacro = evaluate
def createErrorInfo(self, err, position):
return ErrorInfo(err, position)
def getDefault(self):
return _default
def setSourceFile(self, source_file):
self.source_file = source_file
def setPosition(self, position):
self.position = position
def translate(self, msgid, domain=None, mapping=None, default=None):
# custom Context implementations are supposed to customize
# this to call whichever translation routine they want to use
return six.text_type(msgid)
class TALESTracebackSupplement(object):
"""Implementation of zope.exceptions.ITracebackSupplement"""
def __init__(self, context, expression):
self.context = context
self.source_url = context.source_file
self.line = context.position[0]
self.column = context.position[1]
self.expression = repr(expression)
def getInfo(self, as_html=0):
import pprint
data = self.context.contexts.copy()
if 'modules' in data:
del data['modules'] # the list is really long and boring
s = pprint.pformat(data)
if not as_html:
return ' - Names:\n %s' % s.replace('\n', '\n ')
return '<b>Names:</b><pre>%s</pre>' % (escape(s))