# HG changeset patch
# User cmlenz
# Date 1158067826 0
# Node ID 7a426ab6407a4a35cfbf4e84d1da156374c8a29d
# Parent f096ad1d46e9aa771b102305bd2ada2586b80bdf
* Added implementation of a simple text-based template engine. Closes #47.
* Added upgrade instructions.
diff --git a/UPGRADE.txt b/UPGRADE.txt
new file mode 100644
--- /dev/null
+++ b/UPGRADE.txt
@@ -0,0 +1,29 @@
+Upgrading Genshi
+================
+
+Upgrading from Markup
+---------------------
+
+Prior to version 0.3, the name of the Genshi project was "Markup". The
+name change means that you will have to adjust your import statements
+and the namespace URI of XML templates, among other things:
+
+ * The package name was changed from "markup" to "genshi". Please
+ adjust any import statements referring to the old package name.
+ * The namespace URI for directives in Genshi XML templates has changed
+ from http://markup.edgewall.org/ to http://genshi.edgewall.org/.
+ Please update the xmlns:py declaration in your template files
+ accordingly.
+
+Furthermore, due to the inclusion of a text-based template language,
+the class:
+
+ `markup.template.Template`
+
+has been renamed to:
+
+ `markup.template.MarkupTemplate`
+
+If you've been using the Template class directly, you'll need to
+update your code (a simple find/replace should do--the API itself
+did not change).
diff --git a/examples/bench/bigtable.py b/examples/bench/bigtable.py
--- a/examples/bench/bigtable.py
+++ b/examples/bench/bigtable.py
@@ -12,7 +12,7 @@
import cElementTree as cet
from elementtree import ElementTree as et
from genshi.builder import tag
-from genshi.template import Template
+from genshi.template import MarkupTemplate
import neo_cgi
import neo_cs
import neo_util
@@ -38,7 +38,7 @@
table = [dict(a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10)
for x in range(1000)]
-genshi_tmpl = Template("""
+genshi_tmpl = MarkupTemplate("""
""")
diff --git a/examples/transform/run.py b/examples/transform/run.py
--- a/examples/transform/run.py
+++ b/examples/transform/run.py
@@ -5,11 +5,11 @@
import sys
from genshi.input import HTMLParser
-from genshi.template import Context, Template
+from genshi.template import Context, MarkupTemplate
def transform(html_filename, tmpl_filename):
tmpl_fileobj = open(tmpl_filename)
- tmpl = Template(tmpl_fileobj, tmpl_filename)
+ tmpl = MarkupTemplate(tmpl_fileobj, tmpl_filename)
tmpl_fileobj.close()
html_fileobj = open(html_filename)
diff --git a/genshi/plugin.py b/genshi/plugin.py
--- a/genshi/plugin.py
+++ b/genshi/plugin.py
@@ -21,7 +21,7 @@
from genshi.core import Attrs, Stream, QName
from genshi.eval import Undefined
from genshi.input import HTML, XML
-from genshi.template import Context, Template, TemplateLoader
+from genshi.template import Context, MarkupTemplate, Template, TemplateLoader
def ET(element):
"""Converts the given ElementTree element to a markup stream."""
@@ -59,7 +59,7 @@
a string.
"""
if template_string is not None:
- return Template(template_string)
+ return MarkupTemplate(template_string)
divider = templatename.rfind('.')
if divider >= 0:
diff --git a/genshi/template.py b/genshi/template.py
--- a/genshi/template.py
+++ b/genshi/template.py
@@ -31,7 +31,8 @@
from genshi.path import Path
__all__ = ['BadDirectiveError', 'TemplateError', 'TemplateSyntaxError',
- 'TemplateNotFound', 'Template', 'TemplateLoader']
+ 'TemplateNotFound', 'MarkupTemplate', 'TextTemplate',
+ 'TemplateLoader']
class TemplateError(Exception):
@@ -216,7 +217,7 @@
of `(name, value)` tuples. The items in that dictionary or sequence are
added as attributes to the element:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
Bar
...
''')
>>> print tmpl.generate(foo={'class': 'collapse'})
@@ -269,7 +270,7 @@
This directive replaces the content of the element with the result of
evaluating the value of the `py:content` attribute:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
Hello
...
''')
>>> print tmpl.generate(bar='Bye')
@@ -305,7 +306,7 @@
A named template function can be used just like a normal Python function
from template expressions:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
... ${greeting}, ${name}!
...
@@ -321,7 +322,7 @@
If a function does not require parameters, the parenthesis can be omitted
both when defining and when calling it:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
... Hello, world!
...
@@ -394,7 +395,7 @@
"""Implementation of the `py:for` template directive for repeating an
element based on an iterable in the context data.
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
${item}
...
''')
>>> print tmpl.generate(items=[1, 2, 3])
@@ -439,7 +440,7 @@
"""Implementation of the `py:if` template directive for conditionally
excluding elements from being output.
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
... ${bar}
...
''')
>>> print tmpl.generate(foo=True, bar='Hello')
@@ -460,7 +461,7 @@
class MatchDirective(Directive):
"""Implementation of the `py:match` template directive.
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
... Hello ${select('@name')}
...
@@ -496,7 +497,7 @@
This directive replaces the element with the result of evaluating the
value of the `py:replace` attribute:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
... Hello
...
''')
>>> print tmpl.generate(bar='Bye')
@@ -507,7 +508,7 @@
This directive is equivalent to `py:content` combined with `py:strip`,
providing a less verbose way to achieve the same effect:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
... Hello
...
''')
>>> print tmpl.generate(bar='Bye')
@@ -528,7 +529,7 @@
When the value of the `py:strip` attribute evaluates to `True`, the element
is stripped from the output
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
foo
...
''')
>>> print tmpl.generate()
@@ -541,7 +542,7 @@
This directive is particulary interesting for named template functions or
match templates that do not generate a top-level element:
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
...
... ${what}
...
@@ -582,7 +583,7 @@
If no `py:when` directive is matched then the fallback directive
`py:otherwise` will be used.
- >>> tmpl = Template('''
>> tmpl = MarkupTemplate('''
... 0
... 1
@@ -596,7 +597,7 @@
If the `py:choose` directive contains an expression, the nested `py:when`
directives are tested for equality to the `py:choose` expression:
- >>> tmpl = Template('''
>> tmpl = MarkupTemplate('''
... 1
... 2
@@ -679,7 +680,7 @@
"""Implementation of the `py:with` template directive, which allows
shorthand access to variables and expressions.
- >>> tmpl = Template('''
+ >>> tmpl = MarkupTemplate('''
... $x $y $z
...
''')
>>> print tmpl.generate(x=42)
@@ -729,11 +730,22 @@
for name, expr in self.vars]))
+class TemplateMeta(type):
+ """Meta class for templates."""
+
+ def __new__(cls, name, bases, d):
+ if 'directives' in d:
+ d['_dir_by_name'] = dict(d['directives'])
+ d['_dir_order'] = [directive[1] for directive in d['directives']]
+
+ return type.__new__(cls, name, bases, d)
+
+
class Template(object):
"""Can parse a template and transform it into the corresponding output
based on context data.
"""
- NAMESPACE = Namespace('http://genshi.edgewall.org/')
+ __metaclass__ = TemplateMeta
EXPR = StreamEventKind('EXPR') # an expression
SUB = StreamEventKind('SUB') # a "subprogram"
@@ -750,10 +762,8 @@
('content', ContentDirective),
('attrs', AttrsDirective),
('strip', StripDirective)]
- _dir_by_name = dict(directives)
- _dir_order = [directive[1] for directive in directives]
- def __init__(self, source, basedir=None, filename=None):
+ def __init__(self, source, basedir=None, filename=None, loader=None):
"""Initialize a template from either a string or a file-like object."""
if isinstance(source, basestring):
self.source = StringIO(source)
@@ -766,107 +776,22 @@
else:
self.filepath = None
- self.filters = []
- self.stream = self.parse()
+ self.filters = [self._flatten, self._eval]
+
+ self.stream = self._parse()
def __repr__(self):
return '<%s "%s">' % (self.__class__.__name__, self.filename)
- def parse(self):
+ def _parse(self):
"""Parse the template.
- The parsing stage parses the XML template and constructs a list of
+ The parsing stage parses the template and constructs a list of
directives that will be executed in the render stage. The input is
- split up into literal output (markup that does not depend on the
- context data) and actual directives (commands or variable
- substitution).
+ split up into literal output (text that does not depend on the context
+ data) and directives or expressions.
"""
- stream = [] # list of events of the "compiled" template
- dirmap = {} # temporary mapping of directives to elements
- ns_prefix = {}
- depth = 0
-
- for kind, data, pos in XMLParser(self.source, filename=self.filename):
-
- 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:
- stream.append((kind, data, pos))
-
- elif kind is END_NS:
- if data in ns_prefix:
- del ns_prefix[data]
- else:
- stream.append((kind, data, pos))
-
- elif kind is START:
- # Record any directive attributes in start tags
- tag, attrib = data
- directives = []
- strip = False
-
- if tag in self.NAMESPACE:
- cls = self._dir_by_name.get(tag.localname)
- if cls is None:
- raise BadDirectiveError(tag.localname, pos[0], pos[1])
- value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
- directives.append(cls(value, *pos))
- strip = True
-
- new_attrib = []
- for name, value in attrib:
- if name in self.NAMESPACE:
- cls = self._dir_by_name.get(name.localname)
- if cls is None:
- raise BadDirectiveError(name.localname, pos[0],
- pos[1])
- directives.append(cls(value, *pos))
- else:
- if value:
- value = list(self._interpolate(value, *pos))
- if len(value) == 1 and value[0][0] is TEXT:
- value = value[0][1]
- else:
- value = [(TEXT, u'', pos)]
- new_attrib.append((name, value))
-
- if directives:
- directives.sort(lambda a, b: cmp(self._dir_order.index(a.__class__),
- self._dir_order.index(b.__class__)))
- dirmap[(depth, tag)] = (directives, len(stream), strip)
-
- stream.append((kind, (tag, Attrs(new_attrib)), pos))
- depth += 1
-
- elif kind is END:
- depth -= 1
- stream.append((kind, data, pos))
-
- # If there have have directive attributes with the corresponding
- # start tag, move the events inbetween into a "subprogram"
- if (depth, data) in dirmap:
- directives, start_offset, strip = dirmap.pop((depth, data))
- substream = stream[start_offset:]
- if strip:
- substream = substream[1:-1]
- stream[start_offset:] = [(SUB, (directives, substream),
- pos)]
-
- elif kind is TEXT:
- for kind, data, pos in self._interpolate(data, *pos):
- stream.append((kind, data, pos))
-
- elif kind is COMMENT:
- if not data.lstrip().startswith('!'):
- stream.append((kind, data, pos))
-
- else:
- stream.append((kind, data, pos))
-
- return stream
+ raise NotImplementedError
_FULL_EXPR_RE = re.compile(r'(? 0:
+ yield kind, data, pos
+ else:
+ tail[:] = [(kind, data, pos)]
+ break
+
+ for kind, data, pos in stream:
+
+ # We (currently) only care about start and end events for matching
+ # We might care about namespace events in the future, though
+ if not match_templates or kind not in (START, END):
+ yield kind, data, pos
+ continue
+
+ for idx, (test, path, template, directives) in \
+ enumerate(match_templates):
+
+ if test(kind, data, pos, nsprefix, 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)
+
+ # Consume and store all events until an end event
+ # corresponding to this start event is encountered
+ content = [(kind, data, pos)]
+ content += list(self._match(_strip(stream), ctxt)) + tail
+
+ kind, data, pos = tail[0]
+ for test in [mt[0] for mt in match_templates]:
+ test(kind, data, pos, nsprefix, ctxt)
+
+ # Make the select() function available in the body of the
+ # match template
+ def select(path):
+ return Stream(content).select(path)
+ ctxt.push(dict(select=select))
+
+ # Recursively process the output
+ template = _apply_directives(template, ctxt, directives)
+ for event in self._match(self._eval(self._flatten(template,
+ ctxt),
+ ctxt), ctxt,
+ match_templates[:idx] +
+ match_templates[idx + 1:]):
+ yield event
+
+ ctxt.pop()
+ break
+
+ else: # no matches
+ yield kind, data, pos
+
+
+class TextTemplate(Template):
+ """Implementation of a simple text-based template engine.
+
+ >>> tmpl = TextTemplate('''Dear $name,
+ ...
+ ... We have the following items for you:
+ ... #for item in items
+ ... * $item
+ ... #endfor
+ ...
+ ... All the best,
+ ... Foobar''')
+ >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
+ Dear Joe,
+
+ We have the following items for you:
+ * 1
+ * 2
+ * 3
+
+ All the best,
+ Foobar
+ """
+ directives = [('def', DefDirective),
+ ('comment', StripDirective),
+ ('when', WhenDirective),
+ ('otherwise', OtherwiseDirective),
+ ('for', ForDirective),
+ ('if', IfDirective),
+ ('choose', ChooseDirective),
+ ('with', WithDirective)]
+
+ _directive_re = re.compile('^\s*#(\w+.*)\n?', re.MULTILINE)
+
+ def _parse(self):
+ stream = [] # list of events of the "compiled" template
+ dirmap = {} # temporary mapping of directives to elements
+ depth = 0
+
+ source = self.source.read()
+ offset = 0
+ lineno = 1
+ for idx, mo in enumerate(self._directive_re.finditer(source)):
+ start, end = mo.span()
+ if start > offset:
+ text = source[offset:start]
+ for kind, data, pos in self._interpolate(text, self.filename,
+ lineno, 0):
+ stream.append((kind, data, pos))
+ lineno += len(text.splitlines())
+
+ text = source[start:end].lstrip().lstrip('#')
+ lineno += len(text.splitlines())
+ directive = text.split(None, 1)
+ if len(directive) > 1:
+ command, value = directive
+ else:
+ command, value = directive[0], None
+
+ if not command.startswith('end'):
+ cls = self._dir_by_name.get(command)
+ if cls is None:
+ raise BadDirectiveError(command)
+ directive = cls(value, self.filename, lineno, 0)
+ dirmap[depth] = (directive, len(stream))
+ depth += 1
+ else:
+ depth -= 1
+ command = command[3:]
+ if depth in dirmap:
+ directive, start_offset = dirmap.pop(depth)
+ substream = stream[start_offset:]
+ stream[start_offset:] = [(SUB, ([directive], substream),
+ (self.filename, lineno, 0))]
+
+ offset = end
+
+ if offset < len(source):
+ text = source[offset:]
+ for kind, data, pos in self._interpolate(text, self.filename,
+ lineno, 0):
+ stream.append((kind, data, pos))
+
+ return stream
+
+
class TemplateLoader(object):
"""Responsible for loading templates from files on the specified search
path.
@@ -1100,7 +1309,7 @@
template file, and returns the corresponding `Template` object:
>>> template = loader.load(os.path.basename(path))
- >>> isinstance(template, Template)
+ >>> isinstance(template, MarkupTemplate)
True
Template instances are cached: requesting a template with the same name
@@ -1126,7 +1335,7 @@
self._cache = {}
self._mtime = {}
- def load(self, filename, relative_to=None):
+ def load(self, filename, relative_to=None, cls=MarkupTemplate):
"""Load the template with the given name.
If the `filename` parameter is relative, this method searches the search
@@ -1150,9 +1359,8 @@
@param relative_to: the filename of the template from which the new
template is being loaded, or `None` if the template is being loaded
directly
+ @param cls: the class of the template object to instantiate
"""
- from genshi.filters import IncludeFilter
-
if relative_to:
filename = os.path.join(os.path.dirname(relative_to), filename)
filename = os.path.normpath(filename)
@@ -1179,8 +1387,8 @@
try:
fileobj = open(filepath, 'U')
try:
- tmpl = Template(fileobj, basedir=dirname, filename=filename)
- tmpl.filters.append(IncludeFilter(self))
+ tmpl = cls(fileobj, basedir=dirname, filename=filename,
+ loader=self)
finally:
fileobj.close()
self._cache[filename] = tmpl
diff --git a/genshi/tests/template.py b/genshi/tests/template.py
--- a/genshi/tests/template.py
+++ b/genshi/tests/template.py
@@ -18,9 +18,10 @@
import sys
import tempfile
+from genshi import template
from genshi.core import Markup, Stream
-from genshi.template import BadDirectiveError, Template, TemplateLoader, \
- TemplateSyntaxError
+from genshi.template import BadDirectiveError, MarkupTemplate, Template, \
+ TemplateLoader, TemplateSyntaxError, TextTemplate
class AttrsDirectiveTestCase(unittest.TestCase):
@@ -30,7 +31,7 @@
"""
Verify that the directive has access to the loop variables.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""""")
items = [{'id': 1, 'class': 'foo'}, {'id': 2, 'class': 'bar'}]
@@ -43,7 +44,7 @@
Verify that an attribute value that evaluates to `None` removes an
existing attribute of that name.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""""")
self.assertEqual("""
@@ -55,7 +56,7 @@
Verify that an attribute value that evaluates to `None` removes an
existing attribute of that name.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""""")
self.assertEqual("""
@@ -72,7 +73,7 @@
Verify that, if multiple `py:when` bodies match, only the first is
output.
"""
- tmpl = Template("""
OK
@@ -134,7 +135,7 @@
"""
Verify more complex nesting using otherwise.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
FAIL
@@ -155,7 +156,7 @@
Verify that a when directive with a strip directive actually strips of
the outer element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
foo
@@ -169,7 +170,7 @@
Verify that a `when` directive outside of a `choose` directive is
reported as an error.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""""")
self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
@@ -179,7 +180,7 @@
Verify that an `otherwise` directive outside of a `choose` directive is
reported as an error.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""""")
self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
@@ -189,7 +190,7 @@
Verify that an `when` directive that doesn't have a `test` attribute
is reported as an error.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
foo
@@ -201,7 +202,7 @@
Verify that an `otherwise` directive can be used without a `test`
attribute.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
foo
@@ -214,7 +215,7 @@
"""
Verify that the directive can also be used as an element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""12
@@ -225,6 +226,23 @@
1
""", str(tmpl.generate()))
+ def test_in_text_template(self):
+ """
+ Verify that the directive works as expected in a text template.
+ """
+ tmpl = TextTemplate("""#choose
+ #when 1 == 1
+ 1
+ #endwhen
+ #when 2 == 2
+ 2
+ #endwhen
+ #when 3 == 3
+ 3
+ #endwhen
+ #endchoose""")
+ self.assertEqual(""" 1\n""", str(tmpl.generate()))
+
class DefDirectiveTestCase(unittest.TestCase):
"""Tests for the `py:def` template directive."""
@@ -234,7 +252,7 @@
Verify that a named template function with a strip directive actually
strips of the outer element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
@@ -261,7 +279,7 @@
"""
Verify that the directive can also be used as an element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${what}
@@ -276,7 +294,7 @@
Verify that a template function defined inside a conditional block can
be called from outside that block.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${what}
@@ -293,7 +311,7 @@
"""
Verify that keyword arguments work with `py:def` directives.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${what}
${echo('foo')}
""")
@@ -302,7 +320,7 @@
""", str(tmpl.generate()))
def test_invocation_in_attribute(self):
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${what or 'something'}
@@ -331,7 +349,7 @@
self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc))
def test_def_in_matched(self):
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${select('*')}
@@ -342,6 +360,19 @@
True""", str(tmpl.generate()))
+ def test_in_text_template(self):
+ """
+ Verify that the directive works as expected in a text template.
+ """
+ tmpl = TextTemplate("""
+ #def echo(greeting, name='world')
+ ${greeting}, ${name}!
+ #enddef
+ ${echo('Hi', name='you')}
+ """)
+ self.assertEqual(""" Hi, you!
+ """, str(tmpl.generate()))
+
class ForDirectiveTestCase(unittest.TestCase):
"""Tests for the `py:for` template directive."""
@@ -351,7 +382,7 @@
Verify that the combining the `py:for` directive with `py:strip` works
correctly.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${item}
@@ -368,7 +399,7 @@
"""
Verify that the directive can also be used as an element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${item}
@@ -385,7 +416,7 @@
"""
Verify that assignment to tuples works correctly.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
key=$k, value=$v
@@ -399,7 +430,7 @@
"""
Verify that assignment to nested tuples works correctly.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
$idx: key=$k, value=$v
@@ -418,7 +449,7 @@
Verify that the combining the `py:if` directive with `py:strip` works
correctly.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${bar}""")
self.assertEqual("""
@@ -429,7 +460,7 @@
"""
Verify that the directive can also be used as an element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""${bar}""")
self.assertEqual("""
@@ -445,7 +476,7 @@
Verify that a match template can produce the same kind of element that
it matched without entering an infinite recursion.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${select('text()')}
@@ -460,7 +491,7 @@
Verify that a match template can produce the same kind of element that
it matched without entering an infinite recursion.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${select('text()')}
@@ -476,7 +507,7 @@
"""
Verify that the directive can also be used as an element.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${select('text()')}
@@ -491,7 +522,7 @@
Match directives are applied recursively, meaning that they are also
applied to any content they may have produced themselves:
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${select('*')}
@@ -522,7 +553,7 @@
themselves output the element they match, avoiding recursion is even
more complex, but should work.
"""
- tmpl = Template("""
+ tmpl = MarkupTemplate("""
${select('*')}
@@ -543,7 +574,7 @@
""", str(tmpl.generate()))
def test_select_all_attrs(self):
- tmpl = Template("""
+ tmpl = MarkupTemplate("""