mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-153056: Fix a data race compiling the string.Template pattern in free-threading builds (#153057)
* 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>
This commit is contained in:
1 parent
aa533dce22
commit
45729033bf
5 files changed
+62
-1
No files matched your search
@@ -971,7 +971,8 @@ attributes:
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -83,7 +83,14 @@ class Template:
|
||||
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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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()
|
||||
@@ -299,6 +299,20 @@ class TestTemplate(unittest.TestCase):
|
||||
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 $')
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
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.
|
||||
Reference in new issue
Block a user