changeset 14:c7d33e0c9839 trunk

The `<py:match>` directive now protects itself against simple infinite recursion (see MatchDirective), while still allowing recursion in general.
author cmlenz
date Tue, 13 Jun 2006 17:56:42 +0000
parents f9001cd6785b
children b3edbde541c4
files markup/eval.py markup/filters.py markup/template.py
diffstat 3 files changed, 107 insertions(+), 75 deletions(-) [+]
line wrap: on
line diff
--- a/markup/eval.py
+++ b/markup/eval.py
@@ -104,13 +104,10 @@
         self.source = source
         self.ast = None
 
-    def evaluate(self, data, default=None):
+    def evaluate(self, data):
         if not self.ast:
             self.ast = compiler.parse(self.source, 'eval')
-        retval = self._visit(self.ast.node, data)
-        if retval is not None:
-            return retval
-        return default
+        return self._visit(self.ast.node, data)
 
     def __repr__(self):
         return '<Expression "%s">' % self.source
@@ -142,13 +139,12 @@
 
     def _visit_getattr(self, node, data):
         obj = self._visit(node.expr, data)
-        try:
+        if hasattr(obj, node.attrname):
             return getattr(obj, node.attrname)
-        except AttributeError, e:
-            try:
-                return obj[node.attrname]
-            except (KeyError, TypeError):
-                return None
+        elif node.attrname in obj:
+            return obj[node.attrname]
+        else:
+            return None
 
     def _visit_slice(self, node, data):
         obj = self._visit(node.expr, data)
--- a/markup/filters.py
+++ b/markup/filters.py
@@ -22,8 +22,7 @@
 from markup.core import Attributes, Markup, Stream
 from markup.path import Path
 
-__all__ = ['EvalFilter', 'IncludeFilter', 'MatchFilter', 'WhitespaceFilter',
-           'HTMLSanitizer']
+__all__ = ['EvalFilter', 'IncludeFilter', 'WhitespaceFilter', 'HTMLSanitizer']
 
 
 class EvalFilter(object):
@@ -141,7 +140,7 @@
                         # If the included template defines any filters added at
                         # runtime (such as py:match templates), those need to be
                         # applied to the including template, too.
-                        filters = template.filters + template._included_filters
+                        filters = template._included_filters + template.filters
                         for filter_ in filters:
                             stream = filter_(stream, ctxt)
 
@@ -183,40 +182,6 @@
             yield event
 
 
-class MatchFilter(object):
-    """A filter that delegates to a given handler function when the input stream
-    matches some path expression.
-    """
-
-    def __init__(self, path, handler):
-        self.path = Path(path)
-        self.handler = handler
-
-    def __call__(self, stream, ctxt=None):
-        from markup.template import Template
-
-        test = self.path.test()
-        for kind, data, pos in stream:
-            result = test(kind, data, pos)
-            if result is True:
-                content = [(kind, data, pos)]
-                depth = 1
-                while depth > 0:
-                    ev = stream.next()
-                    if ev[0] is Stream.START:
-                        depth += 1
-                    elif ev[0] is Stream.END:
-                        depth -= 1
-                    content.append(ev)
-                    test(*ev)
-
-                yield (Template.SUB,
-                       ([lambda stream, ctxt: self.handler(content, ctxt)], []),
-                       pos)
-            else:
-                yield kind, data, pos
-
-
 class WhitespaceFilter(object):
     """A filter that removes extraneous white space from the stream.
 
--- a/markup/template.py
+++ b/markup/template.py
@@ -49,9 +49,9 @@
 
 from markup.core import Attributes, Stream, StreamEventKind
 from markup.eval import Expression
-from markup.filters import EvalFilter, IncludeFilter, MatchFilter, \
-                           WhitespaceFilter
+from markup.filters import EvalFilter, IncludeFilter, WhitespaceFilter
 from markup.input import HTML, XMLParser, XML
+from markup.path import Path
 
 __all__ = ['Context', 'BadDirectiveError', 'TemplateError',
            'TemplateSyntaxError', 'TemplateNotFound', 'Template',
@@ -347,7 +347,7 @@
         Directive.__init__(self, template, expr_source, pos)
 
     def __call__(self, stream, ctxt):
-        iterable = self.expr.evaluate(ctxt, [])
+        iterable = self.expr.evaluate(ctxt) or []
         if iterable is not None:
             stream = list(stream)
             for item in iter(iterable):
@@ -386,37 +386,108 @@
 
 class MatchDirective(Directive):
     """Implementation of the `py:match` template directive.
-    
-    >>> ctxt = Context()
+
     >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
     ...   <span py:match="div/greeting">
     ...     Hello ${select('@name')}
     ...   </span>
     ...   <greeting name="Dude" />
     ... </div>''')
-    >>> print tmpl.generate(ctxt)
+    >>> print tmpl.generate()
     <div>
       <span>
         Hello Dude
       </span>
     </div>
+    
+    A match template can produce the same kind of element that it matched
+    without entering an infinite recursion:
+    
+    >>> tmpl = Template('''<doc xmlns:py="http://purl.org/kid/ns#">
+    ...   <elem py:match="elem">
+    ...     <div class="elem">${select('*/text()')}</div>
+    ...   </elem>
+    ...   <elem>Hey Joe</elem>
+    ... </doc>''')
+    >>> print tmpl.generate()
+    <doc>
+      <elem>
+        <div class="elem">Hey Joe</div>
+      </elem>
+    </doc>
+    
+    Match directives are applied recursively, meaning that they are also
+    applied to any content they may have produced themselves:
+    
+    >>> tmpl = Template('''<doc xmlns:py="http://purl.org/kid/ns#">
+    ...   <elem py:match="elem">
+    ...     <div class="elem">
+    ...       ${select('*/*')}
+    ...     </div>
+    ...   </elem>
+    ...   <elem>
+    ...     <subelem>
+    ...       <elem/>
+    ...     </subelem>
+    ...   </elem>
+    ... </doc>''')
+    >>> print tmpl.generate()
+    <doc>
+      <elem>
+        <div class="elem">
+          <subelem>
+          <elem>
+        <div class="elem">
+        </div>
+      </elem>
+        </subelem>
+        </div>
+      </elem>
+    </doc>
     """
     __slots__ = ['path', 'stream']
 
     def __init__(self, template, value, pos):
         Directive.__init__(self, template, None, pos)
-        template.filters.append(MatchFilter(value, self._handle_match))
-        self.path = value
+        self.path = Path(value)
         self.stream = []
+        template.filters.append(self._filter)
 
     def __call__(self, stream, ctxt):
         self.stream = list(stream)
         return []
 
     def __repr__(self):
-        return '<%s "%s">' % (self.__class__.__name__, self.path)
+        return '<%s "%s">' % (self.__class__.__name__, self.path.source)
 
-    def _handle_match(self, orig_stream, ctxt):
+    def _filter(self, stream, ctxt=None):
+        test = self.path.test()
+        for event in stream:
+            if self.stream and event in self.stream[::len(self.stream)]:
+                # This is the event this filter produced itself, so matching it
+                # again would result in an infinite loop
+                yield event
+                continue
+            result = test(*event)
+            if result is True:
+                content = [event]
+                depth = 1
+                while depth > 0:
+                    ev = stream.next()
+                    if ev[0] is Stream.START:
+                        depth += 1
+                    elif ev[0] is Stream.END:
+                        depth -= 1
+                    content.append(ev)
+                    test(*ev)
+
+                yield (Template.SUB,
+                       ([lambda stream, ctxt: self._apply(content, ctxt)], []),
+                       content[0][-1])
+            else:
+                yield event
+
+    def _apply(self, orig_stream, ctxt):
         ctxt.push(select=lambda path: Stream(orig_stream).select(path))
         for event in self.stream:
             yield event
@@ -458,11 +529,10 @@
     When the value of the `py:strip` attribute evaluates to `True`, the element
     is stripped from the output
     
-    >>> ctxt = Context()
     >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
     ...   <div py:strip="True"><b>foo</b></div>
     ... </div>''')
-    >>> print tmpl.generate(ctxt)
+    >>> print tmpl.generate()
     <div>
       <b>foo</b>
     </div>
@@ -470,22 +540,20 @@
     On the other hand, when the attribute evaluates to `False`, the element is
     not stripped:
     
-    >>> ctxt = Context()
     >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
     ...   <div py:strip="False"><b>foo</b></div>
     ... </div>''')
-    >>> print tmpl.generate(ctxt)
+    >>> print tmpl.generate()
     <div>
       <div><b>foo</b></div>
     </div>
     
     Leaving the attribute value empty is equivalent to a truth value:
     
-    >>> ctxt = Context()
     >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
     ...   <div py:strip=""><b>foo</b></div>
     ... </div>''')
-    >>> print tmpl.generate(ctxt)
+    >>> print tmpl.generate()
     <div>
       <b>foo</b>
     </div>
@@ -493,14 +561,13 @@
     This directive is particulary interesting for named template functions or
     match templates that do not generate a top-level element:
     
-    >>> ctxt = Context()
     >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
     ...   <div py:def="echo(what)" py:strip="">
     ...     <b>${what}</b>
     ...   </div>
     ...   ${echo('foo')}
     ... </div>''')
-    >>> print tmpl.generate(ctxt)
+    >>> print tmpl.generate()
     <div>
         <b>foo</b>
     </div>
@@ -549,9 +616,9 @@
             self.source = source
         self.filename = filename or '<string>'
 
-        self.pre_filters = [EvalFilter()]
+        self.input_filters = [EvalFilter()]
         self.filters = []
-        self.post_filters = [WhitespaceFilter()]
+        self.output_filters = [WhitespaceFilter()]
         self.parse()
 
     def __repr__(self):
@@ -632,12 +699,15 @@
 
         self.stream = stream
 
-    def generate(self, ctxt):
+    def generate(self, ctxt=None):
         """Transform the template based on the given context data."""
 
+        if ctxt is None:
+            ctxt = Context()
+
         def _transform(stream):
-            # Apply pre and runtime filters
-            for filter_ in chain(self.pre_filters, self.filters):
+            # Apply input filters
+            for filter_ in chain(self.input_filters, self.filters):
                 stream = filter_(iter(stream), ctxt)
 
             try:
@@ -656,14 +726,15 @@
 
                     else:
                         yield kind, data, pos
+
             except SyntaxError, err:
                 raise TemplateSyntaxError(err, self.filename, pos[0],
                                           pos[1] + (err.offset or 0))
 
         stream = _transform(self.stream)
 
-        # Apply post-filters
-        for filter_ in self.post_filters:
+        # Apply output filters
+        for filter_ in self.output_filters:
             stream = filter_(iter(stream), ctxt)
 
         return Stream(stream)
@@ -676,7 +747,7 @@
         
         This method returns a list containing both literal text and `Expression`
         objects.
-
+        
         @param text: the text to parse
         @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
@@ -770,7 +841,7 @@
                 fileobj = file(filepath, 'rt')
                 try:
                     tmpl = Template(fileobj, filename=filepath)
-                    tmpl.pre_filters.append(IncludeFilter(self, tmpl))
+                    tmpl.input_filters.append(IncludeFilter(self, tmpl))
                 finally:
                     fileobj.close()
                 self._cache[filename] = tmpl
Copyright (C) 2012-2017 Edgewall Software