# HG changeset patch # User cmlenz # Date 1150221402 0 # Node ID c7d33e0c9839d231cabe0322332dfd5bba1e21a5 # Parent f9001cd6785b0f16b945afa920d9a27a7677fa99 The `` directive now protects itself against simple infinite recursion (see MatchDirective), while still allowing recursion in general. diff --git a/markup/eval.py b/markup/eval.py --- 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 '' % 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) diff --git a/markup/filters.py b/markup/filters.py --- 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. diff --git a/markup/template.py b/markup/template.py --- 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('''
... ... Hello ${select('@name')} ... ... ...
''') - >>> print tmpl.generate(ctxt) + >>> print tmpl.generate()
Hello Dude
+ + A match template can produce the same kind of element that it matched + without entering an infinite recursion: + + >>> tmpl = Template(''' + ... + ...
${select('*/text()')}
+ ...
+ ... Hey Joe + ...
''') + >>> print tmpl.generate() + + +
Hey Joe
+
+
+ + Match directives are applied recursively, meaning that they are also + applied to any content they may have produced themselves: + + >>> tmpl = Template(''' + ... + ...
+ ... ${select('*/*')} + ...
+ ...
+ ... + ... + ... + ... + ... + ...
''') + >>> print tmpl.generate() + + +
+ + +
+
+
+
+
+
+
""" __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('''
...
foo
...
''') - >>> print tmpl.generate(ctxt) + >>> print tmpl.generate()
foo
@@ -470,22 +540,20 @@ On the other hand, when the attribute evaluates to `False`, the element is not stripped: - >>> ctxt = Context() >>> tmpl = Template('''
...
foo
...
''') - >>> print tmpl.generate(ctxt) + >>> print tmpl.generate()
foo
Leaving the attribute value empty is equivalent to a truth value: - >>> ctxt = Context() >>> tmpl = Template('''
...
foo
...
''') - >>> print tmpl.generate(ctxt) + >>> print tmpl.generate()
foo
@@ -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('''
...
... ${what} ...
... ${echo('foo')} ...
''') - >>> print tmpl.generate(ctxt) + >>> print tmpl.generate()
foo
@@ -549,9 +616,9 @@ self.source = source self.filename = filename or '' - 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