# HG changeset patch # User aflett # Date 1207698332 0 # Node ID 109fb7dfe95e3b3b146468eb103b320b760f7346 # Parent 83d2fe0c70bcd935348232c47e5d0d7c223cf659 update to trunk through r833 diff --git a/ChangeLog b/ChangeLog --- 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 `` 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 diff --git a/doc/upgrade.txt b/doc/upgrade.txt --- 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('%s', name) + +You can simply replace it by the more explicit: + +.. code-block:: python + + Markup('%s') % 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 --------------------- diff --git a/genshi/_speedups.c b/genshi/_speedups.c --- 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*/ diff --git a/genshi/core.py b/genshi/core.py --- 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) diff --git a/genshi/filters/html.py b/genshi/filters/html.py --- 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 diff --git a/genshi/filters/tests/__init__.py b/genshi/filters/tests/__init__.py --- 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__': diff --git a/genshi/filters/tests/html.py b/genshi/filters/tests/html.py --- 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 diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py --- 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 diff --git a/genshi/output.py b/genshi/output.py --- 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: diff --git a/genshi/template/base.py b/genshi/template/base.py --- 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. diff --git a/genshi/template/directives.py b/genshi/template/directives.py --- 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 @@ 42 7 52 """ - __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() diff --git a/genshi/template/eval.py b/genshi/template/eval.py --- 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 = '' % (extract) diff --git a/genshi/template/interpolation.py b/genshi/template/interpolation.py --- 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 = [] diff --git a/genshi/template/loader.py b/genshi/template/loader.py --- 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) diff --git a/genshi/template/markup.py b/genshi/template/markup.py --- 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: diff --git a/genshi/template/tests/directives.py b/genshi/template/tests/directives.py --- 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(""" + + empty + + """, 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 @@ 一二三四五六日 """, str(tmpl.generate())) + + def test_with_empty_value(self): + """ + Verify that an empty py:with works (useless, but legal) + """ + tmpl = MarkupTemplate("""
+ Text
""") + + self.assertEqual("""
+ Text
""", str(tmpl.generate())) def suite(): diff --git a/genshi/template/tests/eval.py b/genshi/template/tests/eval.py --- 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 diff --git a/genshi/template/tests/interpolation.py b/genshi/template/tests/interpolation.py --- 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 diff --git a/genshi/template/tests/loader.py b/genshi/template/tests/loader.py --- 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(""" diff --git a/genshi/template/tests/markup.py b/genshi/template/tests/markup.py --- 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(' 42 42', str(tmpl.generate(var=42))) + def test_pickle(self): + stream = XML('$var') + tmpl = MarkupTemplate(stream) + buf = StringIO() + pickle.dump(tmpl, buf, 2) + buf.seek(0) + unpickled = pickle.load(buf) + self.assertEqual('42', str(unpickled.generate(var=42))) + def test_interpolate_mixed3(self): tmpl = MarkupTemplate(' ${var} $var') self.assertEqual(' 42 42', str(tmpl.generate(var=42))) diff --git a/genshi/template/tests/text.py b/genshi/template/tests/text.py --- 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 diff --git a/genshi/template/text.py b/genshi/template/text.py --- a/genshi/template/text.py +++ b/genshi/template/text.py @@ -130,11 +130,11 @@ _DIRECTIVE_RE = r'((? 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)) diff --git a/genshi/tests/core.py b/genshi/tests/core.py --- 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("", repr(markup)) + def test_repr(self): markup = Markup('foo') self.assertEquals("", repr(markup)) @@ -107,6 +111,16 @@ assert type(markup) is Markup self.assertEquals('& boo', markup) + def test_mod_mapping(self): + markup = Markup('%(foo)s') % {'foo': '&'} + assert type(markup) is Markup + self.assertEquals('&', markup) + + def test_mod_noescape(self): + markup = Markup('%(amp)s') % {'amp': Markup('&')} + assert type(markup) is Markup + self.assertEquals('&', markup) + def test_mul(self): markup = Markup('foo') * 2 assert type(markup) is Markup @@ -150,6 +164,18 @@ self.assertEquals("", 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