changeset 284:65a46e008098 stable-0.3.x

Ported [338], [343] and [345:349/trunk] to 0.3.x stable branch.
author cmlenz
date Thu, 12 Oct 2006 12:40:43 +0000
parents 186d18554183
children e22bce390fd8
files doc/xml-templates.txt genshi/core.py genshi/output.py genshi/path.py genshi/template.py genshi/tests/output.py genshi/tests/path.py genshi/tests/template.py
diffstat 8 files changed, 175 insertions(+), 55 deletions(-) [+]
line wrap: on
line diff
--- 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::
 
   <xi:include href="base.html"><xi:fallback /></xi:include>
 
-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
--- 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
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -298,7 +298,10 @@
     <div><a href="foo"></a><br><hr noshade></div>
     """
 
-    _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.
--- 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)
--- 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='<string>', 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 @@
       </span>
     </div>
     """
-    __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)
--- 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 = """<script xmlns="http://www.w3.org/1999/xhtml">/*<![CDATA[*/
+            if (1 < 2) { alert("Doh"); }
+        /*]]>*/</script>"""
+        output = XML(text).render(XHTMLSerializer)
+        self.assertEqual(text, output)
+
     def test_style_escaping(self):
         text = """<style>/*<![CDATA[*/
             html > body { display: none; }
@@ -117,6 +124,13 @@
         output = XML(text).render(XHTMLSerializer)
         self.assertEqual(text, output)
 
+    def test_style_escaping_with_namespace(self):
+        text = """<style xmlns="http://www.w3.org/1999/xhtml">/*<![CDATA[*/
+            html > body { display: none; }
+        /*]]>*/</style>"""
+        output = XML(text).render(XHTMLSerializer)
+        self.assertEqual(text, output)
+
     def test_embedded_svg(self):
         text = """<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg">
           <body>
@@ -156,12 +170,30 @@
         self.assertEqual('<script>if (1 < 2) { alert("Doh"); }</script>',
                          output)
 
+    def test_script_escaping_with_namespace(self):
+        text = """<script xmlns="http://www.w3.org/1999/xhtml">
+            if (1 &lt; 2) { alert("Doh"); }
+        </script>"""
+        output = XML(text).render(HTMLSerializer)
+        self.assertEqual("""<script>
+            if (1 < 2) { alert("Doh"); }
+        </script>""", output)
+
     def test_style_escaping(self):
         text = '<style>html &gt; body { display: none; }</style>'
         output = XML(text).render(HTMLSerializer)
         self.assertEqual('<style>html > body { display: none; }</style>',
                          output)
 
+    def test_style_escaping_with_namespace(self):
+        text = """<style xmlns="http://www.w3.org/1999/xhtml">
+            html &gt; body { display: none; }
+        </style>"""
+        output = XML(text).render(HTMLSerializer)
+        self.assertEqual("""<style>
+            html > body { display: none; }
+        </style>""", output)
+
 
 class EmptyTagFilterTestCase(unittest.TestCase):
 
--- a/genshi/tests/path.py
+++ b/genshi/tests/path.py
@@ -363,6 +363,8 @@
         xml = XML('<root><foo>bar</foo></root>')
         path = Path('*[starts-with(name(), "f")]')
         self.assertEqual('<foo>bar</foo>', 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('<root><foo>bar</foo></root>')
--- 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("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:when="xy" />
         </doc>""")
-        self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
+        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
 
     def test_otherwise_outside_choose(self):
         """
@@ -183,7 +184,7 @@
         tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:otherwise="" />
         </doc>""")
-        self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
+        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
 
     def test_when_without_test(self):
         """
@@ -195,7 +196,7 @@
             <py:when>foo</py:when>
           </div>
         </doc>""")
-        self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
+        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
 
     def test_otherwise_without_test(self):
         """
@@ -440,6 +441,22 @@
             <p>1: key=b, value=2</p>
         </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:for each="item in foo">
+            $item
+          </py:for>
+        </doc>""", 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 @@
           </p></form>
         </html>""", str(tmpl.generate(fields=fields, values=values)))
 
+    def test_namespace_context(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+                                       xmlns:x="http://www.example.org/">
+          <div py:match="x:foo">Foo</div>
+          <foo xmlns="http://www.example.org/"/>
+        </html>""")
+        # FIXME: there should be a way to strip out unwanted/unused namespaces,
+        #        such as the "x" in this example
+        self.assertEqual("""<html xmlns:x="http://www.example.org/">
+          <div>Foo</div>
+        </html>""", str(tmpl.generate()))
+
     # FIXME
     #def test_match_after_step(self):
     #    tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
@@ -1135,6 +1164,22 @@
               <div>Included</div>
             </html>""", tmpl.generate().render())
 
+    def test_relative_include_from_inmemory_template(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl2 = MarkupTemplate("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+          <xi:include href="../tmpl1.html" />
+        </html>""", filename='subdir/tmpl2.html', loader=loader)
+
+        self.assertEqual("""<html>
+          <div>Included</div>
+        </html>""", tmpl2.generate().render())
+
 
 def suite():
     suite = unittest.TestSuite()
Copyright (C) 2012-2017 Edgewall Software