# HG changeset patch # User cmlenz # Date 1160656843 0 # Node ID c685294b11565b2b89a188a2e7c53a6102b8c623 # Parent 3aca57be8427c500f03b6a0e5458b3eea78e2dc3 Ported [338], [343] and [345:349/trunk] to 0.3.x stable branch. diff --git a/doc/xml-templates.txt b/doc/xml-templates.txt --- a/doc/xml-templates.txt +++ b/doc/xml-templates.txt @@ -565,12 +565,14 @@ By default, an error will be raised if an included file is not found. If that's not what you want, you can specify fallback content that should be used if the include fails. For example, to to make the include above fail silently, you'd -write: +write:: -See the XInclude_ for more about fallback content. Note though that Genshi -currently only supports a small subset of XInclude. +See the `XInclude specification`_ for more about fallback content. Note though +that Genshi currently only supports a small subset of XInclude. + +.. _`xinclude specification`: http://www.w3.org/TR/xinclude/ Incudes in Genshi are fully dynamic: Just like normal attributes, the `href` attribute accepts expressions_, and directives_ can be used on the diff --git a/genshi/core.py b/genshi/core.py --- a/genshi/core.py +++ b/genshi/core.py @@ -146,14 +146,14 @@ return output.encode(encoding, errors) return output - def select(self, path): + def select(self, path, namespaces=None, variables=None): """Return a new stream that contains the events matching the given XPath expression. @param path: a string containing the XPath expression """ from genshi.path import Path - return Path(path).select(self) + return Path(path).select(self, namespaces, variables) def serialize(self, method='xml', **kwargs): """Generate strings corresponding to a specific serialization of the @@ -509,6 +509,9 @@ def __contains__(self, qname): return qname.namespace == self.uri + def __ne__(self, other): + return not self == other + def __eq__(self, other): if isinstance(other, Namespace): return self.uri == other.uri diff --git a/genshi/output.py b/genshi/output.py --- a/genshi/output.py +++ b/genshi/output.py @@ -298,7 +298,10 @@


""" - _NOESCAPE_ELEMS = frozenset([QName('script'), QName('style')]) + _NOESCAPE_ELEMS = frozenset([QName('script'), + QName('http://www.w3.org/1999/xhtml}script'), + QName('style'), + QName('http://www.w3.org/1999/xhtml}style')]) def __init__(self, doctype=None, strip_whitespace=True): """Initialize the HTML serializer. diff --git a/genshi/path.py b/genshi/path.py --- a/genshi/path.py +++ b/genshi/path.py @@ -802,7 +802,7 @@ """ __slots__ = ['string1', 'string2'] def __init__(self, string1, string2): - self.string1 = string2 + self.string1 = string1 self.string2 = string2 def __call__(self, kind, data, pos, namespaces, variables): string1 = self.string1(kind, data, pos, namespaces, variables) diff --git a/genshi/template.py b/genshi/template.py --- a/genshi/template.py +++ b/genshi/template.py @@ -68,6 +68,19 @@ TemplateSyntaxError.__init__(self, message, filename, lineno) +class TemplateRuntimeError(TemplateError): + """Exception raised when an the evualation of a Python expression in a + template causes an error.""" + + def __init__(self, message, filename='', lineno=-1, offset=-1): + self.msg = message + message = '%s (%s, line %d)' % (self.msg, filename, lineno) + TemplateError.__init__(self, message) + self.filename = filename + self.lineno = lineno + self.offset = offset + + class TemplateNotFound(TemplateError): """Exception raised when a specific template file could not be found.""" @@ -161,7 +174,8 @@ """ __slots__ = ['expr'] - def __init__(self, value, filename=None, lineno=-1, offset=-1): + def __init__(self, value, namespaces=None, filename=None, lineno=-1, + offset=-1): try: self.expr = value and Expression(value, filename, lineno) or None except SyntaxError, err: @@ -340,8 +354,9 @@ ATTRIBUTE = 'function' - def __init__(self, args, filename=None, lineno=-1, offset=-1): - Directive.__init__(self, None, filename, lineno, offset) + def __init__(self, args, namespaces=None, filename=None, lineno=-1, + offset=-1): + Directive.__init__(self, None, namespaces, filename, lineno, offset) ast = compiler.parse(args, 'eval').node self.args = [] self.defaults = {} @@ -409,14 +424,16 @@ ATTRIBUTE = 'each' - def __init__(self, value, filename=None, lineno=-1, offset=-1): + def __init__(self, value, namespaces=None, filename=None, lineno=-1, + offset=-1): if ' in ' not in value: raise TemplateSyntaxError('"in" keyword missing in "for" directive', filename, lineno, offset) assign, value = value.split(' in ', 1) ast = compiler.parse(assign, 'exec') self.assign = _assignment(ast.node.nodes[0].expr) - Directive.__init__(self, value.strip(), filename, lineno, offset) + Directive.__init__(self, value.strip(), namespaces, filename, lineno, + offset) def __call__(self, stream, ctxt, directives): iterable = self.expr.evaluate(ctxt) @@ -426,12 +443,16 @@ assign = self.assign scope = {} stream = list(stream) - for item in iter(iterable): - assign(scope, item) - ctxt.push(scope) - for event in _apply_directives(stream, ctxt, directives): - yield event - ctxt.pop() + try: + iterator = iter(iterable) + for item in iterator: + assign(scope, item) + ctxt.push(scope) + for event in _apply_directives(stream, ctxt, directives): + yield event + ctxt.pop() + except TypeError, e: + raise TemplateRuntimeError(str(e), *stream[0][2]) def __repr__(self): return '<%s>' % self.__class__.__name__ @@ -475,17 +496,22 @@ """ - __slots__ = ['path'] + __slots__ = ['path', 'namespaces'] ATTRIBUTE = 'path' - def __init__(self, value, filename=None, lineno=-1, offset=-1): - Directive.__init__(self, None, filename, lineno, offset) + def __init__(self, value, namespaces=None, filename=None, lineno=-1, + offset=-1): + Directive.__init__(self, None, namespaces, filename, lineno, offset) self.path = Path(value, filename, lineno) + if namespaces is None: + namespaces = {} + self.namespaces = namespaces.copy() def __call__(self, stream, ctxt, directives): ctxt._match_templates.append((self.path.test(ignore_context=True), - self.path, list(stream), directives)) + self.path, list(stream), self.namespaces, + directives)) return [] def __repr__(self): @@ -638,14 +664,14 @@ def __call__(self, stream, ctxt, directives): matched, frame = ctxt._find('_choose.matched') if not frame: - raise TemplateSyntaxError('"when" directives can only be used ' - 'inside a "choose" directive', - *stream.next()[2]) + raise TemplateRuntimeError('"when" directives can only be used ' + 'inside a "choose" directive', + *stream.next()[2]) if matched: return [] if not self.expr: - raise TemplateSyntaxError('"when" directive has no test condition', - *stream.next()[2]) + raise TemplateRuntimeError('"when" directive has no test condition', + *stream.next()[2]) value = self.expr.evaluate(ctxt) if '_choose.value' in frame: matched = (value == frame['_choose.value']) @@ -667,9 +693,9 @@ def __call__(self, stream, ctxt, directives): matched, frame = ctxt._find('_choose.matched') if not frame: - raise TemplateSyntaxError('an "otherwise" directive can only be ' - 'used inside a "choose" directive', - *stream.next()[2]) + raise TemplateRuntimeError('an "otherwise" directive can only be ' + 'used inside a "choose" directive', + *stream.next()[2]) if matched: return [] frame['_choose.matched'] = True @@ -693,8 +719,9 @@ ATTRIBUTE = 'vars' - def __init__(self, value, filename=None, lineno=-1, offset=-1): - Directive.__init__(self, None, filename, lineno, offset) + def __init__(self, value, namespaces=None, filename=None, lineno=-1, + offset=-1): + Directive.__init__(self, None, namespaces, filename, lineno, offset) self.vars = [] value = value.strip() try: @@ -970,15 +997,13 @@ if kind is START_NS: # Strip out the namespace declaration for template directives prefix, uri = data - if uri == self.NAMESPACE: - ns_prefix[prefix] = uri - else: + ns_prefix[prefix] = uri + if uri != self.NAMESPACE: stream.append((kind, data, pos)) elif kind is END_NS: - if data in ns_prefix: - del ns_prefix[data] - else: + uri = ns_prefix.pop(data) + if uri != self.NAMESPACE: stream.append((kind, data, pos)) elif kind is START: @@ -992,7 +1017,7 @@ if cls is None: raise BadDirectiveError(tag.localname, pos[0], pos[1]) value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '') - directives.append(cls(value, *pos)) + directives.append(cls(value, ns_prefix, *pos)) strip = True new_attrib = [] @@ -1002,7 +1027,7 @@ if cls is None: raise BadDirectiveError(name.localname, pos[0], pos[1]) - directives.append(cls(value, *pos)) + directives.append(cls(value, ns_prefix, *pos)) else: if value: value = list(self._interpolate(value, *pos)) @@ -1054,7 +1079,6 @@ """ if match_templates is None: match_templates = ctxt._match_templates - nsprefix = {} # mapping of namespace prefixes to URIs tail = [] def _strip(stream): @@ -1079,15 +1103,15 @@ yield kind, data, pos continue - for idx, (test, path, template, directives) in \ + for idx, (test, path, template, namespaces, directives) in \ enumerate(match_templates): - if test(kind, data, pos, nsprefix, ctxt) is True: + if test(kind, data, pos, namespaces, ctxt) is True: # Let the remaining match templates know about the event so # they get a chance to update their internal state for test in [mt[0] for mt in match_templates[idx + 1:]]: - test(kind, data, pos, nsprefix, ctxt) + test(kind, data, pos, namespaces, ctxt) # Consume and store all events until an end event # corresponding to this start event is encountered @@ -1100,12 +1124,12 @@ kind, data, pos = tail[0] for test in [mt[0] for mt in match_templates]: - test(kind, data, pos, nsprefix, ctxt) + test(kind, data, pos, namespaces, ctxt) # Make the select() function available in the body of the # match template def select(path): - return Stream(content).select(path) + return Stream(content).select(path, namespaces, ctxt) ctxt.push(dict(select=select)) # Recursively process the output @@ -1195,7 +1219,7 @@ cls = self._dir_by_name.get(command) if cls is None: raise BadDirectiveError(command) - directive = cls(value, self.filepath, lineno, 0) + directive = cls(value, None, self.filepath, lineno, 0) dirmap[depth] = (directive, len(stream)) depth += 1 @@ -1282,7 +1306,7 @@ directly @param cls: the class of the template object to instantiate """ - if relative_to: + if relative_to and not os.path.isabs(relative_to): filename = os.path.join(os.path.dirname(relative_to), filename) filename = os.path.normpath(filename) @@ -1295,12 +1319,21 @@ except KeyError: pass - # Bypass the search path if the filename is absolute search_path = self.search_path + if os.path.isabs(filename): + # Bypass the search path if the requested filename is absolute search_path = [os.path.dirname(filename)] - if not search_path: + elif relative_to and os.path.isabs(relative_to): + # Make sure that the directory containing the including + # template is on the search path + dirname = os.path.dirname(relative_to) + if dirname not in search_path: + search_path = search_path[:] + [dirname] + + elif not search_path: + # Uh oh, don't know where to look for the template raise TemplateError('Search path for templates not configured') for dirname in search_path: @@ -1318,4 +1351,4 @@ except IOError: continue - raise TemplateNotFound(filename, self.search_path) + raise TemplateNotFound(filename, search_path) diff --git a/genshi/tests/output.py b/genshi/tests/output.py --- a/genshi/tests/output.py +++ b/genshi/tests/output.py @@ -110,6 +110,13 @@ output = XML(text).render(XHTMLSerializer) self.assertEqual(text, output) + def test_script_escaping_with_namespace(self): + text = """""" + output = XML(text).render(XHTMLSerializer) + self.assertEqual(text, output) + def test_style_escaping(self): text = """""" + output = XML(text).render(XHTMLSerializer) + self.assertEqual(text, output) + def test_embedded_svg(self): text = """ @@ -156,12 +170,30 @@ self.assertEqual('', output) + def test_script_escaping_with_namespace(self): + text = """""" + output = XML(text).render(HTMLSerializer) + self.assertEqual("""""", output) + def test_style_escaping(self): text = '' output = XML(text).render(HTMLSerializer) self.assertEqual('', output) + def test_style_escaping_with_namespace(self): + text = """""" + output = XML(text).render(HTMLSerializer) + self.assertEqual("""""", output) + class EmptyTagFilterTestCase(unittest.TestCase): diff --git a/genshi/tests/path.py b/genshi/tests/path.py --- a/genshi/tests/path.py +++ b/genshi/tests/path.py @@ -363,6 +363,8 @@ xml = XML('bar') path = Path('*[starts-with(name(), "f")]') self.assertEqual('bar', path.select(xml).render()) + path = Path('*[starts-with(name(), "b")]') + self.assertEqual('', path.select(xml).render()) def test_predicate_string_length_function(self): xml = XML('bar') diff --git a/genshi/tests/template.py b/genshi/tests/template.py --- a/genshi/tests/template.py +++ b/genshi/tests/template.py @@ -21,7 +21,8 @@ from genshi import template from genshi.core import Markup, Stream from genshi.template import BadDirectiveError, MarkupTemplate, Template, \ - TemplateLoader, TemplateSyntaxError, TextTemplate + TemplateLoader, TemplateRuntimeError, \ + TemplateSyntaxError, TextTemplate class AttrsDirectiveTestCase(unittest.TestCase): @@ -173,7 +174,7 @@ tmpl = MarkupTemplate("""
""") - self.assertRaises(TemplateSyntaxError, str, tmpl.generate()) + self.assertRaises(TemplateRuntimeError, str, tmpl.generate()) def test_otherwise_outside_choose(self): """ @@ -183,7 +184,7 @@ tmpl = MarkupTemplate("""
""") - self.assertRaises(TemplateSyntaxError, str, tmpl.generate()) + self.assertRaises(TemplateRuntimeError, str, tmpl.generate()) def test_when_without_test(self): """ @@ -195,7 +196,7 @@ foo
""") - self.assertRaises(TemplateSyntaxError, str, tmpl.generate()) + self.assertRaises(TemplateRuntimeError, str, tmpl.generate()) def test_otherwise_without_test(self): """ @@ -440,6 +441,22 @@

1: key=b, value=2

""", str(tmpl.generate(items=enumerate(dict(a=1, b=2).items())))) + def test_not_iterable(self): + """ + Verify that assignment to nested tuples works correctly. + """ + tmpl = MarkupTemplate(""" + + $item + + """, filename='test.html') + try: + list(tmpl.generate(foo=12)) + except TemplateRuntimeError, e: + self.assertEqual('test.html', e.filename) + if sys.version_info[:2] >= (2, 4): + self.assertEqual(2, e.lineno) + class IfDirectiveTestCase(unittest.TestCase): """Tests for the `py:if` template directive.""" @@ -690,6 +707,18 @@

""", str(tmpl.generate(fields=fields, values=values))) + def test_namespace_context(self): + tmpl = MarkupTemplate(""" +
Foo
+ + """) + # FIXME: there should be a way to strip out unwanted/unused namespaces, + # such as the "x" in this example + self.assertEqual(""" +
Foo
+ """, str(tmpl.generate())) + # FIXME #def test_match_after_step(self): # tmpl = MarkupTemplate("""
@@ -1135,6 +1164,22 @@
Included
""", tmpl.generate().render()) + def test_relative_include_from_inmemory_template(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
Included
""") + finally: + file1.close() + + loader = TemplateLoader([self.dirname]) + tmpl2 = MarkupTemplate(""" + + """, filename='subdir/tmpl2.html', loader=loader) + + self.assertEqual(""" +
Included
+ """, tmpl2.generate().render()) + def suite(): suite = unittest.TestSuite()