diff Lib/string.py @ 33439:629200d880ea legacy-trunk

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).
author Barry Warsaw <barry@python.org>
date Fri, 10 Sep 2004 03:08:08 +0000
parents 2b136441035c
children 5b31b4c3b7ba
line wrap: on
line diff
--- a/Lib/string.py	Fri Sep 10 01:20:21 2004 +0000
+++ b/Lib/string.py	Fri Sep 10 03:08:08 2004 +0000
@@ -82,60 +82,83 @@
 ####################################################################
 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
 
 
 ####################################################################