From 319c8d7318ae72048027e3ad66d1399c72cffc53 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Sat, 8 Jan 2022 20:41:29 -0700 Subject: [PATCH 1/5] bpo-46307: Add string.Template.get_identifiers() method --- Lib/string.py | 10 +++++++++ Lib/test/test_string.py | 21 +++++++++++++++++++ .../2022-01-10-07-51-43.bpo-46307.SKvOIY.rst | 1 + 3 files changed, 32 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst diff --git a/Lib/string.py b/Lib/string.py index 261789cc10a44c..3174361521ef9f 100644 --- a/Lib/string.py +++ b/Lib/string.py @@ -141,6 +141,16 @@ def convert(mo): self.pattern) return self.pattern.sub(convert, self.template) + def get_identifiers(self, *, raise_on_invalid=True): + 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: + ids.append(named) + elif mo.group('invalid') is not None and raise_on_invalid: + self._invalid(mo) + return ids + # Initialize Template.pattern. __init_subclass__() is automatically called # only for subclasses, not for the Template class itself. Template.__init_subclass__() diff --git a/Lib/test/test_string.py b/Lib/test/test_string.py index 0be28fdb609eae..94edc814e4ed95 100644 --- a/Lib/test/test_string.py +++ b/Lib/test/test_string.py @@ -475,6 +475,27 @@ class PieDelims(Template): self.assertEqual(s.substitute(dict(who='tim', what='ham')), 'tim likes to eat a bag of ham worth $100') + 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 raised + s = Template('$who likes to eat a bag of ${what} worth $100') + raises(ValueError, s.get_identifiers) + + # invalid identifiers are ignored with raise_on_invalid=False + s = Template('$who likes to eat a bag of ${what} worth $100') + ids = s.get_identifiers(raise_on_invalid=False) + eq(ids, ['who', 'what']) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst b/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst new file mode 100644 index 00000000000000..0e7c27aba02a29 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst @@ -0,0 +1 @@ +Add get_identifiers() method to string.Template. From b14f8bcc5046bc63c612b4ad4acffa56b0e4609a Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Mon, 10 Jan 2022 08:05:59 -0700 Subject: [PATCH 2/5] Update Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst Co-authored-by: Nikita Sobolev --- .../next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst b/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst index 0e7c27aba02a29..612f8ff6a5ee21 100644 --- a/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst +++ b/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst @@ -1 +1 @@ -Add get_identifiers() method to string.Template. +Add :meth:`string.Template.get_identifiers` method. From e80bb26926ec44757846fbcbe760742b59d26728 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Mon, 10 Jan 2022 16:00:55 -0700 Subject: [PATCH 3/5] bpo-46307: Add string.Template.is_valid method --- Doc/library/string.rst | 12 ++++++ Lib/string.py | 25 +++++++++-- Lib/test/test_string.py | 42 ++++++++++++++++--- .../2022-01-10-07-51-43.bpo-46307.SKvOIY.rst | 2 +- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index b27782f8d8e9b4..ae241a2d3f9287 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -783,6 +783,18 @@ 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`. + + + .. method:: get_identifiers() + + Returns a list of the valid identifiers in the template, in the order + they first appear, ignoring any invalid identifiers. + :class:`Template` instances also provide one public data attribute: .. attribute:: template diff --git a/Lib/string.py b/Lib/string.py index 3174361521ef9f..2eab6d4f595c4e 100644 --- a/Lib/string.py +++ b/Lib/string.py @@ -141,14 +141,33 @@ def convert(mo): self.pattern) return self.pattern.sub(convert, self.template) - def get_identifiers(self, *, raise_on_invalid=True): + 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 mo.group('invalid') is not None and raise_on_invalid: - self._invalid(mo) + 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 diff --git a/Lib/test/test_string.py b/Lib/test/test_string.py index 94edc814e4ed95..824b89ad517c12 100644 --- a/Lib/test/test_string.py +++ b/Lib/test/test_string.py @@ -475,6 +475,27 @@ 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.*) | + (?P@{2}) | + @(?P[_a-z][._a-z0-9]*) | + @{(?P[_a-z][._a-z0-9]*)} | + (?P@) | + """ + 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 @@ -487,15 +508,24 @@ def test_get_identifiers(self): ids = s.get_identifiers() eq(ids, ['who', 'what']) - # invalid identifiers are raised - s = Template('$who likes to eat a bag of ${what} worth $100') - raises(ValueError, s.get_identifiers) - - # invalid identifiers are ignored with raise_on_invalid=False + # invalid identifiers are ignored s = Template('$who likes to eat a bag of ${what} worth $100') - ids = s.get_identifiers(raise_on_invalid=False) + 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.*) | + (?P@{2}) | + @(?P[_a-z][._a-z0-9]*) | + @{(?P[_a-z][._a-z0-9]*)} | + (?P@) | + """ + s = BadPattern('@bag.foo.who likes to eat a bag of @bag.what') + self.assertRaises(ValueError, s.get_identifiers) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst b/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst index 612f8ff6a5ee21..6207c424ce9c04 100644 --- a/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst +++ b/Misc/NEWS.d/next/Library/2022-01-10-07-51-43.bpo-46307.SKvOIY.rst @@ -1 +1 @@ -Add :meth:`string.Template.get_identifiers` method. +Add :meth:`string.Template.is_valid` and :meth:`string.Template.get_identifiers` methods. From f40258eb208e037b9681204b04e0a6aed4cd1af8 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Mon, 10 Jan 2022 20:39:44 -0700 Subject: [PATCH 4/5] bpo-46307: Update documentation --- Doc/library/string.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index ae241a2d3f9287..77098376ff8dee 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -881,6 +881,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 ---------------- From 2c7e74dcbaffd43b4f69be50e641269c14862e4d Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Tue, 11 Jan 2022 09:42:17 -0700 Subject: [PATCH 5/5] bpo-46307: Update documentation --- Doc/library/string.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 77098376ff8dee..9bc703e70cdaa7 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -789,12 +789,16 @@ these rules. The methods of :class:`Template` are: 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