Skip to content

Commit dce642f

Browse files
author
Ben Kehoe
authored
bpo-46307: Add string.Template.get_identifiers() method (GH-30493)
Add `string.Template.get_identifiers()` method that returns the identifiers within the template. By default, raises an error if it encounters an invalid identifier (like `substitute()`). The keyword-only argument `raise_on_invalid` can be set to `False` to ignore invalid identifiers (like `safe_substitute()`). Automerge-Triggered-By: GH:warsaw
1 parent cf496d6 commit dce642f

File tree

4 files changed

+100
-0
lines changed

4 files changed

+100
-0
lines changed

Doc/library/string.rst

+19
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,22 @@ these rules. The methods of :class:`Template` are:
783783
templates containing dangling delimiters, unmatched braces, or
784784
placeholders that are not valid Python identifiers.
785785

786+
787+
.. method:: is_valid()
788+
789+
Returns false if the template has invalid placeholders that will cause
790+
:meth:`substitute` to raise :exc:`ValueError`.
791+
792+
.. versionadded:: 3.11
793+
794+
795+
.. method:: get_identifiers()
796+
797+
Returns a list of the valid identifiers in the template, in the order
798+
they first appear, ignoring any invalid identifiers.
799+
800+
.. versionadded:: 3.11
801+
786802
:class:`Template` instances also provide one public data attribute:
787803

788804
.. attribute:: template
@@ -869,6 +885,9 @@ rule:
869885
* *invalid* -- This group matches any other delimiter pattern (usually a single
870886
delimiter), and it should appear last in the regular expression.
871887

888+
The methods on this class will raise :exc:`ValueError` if the pattern matches
889+
the template without one of these named groups matching.
890+
872891

873892
Helper functions
874893
----------------

Lib/string.py

+29
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,35 @@ def convert(mo):
141141
self.pattern)
142142
return self.pattern.sub(convert, self.template)
143143

144+
def is_valid(self):
145+
for mo in self.pattern.finditer(self.template):
146+
if mo.group('invalid') is not None:
147+
return False
148+
if (mo.group('named') is None
149+
and mo.group('braced') is None
150+
and mo.group('escaped') is None):
151+
# If all the groups are None, there must be
152+
# another group we're not expecting
153+
raise ValueError('Unrecognized named group in pattern',
154+
self.pattern)
155+
return True
156+
157+
def get_identifiers(self):
158+
ids = []
159+
for mo in self.pattern.finditer(self.template):
160+
named = mo.group('named') or mo.group('braced')
161+
if named is not None and named not in ids:
162+
# add a named group only the first time it appears
163+
ids.append(named)
164+
elif (named is None
165+
and mo.group('invalid') is None
166+
and mo.group('escaped') is None):
167+
# If all the groups are None, there must be
168+
# another group we're not expecting
169+
raise ValueError('Unrecognized named group in pattern',
170+
self.pattern)
171+
return ids
172+
144173
# Initialize Template.pattern. __init_subclass__() is automatically called
145174
# only for subclasses, not for the Template class itself.
146175
Template.__init_subclass__()

Lib/test/test_string.py

+51
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,57 @@ class PieDelims(Template):
475475
self.assertEqual(s.substitute(dict(who='tim', what='ham')),
476476
'tim likes to eat a bag of ham worth $100')
477477

478+
def test_is_valid(self):
479+
eq = self.assertEqual
480+
s = Template('$who likes to eat a bag of ${what} worth $$100')
481+
self.assertTrue(s.is_valid())
482+
483+
s = Template('$who likes to eat a bag of ${what} worth $100')
484+
self.assertFalse(s.is_valid())
485+
486+
# if the pattern has an unrecognized capture group,
487+
# it should raise ValueError like substitute and safe_substitute do
488+
class BadPattern(Template):
489+
pattern = r"""
490+
(?P<badname>.*) |
491+
(?P<escaped>@{2}) |
492+
@(?P<named>[_a-z][._a-z0-9]*) |
493+
@{(?P<braced>[_a-z][._a-z0-9]*)} |
494+
(?P<invalid>@) |
495+
"""
496+
s = BadPattern('@bag.foo.who likes to eat a bag of @bag.what')
497+
self.assertRaises(ValueError, s.is_valid)
498+
499+
def test_get_identifiers(self):
500+
eq = self.assertEqual
501+
raises = self.assertRaises
502+
s = Template('$who likes to eat a bag of ${what} worth $$100')
503+
ids = s.get_identifiers()
504+
eq(ids, ['who', 'what'])
505+
506+
# repeated identifiers only included once
507+
s = Template('$who likes to eat a bag of ${what} worth $$100; ${who} likes to eat a bag of $what worth $$100')
508+
ids = s.get_identifiers()
509+
eq(ids, ['who', 'what'])
510+
511+
# invalid identifiers are ignored
512+
s = Template('$who likes to eat a bag of ${what} worth $100')
513+
ids = s.get_identifiers()
514+
eq(ids, ['who', 'what'])
515+
516+
# if the pattern has an unrecognized capture group,
517+
# it should raise ValueError like substitute and safe_substitute do
518+
class BadPattern(Template):
519+
pattern = r"""
520+
(?P<badname>.*) |
521+
(?P<escaped>@{2}) |
522+
@(?P<named>[_a-z][._a-z0-9]*) |
523+
@{(?P<braced>[_a-z][._a-z0-9]*)} |
524+
(?P<invalid>@) |
525+
"""
526+
s = BadPattern('@bag.foo.who likes to eat a bag of @bag.what')
527+
self.assertRaises(ValueError, s.get_identifiers)
528+
478529

479530
if __name__ == '__main__':
480531
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add :meth:`string.Template.is_valid` and :meth:`string.Template.get_identifiers` methods.

0 commit comments

Comments
 (0)