changeset 718:d143dd73789b experimental-match-fastpaths

update to trunk through r833
author aflett
date Tue, 08 Apr 2008 23:45:32 +0000
parents 0e8b92905741
children 919809e55d16
files ChangeLog doc/upgrade.txt genshi/_speedups.c genshi/core.py genshi/filters/html.py genshi/filters/tests/__init__.py genshi/filters/tests/html.py genshi/filters/tests/i18n.py genshi/output.py genshi/template/base.py genshi/template/directives.py genshi/template/eval.py genshi/template/interpolation.py genshi/template/loader.py genshi/template/markup.py genshi/template/tests/directives.py genshi/template/tests/eval.py genshi/template/tests/interpolation.py genshi/template/tests/loader.py genshi/template/tests/markup.py genshi/template/tests/text.py genshi/template/text.py genshi/tests/core.py
diffstat 23 files changed, 311 insertions(+), 169 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -56,7 +56,7 @@
    including) the matching template itself are applied to the matched content,
    whereas the match templates declared after the matching template are only
    applied to the generated content (ticket #186).
- * The `TemplateLoader` class now provides an `instantiate()` method that can
+ * The `TemplateLoader` class now provides an `_instantiate()` method that can
    be overridden by subclasses to implement advanced template instantiation
    logic (ticket #204).
  * The search path of the `TemplateLoader` class can now contain ''load
@@ -71,6 +71,14 @@
    the `<py:match>` directive.
  * Improve error reporting when accessing an attribute in a Python expression
    raises an `AttributeError` (ticket #191).
+ * The `Markup` class now supports mappings for right hand of the `%` (modulo)
+   operator in the same way the Python string classes do, except that the
+   substituted values are escape. Also, the special constructor which took
+   positional arguments that would be substituted was removed. Thus the
+   `Markup` class now supports the same arguments as that of its `unicode`
+   base class (ticket #211).
+ * The `Template` class and its subclasses, as well as the interpolation API,
+   now take an `filepath` parameter instead of `basedir` (ticket #207).
 
 
 Version 0.4.4
--- a/doc/upgrade.txt
+++ b/doc/upgrade.txt
@@ -1,3 +1,4 @@
+================
 Upgrading Genshi
 ================
 
@@ -7,19 +8,12 @@
 .. sectnum::
 
 
+------------------------------------
 Upgrading from Genshi 0.4.x to 0.5.x
 ------------------------------------
 
-Genshi 0.5 introduces a new, alternative syntax for text templates,
-which is more flexible and powerful compared to the old syntax. For
-backwards compatibility, this new syntax is not used by default,
-though it will be in a future version. It is recommended that you
-migrate to using this new syntax. To do so, simply rename any
-references in your code to ``TextTemplate`` to ``NewTextTemplate``. To
-explicitly use the old syntax, use ``OldTextTemplate`` instead, so
-that you can be sure you'll be using the same language when the
-default in Genshi is changed (at least until the old implementation is
-completely removed).
+Error Handling
+--------------
 
 The default error handling mode has been changed to "strict". This
 means that accessing variables not defined in the template data will
@@ -30,6 +24,9 @@
 warned that lenient error handling may be removed completely in a
 future release.
 
+Match Template Processing
+-------------------------
+
 There has also been a subtle change to how ``py:match`` templates are
 processed: in previous versions, all match templates would be applied
 to the content generated by the matching template, and only the
@@ -42,7 +39,48 @@
 have any effect on most applications, but you may want to check your
 use of match templates to make sure.
 
+Text Templates
+--------------
 
+Genshi 0.5 introduces a new, alternative syntax for text templates,
+which is more flexible and powerful compared to the old syntax. For
+backwards compatibility, this new syntax is not used by default,
+though it will be in a future version. It is recommended that you
+migrate to using this new syntax. To do so, simply rename any
+references in your code to ``TextTemplate`` to ``NewTextTemplate``. To
+explicitly use the old syntax, use ``OldTextTemplate`` instead, so
+that you can be sure you'll be using the same language when the
+default in Genshi is changed (at least until the old implementation is
+completely removed).
+
+``Markup`` Constructor
+----------------------
+
+The ``Markup`` class now longer has a specialized constructor. The old
+(undocumented) constructor provided a shorthand for doing positional
+substitutions. If you have code like this:
+
+.. code-block:: python
+
+  Markup('<b>%s</b>', name)
+
+You can simply replace it by the more explicit:
+
+.. code-block:: python
+
+  Markup('<b>%s</b>') % name
+
+``Template`` Constructor
+------------------------
+
+The constructor of the ``Template`` class and its subclasses has changed
+slightly: instead of the optional ``basedir`` parameter, it now expects
+an (also optional) ``filepath`` parameter, which specifies the absolute
+path to the template. You probably aren't using those constructors
+directly, anyway, but using the ``TemplateLoader`` API instead.
+
+
+------------------------------------
 Upgrading from Genshi 0.3.x to 0.4.x
 ------------------------------------
 
@@ -66,6 +104,7 @@
 information.
 
 
+---------------------
 Upgrading from Markup
 ---------------------
 
--- a/genshi/_speedups.c
+++ b/genshi/_speedups.c
@@ -144,46 +144,6 @@
     return ret;
 }
 
-static PyObject *
-Markup_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
-{
-    PyObject *self, *text, *tmp, *args2;
-    int nargs, i;
-
-    nargs = PyTuple_GET_SIZE(args);
-    if (nargs < 2) {
-        return PyUnicode_Type.tp_new(type, args, NULL);
-    }
-
-    text = PyTuple_GET_ITEM(args, 0);
-    args2 = PyTuple_New(nargs - 1);
-    if (args2 == NULL) {
-        return NULL;
-    }
-    for (i = 1; i < nargs; i++) {
-        tmp = escape(PyTuple_GET_ITEM(args, i), 1);
-        if (tmp == NULL) {
-            Py_DECREF(args2);
-            return NULL;
-        }
-        PyTuple_SET_ITEM(args2, i - 1, tmp);
-    }
-    tmp = PyUnicode_Format(text, args2);
-    Py_DECREF(args2);
-    if (tmp == NULL) {
-        return NULL;
-    }
-    args = PyTuple_New(1);
-    if (args == NULL) {
-        Py_DECREF(tmp);
-        return NULL;
-    }
-    PyTuple_SET_ITEM(args, 0, tmp);
-    self = PyUnicode_Type.tp_new(type, args, NULL);
-    Py_DECREF(args);
-    return self;
-}
-
 PyDoc_STRVAR(escape__doc__,
 "Create a Markup instance from a string and escape special characters\n\
 it may contain (<, >, & and \").\n\
@@ -325,9 +285,38 @@
 Markup_mod(PyObject *self, PyObject *args)
 {
     PyObject *tmp, *tmp2, *ret, *args2;
-    int i, nargs;
+    int i, nargs = 0;
+    PyObject *kwds = NULL;
 
-    if (PyTuple_Check(args)) {
+    if (PyDict_Check(args)) {
+        kwds = args;
+    }
+    if (kwds && PyDict_Size(kwds)) {
+        PyObject *kwcopy, *key, *value;
+        Py_ssize_t pos = 0;
+
+        kwcopy = PyDict_Copy( kwds );
+        if (kwcopy == NULL) {
+            return NULL;
+        }
+        while (PyDict_Next(kwcopy, &pos, &key, &value)) {
+            tmp = escape(value, 1);
+            if (tmp == NULL) {
+                Py_DECREF(kwcopy);
+                return NULL;
+            }
+            if (PyDict_SetItem(kwcopy, key, tmp) < 0) {
+                Py_DECREF(tmp);
+                Py_DECREF(kwcopy);
+                return NULL;
+            }
+        }
+        tmp = PyUnicode_Format(self, kwcopy);
+        Py_DECREF(kwcopy);
+        if (tmp == NULL) {
+            return NULL;
+        }
+    } else if (PyTuple_Check(args)) {
         nargs = PyTuple_GET_SIZE(args);
         args2 = PyTuple_New(nargs);
         if (args2 == NULL) {
@@ -589,7 +578,7 @@
     
     0,          /*tp_init*/
     0,          /*tp_alloc  will be set to PyType_GenericAlloc in module init*/
-    Markup_new, /*tp_new*/
+    0,          /*tp_new*/
     0,          /*tp_free  Low-level free-memory routine */
     0,          /*tp_is_gc For PyObject_IS_GC */
     0,          /*tp_bases*/
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -419,11 +419,6 @@
     """
     __slots__ = []
 
-    def __new__(cls, text='', *args):
-        if args:
-            text %= tuple(map(escape, args))
-        return unicode.__new__(cls, text)
-
     def __add__(self, other):
         return Markup(unicode(self) + unicode(escape(other)))
 
@@ -431,9 +426,13 @@
         return Markup(unicode(escape(other)) + unicode(self))
 
     def __mod__(self, args):
-        if not isinstance(args, (list, tuple)):
-            args = [args]
-        return Markup(unicode.__mod__(self, tuple(map(escape, args))))
+        if isinstance(args, dict):
+            args = dict(zip(args.keys(), map(escape, args.values())))
+        elif isinstance(args, (list, tuple)):
+            args = tuple(map(escape, args))
+        else:
+            args = escape(args)
+        return Markup(unicode.__mod__(self, args))
 
     def __mul__(self, num):
         return Markup(unicode(self) * num)
--- a/genshi/filters/html.py
+++ b/genshi/filters/html.py
@@ -14,9 +14,10 @@
 """Implementation of a number of stream filters."""
 
 try:
-    frozenset
+    set
 except NameError:
     from sets import ImmutableSet as frozenset
+    from sets import Set as set
 import re
 
 from genshi.core import Attrs, QName, stripentities
--- a/genshi/filters/tests/__init__.py
+++ b/genshi/filters/tests/__init__.py
@@ -11,6 +11,7 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://genshi.edgewall.org/log/.
 
+import doctest
 import unittest
 
 def suite():
@@ -18,7 +19,8 @@
     suite = unittest.TestSuite()
     suite.addTest(html.suite())
     suite.addTest(i18n.suite())
-    suite.addTest(transform.suite())
+    if hasattr(doctest, 'NORMALIZE_WHITESPACE'):
+        suite.addTest(transform.suite())
     return suite
 
 if __name__ == '__main__':
--- a/genshi/filters/tests/html.py
+++ b/genshi/filters/tests/html.py
@@ -12,6 +12,10 @@
 # history and logs, available at http://genshi.edgewall.org/log/.
 
 import doctest
+try:
+    set
+except NameError:
+    from sets import Set as set
 import unittest
 
 from genshi.input import HTML, ParseError
--- a/genshi/filters/tests/i18n.py
+++ b/genshi/filters/tests/i18n.py
@@ -387,7 +387,7 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTests(doctest.DocTestSuite(Translator.__module__))
+    suite.addTest(doctest.DocTestSuite(Translator.__module__))
     suite.addTest(unittest.makeSuite(TranslatorTestCase, 'test'))
     suite.addTest(unittest.makeSuite(ExtractTestCase, 'test'))
     return suite
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -244,7 +244,7 @@
                 if sysid:
                     buf.append(' "%s"')
                 buf.append('>\n')
-                yield Markup(u''.join(buf), *filter(None, data))
+                yield Markup(u''.join(buf)) % filter(None, data)
                 have_doctype = True
 
             elif kind is START_CDATA:
@@ -343,7 +343,7 @@
                 if sysid:
                     buf.append(' "%s"')
                 buf.append('>\n')
-                yield Markup(u''.join(buf), *filter(None, data))
+                yield Markup(u''.join(buf)) % filter(None, data)
                 have_doctype = True
 
             elif kind is START_CDATA:
@@ -446,7 +446,7 @@
                 if sysid:
                     buf.append(' "%s"')
                 buf.append('>\n')
-                yield Markup(u''.join(buf), *filter(None, data))
+                yield Markup(u''.join(buf)) % filter(None, data)
                 have_doctype = True
 
             elif kind is PI:
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006-2007 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -338,19 +338,16 @@
     serializer = None
     _number_conv = unicode # function used to convert numbers to event data
 
-    def __init__(self, source, basedir=None, filename=None, loader=None,
+    def __init__(self, source, filepath=None, filename=None, loader=None,
                  encoding=None, lookup='strict', allow_exec=True):
         """Initialize a template from either a string, a file-like object, or
         an already parsed markup stream.
         
         :param source: a string, file-like object, or markup stream to read the
                        template from
-        :param basedir: the base directory containing the template file; when
-                        loaded from a `TemplateLoader`, this will be the
-                        directory on the template search path in which the
-                        template was found
-        :param filename: the name of the template file, relative to the given
-                         base directory
+        :param filepath: the absolute path to the template file
+        :param filename: the path to the template file relative to the search
+                         path
         :param loader: the `TemplateLoader` to use for loading included
                        templates
         :param encoding: the encoding of the `source`
@@ -361,19 +358,12 @@
         
         :note: Changed in 0.5: Added the `allow_exec` argument
         """
-        self.basedir = basedir
+        self.filepath = filepath or filename
         self.filename = filename
-        if basedir and filename:
-            self.filepath = os.path.join(basedir, filename)
-        else:
-            self.filepath = filename
         self.loader = loader
         self.lookup = lookup
         self.allow_exec = allow_exec
-
-        self.filters = [self._flatten, self._eval, self._exec]
-        if loader:
-            self.filters.append(self._include)
+        self._init_filters()
 
         if isinstance(source, basestring):
             source = StringIO(source)
@@ -384,9 +374,23 @@
         except ParseError, e:
             raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
 
+    def __getstate__(self):
+        state = self.__dict__.copy()
+        state['filters'] = []
+        return state
+
+    def __setstate__(self, state):
+        self.__dict__ = state
+        self._init_filters()
+
     def __repr__(self):
         return '<%s "%s">' % (self.__class__.__name__, self.filename)
 
+    def _init_filters(self):
+        self.filters = [self._flatten, self._eval, self._exec]
+        if self.loader:
+            self.filters.append(self._include)
+
     def _parse(self, source, encoding):
         """Parse the template.
         
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -24,8 +24,7 @@
 from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \
                                  EXPR, _apply_directives, _eval_expr, \
                                  _exec_suite
-from genshi.template.eval import Expression, Suite, ExpressionASTTransformer, \
-                                 _parse
+from genshi.template.eval import Expression, ExpressionASTTransformer, _parse
 from genshi.template.match import MatchSet
 
 __all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective',
@@ -705,27 +704,30 @@
       <span>42 7 52</span>
     </div>
     """
-    __slots__ = ['suite']
+    __slots__ = ['vars']
 
     def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
         Directive.__init__(self, None, template, namespaces, lineno, offset)
+        self.vars = [] 
+        value = value.strip() 
         try:
-            self.suite = Suite(value, template.filepath, lineno,
-                               lookup=template.lookup,
-                               xform=ExpressionASTTransformer)
+            ast = _parse(value, 'exec').node 
+            for node in ast.nodes: 
+                if isinstance(node, compiler.ast.Discard): 
+                    continue 
+                elif not isinstance(node, compiler.ast.Assign): 
+                    raise TemplateSyntaxError('only assignment allowed in ' 
+                                              'value of the "with" directive', 
+                                              template.filepath, lineno, offset) 
+                self.vars.append(([_assignment(n) for n in node.nodes], 
+                                  Expression(node.expr, template.filepath, 
+                                             lineno, lookup=template.lookup))) 
         except SyntaxError, err:
             err.msg += ' in expression "%s" of "%s" directive' % (value,
                                                                   self.tagname)
             raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
 
-        for node in self.suite.ast.node.nodes:
-            if not isinstance(node, (compiler.ast.Discard,
-                                     compiler.ast.Assign)):
-                raise TemplateSyntaxError('only assignment allowed in value of '
-                                          'the "with" directive',
-                                          template.filepath, lineno, offset)
-
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('vars')
@@ -734,8 +736,12 @@
     attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
-        ctxt.push({})
-        _exec_suite(self.suite, ctxt, **vars)
+        frame = {}
+        ctxt.push(frame)
+        for targets, expr in self.vars: 
+            value = _eval_expr(expr, ctxt, **vars)
+            for assign in targets:
+                assign(frame, value)
         for event in _apply_directives(stream, directives, ctxt, **vars):
             yield event
         ctxt.pop()
--- a/genshi/template/eval.py
+++ b/genshi/template/eval.py
@@ -75,6 +75,21 @@
             lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
         self._globals = lookup.globals
 
+    def __getstate__(self):
+        state = {'source': self.source, 'ast': self.ast,
+                 'lookup': self._globals.im_self}
+        c = self.code
+        state['code'] = (c.co_nlocals, c.co_stacksize, c.co_flags, c.co_code,
+                         c.co_consts, c.co_names, c.co_varnames, c.co_filename,
+                         c.co_name, c.co_firstlineno, c.co_lnotab, (), ())
+        return state
+
+    def __setstate__(self, state):
+        self.source = state['source']
+        self.ast = state['ast']
+        self.code = new.code(0, *state['code'])
+        self._globals = state['lookup'].globals
+
     def __eq__(self, other):
         return (type(other) == type(self)) and (self.code == other.code)
 
@@ -372,11 +387,12 @@
     source = source.strip()
     if mode == 'exec':
         lines = [line.expandtabs() for line in source.splitlines()]
-        first = lines[0]
-        rest = dedent('\n'.join(lines[1:])).rstrip()
-        if first.rstrip().endswith(':') and not rest[0].isspace():
-            rest = '\n'.join(['    %s' % line for line in rest.splitlines()])
-        source = '\n'.join([first, rest])
+        if lines:
+            first = lines[0]
+            rest = dedent('\n'.join(lines[1:])).rstrip()
+            if first.rstrip().endswith(':') and not rest[0].isspace():
+                rest = '\n'.join(['    %s' % line for line in rest.splitlines()])
+            source = '\n'.join([first, rest])
     if isinstance(source, unicode):
         source = '\xef\xbb\xbf' + source.encode('utf-8')
     return parse(source, mode)
@@ -402,7 +418,10 @@
     else:
         gen = ModuleCodeGenerator(tree)
         lines = source.splitlines()
-        extract = lines[0]
+        if not lines:
+            extract = ''
+        else:
+            extract = lines[0]
         if len(lines) > 1:
             extract += ' ...'
         name = '<Suite %r>' % (extract)
--- a/genshi/template/interpolation.py
+++ b/genshi/template/interpolation.py
@@ -30,8 +30,7 @@
 NAMECHARS = NAMESTART + '.0123456789'
 PREFIX = '$'
 
-def interpolate(text, basedir=None, filename=None, lineno=-1, offset=0,
-                lookup='strict'):
+def interpolate(text, filepath=None, lineno=-1, offset=0, lookup='strict'):
     """Parse the given string and extract expressions.
     
     This function is a generator that yields `TEXT` events for literal strings,
@@ -45,9 +44,8 @@
     TEXT u'bar'
     
     :param text: the text to parse
-    :param basedir: base directory of the file in which the text was found
-                    (optional)
-    :param filename: basename of the file in which the text was found (optional)
+    :param filepath: absolute path to the file in which the text was found
+                     (optional)
     :param lineno: the line number at which the text was found (optional)
     :param offset: the column number at which the text starts in the source
                    (optional)
@@ -57,9 +55,6 @@
     :raise TemplateSyntaxError: when a syntax error in an expression is
                                 encountered
     """
-    filepath = filename
-    if filepath and basedir:
-        filepath = os.path.join(basedir, filepath)
     pos = [filepath, lineno, offset]
 
     textbuf = []
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -127,7 +127,7 @@
             raise TypeError('The "callback" parameter needs to be callable')
         self.callback = callback
         self._cache = LRUCache(max_cache_size)
-        self._mtime = {}
+        self._uptodate = {}
         self._lock = threading.RLock()
 
     def load(self, filename, relative_to=None, cls=None, encoding=None):
@@ -175,8 +175,8 @@
                 tmpl = self._cache[cachekey]
                 if not self.auto_reload:
                     return tmpl
-                mtime = self._mtime[cachekey]
-                if mtime and mtime == os.path.getmtime(tmpl.filepath):
+                uptodate = self._uptodate[cachekey]
+                if uptodate is not None and uptodate():
                     return tmpl
             except KeyError, OSError:
                 pass
@@ -205,7 +205,7 @@
                 if isinstance(loadfunc, basestring):
                     loadfunc = directory(loadfunc)
                 try:
-                    dirname, filename, fileobj, mtime = loadfunc(filename)
+                    filepath, filename, fileobj, uptodate = loadfunc(filename)
                 except IOError:
                     continue
                 else:
@@ -216,14 +216,13 @@
                             # included template gets an absolute path, too,
                             # so that nested includes work properly without a
                             # search path
-                            filename = os.path.join(dirname, filename)
-                            dirname = ''
-                        tmpl = self.instantiate(cls, fileobj, dirname,
-                                                filename, encoding=encoding)
+                            filename = filepath
+                        tmpl = self._instantiate(cls, fileobj, filepath,
+                                                 filename, encoding=encoding)
                         if self.callback:
                             self.callback(tmpl)
                         self._cache[cachekey] = tmpl
-                        self._mtime[cachekey] = mtime
+                        self._uptodate[cachekey] = uptodate
                     finally:
                         if hasattr(fileobj, 'close'):
                             fileobj.close()
@@ -234,7 +233,7 @@
         finally:
             self._lock.release()
 
-    def instantiate(self, cls, fileobj, dirname, filename, encoding=None):
+    def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
         """Instantiate and return the `Template` object based on the given
         class and parameters.
         
@@ -245,10 +244,9 @@
         :param cls: the class of the template object to instantiate
         :param fileobj: a readable file-like object containing the template
                         source
-        :param dirname: the name of the base directory containing the template
-                        file
-        :param filename: the name of the template file, relative to the given
-                         base directory
+        :param filepath: the absolute path to the template file
+        :param filename: the path to the template file relative to the search
+                         path
         :param encoding: the encoding of the template to load; defaults to the
                          ``default_encoding`` of the loader instance
         :return: the loaded `Template` instance
@@ -256,7 +254,7 @@
         """
         if encoding is None:
             encoding = self.default_encoding
-        return cls(fileobj, basedir=dirname, filename=filename, loader=self,
+        return cls(fileobj, filepath=filepath, filename=filename, loader=self,
                    encoding=encoding, lookup=self.variable_lookup,
                    allow_exec=self.allow_exec)
 
@@ -270,7 +268,10 @@
         def _load_from_directory(filename):
             filepath = os.path.join(path, filename)
             fileobj = open(filepath, 'U')
-            return path, filename, fileobj, os.path.getmtime(filepath)
+            mtime = os.path.getmtime(filepath)
+            def _uptodate():
+                return mtime == os.path.getmtime(filepath)
+            return filepath, filename, fileobj, _uptodate
         return _load_from_directory
     directory = staticmethod(directory)
 
@@ -285,7 +286,7 @@
         from pkg_resources import resource_stream
         def _load_from_package(filename):
             filepath = os.path.join(path, filename)
-            return path, filename, resource_stream(name, filepath), None
+            return filepath, filename, resource_stream(name, filepath), None
         return _load_from_package
     package = staticmethod(package)
 
@@ -301,10 +302,10 @@
         ...     app2 = lambda filename: ('app2', filename, None, None)
         ... )
         >>> print load('app1/foo.html')
-        ('', 'app1/foo.html', None, None)
+        ('app1', 'app1/foo.html', None, None)
         >>> print load('app2/bar.html')
-        ('', 'app2/bar.html', None, None)
-
+        ('app2', 'app2/bar.html', None, None)
+        
         :param delegates: mapping of path prefixes to loader functions
         :return: the loader function
         :rtype: ``function``
@@ -314,11 +315,10 @@
                 if filename.startswith(prefix):
                     if isinstance(delegate, basestring):
                         delegate = directory(delegate)
-                    path, _, fileobj, mtime = delegate(
+                    filepath, _, fileobj, uptodate = delegate(
                         filename[len(prefix):].lstrip('/\\')
                     )
-                    dirname = path[len(prefix):].rstrip('/\\')
-                    return dirname, filename, fileobj, mtime
+                    return filepath, filename, fileobj, uptodate
             raise TemplateNotFound(filename, delegates.keys())
         return _dispatch_by_prefix
     prefixed = staticmethod(prefixed)
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -60,16 +60,13 @@
     serializer = 'xml'
     _number_conv = Markup
 
-    def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None, lookup='strict', allow_exec=True):
-        Template.__init__(self, source, basedir=basedir, filename=filename,
-                          loader=loader, encoding=encoding, lookup=lookup,
-                          allow_exec=allow_exec)
+    def _init_filters(self):
+        Template._init_filters(self)
         # Make sure the include filter comes after the match filter
-        if loader:
+        if self.loader:
             self.filters.remove(self._include)
         self.filters += [self._match]
-        if loader:
+        if self.loader:
             self.filters.append(self._include)
 
     def _parse(self, source, encoding):
@@ -127,8 +124,8 @@
                         directives.append((cls, value, ns_prefix.copy(), pos))
                     else:
                         if value:
-                            value = list(interpolate(value, self.basedir,
-                                                     pos[0], pos[1], pos[2],
+                            value = list(interpolate(value, self.filepath,
+                                                     pos[1], pos[2],
                                                      lookup=self.lookup))
                             if len(value) == 1 and value[0][0] is TEXT:
                                 value = value[0][1]
@@ -210,9 +207,8 @@
                 stream.append((EXEC, suite, pos))
 
             elif kind is TEXT:
-                for kind, data, pos in interpolate(data, self.basedir, pos[0],
-                                                   pos[1], pos[2],
-                                                   lookup=self.lookup):
+                for kind, data, pos in interpolate(data, self.filepath, pos[1],
+                                                   pos[2], lookup=self.lookup):
                     stream.append((kind, data, pos))
 
             elif kind is COMMENT:
--- a/genshi/template/tests/directives.py
+++ b/genshi/template/tests/directives.py
@@ -496,7 +496,24 @@
                              frames[-1].tb_frame.f_code.co_name)
             self.assertEqual('test.html',
                              frames[-1].tb_frame.f_code.co_filename)
-            self.assertEqual(2, frames[-1].tb_lineno)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(2, frames[-1].tb_lineno)
+
+    def test_for_with_empty_value(self):
+        """
+        Verify an empty 'for' value is an error
+        """
+        try:
+            MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:for each="">
+            empty
+          </py:for>
+        </doc>""", filename='test.html')
+            self.fail('ExpectedTemplateSyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] > (2,4):
+                self.assertEqual(2, e.lineno)
 
 
 class IfDirectiveTestCase(unittest.TestCase):
@@ -1103,6 +1120,16 @@
             一二三四五六日
           </span>
         </div>""", str(tmpl.generate()))
+        
+    def test_with_empty_value(self):
+        """
+        Verify that an empty py:with works (useless, but legal)
+        """
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <span py:with="">Text</span></div>""")
+
+        self.assertEqual("""<div>
+          <span>Text</span></div>""", str(tmpl.generate()))
 
 
 def suite():
--- a/genshi/template/tests/eval.py
+++ b/genshi/template/tests/eval.py
@@ -12,6 +12,8 @@
 # history and logs, available at http://genshi.edgewall.org/log/.
 
 import doctest
+import pickle
+from StringIO import StringIO
 import sys
 import unittest
 
@@ -32,6 +34,14 @@
         self.assertEqual(hash(expr), hash(Expression('x,y')))
         self.assertNotEqual(hash(expr), hash(Expression('y, x')))
 
+    def test_pickle(self):
+        expr = Expression('1 < 2')
+        buf = StringIO()
+        pickle.dump(expr, buf, 2)
+        buf.seek(0)
+        unpickled = pickle.load(buf)
+        assert unpickled.evaluate({}) is True
+
     def test_name_lookup(self):
         self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'}))
         self.assertEqual(id, Expression('id').evaluate({}))
@@ -443,6 +453,16 @@
 
 class SuiteTestCase(unittest.TestCase):
 
+    def test_pickle(self):
+        suite = Suite('foo = 42')
+        buf = StringIO()
+        pickle.dump(suite, buf, 2)
+        buf.seek(0)
+        unpickled = pickle.load(buf)
+        data = {}
+        unpickled.execute(data)
+        self.assertEqual(42, data['foo'])
+
     def test_internal_shadowing(self):
         # The context itself is stored in the global execution scope of a suite
         # It used to get stored under the name 'data', which meant the
--- a/genshi/template/tests/interpolation.py
+++ b/genshi/template/tests/interpolation.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2007 Edgewall Software
+# Copyright (C) 2007-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
--- a/genshi/template/tests/loader.py
+++ b/genshi/template/tests/loader.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -346,8 +346,8 @@
             file3.close()
 
         loader = TemplateLoader([dir1, TemplateLoader.prefixed(
-            sub1 = os.path.join(dir2),
-            sub2 = os.path.join(dir3)
+            sub1 = dir2,
+            sub2 = dir3
         )])
         tmpl = loader.load('sub1/tmpl1.html')
         self.assertEqual("""<html>
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006-2007 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -13,6 +13,7 @@
 
 import doctest
 import os
+import pickle
 import shutil
 from StringIO import StringIO
 import sys
@@ -39,6 +40,15 @@
         tmpl = MarkupTemplate(stream)
         self.assertEqual('<root> 42 42</root>', str(tmpl.generate(var=42)))
 
+    def test_pickle(self):
+        stream = XML('<root>$var</root>')
+        tmpl = MarkupTemplate(stream)
+        buf = StringIO()
+        pickle.dump(tmpl, buf, 2)
+        buf.seek(0)
+        unpickled = pickle.load(buf)
+        self.assertEqual('<root>42</root>', str(unpickled.generate(var=42)))
+
     def test_interpolate_mixed3(self):
         tmpl = MarkupTemplate('<root> ${var} $var</root>')
         self.assertEqual('<root> 42 42</root>', str(tmpl.generate(var=42)))
--- a/genshi/template/tests/text.py
+++ b/genshi/template/tests/text.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -130,11 +130,11 @@
     _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)'
     _ESCAPE_RE = r'\\\n|\\(\\)|\\(%s)|\\(%s)'
 
-    def __init__(self, source, basedir=None, filename=None, loader=None,
+    def __init__(self, source, filepath=None, filename=None, loader=None,
                  encoding=None, lookup='strict', allow_exec=False,
                  delims=('{%', '%}', '{#', '#}')):
         self.delimiters = delims
-        Template.__init__(self, source, basedir=basedir, filename=filename,
+        Template.__init__(self, source, filepath=filepath, filename=filename,
                           loader=loader, encoding=encoding, lookup=lookup)
 
     def _get_delims(self):
@@ -178,8 +178,7 @@
             start, end = mo.span(1)
             if start > offset:
                 text = _escape_sub(_escape_repl, source[offset:start])
-                for kind, data, pos in interpolate(text, self.basedir,
-                                                   self.filename, lineno,
+                for kind, data, pos in interpolate(text, self.filepath, lineno,
                                                    lookup=self.lookup):
                     stream.append((kind, data, pos))
                 lineno += len(text.splitlines())
@@ -189,8 +188,8 @@
 
             if command == 'include':
                 pos = (self.filename, lineno, 0)
-                value = list(interpolate(value, self.basedir, self.filename,
-                                         lineno, 0, lookup=self.lookup))
+                value = list(interpolate(value, self.filepath, lineno, 0,
+                                         lookup=self.lookup))
                 if len(value) == 1 and value[0][0] is TEXT:
                     value = value[0][1]
                 stream.append((INCLUDE, (value, None, []), pos))
@@ -228,8 +227,7 @@
 
         if offset < len(source):
             text = _escape_sub(_escape_repl, source[offset:])
-            for kind, data, pos in interpolate(text, self.basedir,
-                                               self.filename, lineno,
+            for kind, data, pos in interpolate(text, self.filepath, lineno,
                                                lookup=self.lookup):
                 stream.append((kind, data, pos))
 
@@ -290,8 +288,7 @@
             start, end = mo.span()
             if start > offset:
                 text = source[offset:start]
-                for kind, data, pos in interpolate(text, self.basedir,
-                                                   self.filename, lineno,
+                for kind, data, pos in interpolate(text, self.filepath, lineno,
                                                    lookup=self.lookup):
                     stream.append((kind, data, pos))
                 lineno += len(text.splitlines())
@@ -326,8 +323,7 @@
 
         if offset < len(source):
             text = source[offset:].replace('\\#', '#')
-            for kind, data, pos in interpolate(text, self.basedir,
-                                               self.filename, lineno,
+            for kind, data, pos in interpolate(text, self.filepath, lineno,
                                                lookup=self.lookup):
                 stream.append((kind, data, pos))
 
--- a/genshi/tests/core.py
+++ b/genshi/tests/core.py
@@ -21,7 +21,7 @@
 import unittest
 
 from genshi import core
-from genshi.core import Markup, Namespace, QName, escape, unescape
+from genshi.core import Markup, Attrs, Namespace, QName, escape, unescape
 from genshi.input import XML, ParseError
 
 
@@ -62,6 +62,10 @@
 
 class MarkupTestCase(unittest.TestCase):
 
+    def test_new_with_encoding(self):
+        markup = Markup('Döner', encoding='utf-8')
+        self.assertEquals("<Markup u'D\\xf6ner'>", repr(markup))
+
     def test_repr(self):
         markup = Markup('foo')
         self.assertEquals("<Markup u'foo'>", repr(markup))
@@ -107,6 +111,16 @@
         assert type(markup) is Markup
         self.assertEquals('<b>&amp;</b> boo', markup)
 
+    def test_mod_mapping(self):
+        markup = Markup('<b>%(foo)s</b>') % {'foo': '&'}
+        assert type(markup) is Markup
+        self.assertEquals('<b>&amp;</b>', markup)
+
+    def test_mod_noescape(self):
+        markup = Markup('<b>%(amp)s</b>') % {'amp': Markup('&amp;')}
+        assert type(markup) is Markup
+        self.assertEquals('<b>&amp;</b>', markup)
+
     def test_mul(self):
         markup = Markup('<b>foo</b>') * 2
         assert type(markup) is Markup
@@ -150,6 +164,18 @@
         self.assertEquals("<Markup u'foo'>", repr(pickle.load(buf)))
 
 
+class AttrsTestCase(unittest.TestCase):
+
+    def test_pickle(self):
+        attrs = Attrs([("attr1", "foo"), ("attr2", "bar")])
+        buf = StringIO()
+        pickle.dump(attrs, buf, 2)
+        buf.seek(0)
+        unpickled = pickle.load(buf)
+        self.assertEquals("Attrs([('attr1', 'foo'), ('attr2', 'bar')])",
+                          repr(unpickled))
+
+
 class NamespaceTestCase(unittest.TestCase):
 
     def test_pickle(self):
@@ -192,6 +218,7 @@
     suite.addTest(unittest.makeSuite(StreamTestCase, 'test'))
     suite.addTest(unittest.makeSuite(MarkupTestCase, 'test'))
     suite.addTest(unittest.makeSuite(NamespaceTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(AttrsTestCase, 'test'))
     suite.addTest(unittest.makeSuite(QNameTestCase, 'test'))
     suite.addTest(doctest.DocTestSuite(core))
     return suite
Copyright (C) 2012-2017 Edgewall Software