diff --git a/.coveragerc b/.coveragerc
index 5724d01..1e8ed43 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,5 +1,7 @@
 [run]
 source = zope.tales
+omit =
+    */flycheck_*py
 
 [report]
 precision = 2
diff --git a/.travis.yml b/.travis.yml
index b07bcf7..e903051 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,12 +5,8 @@ python:
     - 3.4
     - 3.5
     - 3.6
-    # Force a newer PyPy on the old 'precise' CI image
-    # in order to install 'cryptography' needed for coveralls
-    # After September 2017 this should be the default and the version
-    # pin can be removed.
-    - pypy-5.6.0
-    - pypy3.5-5.8.0
+    - pypy
+    - pypy3
 install:
     - pip install -U pip setuptools
     - pip install -U coverage coveralls
diff --git a/CHANGES.rst b/CHANGES.rst
index d9d2dae..684e92f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -11,6 +11,8 @@
 
 - Drop support for ``python setup.py test``.
 
+- Reach 100% test coverage and maintain it via tox.ini and Travis CI.
+
 4.1.1 (2015-06-06)
 ==================
 
diff --git a/src/zope/tales/expressions.py b/src/zope/tales/expressions.py
index 28fb81e..6203cc7 100644
--- a/src/zope/tales/expressions.py
+++ b/src/zope/tales/expressions.py
@@ -90,7 +90,6 @@ class SubPathExpr(object):
             compiledpath.append(tuple(currentpath))
 
         first = compiledpath[0]
-        base = first[0]
 
         if callable(first):
             # check for initial function
@@ -101,6 +100,7 @@ class SubPathExpr(object):
             raise engine.getCompilerError()(
                 'Dynamic name specified in first subpath element')
 
+        base = first[0]
         if base and not _valid_name(base):
             raise engine.getCompilerError()(
                 'Invalid variable name "%s"' % element)
@@ -211,9 +211,7 @@ class PathExpr(object):
         # __call__.
         if getattr(ob, '__call__', _marker) is not _marker:
             return ob()
-        if PY2 and isinstance(ob, types.ClassType):
-            return ob()
-        return ob
+        return ob() if PY2 and isinstance(ob, types.ClassType) else ob
 
     def __call__(self, econtext):
         if self._name == 'exists':
@@ -229,8 +227,8 @@ class PathExpr(object):
 
 
 _interp = re.compile(
-        r'\$(%(n)s)|\${(%(n)s(?:/[^}|]*)*(?:\|%(n)s(?:/[^}|]*)*)*)}'
-            % {'n': NAME_RE})
+    r'\$(%(n)s)|\${(%(n)s(?:/[^}|]*)*(?:\|%(n)s(?:/[^}|]*)*)*)}'
+    % {'n': NAME_RE})
 
 @implementer(ITALESExpression)
 class StringExpr(object):
@@ -318,9 +316,10 @@ class DeferExpr(object):
 class LazyWrapper(DeferWrapper):
     """Wrapper for lazy: expression
     """
+    _result = _marker
+
     def __init__(self, expr, econtext):
         DeferWrapper.__init__(self, expr, econtext)
-        self._result = _marker
 
     def __call__(self):
         r = self._result
diff --git a/src/zope/tales/tales.py b/src/zope/tales/tales.py
index fd3295e..1256153 100644
--- a/src/zope/tales/tales.py
+++ b/src/zope/tales/tales.py
@@ -21,18 +21,21 @@ import re
 from zope.interface import implementer
 import six
 
+from zope.interface import Interface
+class ITALExpressionEngine(Interface):
+    pass
+class ITALExpressionCompiler(Interface):
+    pass
+class ITALExpressionErrorInfo(Interface):
+    pass
+
 try:
-    from zope.tal.interfaces import ITALExpressionEngine
-    from zope.tal.interfaces import ITALExpressionCompiler
-    from zope.tal.interfaces import ITALExpressionErrorInfo
+    # Override with real, if present
+    from zope.tal.interfaces import (ITALExpressionEngine,
+                                     ITALExpressionCompiler,
+                                     ITALExpressionErrorInfo)
 except ImportError:
-    from zope.interface import Interface
-    class ITALExpressionEngine(Interface):
-        pass
-    class ITALExpressionCompiler(Interface):
-        pass
-    class ITALExpressionErrorInfo(Interface):
-        pass
+    pass
 
 from zope.tales.interfaces import ITALESIterator
 
@@ -107,7 +110,7 @@ class Iterator(object):
         # but we can't know that without trying to get it. :(
         self._last = False
         try:
-            self._next = six.advance_iterator(i)
+            self._next = next(i)
         except StopIteration:
             self._done = True
         else:
@@ -159,7 +162,7 @@ class Iterator(object):
             return False
         self._item = v = self._next
         try:
-            self._next = six.advance_iterator(self._iter)
+            self._next = next(self._iter)
         except StopIteration:
             self._done = True
             self._last = True
@@ -175,6 +178,10 @@ class Iterator(object):
 
         >>> 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()
@@ -280,6 +287,10 @@ class Iterator(object):
 
         >>> 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()
@@ -300,7 +311,8 @@ class Iterator(object):
         while 1:
             index, off = divmod(index, radix)
             s = chr(base + off) + s
-            if not index: return s
+            if not index:
+                return s
 
     def Letter(self):
         """Get the iterator position as an upper-case letter
@@ -323,9 +335,9 @@ class Iterator(object):
         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')) ):
+            (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(), {})
@@ -431,6 +443,10 @@ class Iterator(object):
 
         >>> 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()
@@ -495,13 +511,14 @@ class Iterator(object):
 class ErrorInfo(object):
     """Information about an exception passed to an on-error handler."""
 
+    value = None
+
     def __init__(self, err, position=(None, None)):
+        self.type = err
         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]
 
@@ -691,12 +708,15 @@ class Context(object):
         if isinstance(expression, str):
             expression = self._engine.compile(expression)
         __traceback_supplement__ = (
-           TALESTracebackSupplement, self, expression)
+            TALESTracebackSupplement, self, expression)
         return expression(self)
 
     evaluateValue = evaluate
 
     def evaluateBoolean(self, expr):
+        # "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):
@@ -708,13 +728,9 @@ class Context(object):
             return text
         return six.text_type(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)
+    # TODO: Should return None or a macro definition
     evaluateMacro = evaluate
 
     def createErrorInfo(self, err, position):
@@ -749,11 +765,10 @@ class TALESTracebackSupplement(object):
         import pprint
         data = self.context.contexts.copy()
         if 'modules' in data:
-            del data['modules']     # the list is really long and boring
+            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
+
+        from cgi import escape
+        return '<b>Names:</b><pre>%s</pre>' % (escape(s))
diff --git a/tox.ini b/tox.ini
index 47d0c9f..927f900 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,10 +11,10 @@ deps =
 [testenv:coverage]
 usedevelop = true
 basepython =
-    python2.7
+    python3.6
 commands =
     coverage run -m zope.testrunner --test-path=src []
-    coverage report --fail-under=88
+    coverage report --fail-under=100
 deps =
     {[testenv]deps}
     coverage

Reach 100% coverage
diff --git a/src/zope/tales/tests/test_expressions.py b/src/zope/tales/tests/test_expressions.py
index b39b1b1..bc820cf 100644
--- a/src/zope/tales/tests/test_expressions.py
+++ b/src/zope/tales/tests/test_expressions.py
@@ -22,11 +22,7 @@ from zope.tales.interfaces import ITALESFunctionNamespace
 from zope.tales.tales import Undefined
 from zope.interface import implementer
 
-try:
-    unicode
-except NameError:
-    # Py3: Make the unicode name available again.
-    unicode = str
+text_type = str if str is not bytes else unicode
 
 PY3 = sys.version_info[0] == 3
 
@@ -35,95 +31,156 @@ class Data(object):
     def __init__(self, **kw):
         self.__dict__.update(kw)
 
-    def __repr__(self): return self.name
+    def __getattr__(self, name):
+        # Let linters (like pylint) know this is a dynamic class and they shouldn't
+        # emit "Data has no attribute" errors
+        return object.__getattribute__(self, name)
+
+    def __repr__(self):
+        return self.name
 
     __str__ = __repr__
 
-class ErrorGenerator:
+class ErrorGenerator(object):
 
     def __getitem__(self, name):
         from six.moves import builtins
         if name == 'Undefined':
             e = Undefined
         else:
-            e = getattr(builtins, name, None)
-        if e is None:
-            e = SystemError
+            e = getattr(builtins, name, None) or SystemError
         raise e('mess')
 
+class Callable(object):
+
+    def __call__(self):
+        return 42
+
+class OldStyleCallable: # NOT object
+
+    pass
+
 class ExpressionTestBase(unittest.TestCase):
 
     def setUp(self):
         # Test expression compilation
-        from six import u
         d = Data(
-                 name = 'xander',
-                 y = Data(
-                    name = 'yikes',
-                    z = Data(name = 'zope')
-                    )
-                 )
+            name='xander',
+            y=Data(
+                name='yikes',
+                z=Data(name='zope')
+            )
+        )
         at = Data(
-                  name = 'yikes',
-                  _d = d
-                 )
+            name='yikes',
+            _d=d
+        )
         self.context = Data(
-            vars = dict(
-              x = d,
-              y = Data(z = 3),
-              b = 'boot',
-              B = 2,
-              adapterTest = at,
-              dynamic = 'z',
-              eightBits = u('déjà vu').encode('utf-8'),
-              ErrorGenerator = ErrorGenerator(),
-              )
+            vars=dict(
+                x=d,
+                y=Data(z=3),
+                b='boot',
+                B=2,
+                adapterTest=at,
+                dynamic='z',
+                eightBits=u'déjà vu'.encode('utf-8'),
+                ErrorGenerator=ErrorGenerator(),
+                callable=Callable(),
+                old_callable_class=OldStyleCallable,
             )
+        )
 
         self.engine = Engine
 
+        self.py3BrokenEightBits = "a b'd\\xc3\\xa9j\\xc3\\xa0 vu'"
 
-class ExpressionTests(ExpressionTestBase):
+
+    def _compiled_expr(self, expr):
+        return self.engine.compile(expr) if isinstance(expr, str) else expr
+
+    def _check_evals_to(self, expr, result):
+        expr = self._compiled_expr(expr)
+        self.assertEqual(expr(self.context), result)
+        return expr
+
+    def _check_evals_to_instance(self, expr, result_kind):
+        expr = self._compiled_expr(expr)
+        self.assertIsInstance(expr(self.context), result_kind)
+        return expr
+
+    def _check_raises_compiler_error(self, expr_str, regex=None):
+        from zope.tales.tales import CompilerError
+        meth = self.assertRaises if regex is None else self.assertRaisesRegexp
+        args = (regex,) if regex is not None else ()
+        with meth(CompilerError, *args) as exc:
+            self.engine.compile(expr_str)
+        return exc.exception
+
+    def _check_subexpr_raises_compiler_error(self, expr, regexp):
+        from zope.tales.expressions import SubPathExpr
+        from zope.tales.tales import CompilerError
+        with self.assertRaisesRegexp(CompilerError,
+                                     regexp):
+            SubPathExpr(expr, None, self.engine)
+
+
+class TestParsedExpressions(ExpressionTestBase):
+    # Whitebox tests of expressions that have been compiled by the engine
 
     def testSimple(self):
         expr = self.engine.compile('x')
-        context=self.context
-        self.assertEqual(expr(context), context.vars['x'])
+        self._check_evals_to(expr, self.context.vars['x'])
 
     def testPath(self):
         expr = self.engine.compile('x/y')
-        context=self.context
-        self.assertEqual(expr(context), context.vars['x'].y)
+        self._check_evals_to(expr, self.context.vars['x'].y)
+        self.assertEqual("standard expression ('x/y')", str(expr))
+        self.assertEqual("<PathExpr standard:'x/y'>", repr(expr))
+
+    def test_path_nocall_call(self):
+        self._check_evals_to('callable', 42)
+        self._check_evals_to('nocall:callable', self.context.vars['callable'])
+
+        self._check_evals_to_instance('old_callable_class', OldStyleCallable)
+        self._check_evals_to('nocall:old_callable_class', OldStyleCallable)
+
+    def test_path_exists(self):
+        self._check_evals_to('exists:x', 1)
+        self._check_evals_to('exists:' + 'i' + str(id(self)), 0)
 
     def testLongPath(self):
         expr = self.engine.compile('x/y/z')
-        context=self.context
-        self.assertEqual(expr(context), context.vars['x'].y.z)
+        self._check_evals_to(expr, self.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')
+        self._check_evals_to(expr, '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')
+            self._check_evals_to(expr, 'boot')
+
+    def test_path_CONTEXTS(self):
+        self.context.contexts = 42
+        self._check_evals_to('CONTEXTS', 42)
 
     def testDynamic(self):
         expr = self.engine.compile('x/y/?dynamic')
-        context=self.context
-        self.assertEqual(expr(context),context.vars['x'].y.z)
+        self._check_evals_to(expr, self.context.vars['x'].y.z)
 
     def testBadInitalDynamic(self):
         from zope.tales.tales import CompilerError
-        try:
+        with self.assertRaises(CompilerError) as exc:
             self.engine.compile('?x')
-        except CompilerError as e:
-            self.assertEqual(e.args[0],
-                             'Dynamic name specified in first subpath element')
-        else:
-            self.fail('Engine accepted first subpath element as dynamic')
+        e = exc.exception
+        self.assertEqual(e.args[0],
+                         'Dynamic name specified in first subpath element')
+
+    def test_dynamic_invalid_variable_name(self):
+        from zope.tales.tales import CompilerError
+        with self.assertRaisesRegexp(CompilerError,
+                                     "Invalid variable name"):
+            self.engine.compile('path:a/?123')
 
     def testOldStyleClassIsCalled(self):
         class AnOldStyleClass:
@@ -134,67 +191,67 @@ class ExpressionTests(ExpressionTestBase):
 
     def testString(self):
         expr = self.engine.compile('string:Fred')
-        context=self.context
+        context = self.context
         result = expr(context)
         self.assertEqual(result, 'Fred')
-        self.assertTrue(isinstance(result, str))
+        self.assertIsInstance(result, str)
+        self.assertEqual("string expression ('Fred')", str(expr))
+        self.assertEqual("<StringExpr 'Fred'>", repr(expr))
 
     def testStringSub(self):
         expr = self.engine.compile('string:A$B')
-        context=self.context
-        self.assertEqual(expr(context), 'A2')
+        self._check_evals_to(expr, 'A2')
 
     def testStringSub_w_python(self):
         CompilerError = self.engine.getCompilerError()
         self.assertRaises(CompilerError,
                           self.engine.compile,
-                                'string:${python:1}')
+                          'string:${python:1}')
 
     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')
+        self._check_evals_to(expr, 'a yikes b 3 c')
 
     def testStringSubComplex_w_miss_and_python(self):
         # See https://bugs.launchpad.net/zope.tales/+bug/1002242
         CompilerError = self.engine.getCompilerError()
         self.assertRaises(CompilerError,
                           self.engine.compile,
-                            'string:${nothig/nothing|python:1}')
+                          'string:${nothig/nothing|python:1}')
 
     def testString8Bits(self):
         # Simple eight bit string interpolation should just work.
-        from six import u
-        if PY3:
-            # Py3: We simply do not handle 8-bit strings.
-            return
+        # Except on Py3, where we really mess it up.
         expr = self.engine.compile('string:a ${eightBits}')
-        context=self.context
-        self.assertEqual(expr(context), u('a déjà vu').encode('utf8'))
+        expected = 'a ' + self.context.vars['eightBits'] if not PY3 else self.py3BrokenEightBits
+        self._check_evals_to(expr, expected)
 
     def testStringUnicode(self):
         # Unicode string expressions should return unicode strings
-        from six import u
-        expr = self.engine.compile(u('string:Fred'))
-        context=self.context
+        expr = self.engine.compile(u'string:Fred')
+        context = self.context
         result = expr(context)
-        self.assertEqual(result, u('Fred'))
-        self.assertTrue(isinstance(result, unicode))
+        self.assertEqual(result, u'Fred')
+        self.assertIsInstance(result, text_type)
 
     def testStringFailureWhenMixed(self):
         # Mixed Unicode and 8bit string interpolation fails with a
         # UnicodeDecodeError, assuming there is no default encoding
-        from six import u
-        if PY3:
-            # Py3: We simply do not handle 8-bit strings.
-            return
-        expr = self.engine.compile(u('string:a ${eightBits}'))
-        self.assertRaises(UnicodeDecodeError, expr, self.context)
+        expr = self.engine.compile(u'string:a ${eightBits}')
+        with self.assertRaises(UnicodeDecodeError):
+            result = expr(self.context)
+            # If we get here, we're on Python 3, which handles this
+            # poorly.
+            self.assertTrue(PY3)
+            self.assertEqual(result, self.py3BrokenEightBits)
+            self.context.vars['eightBits'].decode('ascii') # raise UnicodeDecodeError
+
+    def test_string_escape_percent(self):
+        self._check_evals_to('string:%', '%')
 
     def testPython(self):
         expr = self.engine.compile('python: 2 + 2')
-        context=self.context
-        self.assertEqual(expr(context), 4)
+        self._check_evals_to(expr, 4)
 
     def testPythonCallableIsntCalled(self):
         self.context.vars['acallable'] = lambda: 23
@@ -203,13 +260,11 @@ class ExpressionTests(ExpressionTestBase):
 
     def testPythonNewline(self):
         expr = self.engine.compile('python: 2 \n+\n 2\n')
-        context=self.context
-        self.assertEqual(expr(context), 4)
+        self._check_evals_to(expr, 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)
+        self._check_evals_to(expr, 4)
 
     def testPythonErrorRaisesCompilerError(self):
         self.assertRaises(self.engine.getCompilerError(),
@@ -261,15 +316,55 @@ class ExpressionTests(ExpressionTestBase):
 
     def test_defer_expression_returns_wrapper(self):
         from zope.tales.expressions import DeferWrapper
+        from zope.tales.expressions import DeferExpr
+        expr = self.engine.compile('defer: B')
+        self.assertIsInstance(expr, DeferExpr)
+        self.assertEqual(str(expr), "<DeferExpr 'B'>")
+        self._check_evals_to_instance(expr, DeferWrapper)
+
+        wrapper = expr(self.context)
+        # It evaluates to what its underlying expression evaluates to
+        self.assertEqual(wrapper(), self.context.vars['B'])
+        # The str() of defer is the same as the str() of evaluating it
+        self.assertEqual(str(wrapper), str(self.context.vars['B']))
+        self.assertEqual(str(wrapper()), str(self.context.vars['B']))
+
+    def test_eval_defer_wrapper(self):
         expr = self.engine.compile('defer: b')
-        context=self.context
-        self.assertTrue(isinstance(expr(context), DeferWrapper))
+        self.context.vars['deferred'] = expr(self.context)
+        self._check_evals_to('deferred', self.context.vars['b'])
 
     def test_lazy_expression_returns_wrapper(self):
         from zope.tales.expressions import LazyWrapper
+        from zope.tales.expressions import LazyExpr
         expr = self.engine.compile('lazy: b')
-        context=self.context
-        self.assertTrue(isinstance(expr(context), LazyWrapper))
+        self.assertIsInstance(expr, LazyExpr)
+        self.assertEqual(repr(expr), "lazy:'b'")
+        lazy = expr(self.context)
+        self.assertIsInstance(lazy, LazyWrapper)
+
+        first_result = lazy()
+        second_result = lazy()
+        self.assertIs(first_result, second_result)
+
+    def test_not(self):
+        # self.context is a Data object, not a real
+        # zope.tales.tales.Context object, and as such
+        # it doesn't define the evaluateBoolean function that
+        # not expressions need. Add it locally to avoid disturbing
+        # other tests.
+        def evaluateBoolean(expr):
+            return bool(expr(self.context))
+        self.context.evaluateBoolean = evaluateBoolean
+        self._check_evals_to('not:exists:x', 0)
+        expr = self._check_evals_to('not:exists:v_42', 1)
+        self.assertEqual("<NotExpr 'exists:v_42'>", repr(expr))
+
+    def test_bad_initial_name_subexpr(self):
+        self._check_subexpr_raises_compiler_error(
+            '123',
+            "Invalid variable name"
+        )
 
 
 class FunctionTests(ExpressionTestBase):
@@ -293,13 +388,14 @@ class FunctionTests(ExpressionTestBase):
             def upper(self):
                 return str(self.context).upper()
 
-            def __getitem__(self,key):
-                if key=='jump':
+            def __getitem__(self, key):
+                if key == 'jump':
                     return self.context._d
                 raise KeyError(key)
 
         self.TestNameSpace = TestNameSpace
         self.engine.registerFunctionNamespace('namespace', self.TestNameSpace)
+        self.engine.registerFunctionNamespace('not_callable_ns', None)
 
     ## framework-ish tests
 
@@ -311,7 +407,7 @@ class FunctionTests(ExpressionTestBase):
         self.assertEqual(
             self.engine.getFunctionNamespace('namespace'),
             self.TestNameSpace
-            )
+        )
 
     def testGetFunctionNamespaceBadNamespace(self):
         self.assertRaises(KeyError,
@@ -322,20 +418,15 @@ class FunctionTests(ExpressionTestBase):
 
     def testBadNamespace(self):
         # namespace doesn't exist
-        from zope.tales.tales import CompilerError
-        try:
-            self.engine.compile('adapterTest/badnamespace:title')
-        except CompilerError as e:
-            self.assertEqual(e.args[0],'Unknown namespace "badnamespace"')
-        else:
-            self.fail('Engine accepted unknown namespace')
+        self._check_raises_compiler_error(
+            'adapterTest/badnamespace:title',
+            'Unknown namespace "badnamespace"')
 
     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')
+        self._check_raises_compiler_error(
+            'namespace:title',
+            'Unrecognized expression type "namespace"')
 
         # In an ideal world there would be another test here to test
         # that a nicer error was raised when you tried to use
@@ -350,25 +441,24 @@ class FunctionTests(ExpressionTestBase):
         # should be nested are nestable, then the additional test
         # should be added here.
 
+    def test_bad_initial_namespace_subpath(self):
+        self._check_subexpr_raises_compiler_error(
+            'namespace:title',
+            "Namespace function specified in first subpath element")
+
     def testInvalidNamespaceName(self):
-        from zope.tales.tales import CompilerError
-        try:
-            self.engine.compile('adapterTest/1foo:bar')
-        except CompilerError as e:
-            self.assertEqual(e.args[0],
-                             'Invalid namespace name "1foo"')
-        else:
-            self.fail('Engine accepted invalid namespace name')
+        self._check_raises_compiler_error(
+            'adapterTest/1foo:bar',
+            'Invalid namespace name "1foo"')
 
     def testBadFunction(self):
         # namespace is fine, adapter is not defined
-        try:
-            expr = self.engine.compile('adapterTest/namespace:title')
+        expr = self.engine.compile('adapterTest/namespace:title')
+        with self.assertRaises(KeyError) as exc:
             expr(self.context)
-        except KeyError as e:
-            self.assertEqual(e.args[0],'title')
-        else:
-            self.fail('Engine accepted unknown function')
+
+        e = exc.exception
+        self.assertEqual(e.args[0], 'title')
 
     ## runtime tests
 
@@ -382,14 +472,36 @@ class FunctionTests(ExpressionTestBase):
 
     def testPathOnFunction(self):
         expr = self.engine.compile('adapterTest/namespace:jump/y/z')
-        context = self.context
-        self.assertEqual(expr(context), context.vars['x'].y.z)
+        self._check_evals_to(expr, self.context.vars['x'].y.z)
+
+    def test_path_through_non_callable_nampspace(self):
+        expr = self.engine.compile('adapterTest/not_callable_ns:nope')
+        with self.assertRaisesRegexp(ValueError,
+                                     'None'):
+            expr(self.context)
+
+class TestSimpleModuleImporter(unittest.TestCase):
+
+    def _makeOne(self):
+        from zope.tales.expressions import SimpleModuleImporter
+        return SimpleModuleImporter()
+
+    def test_simple(self):
+        imp = self._makeOne()
+        import os
+        import os.path
+        self.assertIs(os, imp['os'])
+        self.assertIs(os.path, imp['os.path'])
+
+    def test_no_such_toplevel_possible(self):
+        with self.assertRaises(ImportError):
+            self._makeOne()['this cannot exist']
+
 
-def test_suite():
-    return unittest.TestSuite((
-        unittest.makeSuite(ExpressionTests),
-        unittest.makeSuite(FunctionTests),
-                        ))
+    def test_no_such_submodule_not_package(self):
+        with self.assertRaises(ImportError):
+            self._makeOne()['zope.tales.tests.test_expressions.submodule']
 
-if __name__ == '__main__':
-    unittest.TextTestRunner().run(test_suite())
+    def test_no_such_submodule_package(self):
+        with self.assertRaises(ImportError):
+            self._makeOne()['zope.tales.tests.submodule']
diff --git a/src/zope/tales/tests/test_pythonexpr.py b/src/zope/tales/tests/test_pythonexpr.py
new file mode 100644
index 0000000..7410c41
--- /dev/null
+++ b/src/zope/tales/tests/test_pythonexpr.py
@@ -0,0 +1,50 @@
+##############################################################################
+#
+# Copyright (c) 2017 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.
+#
+##############################################################################
+
+import unittest
+
+from zope.tales.engine import Engine
+from zope.tales.tales import Context
+
+from zope.tales.pythonexpr import PythonExpr
+from zope.tales.pythonexpr import ExprTypeProxy
+
+class TestPythonExpr(unittest.TestCase):
+
+    def setUp(self):
+        self.context = Context(Engine, {})
+        self.engine = Engine
+
+    def test_repr_str(self):
+        expr = PythonExpr(None, 'a', None)
+        self.assertEqual('Python expression "(a)"', str(expr))
+        self.assertEqual('<PythonExpr (a)>', repr(expr))
+
+    def test_bind_not_dict(self):
+        expr = PythonExpr(None, 'test_bind_not_dict', None)
+        names = expr._bind_used_names(self.context, type(self))
+
+        # It found it in type(self).__dict__, so it only added
+        # __builtins__ to the names
+        self.assertEqual(names,
+                         {'__builtins__': type(self).__dict__})
+
+    def test_bind_as_expression(self):
+        expr = PythonExpr(None, 'string("abc")', None)
+        names = expr._bind_used_names(self.context, {})
+
+        string = names['string']
+        self.assertIsInstance(string, ExprTypeProxy)
+
+        self.assertEqual(expr(self.context), "abc")
diff --git a/src/zope/tales/tests/test_tales.py b/src/zope/tales/tests/test_tales.py
index 0bbf4ff..356413c 100644
--- a/src/zope/tales/tests/test_tales.py
+++ b/src/zope/tales/tests/test_tales.py
@@ -13,17 +13,17 @@
 ##############################################################################
 """TALES Tests
 """
+from doctest import DocTestSuite
 import unittest
 import re
 
 import six
 from zope.tales import tales
 from zope.tales.tests.simpleexpr import SimpleExpr
-from doctest import DocTestSuite
 from zope.testing import renormalizing
 
 
-class TALESTests(unittest.TestCase):
+class TestIterator(unittest.TestCase):
 
     def testIterator0(self):
         # Test sample Iterator class
@@ -51,6 +51,8 @@ class TALESTests(unittest.TestCase):
         self.assertTrue(not next(it), "Multi-element iterator")
         context._complete_()
 
+class TALESTests(unittest.TestCase):
+
     def testRegisterType(self):
         # Test expression type registration
         e = tales.ExpressionEngine()
@@ -110,6 +112,17 @@ class TALESTests(unittest.TestCase):
         self.assertTrue(isinstance(se, six.text_type))
         self.assertEqual(se, "('simple', 'x')")
 
+    def test_context_createErrorInfo(self):
+        ei = self.getContext().createErrorInfo(self, (0, 0))
+        self.assertEqual(ei.type, self)
+        self.assertEqual(ei.value, None)
+
+        e = Exception()
+        ei = self.getContext().createErrorInfo(e, (0, 0))
+        self.assertEqual(ei.type, Exception)
+        self.assertEqual(ei.value, e)
+
+
     def testVariables(self):
         # Test variables
         ctxt = self.getContext()
@@ -137,6 +150,145 @@ class TALESTests(unittest.TestCase):
 
         ctxt.endScope()
 
+class TestExpressionEngine(unittest.TestCase):
+
+    def setUp(self):
+        self.engine = tales.ExpressionEngine()
+
+    def test_register_invalid_name(self):
+        with self.assertRaisesRegexp(tales.RegistrationError,
+                                     "Invalid base name"):
+            self.engine.registerBaseName('123', None)
+
+    def test_register_duplicate_name(self):
+        self.engine.registerBaseName('abc', 123)
+        with self.assertRaisesRegexp(tales.RegistrationError,
+                                     "Multiple registrations"):
+            self.engine.registerBaseName('abc', None)
+
+        self.assertEqual({'abc': 123}, self.engine.getBaseNames())
+
+    def test_getContext(self):
+        contexts = {}
+        ctx = self.engine.getContext(contexts)
+        self.assertIs(ctx.contexts, contexts)
+
+        ctx = self.engine.getContext(b=2, c=3)
+        self.assertEqual(ctx.contexts['b'], 2)
+        self.assertEqual(ctx.contexts['c'], 3)
+
+        contexts = {'a': 1, 'c': 1}
+        ctx = self.engine.getContext(contexts, b=2, c=3)
+        self.assertEqual(ctx.contexts['a'], 1)
+        self.assertEqual(ctx.contexts['b'], 2)
+        self.assertEqual(ctx.contexts['c'], 1)
+
+class TestContext(unittest.TestCase):
+
+    def setUp(self):
+        from zope.tales.engine import Engine
+        self.engine = Engine
+        self.context = tales.Context(self.engine, {})
+
+    def test_setRepeat_false(self):
+        self.context.vars['it'] = ()
+        self.context.beginScope()
+        self.context.setRepeat('name', 'it')
+        self.assertNotIn('name', self.context.repeat_vars)
+
+    def test_endScope_with_repeat_active(self):
+        self.context.vars['it'] = [1, 2, 3]
+        self.context.vars['it2'] = [1, 2, 3]
+        self.context.beginScope()
+        self.context.setRepeat('name', 'it')
+        # shadow it
+        self.context.setRepeat('name', 'it2')
+        self.assertIn('name', self.context.repeat_vars)
+        self.context.endScope()
+        self.assertNotIn('name', self.context.repeat_vars)
+
+    def test_getValue_simple(self):
+        self.context.vars['it'] = 1
+        self.assertEqual(self.context.getValue('it'), 1)
+        self.assertEqual(self.context.getValue('missing', default=self), self)
+
+    def test_getValue_nested(self):
+        self.context.vars['it'] = 1
+        self.context.beginScope()
+        self.context.vars['it'] = 2
+        self.assertEqual(self.context.getValue('it'), 1)
+
+    def test_evaluate_boolean(self):
+        # Make sure it always returns a regular bool, no matter
+        # what the class returns
+        class WithCustomBool(object):
+
+            def __init__(self, value):
+                self.value = value
+
+            def __bool__(self):
+                return self.value
+
+            __nonzero__ = __bool__
+
+        # On Python 2, you can return only bool or int from __nonzero__
+        # Python 3 requires just a bool from __bool__. This is true whether
+        # you pass it to the bool builtin on the not operator
+        # On both 2 and 3, you cannot subclass bool()
+        bool_value = 1 if str is bytes else True
+        self.context.vars['it'] = WithCustomBool(bool_value)
+        self.assertEqual(bool_value, self.context.evaluate('it').__bool__())
+        self.assertIs(True, self.context.evaluateBoolean('it'))
+
+    def test_evaluateText_none(self):
+        self.context.vars['it'] = None
+        self.assertIsNone(self.context.evaluateText('it'))
+
+    def test_evaluateText_text(self):
+        self.context.vars['it'] = u'text'
+        self.assertEqual(u'text', self.context.evaluateText("it"))
+
+    def test_traceback_supplement(self):
+        import sys
+        def raises(self):
+            raise Exception()
+
+        self.context.contexts['modules'] = 1
+        self.context.setSourceFile("source")
+        self.context.setPosition((0, 1))
+
+        try:
+            self.context.evaluate(raises)
+        except Exception:
+            tb = sys.exc_info()[2]
+
+        try:
+            supp = tb.tb_next.tb_frame.f_locals['__traceback_supplement__']
+        finally:
+            del tb
+
+        supp = supp[0](supp[1], supp[2])
+        self.assertIs(supp.context, self.context)
+        self.assertEqual(supp.source_url, self.context.source_file)
+        self.assertEqual(supp.line, 0)
+        self.assertEqual(supp.column, 1)
+        self.assertEqual(supp.expression, repr(raises))
+
+        info = supp.getInfo()
+        # We stripped this info
+        self.assertNotIn('modules', info)
+        self.assertIn(' - Names', info)
+
+        html_info = supp.getInfo(as_html=True)
+        self.assertIn('<b>Names', html_info)
+
+        # And we didn't change the data in the context
+        self.assertIn('modules', self.context.contexts)
+
+    def test_translate(self):
+        import six
+        self.assertIsInstance(self.context.translate(b'abc'), six.text_type)
+
 
 class Harness(object):
     def __init__(self, testcase):
@@ -148,7 +300,7 @@ class Harness(object):
 
     def _complete_(self):
         self._testcase.assertEqual(len(self._callstack), 0,
-                               "Harness methods called")
+                                   "Harness methods called")
 
     def __getattr__(self, name):
         return HarnessMethod(self, name)
@@ -167,28 +319,26 @@ class HarnessMethod(object):
         self._testcase.assertTrue(
             len(cs),
             'Unexpected harness method call "%s".' % name
-            )
+        )
         self._testcase.assertEqual(
-            cs[0][0], name, 
+            cs[0][0], name,
             'Harness method name "%s" called, "%s" expected.' %
             (name, cs[0][0])
-            )
-        
+        )
+
         name, aargs, akwargs = self._callstack.pop(0)
         self._testcase.assertEqual(aargs, args, "Harness method arguments")
         self._testcase.assertEqual(akwargs, kwargs,
-                                "Harness method keyword args")
+                                   "Harness method keyword args")
 
 
 def test_suite():
-    checker = renormalizing.RENormalizing([
-  	(re.compile(r"object of type 'MyIter' has no len\(\)"),
-  	            r"len() of unsized object"),
-  	 ])
-    suite = unittest.makeSuite(TALESTests)
-    suite.addTest(DocTestSuite("zope.tales.tales",checker=checker))
+    checker = renormalizing.RENormalizing(
+        [(re.compile(r"object of type 'MyIter' has no len\(\)"),
+          r"len() of unsized object"),
+        ]
+    )
+    suite = unittest.defaultTestLoader.loadTestsFromName(__name__)
+    suite.addTest(DocTestSuite("zope.tales.tales",
+                               checker=checker))
     return suite
-
-
-if __name__ == '__main__':
-    unittest.TextTestRunner().run(test_suite())

pip install -e .[test]
parse_log_pytest
pytest
zope.testrunner pytest
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
six==1.17.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work
zope.exceptions==4.6
zope.interface==5.5.2
-e git+https://github.com/zopefoundation/zope.tales.git@b5f635089bee4ef7b14bb97f090f2cf4763cc9da#egg=zope.tales
zope.testing==5.0.1
zope.testrunner==5.6

name: zope.tales
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - six==1.17.0
      - zope-exceptions==4.6
      - zope-interface==5.5.2
      - zope-testing==5.0.1
      - zope-testrunner==5.6
prefix: /opt/conda/envs/zope.tales

src/zope/tales/tests/test_expressions.py::FunctionTests::test_bad_initial_namespace_subpath
src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testBadInitalDynamic src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testDynamic src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testEmptyPathSegmentRaisesCompilerError src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testHybridPathExpressions src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testLongPath src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testOldStyleClassIsCalled src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testOrPath src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testPath src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testPython src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testPythonCallableIsntCalled src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testPythonDosNewline src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testPythonErrorRaisesCompilerError src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testPythonNewline src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testSimple src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testString src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testString8Bits src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testStringFailureWhenMixed src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testStringSub src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testStringSubComplex src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testStringSubComplex_w_miss_and_python src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testStringSub_w_python src/zope/tales/tests/test_expressions.py::TestParsedExpressions::testStringUnicode src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_bad_initial_name_subexpr src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_defer_expression_returns_wrapper src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_dynamic_invalid_variable_name src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_eval_defer_wrapper src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_lazy_expression_returns_wrapper src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_not src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_path_CONTEXTS src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_path_exists src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_path_nocall_call src/zope/tales/tests/test_expressions.py::TestParsedExpressions::test_string_escape_percent src/zope/tales/tests/test_expressions.py::FunctionTests::testBadFunction src/zope/tales/tests/test_expressions.py::FunctionTests::testBadInitialNamespace src/zope/tales/tests/test_expressions.py::FunctionTests::testBadNamespace src/zope/tales/tests/test_expressions.py::FunctionTests::testFunctionOnFunction src/zope/tales/tests/test_expressions.py::FunctionTests::testGetFunctionNamespace src/zope/tales/tests/test_expressions.py::FunctionTests::testGetFunctionNamespaceBadNamespace src/zope/tales/tests/test_expressions.py::FunctionTests::testInvalidNamespaceName src/zope/tales/tests/test_expressions.py::FunctionTests::testNormalFunction src/zope/tales/tests/test_expressions.py::FunctionTests::testPathOnFunction src/zope/tales/tests/test_expressions.py::FunctionTests::testSetEngine src/zope/tales/tests/test_expressions.py::FunctionTests::test_path_through_non_callable_nampspace src/zope/tales/tests/test_expressions.py::TestSimpleModuleImporter::test_no_such_submodule_not_package src/zope/tales/tests/test_expressions.py::TestSimpleModuleImporter::test_no_such_submodule_package src/zope/tales/tests/test_expressions.py::TestSimpleModuleImporter::test_no_such_toplevel_possible src/zope/tales/tests/test_expressions.py::TestSimpleModuleImporter::test_simple src/zope/tales/tests/test_pythonexpr.py::TestPythonExpr::test_bind_as_expression src/zope/tales/tests/test_pythonexpr.py::TestPythonExpr::test_bind_not_dict src/zope/tales/tests/test_pythonexpr.py::TestPythonExpr::test_repr_str src/zope/tales/tests/test_tales.py::TestIterator::testIterator0 src/zope/tales/tests/test_tales.py::TestIterator::testIterator1 src/zope/tales/tests/test_tales.py::TestIterator::testIterator2 src/zope/tales/tests/test_tales.py::TALESTests::testCompile src/zope/tales/tests/test_tales.py::TALESTests::testContext_evaluate src/zope/tales/tests/test_tales.py::TALESTests::testContext_evaluateText src/zope/tales/tests/test_tales.py::TALESTests::testGetContext src/zope/tales/tests/test_tales.py::TALESTests::testRegisterType src/zope/tales/tests/test_tales.py::TALESTests::testRegisterTypeNameConstraints src/zope/tales/tests/test_tales.py::TALESTests::testRegisterTypeUnique src/zope/tales/tests/test_tales.py::TALESTests::testVariables src/zope/tales/tests/test_tales.py::TALESTests::test_context_createErrorInfo src/zope/tales/tests/test_tales.py::TestExpressionEngine::test_getContext src/zope/tales/tests/test_tales.py::TestExpressionEngine::test_register_duplicate_name src/zope/tales/tests/test_tales.py::TestExpressionEngine::test_register_invalid_name src/zope/tales/tests/test_tales.py::TestContext::test_endScope_with_repeat_active src/zope/tales/tests/test_tales.py::TestContext::test_evaluateText_none src/zope/tales/tests/test_tales.py::TestContext::test_evaluateText_text src/zope/tales/tests/test_tales.py::TestContext::test_evaluate_boolean src/zope/tales/tests/test_tales.py::TestContext::test_getValue_nested src/zope/tales/tests/test_tales.py::TestContext::test_getValue_simple src/zope/tales/tests/test_tales.py::TestContext::test_setRepeat_false src/zope/tales/tests/test_tales.py::TestContext::test_traceback_supplement src/zope/tales/tests/test_tales.py::TestContext::test_translate src/zope/tales/tests/test_tales.py::test_suite
src/zope/tales/tales.py .travis.yml tox.ini .coveragerc CHANGES.rst src/zope/tales/expressions.py
src/zope/tales/tales.py .travis.yml tox.ini .coveragerc CHANGES.rst src/zope/tales/expressions.py