Many updates to PEP 292 templates. Summary:

- Template no longer inherits from unicode.

- SafeTemplate is removed.  Now Templates have both a substitute() and a
  safe_substitute() method, so we don't need separate classes.  No more
  __mod__() operator.

- Adopt Tim Peter's idea for giving Template a metaclass, which makes the
  delimiter, the identifier pattern, or the entire pattern easy to override
  and document, while retaining efficiency of class-time compilation of the
  regexp.

- More informative ValueError messages which will help a user narrow down the
  bogus delimiter to the line and column in the original string (helpful for
  long triple quoted strings).
This commit is contained in:
Barry Warsaw 2004-09-10 03:08:08 +00:00
parent 961c2882a9
commit 12827c1fa9
2 changed files with 135 additions and 62 deletions

View File

@ -82,60 +82,83 @@ def maketrans(fromstr, tostr):
####################################################################
import re as _re
class Template(unicode):
class _TemplateMetaclass(type):
pattern = r"""
(?P<escaped>%(delim)s{2}) | # Escape sequence of two delimiters
%(delim)s(?P<named>%(id)s) | # delimiter and a Python identifier
%(delim)s{(?P<braced>%(id)s)} | # delimiter and a braced identifier
(?P<bogus>%(delim)s) # Other ill-formed delimiter exprs
"""
def __init__(cls, name, bases, dct):
super(_TemplateMetaclass, cls).__init__(name, bases, dct)
if 'pattern' in dct:
pattern = cls.pattern
else:
pattern = _TemplateMetaclass.pattern % {
'delim' : cls.delimiter,
'id' : cls.idpattern,
}
cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE)
class Template:
"""A string class for supporting $-substitutions."""
__slots__ = []
__metaclass__ = _TemplateMetaclass
delimiter = r'\$'
idpattern = r'[_a-z][_a-z0-9]*'
def __init__(self, template):
self.template = template
# Search for $$, $identifier, ${identifier}, and any bare $'s
pattern = _re.compile(r"""
(?P<escaped>\${2})| # Escape sequence of two $ signs
\$(?P<named>[_a-z][_a-z0-9]*)| # $ and a Python identifier
\${(?P<braced>[_a-z][_a-z0-9]*)}| # $ and a brace delimited identifier
(?P<bogus>\$) # Other ill-formed $ expressions
""", _re.IGNORECASE | _re.VERBOSE)
def __mod__(self, mapping):
def _bogus(self, mo):
i = mo.start('bogus')
lines = self.template[:i].splitlines(True)
if not lines:
colno = 1
lineno = 1
else:
colno = i - len(''.join(lines[:-1]))
lineno = len(lines)
raise ValueError('Invalid placeholder in string: line %d, col %d' %
(lineno, colno))
def substitute(self, mapping):
def convert(mo):
if mo.group('escaped') is not None:
return '$'
if mo.group('bogus') is not None:
raise ValueError('Invalid placeholder at index %d' %
mo.start('bogus'))
self._bogus(mo)
val = mapping[mo.group('named') or mo.group('braced')]
return unicode(val)
return self.pattern.sub(convert, self)
# We use this idiom instead of str() because the latter will fail
# if val is a Unicode containing non-ASCII characters.
return '%s' % val
return self.pattern.sub(convert, self.template)
class SafeTemplate(Template):
"""A string class for supporting $-substitutions.
This class is 'safe' in the sense that you will never get KeyErrors if
there are placeholders missing from the interpolation dictionary. In that
case, you will get the original placeholder in the value string.
"""
__slots__ = []
def __mod__(self, mapping):
def safe_substitute(self, mapping):
def convert(mo):
if mo.group('escaped') is not None:
return '$'
if mo.group('bogus') is not None:
raise ValueError('Invalid placeholder at index %d' %
mo.start('bogus'))
self._bogus(mo)
named = mo.group('named')
if named is not None:
try:
return unicode(mapping[named])
# We use this idiom instead of str() because the latter
# will fail if val is a Unicode containing non-ASCII
return '%s' % mapping[named]
except KeyError:
return '$' + named
braced = mo.group('braced')
try:
return unicode(mapping[braced])
return '%s' % mapping[braced]
except KeyError:
return '${' + braced + '}'
return self.pattern.sub(convert, self)
return self.pattern.sub(convert, self.template)
del _re
####################################################################

View File

@ -3,70 +3,120 @@
# License: http://www.opensource.org/licenses/PythonSoftFoundation.php
import unittest
from string import Template, SafeTemplate
from string import Template
class Bag:
pass
class Mapping:
def __getitem__(self, name):
obj = self
for part in name.split('.'):
try:
obj = getattr(obj, part)
except AttributeError:
raise KeyError(name)
return obj
class TestTemplate(unittest.TestCase):
def test_regular_templates(self):
s = Template('$who likes to eat a bag of $what worth $$100')
self.assertEqual(s % dict(who='tim', what='ham'),
self.assertEqual(s.substitute(dict(who='tim', what='ham')),
'tim likes to eat a bag of ham worth $100')
self.assertRaises(KeyError, lambda s, d: s % d, s, dict(who='tim'))
self.assertRaises(KeyError, s.substitute, dict(who='tim'))
def test_regular_templates_with_braces(self):
s = Template('$who likes ${what} for ${meal}')
self.assertEqual(s % dict(who='tim', what='ham', meal='dinner'),
'tim likes ham for dinner')
self.assertRaises(KeyError, lambda s, d: s % d,
s, dict(who='tim', what='ham'))
d = dict(who='tim', what='ham', meal='dinner')
self.assertEqual(s.substitute(d), 'tim likes ham for dinner')
self.assertRaises(KeyError, s.substitute,
dict(who='tim', what='ham'))
def test_escapes(self):
eq = self.assertEqual
s = Template('$who likes to eat a bag of $$what worth $$100')
eq(s % dict(who='tim', what='ham'),
eq(s.substitute(dict(who='tim', what='ham')),
'tim likes to eat a bag of $what worth $100')
s = Template('$who likes $$')
eq(s % dict(who='tim', what='ham'), 'tim likes $')
eq(s.substitute(dict(who='tim', what='ham')), 'tim likes $')
def test_percents(self):
eq = self.assertEqual
s = Template('%(foo)s $foo ${foo}')
self.assertEqual(s % dict(foo='baz'), '%(foo)s baz baz')
s = SafeTemplate('%(foo)s $foo ${foo}')
self.assertEqual(s % dict(foo='baz'), '%(foo)s baz baz')
d = dict(foo='baz')
eq(s.substitute(d), '%(foo)s baz baz')
eq(s.safe_substitute(d), '%(foo)s baz baz')
def test_stringification(self):
eq = self.assertEqual
s = Template('tim has eaten $count bags of ham today')
self.assertEqual(s % dict(count=7),
'tim has eaten 7 bags of ham today')
s = SafeTemplate('tim has eaten $count bags of ham today')
self.assertEqual(s % dict(count=7),
'tim has eaten 7 bags of ham today')
s = SafeTemplate('tim has eaten ${count} bags of ham today')
self.assertEqual(s % dict(count=7),
'tim has eaten 7 bags of ham today')
d = dict(count=7)
eq(s.substitute(d), 'tim has eaten 7 bags of ham today')
eq(s.safe_substitute(d), 'tim has eaten 7 bags of ham today')
s = Template('tim has eaten ${count} bags of ham today')
eq(s.substitute(d), 'tim has eaten 7 bags of ham today')
def test_SafeTemplate(self):
eq = self.assertEqual
s = SafeTemplate('$who likes ${what} for ${meal}')
eq(s % dict(who='tim'),
'tim likes ${what} for ${meal}')
eq(s % dict(what='ham'),
'$who likes ham for ${meal}')
eq(s % dict(what='ham', meal='dinner'),
s = Template('$who likes ${what} for ${meal}')
eq(s.safe_substitute(dict(who='tim')), 'tim likes ${what} for ${meal}')
eq(s.safe_substitute(dict(what='ham')), '$who likes ham for ${meal}')
eq(s.safe_substitute(dict(what='ham', meal='dinner')),
'$who likes ham for dinner')
eq(s % dict(who='tim', what='ham'),
eq(s.safe_substitute(dict(who='tim', what='ham')),
'tim likes ham for ${meal}')
eq(s % dict(who='tim', what='ham', meal='dinner'),
eq(s.safe_substitute(dict(who='tim', what='ham', meal='dinner')),
'tim likes ham for dinner')
def test_invalid_placeholders(self):
raises = self.assertRaises
s = Template('$who likes $')
raises(ValueError, lambda s, d: s % d, s, dict(who='tim'))
raises(ValueError, s.substitute, dict(who='tim'))
s = Template('$who likes ${what)')
raises(ValueError, lambda s, d: s % d, s, dict(who='tim'))
raises(ValueError, s.substitute, dict(who='tim'))
s = Template('$who likes $100')
raises(ValueError, lambda s, d: s % d, s, dict(who='tim'))
raises(ValueError, s.substitute, dict(who='tim'))
def test_delimiter_override(self):
class PieDelims(Template):
delimiter = '@'
s = PieDelims('@who likes to eat a bag of @{what} worth $100')
self.assertEqual(s.substitute(dict(who='tim', what='ham')),
'tim likes to eat a bag of ham worth $100')
def test_idpattern_override(self):
class PathPattern(Template):
idpattern = r'[_a-z][._a-z0-9]*'
m = Mapping()
m.bag = Bag()
m.bag.foo = Bag()
m.bag.foo.who = 'tim'
m.bag.what = 'ham'
s = PathPattern('$bag.foo.who likes to eat a bag of $bag.what')
self.assertEqual(s.substitute(m), 'tim likes to eat a bag of ham')
def test_pattern_override(self):
class MyPattern(Template):
pattern = r"""
(?P<escaped>@{2}) |
@(?P<named>[_a-z][._a-z0-9]*) |
@{(?P<braced>[_a-z][._a-z0-9]*)} |
(?P<bogus>@)
"""
m = Mapping()
m.bag = Bag()
m.bag.foo = Bag()
m.bag.foo.who = 'tim'
m.bag.what = 'ham'
s = MyPattern('@bag.foo.who likes to eat a bag of @bag.what')
self.assertEqual(s.substitute(m), 'tim likes to eat a bag of ham')
def test_unicode_values(self):
s = Template('$who likes $what')
d = dict(who=u't\xffm', what=u'f\xfe\fed')
self.assertEqual(s.substitute(d), u't\xffm likes f\xfe\x0ced')
def suite():