* gh-153056: Fix a data race compiling the string.Template pattern in free-threading builds
Template compiles its substitution pattern lazily and caches it on the class. On the free-threaded build two concurrent first uses could race: a thread that observed the pattern another thread had just compiled would try to recompile it, and re.compile() rejects flags on an already-compiled pattern, raising a spurious ValueError. Return the already-compiled pattern instead.
As a side effect, a subclass that supplies an already-compiled pattern now works too; previously it raised the same ValueError at class definition.
* Trim test comments and NEWS wording
* Document that the pattern attribute accepts a string or a compiled regex
* Comment the three states of pattern and note the documented-behavior fix in NEWS
* Update Doc/library/string.rst
---------
Co-authored-by: Barry Warsaw <barry@python.org>
Alternatively, you can provide the entire regular expression pattern by
overriding the class attribute *pattern*. If you do this, the value must be a
-regular expression object with four named capturing groups. The capturing
+regular expression pattern string, or a compiled regular expression
+object, with four named capturing groups. The capturing
groups correspond to the rules given above, along with the invalid placeholder
rule:
def _compile_pattern(cls):
import re # deferred import, for performance
+ # `pattern` may be the `_TemplatePattern` sentinel (not yet compiled), an
+ # already-compiled regular expression object (as documented), or a string
+ # regular expression. An already-compiled object is returned as-is; the
+ # other two are compiled and cached back on the class.
pattern = cls.__dict__.get('pattern', _TemplatePattern)
+ if isinstance(pattern, re.Pattern):
+ # re.compile() rejects flags on an already-compiled pattern.
+ return pattern
if pattern is _TemplatePattern:
delim = re.escape(cls.delimiter)
id = cls.idpattern
--- /dev/null
+import string
+import unittest
+from string import Template
+
+from test.support import threading_helper
+
+
+@threading_helper.requires_working_threading()
+class TestTemplateCompileRace(unittest.TestCase):
+ def test_concurrent_first_use(self):
+ # Racing the lazy pattern compile must not raise a spurious ValueError
+ # from recompiling an already-compiled pattern. A throwaway subclass,
+ # re-armed to the sentinel each round, keeps string.Template unmutated
+ # (subclasses precompile eagerly in __init_subclass__).
+ uncompiled = string._TemplatePattern
+ errors = []
+
+ def use_template(cls):
+ try:
+ cls("$x and ${y}").substitute(x=1, y=2)
+ except Exception as e:
+ errors.append(e)
+
+ for _ in range(20):
+ class T(Template):
+ pass
+ T.pattern = uncompiled
+ T.flags = None
+ threading_helper.run_concurrently(use_template, nthreads=10, args=(T,))
+
+ self.assertEqual(errors, [], msg=f"unexpected errors: {errors}")
+
+
+if __name__ == "__main__":
+ unittest.main()
eq(s.safe_substitute(dict(who='tim', what='ham', meal='dinner')),
'tim likes ham for dinner')
+ def test_precompiled_pattern(self):
+ # A subclass may supply an already-compiled pattern; it must be reused,
+ # not recompiled (re.compile() rejects flags on a compiled pattern).
+ import re
+ compiled = re.compile(
+ r'\$(?:(?P<escaped>\$)|(?P<named>[a-z]+)|'
+ r'\{(?P<braced>[a-z]+)\}|(?P<invalid>))')
+ class MyTemplate(Template):
+ pattern = compiled
+ self.assertIs(MyTemplate.pattern, compiled)
+ self.assertEqual(
+ MyTemplate('$who likes $what').substitute(who='tim', what='ham'),
+ 'tim likes ham')
+
def test_invalid_placeholders(self):
raises = self.assertRaises
s = Template('$who likes $')
--- /dev/null
+Fix :class:`string.Template` raising a spurious :exc:`ValueError` when the
+*pattern* attribute is a compiled regular expression object, which the
+documentation allows. On the free-threaded build this also occurred as a data
+race on the first concurrent use.