summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/zope/tales/DEPENDENCIES.cfg3
-rw-r--r--src/zope/tales/__init__.py18
-rw-r--r--src/zope/tales/engine.py37
-rw-r--r--src/zope/tales/expressions.py324
-rw-r--r--src/zope/tales/interfaces.py116
-rw-r--r--src/zope/tales/pythonexpr.py77
-rw-r--r--src/zope/tales/tales.py760
-rw-r--r--src/zope/tales/tests/__init__.py2
-rw-r--r--src/zope/tales/tests/simpleexpr.py30
-rw-r--r--src/zope/tales/tests/test_expressions.py330
-rw-r--r--src/zope/tales/tests/test_tales.py183
-rw-r--r--src/zope/tales/tests/test_traverser.py120
12 files changed, 2000 insertions, 0 deletions
diff --git a/src/zope/tales/DEPENDENCIES.cfg b/src/zope/tales/DEPENDENCIES.cfg
new file mode 100644
index 0000000..e4327ee
--- /dev/null
+++ b/src/zope/tales/DEPENDENCIES.cfg
@@ -0,0 +1,3 @@
+zope.interface
+zope.tal
+zope.testing
diff --git a/src/zope/tales/__init__.py b/src/zope/tales/__init__.py
new file mode 100644
index 0000000..d998090
--- /dev/null
+++ b/src/zope/tales/__init__.py
@@ -0,0 +1,18 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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.
+#
+##############################################################################
+"""Template Attribute Language - Expression Syntax
+
+$Id$
+"""
+
diff --git a/src/zope/tales/engine.py b/src/zope/tales/engine.py
new file mode 100644
index 0000000..4affe46
--- /dev/null
+++ b/src/zope/tales/engine.py
@@ -0,0 +1,37 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation 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.
+#
+##############################################################################
+"""Expression engine configuration and registration.
+
+Each expression engine can have its own expression types and base names.
+
+$Id$
+"""
+from zope.tales.tales import ExpressionEngine
+from zope.tales.expressions import PathExpr, StringExpr, NotExpr, DeferExpr
+from zope.tales.expressions import SimpleModuleImporter
+from zope.tales.pythonexpr import PythonExpr
+
+def Engine():
+ e = ExpressionEngine()
+ reg = e.registerType
+ for pt in PathExpr._default_type_names:
+ reg(pt, PathExpr)
+ reg('string', StringExpr)
+ reg('python', PythonExpr)
+ reg('not', NotExpr)
+ reg('defer', DeferExpr)
+ e.registerBaseName('modules', SimpleModuleImporter())
+ return e
+
+Engine = Engine()
diff --git a/src/zope/tales/expressions.py b/src/zope/tales/expressions.py
new file mode 100644
index 0000000..c1add70
--- /dev/null
+++ b/src/zope/tales/expressions.py
@@ -0,0 +1,324 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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.
+#
+##############################################################################
+"""Basic Page Template expression types.
+
+$Id$
+"""
+import re, types
+
+from zope.interface import implements
+from zope.tales.tales import _valid_name, _parse_expr, NAME_RE, Undefined
+from zope.tales.interfaces import ITALESExpression, ITALESFunctionNamespace
+
+Undefs = (Undefined, AttributeError, LookupError, TypeError)
+
+_marker = object()
+namespace_re = re.compile(r'(\w+):(.+)')
+
+def simpleTraverse(object, path_items, econtext):
+ """Traverses a sequence of names, first trying attributes then items.
+ """
+
+ for name in path_items:
+ next = getattr(object, name, _marker)
+ if next is not _marker:
+ object = next
+ elif hasattr(object, '__getitem__'):
+ object = object[name]
+ else:
+ # Allow AttributeError to propagate
+ object = getattr(object, name)
+ return object
+
+
+class SubPathExpr(object):
+
+ def __init__(self, path, traverser, engine):
+ self._traverser = traverser
+ self._engine = engine
+
+ # Parse path
+ compiledpath = []
+ currentpath = []
+ for element in str(path).strip().split('/'):
+ if not element:
+ raise engine.getCompilerError()(
+ 'Path element may not be empty in %r' % path)
+ if element.startswith('?'):
+ if currentpath:
+ compiledpath.append(tuple(currentpath))
+ currentpath = []
+ if not _valid_name(element[1:]):
+ raise engine.getCompilerError()(
+ 'Invalid variable name "%s"' % element[1:])
+ compiledpath.append(element[1:])
+ else:
+ match = namespace_re.match(element)
+ if match:
+ if currentpath:
+ compiledpath.append(tuple(currentpath))
+ currentpath = []
+ namespace, functionname = match.groups()
+ if not _valid_name(namespace):
+ raise engine.getCompilerError()(
+ 'Invalid namespace name "%s"' % namespace)
+ try:
+ compiledpath.append(
+ self._engine.getFunctionNamespace(namespace))
+ except KeyError:
+ raise engine.getCompilerError()(
+ 'Unknown namespace "%s"' % namespace)
+ currentpath.append(functionname)
+ else:
+ currentpath.append(element)
+
+ if currentpath:
+ compiledpath.append(tuple(currentpath))
+
+ first = compiledpath[0]
+ base = first[0]
+
+ if callable(first):
+ # check for initial function
+ raise engine.getCompilerError()(
+ 'Namespace function specified in first subpath element')
+ elif isinstance(first, basestring):
+ # check for initial ?
+ raise engine.getCompilerError()(
+ 'Dynamic name specified in first subpath element')
+
+ if base and not _valid_name(base):
+ raise engine.getCompilerError()(
+ 'Invalid variable name "%s"' % element)
+ self._base = base
+ compiledpath[0] = first[1:]
+ self._compiled_path = tuple(compiledpath)
+
+ def _eval(self, econtext,
+ isinstance=isinstance):
+ vars = econtext.vars
+
+ compiled_path = self._compiled_path
+
+ base = self._base
+ if base == 'CONTEXTS' or not base: # Special base name
+ ob = econtext.contexts
+ else:
+ ob = vars[base]
+ if isinstance(ob, DeferWrapper):
+ ob = ob()
+
+ for element in compiled_path:
+ if isinstance(element, tuple):
+ ob = self._traverser(ob, element, econtext)
+ elif isinstance(element, basestring):
+ val = vars[element]
+ # If the value isn't a string, assume it's a sequence
+ # of path names.
+ if isinstance(val, basestring):
+ val = (val,)
+ ob = self._traverser(ob, val, econtext)
+ elif callable(element):
+ ob = element(ob)
+ # TODO: Once we have n-ary adapters, use them.
+ if ITALESFunctionNamespace.providedBy(ob):
+ ob.setEngine(econtext)
+ else:
+ raise ValueError(repr(element))
+ return ob
+
+
+class PathExpr(object):
+ """One or more subpath expressions, separated by '|'."""
+ implements(ITALESExpression)
+
+ # _default_type_names contains the expression type names this
+ # class is usually registered for.
+ _default_type_names = (
+ 'standard',
+ 'path',
+ 'exists',
+ 'nocall',
+ )
+
+ def __init__(self, name, expr, engine, traverser=simpleTraverse):
+ self._s = expr
+ self._name = name
+ self._hybrid = False
+ paths = expr.split('|')
+ self._subexprs = []
+ add = self._subexprs.append
+ for i in range(len(paths)):
+ path = paths[i].lstrip()
+ if _parse_expr(path):
+ # This part is the start of another expression type,
+ # so glue it back together and compile it.
+ add(engine.compile('|'.join(paths[i:]).lstrip()))
+ self._hybrid = True
+ break
+ add(SubPathExpr(path, traverser, engine)._eval)
+
+ def _exists(self, econtext):
+ for expr in self._subexprs:
+ try:
+ expr(econtext)
+ except Undefs:
+ pass
+ else:
+ return 1
+ return 0
+
+ def _eval(self, econtext):
+ for expr in self._subexprs[:-1]:
+ # Try all but the last subexpression, skipping undefined ones.
+ try:
+ ob = expr(econtext)
+ except Undefs:
+ pass
+ else:
+ break
+ else:
+ # On the last subexpression allow exceptions through, and
+ # don't autocall if the expression was not a subpath.
+ ob = self._subexprs[-1](econtext)
+ if self._hybrid:
+ return ob
+
+ if self._name == 'nocall':
+ return ob
+
+ # Call the object if it is callable. Note that checking for
+ # callable() isn't safe because the object might be security
+ # proxied (and security proxies report themselves callable, no
+ # matter what the underlying object is). We therefore check
+ # for the __call__ attribute, but not with hasattr as that
+ # eats babies, err, exceptions. In addition to that, we
+ # support calling old style classes which don't have a
+ # __call__.
+ if (getattr(ob, '__call__', _marker) is not _marker
+ or isinstance(ob, types.ClassType)):
+ return ob()
+ return ob
+
+ def __call__(self, econtext):
+ if self._name == 'exists':
+ return self._exists(econtext)
+ return self._eval(econtext)
+
+ def __str__(self):
+ return '%s expression (%s)' % (self._name, `self._s`)
+
+ def __repr__(self):
+ return '<PathExpr %s:%s>' % (self._name, `self._s`)
+
+
+
+_interp = re.compile(r'\$(%(n)s)|\${(%(n)s(?:/[^}]*)*)}' % {'n': NAME_RE})
+
+class StringExpr(object):
+ implements(ITALESExpression)
+
+ def __init__(self, name, expr, engine):
+ self._s = expr
+ if '%' in expr:
+ expr = expr.replace('%', '%%')
+ self._vars = vars = []
+ if '$' in expr:
+ # Use whatever expr type is registered as "path".
+ path_type = engine.getTypes()['path']
+ parts = []
+ for exp in expr.split('$$'):
+ if parts: parts.append('$')
+ m = _interp.search(exp)
+ while m is not None:
+ parts.append(exp[:m.start()])
+ parts.append('%s')
+ vars.append(path_type(
+ 'path', m.group(1) or m.group(2), engine))
+ exp = exp[m.end():]
+ m = _interp.search(exp)
+ if '$' in exp:
+ raise engine.getCompilerError()(
+ '$ must be doubled or followed by a simple path')
+ parts.append(exp)
+ expr = ''.join(parts)
+ self._expr = expr
+
+ def __call__(self, econtext):
+ vvals = []
+ for var in self._vars:
+ v = var(econtext)
+ vvals.append(v)
+ return self._expr % tuple(vvals)
+
+ def __str__(self):
+ return 'string expression (%s)' % `self._s`
+
+ def __repr__(self):
+ return '<StringExpr %s>' % `self._s`
+
+
+class NotExpr(object):
+ implements(ITALESExpression)
+
+ def __init__(self, name, expr, engine):
+ self._s = expr = expr.lstrip()
+ self._c = engine.compile(expr)
+
+ def __call__(self, econtext):
+ return int(not econtext.evaluateBoolean(self._c))
+
+ def __repr__(self):
+ return '<NotExpr %s>' % `self._s`
+
+
+class DeferWrapper(object):
+ def __init__(self, expr, econtext):
+ self._expr = expr
+ self._econtext = econtext
+
+ def __str__(self):
+ return str(self())
+
+ def __call__(self):
+ return self._expr(self._econtext)
+
+
+class DeferExpr(object):
+ implements(ITALESExpression)
+
+ def __init__(self, name, expr, compiler):
+ self._s = expr = expr.lstrip()
+ self._c = compiler.compile(expr)
+
+ def __call__(self, econtext):
+ return DeferWrapper(self._c, econtext)
+
+ def __repr__(self):
+ return '<DeferExpr %s>' % `self._s`
+
+
+class SimpleModuleImporter(object):
+ """Minimal module importer with no security."""
+
+ def __getitem__(self, module):
+ mod = self._get_toplevel_module(module)
+ path = module.split('.')
+ for name in path[1:]:
+ mod = getattr(mod, name)
+ return mod
+
+ def _get_toplevel_module(self, module):
+ # This can be overridden to add security proxies.
+ return __import__(module)
diff --git a/src/zope/tales/interfaces.py b/src/zope/tales/interfaces.py
new file mode 100644
index 0000000..c33606b
--- /dev/null
+++ b/src/zope/tales/interfaces.py
@@ -0,0 +1,116 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation 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.
+#
+##############################################################################
+"""Interface that describes the 'macros' attribute of a PageTemplate.
+
+$Id$
+"""
+from zope.interface import Interface
+
+try:
+ from zope import tal
+except ImportError:
+ tal = None
+
+
+class ITALESFunctionNamespace(Interface):
+ """Function namespaces can be used in TALES path expressions to extract
+ information in non-default ways."""
+
+ def setEngine(engine):
+ """Sets the engine that is used to evaluate TALES expressions."""
+
+class ITALESExpression(Interface):
+ """TALES expression
+
+ These are expression handlers that handle a specific type of
+ expression in TALES, e.g. path or string expression.
+ """
+
+ def __call__(econtext):
+ """Evaluate expression according to the given execution
+ context 'econtext' and return computed value.
+ """
+
+if tal is not None:
+ from zope.tal.interfaces import ITALIterator
+
+ class ITALESIterator(ITALIterator):
+ """TAL Iterator provided by TALES
+
+ Values of this iterator are assigned to items in the repeat namespace.
+
+ For example, with a TAL statement like: tal:repeat="item items",
+ an iterator will be assigned to "repeat/item". The iterator
+ provides a number of handy methods useful in writing TAL loops.
+
+ The results are undefined of calling any of the methods except
+ 'length' before the first iteration.
+ """
+
+ def index():
+ """Return the position (starting with "0") within the iteration
+ """
+
+ def number():
+ """Return the position (starting with "1") within the iteration
+ """
+
+ def even():
+ """Return whether the current position is even
+ """
+
+ def odd():
+ """Return whether the current position is odd
+ """
+
+ def parity():
+ """Return 'odd' or 'even' depending on the position's parity
+
+ Useful for assigning CSS class names to table rows.
+ """
+
+ def start():
+ """Return whether the current position is the first position
+ """
+
+ def end():
+ """Return whether the current position is the last position
+ """
+
+ def letter():
+ """Return the position (starting with "a") within the iteration
+ """
+
+ def Letter():
+ """Return the position (starting with "A") within the iteration
+ """
+
+ def roman():
+ """Return the position (starting with "i") within the iteration
+ """
+
+ def Roman():
+ """Return the position (starting with "I") within the iteration
+ """
+
+ def item():
+ """Return the item at the current position
+ """
+
+ def length():
+ """Return the length of the sequence
+
+ Note that this may fail if the TAL iterator was created on a Python
+ iterator.
+ """
diff --git a/src/zope/tales/pythonexpr.py b/src/zope/tales/pythonexpr.py
new file mode 100644
index 0000000..7fab3bb
--- /dev/null
+++ b/src/zope/tales/pythonexpr.py
@@ -0,0 +1,77 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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.
+#
+##############################################################################
+"""Generic Python Expression Handler
+
+$Id$
+"""
+
+class PythonExpr(object):
+ def __init__(self, name, expr, engine):
+ text = '\n'.join(expr.splitlines()) # normalize line endings
+ text = '(' + text + ')' # Put text in parens so newlines don't matter
+ self.text = text
+ try:
+ code = self._compile(text, '<string>')
+ except SyntaxError, e:
+ raise engine.getCompilerError()(str(e))
+ self._code = code
+ self._varnames = code.co_names
+
+ def _compile(self, text, filename):
+ return compile(text, filename, 'eval')
+
+ def _bind_used_names(self, econtext, builtins):
+ # Construct a dictionary of globals with which the Python
+ # expression should be evaluated.
+ names = {}
+ vars = econtext.vars
+ marker = self
+ if not isinstance(builtins, dict):
+ builtins = builtins.__dict__
+ for vname in self._varnames:
+ val = vars.get(vname, marker)
+ if val is not marker:
+ names[vname] = val
+ elif vname not in builtins:
+ # Fall back to using expression types as variable values.
+ val = econtext._engine.getTypes().get(vname, marker)
+ if val is not marker:
+ val = ExprTypeProxy(vname, val, econtext)
+ names[vname] = val
+
+ names['__builtins__'] = builtins
+ return names
+
+ def __call__(self, econtext):
+ __traceback_info__ = self.text
+ vars = self._bind_used_names(econtext, __builtins__)
+ return eval(self._code, vars)
+
+ def __str__(self):
+ return 'Python expression "%s"' % self.text
+
+ def __repr__(self):
+ return '<PythonExpr %s>' % self.text
+
+
+class ExprTypeProxy(object):
+ '''Class that proxies access to an expression type handler'''
+ def __init__(self, name, handler, econtext):
+ self._name = name
+ self._handler = handler
+ self._econtext = econtext
+
+ def __call__(self, text):
+ return self._handler(self._name, text,
+ self._econtext._engine)(self._econtext)
diff --git a/src/zope/tales/tales.py b/src/zope/tales/tales.py
new file mode 100644
index 0000000..ac5f6bf
--- /dev/null
+++ b/src/zope/tales/tales.py
@@ -0,0 +1,760 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import re
+
+from zope.interface import implements
+
+try:
+ from zope import tal
+except ImportError:
+ tal = None
+
+if tal:
+ from zope.tal.interfaces import ITALExpressionEngine
+ from zope.tal.interfaces import ITALExpressionCompiler
+ from zope.tal.interfaces import ITALExpressionErrorInfo
+ from zope.tales.interfaces import ITALESIterator
+
+
+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
+
+
+class TALESError(Exception):
+ """Error during TALES evaluation"""
+
+class Undefined(TALESError):
+ '''Exception raised on traversal of an undefined path'''
+
+class CompilerError(Exception):
+ '''TALES Compiler Error'''
+
+class RegistrationError(Exception):
+ '''Expression type or base name registration Error'''
+
+
+_default = object()
+
+class Iterator(object):
+ """TALES Iterator
+ """
+
+ if tal:
+ implements(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 = i.next()
+ 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 = self._iter.next()
+ except StopIteration:
+ self._done = True
+ self._last = True
+
+ self._nextIndex += 1
+ self._setLocal(self._name, v)
+ return True
+
+ def index(self):
+ """Get the iterator index
+
+ >>> context = Context(ExpressionEngine(), {})
+ >>> it = Iterator('foo', ("apple", "pear", "orange"), context)
+ >>> 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
+
+ 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
+
+ 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)
+
+ 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)
+
+ 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'
+
+ 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.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
+
+ 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'))
+
+ 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
+
+ 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()
+
+ 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
+
+ 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
+
+ def item(self):
+ """Get the iterator value
+
+ >>> context = Context(ExpressionEngine(), {})
+ >>> it = Iterator('foo', ("apple", "pear", "orange"), context)
+ >>> 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
+
+ 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._next = iter(seq).next
+ ... def __iter__(self):
+ ... return self
+ ... def next(self):
+ ... return self._next()
+ >>> it = Iterator('foo', MyIter({"apple":1, "pear":2}), context)
+ >>> it.length()
+ Traceback (most recent call last):
+ ...
+ TypeError: len() of unsized object
+
+ """
+ return len(self._seq)
+
+
+class ErrorInfo(object):
+ """Information about an exception passed to an on-error handler."""
+ if tal:
+ implements(ITALExpressionErrorInfo)
+
+ def __init__(self, err, position=(None, None)):
+ if isinstance(err, Exception):
+ self.type = err.__class__
+ self.value = err
+ else:
+ self.type = err
+ self.value = None
+ self.lineno = position[0]
+ self.offset = position[1]
+
+
+class ExpressionEngine(object):
+ '''Expression Engine
+
+ An instance of this class keeps a mutable collection of expression
+ type handlers. It can compile expression strings by delegating to
+ these handlers. It can provide an expression Context, which is
+ capable of holding state and evaluating compiled expressions.
+ '''
+ if tal:
+ implements(ITALExpressionCompiler)
+
+ def __init__(self):
+ self.types = {}
+ self.base_names = {}
+ self.namespaces = {}
+ self.iteratorFactory = Iterator
+
+ def registerFunctionNamespace(self, namespacename, namespacecallable):
+ """Register a function namespace
+
+ namespace - a string containing the name of the namespace to
+ be registered
+
+ 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.
+
+ 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
+
+
+ def getFunctionNamespace(self, namespacename):
+ """ Returns the function namespace """
+ return self.namespaces[namespacename]
+
+ def registerType(self, name, handler):
+ 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)
+
+ def getContext(self, contexts=None, **kwcontexts):
+ if contexts is not None:
+ if kwcontexts:
+ kwcontexts.update(contexts)
+ else:
+ kwcontexts = contexts
+ return Context(self, kwcontexts)
+
+ def getCompilerError(self):
+ return CompilerError
+
+
+class Context(object):
+ '''Expression Context
+
+ An instance of this class holds context information that it can
+ use to evaluate compiled expressions.
+ '''
+
+ if tal:
+ implements(ITALExpressionEngine)
+
+ position = (None, None)
+ source_file = None
+
+ def __init__(self, engine, contexts):
+ 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 = []
+
+ 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
+
+ def evaluate(self, expression):
+ if isinstance(expression, str):
+ expression = self._engine.compile(expression)
+ __traceback_supplement__ = (
+ TALESTracebackSupplement, self, expression)
+ return expression(self)
+
+ evaluateValue = evaluate
+
+ def evaluateBoolean(self, expr):
+ 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, basestring):
+ # text could already be something text-ish, e.g. a Message object
+ return text
+ return unicode(text)
+
+ def evaluateStructure(self, expr):
+ return self.evaluate(expr)
+ evaluateStructure = evaluate
+
+ def evaluateMacro(self, expr):
+ # TODO: Should return None or a macro definition
+ return self.evaluate(expr)
+ 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 unicode(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 ')
+ else:
+ from cgi import escape
+ return '<b>Names:</b><pre>%s</pre>' % (escape(s))
+ return None
diff --git a/src/zope/tales/tests/__init__.py b/src/zope/tales/tests/__init__.py
new file mode 100644
index 0000000..b711d36
--- /dev/null
+++ b/src/zope/tales/tests/__init__.py
@@ -0,0 +1,2 @@
+#
+# This file is necessary to make this directory a package.
diff --git a/src/zope/tales/tests/simpleexpr.py b/src/zope/tales/tests/simpleexpr.py
new file mode 100644
index 0000000..19f43ad
--- /dev/null
+++ b/src/zope/tales/tests/simpleexpr.py
@@ -0,0 +1,30 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation 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.
+#
+##############################################################################
+"""Simple TALES Expression
+
+$Id$
+"""
+
+class SimpleExpr(object):
+ '''Simple example of an expression type handler
+
+ for testing
+ '''
+ def __init__(self, name, expr, engine):
+ self._name = name
+ self._expr = expr
+ def __call__(self, econtext):
+ return self._name, self._expr
+ def __repr__(self):
+ return '<SimpleExpr %s %s>' % (self._name, `self._expr`)
diff --git a/src/zope/tales/tests/test_expressions.py b/src/zope/tales/tests/test_expressions.py
new file mode 100644
index 0000000..d3b4793
--- /dev/null
+++ b/src/zope/tales/tests/test_expressions.py
@@ -0,0 +1,330 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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.
+#
+##############################################################################
+"""Default TALES expression implementations tests.
+
+$Id$
+"""
+import unittest
+
+from zope.tales.engine import Engine
+from zope.tales.interfaces import ITALESFunctionNamespace
+from zope.tales.tales import Undefined
+from zope.interface import implements
+
+class Data(object):
+
+ def __init__(self, **kw):
+ self.__dict__.update(kw)
+
+ def __repr__(self): return self.name
+
+ __str__ = __repr__
+
+class ErrorGenerator:
+
+ def __getitem__(self, name):
+ import __builtin__
+ if name == 'Undefined':
+ e = Undefined
+ else:
+ e = getattr(__builtin__, name, None)
+ if e is None:
+ e = SystemError
+ raise e('mess')
+
+class ExpressionTestBase(unittest.TestCase):
+
+ def setUp(self):
+ # Test expression compilation
+ d = Data(
+ name = 'xander',
+ y = Data(
+ name = 'yikes',
+ z = Data(name = 'zope')
+ )
+ )
+ at = Data(
+ name = 'yikes',
+ _d = d
+ )
+ self.context = Data(
+ vars = dict(
+ x = d,
+ y = Data(z = 3),
+ b = 'boot',
+ B = 2,
+ adapterTest = at,
+ dynamic = 'z',
+ ErrorGenerator = ErrorGenerator(),
+ )
+ )
+
+ self.engine = Engine
+
+
+class ExpressionTests(ExpressionTestBase):
+
+ def testSimple(self):
+ expr = self.engine.compile('x')
+ context=self.context
+ self.assertEqual(expr(context), context.vars['x'])
+
+ def testPath(self):
+ expr = self.engine.compile('x/y')
+ context=self.context
+ self.assertEqual(expr(context), context.vars['x'].y)
+
+ def testLongPath(self):
+ expr = self.engine.compile('x/y/z')
+ context=self.context
+ self.assertEqual(expr(context), context.vars['x'].y.z)
+
+ def testOrPath(self):
+ expr = self.engine.compile('path:a|b|c/d/e')
+ context=self.context
+ self.assertEqual(expr(context), 'boot')
+
+ for e in 'Undefined', 'AttributeError', 'LookupError', 'TypeError':
+ expr = self.engine.compile('path:ErrorGenerator/%s|b|c/d/e' % e)
+ context=self.context
+ self.assertEqual(expr(context), 'boot')
+
+ def testDynamic(self):
+ expr = self.engine.compile('x/y/?dynamic')
+ context=self.context
+ self.assertEqual(expr(context),context.vars['x'].y.z)
+
+ def testBadInitalDynamic(self):
+ from zope.tales.tales import CompilerError
+ try:
+ self.engine.compile('?x')
+ except CompilerError,e:
+ self.assertEqual(e.args[0],
+ 'Dynamic name specified in first subpath element')
+ else:
+ self.fail('Engine accepted first subpath element as dynamic')
+
+ def testOldStyleClassIsCalled(self):
+ class AnOldStyleClass:
+ pass
+ self.context.vars['oldstyleclass'] = AnOldStyleClass
+ expr = self.engine.compile('oldstyleclass')
+ self.assert_(isinstance(expr(self.context), AnOldStyleClass))
+
+ def testString(self):
+ expr = self.engine.compile('string:Fred')
+ context=self.context
+ self.assertEqual(expr(context), 'Fred')
+
+ def testStringSub(self):
+ expr = self.engine.compile('string:A$B')
+ context=self.context
+ self.assertEqual(expr(context), 'A2')
+
+ def testStringSubComplex(self):
+ expr = self.engine.compile('string:a ${x/y} b ${y/z} c')
+ context=self.context
+ self.assertEqual(expr(context), 'a yikes b 3 c')
+
+ def testPython(self):
+ expr = self.engine.compile('python: 2 + 2')
+ context=self.context
+ self.assertEqual(expr(context), 4)
+
+ def testPythonCallableIsntCalled(self):
+ self.context.vars['acallable'] = lambda: 23
+ expr = self.engine.compile('python: acallable')
+ self.assertEqual(expr(self.context), self.context.vars['acallable'])
+
+ def testPythonNewline(self):
+ expr = self.engine.compile('python: 2 \n+\n 2\n')
+ context=self.context
+ self.assertEqual(expr(context), 4)
+
+ def testPythonDosNewline(self):
+ expr = self.engine.compile('python: 2 \r\n+\r\n 2\r\n')
+ context=self.context
+ self.assertEqual(expr(context), 4)
+
+ def testPythonErrorRaisesCompilerError(self):
+ self.assertRaises(self.engine.getCompilerError(),
+ self.engine.compile, 'python: splat.0')
+
+ def testHybridPathExpressions(self):
+ def eval(expr):
+ e = self.engine.compile(expr)
+ return e(self.context)
+ self.context.vars['one'] = 1
+ self.context.vars['acallable'] = lambda: 23
+
+ self.assertEqual(eval('foo | python:1+1'), 2)
+ self.assertEqual(eval('foo | python:acallable'),
+ self.context.vars['acallable'])
+ self.assertEqual(eval('foo | string:x'), 'x')
+ self.assertEqual(eval('foo | string:$one'), '1')
+ self.assert_(eval('foo | exists:x'))
+
+ def testEmptyPathSegmentRaisesCompilerError(self):
+ CompilerError = self.engine.getCompilerError()
+ def check(expr):
+ self.assertRaises(CompilerError, self.engine.compile, expr)
+
+ # path expressions on their own:
+ check('/ab/cd | c/d | e/f')
+ check('ab//cd | c/d | e/f')
+ check('ab/cd/ | c/d | e/f')
+ check('ab/cd | /c/d | e/f')
+ check('ab/cd | c//d | e/f')
+ check('ab/cd | c/d/ | e/f')
+ check('ab/cd | c/d | /e/f')
+ check('ab/cd | c/d | e//f')
+ check('ab/cd | c/d | e/f/')
+
+ # path expressions embedded in string: expressions:
+ check('string:${/ab/cd}')
+ check('string:${ab//cd}')
+ check('string:${ab/cd/}')
+ check('string:foo${/ab/cd | c/d | e/f}bar')
+ check('string:foo${ab//cd | c/d | e/f}bar')
+ check('string:foo${ab/cd/ | c/d | e/f}bar')
+ check('string:foo${ab/cd | /c/d | e/f}bar')
+ check('string:foo${ab/cd | c//d | e/f}bar')
+ check('string:foo${ab/cd | c/d/ | e/f}bar')
+ check('string:foo${ab/cd | c/d | /e/f}bar')
+ check('string:foo${ab/cd | c/d | e//f}bar')
+ check('string:foo${ab/cd | c/d | e/f/}bar')
+
+
+class FunctionTests(ExpressionTestBase):
+
+ def setUp(self):
+ ExpressionTestBase.setUp(self)
+
+ # a test namespace
+ class TestNameSpace(object):
+ implements(ITALESFunctionNamespace)
+
+ def __init__(self, context):
+ self.context = context
+
+ def setEngine(self, engine):
+ self._engine = engine
+
+ def engine(self):
+ return self._engine
+
+ def upper(self):
+ return str(self.context).upper()
+
+ def __getitem__(self,key):
+ if key=='jump':
+ return self.context._d
+ raise KeyError,key
+
+ self.TestNameSpace = TestNameSpace
+ self.engine.registerFunctionNamespace('namespace', self.TestNameSpace)
+
+ ## framework-ish tests
+
+ def testSetEngine(self):
+ expr = self.engine.compile('adapterTest/namespace:engine')
+ self.assertEqual(expr(self.context), self.context)
+
+ def testGetFunctionNamespace(self):
+ self.assertEqual(
+ self.engine.getFunctionNamespace('namespace'),
+ self.TestNameSpace
+ )
+
+ def testGetFunctionNamespaceBadNamespace(self):
+ self.assertRaises(KeyError,
+ self.engine.getFunctionNamespace,
+ 'badnamespace')
+
+ ## compile time tests
+
+ def testBadNamespace(self):
+ # namespace doesn't exist
+ from zope.tales.tales import CompilerError
+ try:
+ self.engine.compile('adapterTest/badnamespace:title')
+ except CompilerError,e:
+ self.assertEqual(e.args[0],'Unknown namespace "badnamespace"')
+ else:
+ self.fail('Engine accepted unknown namespace')
+
+ def testBadInitialNamespace(self):
+ # first segment in a path must not have modifier
+ from zope.tales.tales import CompilerError
+ self.assertRaises(CompilerError,
+ self.engine.compile,
+ 'namespace:title')
+
+ # In an ideal world ther ewould be another test here to test
+ # that a nicer error was raised when you tried to use
+ # something like:
+ # standard:namespace:upper
+ # ...as a path.
+ # However, the compilation stage of PathExpr currently
+ # allows any expression type to be nested, so something like:
+ # standard:standard:context/attribute
+ # ...will work fine.
+ # When that is changed so that only expression types which
+ # should be nested are nestable, then the additional test
+ # should be added here.
+
+ def testInvalidNamespaceName(self):
+ from zope.tales.tales import CompilerError
+ try:
+ self.engine.compile('adapterTest/1foo:bar')
+ except CompilerError,e:
+ self.assertEqual(e.args[0],
+ 'Invalid namespace name "1foo"')
+ else:
+ self.fail('Engine accepted invalid namespace name')
+
+ def testBadFunction(self):
+ from zope.tales.tales import CompilerError
+ # namespace is fine, adapter is not defined
+ try:
+ expr = self.engine.compile('adapterTest/namespace:title')
+ expr(self.context)
+ except KeyError,e:
+ self.assertEquals(e.args[0],'title')
+ else:
+ self.fail('Engine accepted unknown function')
+
+ ## runtime tests
+
+ def testNormalFunction(self):
+ expr = self.engine.compile('adapterTest/namespace:upper')
+ self.assertEqual(expr(self.context), 'YIKES')
+
+ def testFunctionOnFunction(self):
+ expr = self.engine.compile('adapterTest/namespace:jump/namespace:upper')
+ self.assertEqual(expr(self.context), 'XANDER')
+
+ def testPathOnFunction(self):
+ expr = self.engine.compile('adapterTest/namespace:jump/y/z')
+ context = self.context
+ self.assertEqual(expr(context), context.vars['x'].y.z)
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(ExpressionTests),
+ unittest.makeSuite(FunctionTests),
+ ))
+
+if __name__ == '__main__':
+ unittest.TextTestRunner().run(test_suite())
diff --git a/src/zope/tales/tests/test_tales.py b/src/zope/tales/tests/test_tales.py
new file mode 100644
index 0000000..31fbda1
--- /dev/null
+++ b/src/zope/tales/tests/test_tales.py
@@ -0,0 +1,183 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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 Tests
+
+$Id$
+"""
+import unittest
+
+from zope.tales import tales
+from zope.tales.tests.simpleexpr import SimpleExpr
+from zope.testing.doctestunit import DocTestSuite
+
+
+class TALESTests(unittest.TestCase):
+
+ def testIterator0(self):
+ # Test sample Iterator class
+ context = Harness(self)
+ it = tales.Iterator('name', (), context)
+ self.assert_( not it.next(), "Empty iterator")
+ context._complete_()
+
+ def testIterator1(self):
+ # Test sample Iterator class
+ context = Harness(self)
+ it = tales.Iterator('name', (1,), context)
+ context._assert_('setLocal', 'name', 1)
+ self.assert_( it.next() and not it.next(), "Single-element iterator")
+ context._complete_()
+
+ def testIterator2(self):
+ # Test sample Iterator class
+ context = Harness(self)
+ it = tales.Iterator('text', 'text', context)
+ for c in 'text':
+ context._assert_('setLocal', 'text', c)
+ for c in 'text':
+ self.assert_(it.next(), "Multi-element iterator")
+ self.assert_( not it.next(), "Multi-element iterator")
+ context._complete_()
+
+ def testRegisterType(self):
+ # Test expression type registration
+ e = tales.ExpressionEngine()
+ e.registerType('simple', SimpleExpr)
+ self.assert_( e.getTypes()['simple'] == SimpleExpr)
+
+ def testRegisterTypeUnique(self):
+ # Test expression type registration uniqueness
+ e = tales.ExpressionEngine()
+ e.registerType('simple', SimpleExpr)
+ try:
+ e.registerType('simple', SimpleExpr)
+ except tales.RegistrationError:
+ pass
+ else:
+ self.assert_( 0, "Duplicate registration accepted.")
+
+ def testRegisterTypeNameConstraints(self):
+ # Test constraints on expression type names
+ e = tales.ExpressionEngine()
+ for name in '1A', 'A!', 'AB ':
+ try:
+ e.registerType(name, SimpleExpr)
+ except tales.RegistrationError:
+ pass
+ else:
+ self.assert_( 0, 'Invalid type name "%s" accepted.' % name)
+
+ def testCompile(self):
+ # Test expression compilation
+ e = tales.ExpressionEngine()
+ e.registerType('simple', SimpleExpr)
+ ce = e.compile('simple:x')
+ self.assert_( ce(None) == ('simple', 'x'), (
+ 'Improperly compiled expression %s.' % `ce`))
+
+ def testGetContext(self):
+ # Test Context creation
+ tales.ExpressionEngine().getContext()
+ tales.ExpressionEngine().getContext(v=1)
+ tales.ExpressionEngine().getContext(x=1, y=2)
+
+ def getContext(self, **kws):
+ e = tales.ExpressionEngine()
+ e.registerType('simple', SimpleExpr)
+ return apply(e.getContext, (), kws)
+
+ def testContext0(self):
+ # Test use of Context
+ se = self.getContext().evaluate('simple:x')
+ self.assert_( se == ('simple', 'x'), (
+ 'Improperly evaluated expression %s.' % `se`))
+
+ def testVariables(self):
+ # Test variables
+ ctxt = self.getContext()
+ ctxt.beginScope()
+ ctxt.setLocal('v1', 1)
+ ctxt.setLocal('v2', 2)
+
+ c = ctxt.vars
+ self.assert_( c['v1'] == 1, 'Variable "v1"')
+
+ ctxt.beginScope()
+ ctxt.setLocal('v1', 3)
+ ctxt.setGlobal('g', 1)
+
+ c = ctxt.vars
+ self.assert_( c['v1'] == 3, 'Inner scope')
+ self.assert_( c['v2'] == 2, 'Outer scope')
+ self.assert_( c['g'] == 1, 'Global')
+
+ ctxt.endScope()
+
+ c = ctxt.vars
+ self.assert_( c['v1'] == 1, "Uncovered local")
+ self.assert_( c['g'] == 1, "Global from inner scope")
+
+ ctxt.endScope()
+
+
+class Harness(object):
+ def __init__(self, testcase):
+ self._callstack = []
+ self._testcase = testcase
+
+ def _assert_(self, name, *args, **kwargs):
+ self._callstack.append((name, args, kwargs))
+
+ def _complete_(self):
+ self._testcase.assert_(len(self._callstack) == 0,
+ "Harness methods called")
+
+ def __getattr__(self, name):
+ return HarnessMethod(self, name)
+
+class HarnessMethod(object):
+
+ def __init__(self, harness, name):
+ self._harness = harness
+ self._name = name
+
+ def __call__(self, *args, **kwargs):
+ name = self._name
+ self = self._harness
+
+ cs = self._callstack
+ self._testcase.assert_(
+ len(cs),
+ 'Unexpected harness method call "%s".' % name
+ )
+ self._testcase.assert_(
+ cs[0][0] == name,
+ 'Harness method name "%s" called, "%s" expected.' %
+ (name, cs[0][0])
+ )
+
+ name, aargs, akwargs = self._callstack.pop(0)
+ self._testcase.assert_(aargs == args, "Harness method arguments")
+ self._testcase.assert_(akwargs == kwargs,
+ "Harness method keyword args")
+
+
+def test_suite():
+ suite = unittest.makeSuite(TALESTests)
+ suite.addTest(DocTestSuite("zope.tales.tales"))
+ return suite
+
+
+if __name__ == '__main__':
+ unittest.TextTestRunner().run(test_suite())
diff --git a/src/zope/tales/tests/test_traverser.py b/src/zope/tales/tests/test_traverser.py
new file mode 100644
index 0000000..eb70cd6
--- /dev/null
+++ b/src/zope/tales/tests/test_traverser.py
@@ -0,0 +1,120 @@
+"""
+Tests for zope.tales.expressions.simpleTraverse
+
+$Id:$
+"""
+
+from unittest import TestCase, TestSuite, makeSuite, main
+from zope.tales.expressions import simpleTraverse
+
+
+class AttrTraversable(object):
+ """Traversable by attribute access"""
+ attr = 'foo'
+
+class ItemTraversable(object):
+ """Traversable by item access"""
+ def __getitem__(self, name):
+ if name == 'attr':
+ return 'foo'
+ raise KeyError, name
+
+class AllTraversable(AttrTraversable, ItemTraversable):
+ """Traversable by attribute and item access"""
+ pass
+
+
+_marker = object()
+
+def getitem(ob, name, default=_marker):
+ """Helper a la getattr(ob, name, default)."""
+ try:
+ item = ob[name]
+ except KeyError:
+ if default is not _marker:
+ return default
+ raise KeyError, name
+ else:
+ return item
+
+
+class TraverserTests(TestCase):
+
+ def testGetItem(self):
+ # getitem helper should behave like __getitem__
+ ob = {'attr': 'foo'}
+ self.assertEqual(getitem(ob, 'attr', None), 'foo')
+ self.assertEqual(getitem(ob, 'attr'), 'foo')
+ self.assertEqual(getitem(ob, 'missing_attr', None), None)
+ self.assertRaises(KeyError, getitem, ob, 'missing_attr')
+ self.assertRaises(TypeError, getitem, object(), 'attr')
+
+ def testAttrTraversable(self):
+ # An object without __getitem__ should raise AttributeError
+ # for missing attribute.
+ ob = AttrTraversable()
+ self.assertEqual(getattr(ob, 'attr', None), 'foo')
+ self.assertRaises(AttributeError, getattr, ob, 'missing_attr')
+
+ def testItemTraversable(self):
+ # An object with __getitem__ (but without attr) should raise
+ # KeyError for missing attribute.
+ ob = ItemTraversable()
+ self.assertEqual(getitem(ob, 'attr', None), 'foo')
+ self.assertRaises(KeyError, getitem, ob, 'missing_attr')
+
+ def testAllTraversable(self):
+ # An object with attr and __getitem__ should raise either
+ # exception, depending on method of access.
+ ob = AllTraversable()
+ self.assertEqual(getattr(ob, 'attr', None), 'foo')
+ self.assertRaises(AttributeError, getattr, ob, 'missing_attr')
+ self.assertEqual(getitem(ob, 'attr', None), 'foo')
+ self.assertRaises(KeyError, getitem, ob, 'missing_attr')
+
+ def testTraverseEmptyPath(self):
+ # simpleTraverse should return the original object if the path is emtpy
+ ob = object()
+ self.assertEqual(simpleTraverse(ob, [], None), ob)
+
+ def testTraverseByAttr(self):
+ # simpleTraverse should find attr through attribute access
+ ob = AttrTraversable()
+ self.assertEqual(simpleTraverse(ob, ['attr'], None), 'foo')
+
+ def testTraverseByMissingAttr(self):
+ # simpleTraverse should raise AttributeError
+ ob = AttrTraversable()
+ # Here lurks the bug (unexpected NamError raised)
+ self.assertRaises(AttributeError, simpleTraverse, ob, ['missing_attr'], None)
+
+ def testTraverseByItem(self):
+ # simpleTraverse should find attr through item access
+ ob = ItemTraversable()
+ self.assertEqual(simpleTraverse(ob, ['attr'], None), 'foo')
+
+ def testTraverseByMissingItem(self):
+ # simpleTraverse should raise KeyError
+ ob = ItemTraversable()
+ self.assertRaises(KeyError, simpleTraverse, ob, ['missing_attr'], None)
+
+ def testTraverseByAll(self):
+ # simpleTraverse should find attr through attribute access
+ ob = AllTraversable()
+ self.assertEqual(simpleTraverse(ob, ['attr'], None), 'foo')
+
+ def testTraverseByMissingAll(self):
+ # simpleTraverse should raise KeyError (because ob implements __getitem__)
+ ob = AllTraversable()
+ self.assertRaises(KeyError, simpleTraverse, ob, ['missing_attr'], None)
+
+
+def test_suite():
+ return TestSuite((
+ makeSuite(TraverserTests),
+ ))
+
+
+if __name__ == '__main__':
+ main(defaultTest='test_suite')
+