Skip to content

bpo-46307: Add string.Template.get_identifiers() method #30493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Doc/library/string.rst
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,22 @@ these rules. The methods of :class:`Template` are:
templates containing dangling delimiters, unmatched braces, or
placeholders that are not valid Python identifiers.


.. method:: is_valid()

Returns false if the template has invalid placeholders that will cause
:meth:`substitute` to raise :exc:`ValueError`.

.. versionadded:: 3.11


.. method:: get_identifiers()

Returns a list of the valid identifiers in the template, in the order
they first appear, ignoring any invalid identifiers.

.. versionadded:: 3.11

:class:`Template` instances also provide one public data attribute:

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

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


Helper functions
----------------
Expand Down
29 changes: 29 additions & 0 deletions Lib/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,35 @@ def convert(mo):
self.pattern)
return self.pattern.sub(convert, self.template)

def is_valid(self):
for mo in self.pattern.finditer(self.template):
if mo.group('invalid') is not None:
return False
if (mo.group('named') is None
and mo.group('braced') is None
and mo.group('escaped') is None):
# If all the groups are None, there must be
# another group we're not expecting
raise ValueError('Unrecognized named group in pattern',
self.pattern)
return True

def get_identifiers(self):
ids = []
for mo in self.pattern.finditer(self.template):
named = mo.group('named') or mo.group('braced')
if named is not None and named not in ids:
# add a named group only the first time it appears
ids.append(named)
elif (named is None
and mo.group('invalid') is None
and mo.group('escaped') is None):
# If all the groups are None, there must be
# another group we're not expecting
raise ValueError('Unrecognized named group in pattern',
self.pattern)
return ids

# Initialize Template.pattern. __init_subclass__() is automatically called
# only for subclasses, not for the Template class itself.
Template.__init_subclass__()
Expand Down
51 changes: 51 additions & 0 deletions Lib/test/test_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,57 @@ class PieDelims(Template):
self.assertEqual(s.substitute(dict(who='tim', what='ham')),
'tim likes to eat a bag of ham worth $100')

def test_is_valid(self):
eq = self.assertEqual
s = Template('$who likes to eat a bag of ${what} worth $$100')
self.assertTrue(s.is_valid())

s = Template('$who likes to eat a bag of ${what} worth $100')
self.assertFalse(s.is_valid())

# if the pattern has an unrecognized capture group,
# it should raise ValueError like substitute and safe_substitute do
class BadPattern(Template):
pattern = r"""
(?P<badname>.*) |
(?P<escaped>@{2}) |
@(?P<named>[_a-z][._a-z0-9]*) |
@{(?P<braced>[_a-z][._a-z0-9]*)} |
(?P<invalid>@) |
"""
s = BadPattern('@bag.foo.who likes to eat a bag of @bag.what')
self.assertRaises(ValueError, s.is_valid)

def test_get_identifiers(self):
eq = self.assertEqual
raises = self.assertRaises
s = Template('$who likes to eat a bag of ${what} worth $$100')
ids = s.get_identifiers()
eq(ids, ['who', 'what'])

# repeated identifiers only included once
s = Template('$who likes to eat a bag of ${what} worth $$100; ${who} likes to eat a bag of $what worth $$100')
ids = s.get_identifiers()
eq(ids, ['who', 'what'])

# invalid identifiers are ignored
s = Template('$who likes to eat a bag of ${what} worth $100')
ids = s.get_identifiers()
eq(ids, ['who', 'what'])

# if the pattern has an unrecognized capture group,
# it should raise ValueError like substitute and safe_substitute do
class BadPattern(Template):
pattern = r"""
(?P<badname>.*) |
(?P<escaped>@{2}) |
@(?P<named>[_a-z][._a-z0-9]*) |
@{(?P<braced>[_a-z][._a-z0-9]*)} |
(?P<invalid>@) |
"""
s = BadPattern('@bag.foo.who likes to eat a bag of @bag.what')
self.assertRaises(ValueError, s.get_identifiers)


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