# HG changeset patch
# User cmlenz
# Date 1152014228 0
# Node ID 512eb72dbb1956957543e90e7602480c8d3b8405
# Parent 1340e3297d19eec2f7cd5a34e3f114916d7d8564
* 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)
-
-
+
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("""
+
+ """)
+ self.assertEqual("""
+
+ """, 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'))