# HG changeset patch # User cmlenz # Date 1152014228 0 # Node ID 60f1a556690ef1cba6aa09ebb4f5052e00c345e7 # Parent 584dff20e91f76c3735be1b846fdd8df1124b28d * Add helper function to let directives apply any remaining directives, and use that helper consistently in every directive. * Fix the order of the `py:choose`, `py:when`, and `py:otherwise` directives. * Moved some of the `py:choose` tests to a new `unittest` suite to keep the docstring compact. diff --git a/markup/template.py b/markup/template.py --- a/markup/template.py +++ b/markup/template.py @@ -176,7 +176,7 @@ def __init__(self, value): self.expr = value and Expression(value) or None - def __call__(self, stream, ctxt, directives=None): + def __call__(self, stream, ctxt, directives): raise NotImplementedError def __repr__(self): @@ -185,6 +185,11 @@ expr = ' "%s"' % self.expr.source return '<%s%s>' % (self.__class__.__name__, expr) + def _apply_directives(self, stream, ctxt, directives): + if directives: + stream = directives[0](iter(stream), ctxt, directives[1:]) + return stream + class AttrsDirective(Directive): """Implementation of the `py:attrs` template directive. @@ -212,21 +217,23 @@ """ __slots__ = [] - def __call__(self, stream, ctxt, directives=None): - kind, (tag, attrib), pos = stream.next() - attrs = self.expr.evaluate(ctxt) - if attrs: - attrib = Attributes(attrib[:]) - if not isinstance(attrs, list): # assume it's a dict - attrs = attrs.items() - for name, value in attrs: - if value is None: - attrib.remove(name) - else: - attrib.set(name, unicode(value).strip()) - yield kind, (tag, attrib), pos - for event in stream: - yield event + def __call__(self, stream, ctxt, directives): + def _generate(): + kind, (tag, attrib), pos = stream.next() + attrs = self.expr.evaluate(ctxt) + if attrs: + attrib = Attributes(attrib[:]) + if not isinstance(attrs, list): # assume it's a dict + attrs = attrs.items() + for name, value in attrs: + if value is None: + attrib.remove(name) + else: + attrib.set(name, unicode(value).strip()) + yield kind, (tag, attrib), pos + for event in stream: + yield event + return self._apply_directives(_generate(), ctxt, directives) class ContentDirective(Directive): @@ -247,7 +254,7 @@ __slots__ = [] def __call__(self, stream, ctxt, directives): - def generate(): + def _generate(): kind, data, pos = stream.next() if kind is Stream.START: yield kind, data, pos # emit start tag @@ -257,10 +264,7 @@ previous = event if previous is not None: yield previous - output = generate() - if directives: - output = directives[0](output, ctxt, directives[1:]) - return output + return self._apply_directives(_generate(), ctxt, directives) class DefDirective(Directive): @@ -337,9 +341,7 @@ else: scope[name] = kwargs.pop(name, self.defaults.get(name)) ctxt.push(**scope) - stream = iter(self.stream) - if self.directives: - stream = self.directives[0](stream, ctxt, self.directives[1:]) + stream = self._apply_directives(self.stream, ctxt, self.directives) for event in stream: yield event ctxt.pop() @@ -366,7 +368,7 @@ Directive.__init__(self, value) def __call__(self, stream, ctxt, directives): - iterable = self.expr.evaluate(ctxt) or [] + iterable = self.expr.evaluate(ctxt) if iterable is not None: stream = list(stream) for item in iter(iterable): @@ -376,10 +378,7 @@ for idx, name in enumerate(self.targets): scope[name] = item[idx] ctxt.push(**scope) - output = stream - if directives: - output = directives[0](iter(output), ctxt, directives[1:]) - for event in output: + for event in self._apply_directives(stream, ctxt, directives): yield event ctxt.pop() @@ -405,9 +404,7 @@ def __call__(self, stream, ctxt, directives): if self.expr.evaluate(ctxt): - if directives: - stream = directives[0](stream, ctxt, directives[1:]) - return stream + return self._apply_directives(stream, ctxt, directives) return [] @@ -515,6 +512,7 @@ strip = self.expr.evaluate(ctxt) else: strip = True + stream = self._apply_directives(stream, ctxt, directives) if strip: stream.next() # skip start tag previous = stream.next() @@ -529,34 +527,27 @@ class ChooseDirective(Directive): """Implementation of the `py:choose` directive for conditionally selecting one of several body elements to display. - + If the `py:choose` expression is empty the expressions of nested `py:when` directives are tested for truth. The first true `py:when` body is output. - + If no `py:when` directive is matched then the fallback directive + `py:otherwise` will be used. + >>> ctxt = Context() >>> tmpl = Template('''
... 0 ... 1 + ... 2 ...
''') >>> print tmpl.generate(ctxt)
1
- - If multiple `py:when` bodies match only the first is output. - >>> tmpl = Template('''
- ... 1 - ... 2 - ...
''') - >>> print tmpl.generate(ctxt) -
- 1 -
- + If the `py:choose` directive contains an expression, the nested `py:when` - directives are tested for equality to the `py:choose` expression. + directives are tested for equality to the `py:choose` expression: + >>> tmpl = Template('''
... 1 @@ -566,46 +557,19 @@
2
- - If no `py:when` directive is matched then the fallback directive - `py:otherwise` will be used. - >>> tmpl = Template('''
- ... hidden - ... hello - ...
''') - >>> print tmpl.generate(ctxt) -
- hello -
- - `py:choose` blocks can be nested: - >>> tmpl = Template('''
- ...
- ... 2 - ... 3 - ...
- ...
''') - >>> print tmpl.generate(ctxt) -
-
- 3 -
-
- + Behavior is undefined if a `py:choose` block contains content outside a `py:when` or `py:otherwise` block. Behavior is also undefined if a `py:otherwise` occurs before `py:when` blocks. """ __slots__ = ['matched', 'value'] - def __call__(self, stream, ctxt, directives=None): + def __call__(self, stream, ctxt, directives): if self.expr: self.value = self.expr.evaluate(ctxt) self.matched = False ctxt.push(_choose=self) - for event in stream: + for event in self._apply_directives(stream, ctxt, directives): yield event ctxt.pop() @@ -624,11 +588,11 @@ try: if value == choose.value: choose.matched = True - return stream + return self._apply_directives(stream, ctxt, directives) except AttributeError: if value: choose.matched = True - return stream + return self._apply_directives(stream, ctxt, directives) return [] @@ -643,7 +607,7 @@ if choose.matched: return [] choose.matched = True - return stream + return self._apply_directives(stream, ctxt, directives) class Template(object): @@ -659,9 +623,9 @@ ('match', MatchDirective), ('for', ForDirective), ('if', IfDirective), - ('choose', ChooseDirective), ('when', WhenDirective), ('otherwise', OtherwiseDirective), + ('choose', ChooseDirective), ('replace', ReplaceDirective), ('content', ContentDirective), ('attrs', AttrsDirective), diff --git a/markup/tests/template.py b/markup/tests/template.py --- a/markup/tests/template.py +++ b/markup/tests/template.py @@ -36,13 +36,75 @@ """, str(tmpl.generate(Context(items=items)))) +class ChooseDirectiveTestCase(unittest.TestCase): + """Tests for the `py:choose` template directive and the complementary + directives `py:when` and `py:otherwise`.""" + + def test_multiple_true_whens(self): + """ + Verify that, if multiple `py:when` bodies match, only the first is + output. + """ + tmpl = Template("""
+ 1 + 2 + 3 +
""") + self.assertEqual("""
+ 1 +
""", str(tmpl.generate())) + + def test_otherwise(self): + tmpl = Template("""
+ hidden + hello +
""") + self.assertEqual("""
+ hello +
""", str(tmpl.generate())) + + def test_nesting(self): + """ + Verify that `py:choose` blocks can be nested: + """ + tmpl = Template(""" +
+
+ 2 + 3 +
+
+
""") + self.assertEqual(""" +
+
+ 3 +
+
+
""", str(tmpl.generate())) + + def test_when_with_strip(self): + """ + Verify that a when directive with a strip directive actually strips of + the outer element. + """ + tmpl = Template(""" +
+ foo +
+
""") + self.assertEqual(""" + foo + """, str(tmpl.generate())) + + class DefDirectiveTestCase(unittest.TestCase): """Tests for the `py:def` template directive.""" def test_function_with_strip(self): """ - Verify that the a named template function with a strip directive - actually strips of the outer element. + Verify that a named template function with a strip directive actually + strips of the outer element. """ tmpl = Template("""
@@ -56,12 +118,12 @@ class ForDirectiveTestCase(unittest.TestCase): - """Tests for the `py:def` template directive.""" + """Tests for the `py:for` template directive.""" def test_loop_with_strip(self): """ - Verify that the a named template function with a strip directive - actually strips of the outer element. + Verify that the combining the `py:for` directive with `py:strip` works + correctly. """ tmpl = Template("""
@@ -283,6 +345,7 @@ suite.addTest(doctest.DocTestSuite(Template.__module__)) suite.addTest(unittest.makeSuite(TemplateTestCase, 'test')) suite.addTest(unittest.makeSuite(AttrsDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(DefDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test'))