mirror of
https://github.com/python/cpython
synced 2026-11-06 09:02:40 +03:00
Setting __name__, __qualname__, __doc__ or __module__ on a function
stopped the world in the free-threaded build since gh-154826 and
gh-154851, making functools.wraps() orders of magnitude slower with
multiple threads.
Use the per-object critical section and _PyObject_XSetRefDelayed()
instead, as gen_set_name() already does. Use acquire loads in the
readers and hold strong references across error reporting, tracing, and
eval-frame recording that may execute Python or detach the thread. The
__code__, __defaults__, __kwdefaults__ and __closure__ setters still
stop the world to clear func_version.
Document synchronization requirements for direct field access and the
lifetime of borrowed function attributes.
(cherry picked from commit 010bebbf9f)
221 lines
6.7 KiB
Python
221 lines
6.7 KiB
Python
import random
|
|
import unittest
|
|
from unittest import TestCase
|
|
|
|
from test.support import threading_helper
|
|
|
|
threading_helper.requires_working_threading(module=True)
|
|
|
|
NUM_THREADS = 8
|
|
ITERS = 200
|
|
|
|
|
|
def random_string():
|
|
return ''.join(random.choice('0123456789ABCDEF') for _ in range(10))
|
|
|
|
|
|
def template_a(): pass
|
|
def template_b(): pass
|
|
|
|
|
|
class TestFTFunctionAttributes(TestCase):
|
|
|
|
def stress_attribute(self, attr, make_value):
|
|
def target(x=1):
|
|
return x
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
setattr(target, attr, make_value())
|
|
getattr(target, attr)
|
|
|
|
threading_helper.run_concurrently(writer, NUM_THREADS)
|
|
|
|
def test_name(self):
|
|
self.stress_attribute("__name__", random_string)
|
|
|
|
def test_qualname(self):
|
|
self.stress_attribute("__qualname__", random_string)
|
|
|
|
def test_code(self):
|
|
codes = (template_a.__code__, template_b.__code__)
|
|
self.stress_attribute("__code__", lambda: random.choice(codes))
|
|
|
|
def test_defaults(self):
|
|
self.stress_attribute("__defaults__", lambda: (random_string(),))
|
|
|
|
def test_kwdefaults(self):
|
|
self.stress_attribute("__kwdefaults__", lambda: {"x": random_string()})
|
|
|
|
def test_annotations(self):
|
|
self.stress_attribute("__annotations__",
|
|
lambda: {"x": random_string()})
|
|
|
|
def test_annotate(self):
|
|
self.stress_attribute("__annotate__",
|
|
lambda: (lambda format: {"x": str}))
|
|
|
|
def test_type_params(self):
|
|
self.stress_attribute("__type_params__", lambda: (random_string(),))
|
|
|
|
def test_doc(self):
|
|
self.stress_attribute("__doc__", random_string)
|
|
|
|
def test_module(self):
|
|
self.stress_attribute("__module__", random_string)
|
|
|
|
def test_annotations_and_annotate(self):
|
|
# The __annotations__ and __annotate__ setters clear each other.
|
|
def target(): pass
|
|
|
|
def set_annotations():
|
|
for _ in range(ITERS):
|
|
target.__annotations__ = {"x": random_string()}
|
|
target.__annotations__
|
|
|
|
def set_annotate():
|
|
for _ in range(ITERS):
|
|
target.__annotate__ = lambda format: {"x": str}
|
|
target.__annotate__
|
|
|
|
threading_helper.run_concurrently(
|
|
[set_annotations, set_annotate] * (NUM_THREADS // 2))
|
|
|
|
def test_read_while_replacing_name_attrs(self):
|
|
# Readers load __name__, __qualname__, __doc__ and __module__
|
|
# without holding a lock. A writer thread replacing them must not
|
|
# free a value a reader thread is about to incref.
|
|
def target():
|
|
yield
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
target.__name__ = random_string()
|
|
target.__qualname__ = random_string()
|
|
target.__doc__ = random_string()
|
|
target.__module__ = random_string()
|
|
|
|
def reader():
|
|
for _ in range(ITERS):
|
|
target.__name__
|
|
target.__qualname__
|
|
target.__doc__
|
|
target.__module__
|
|
# make_gen() copies __name__ and __qualname__.
|
|
gen = target()
|
|
gen.__name__
|
|
gen.__qualname__
|
|
gen.close()
|
|
|
|
threading_helper.run_concurrently(
|
|
[writer, reader] * (NUM_THREADS // 2))
|
|
|
|
def test_repr_while_replacing_qualname(self):
|
|
def target():
|
|
pass
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
target.__qualname__ = random_string()
|
|
|
|
def reader():
|
|
for _ in range(ITERS):
|
|
self.assertRegex(repr(target), r"^<function .* at 0x[0-9a-fA-F]+>$")
|
|
|
|
threading_helper.run_concurrently(
|
|
[writer, reader] * (NUM_THREADS // 2))
|
|
|
|
def test_c_api_read_while_replacing_module(self):
|
|
from test.support import import_helper
|
|
_testcapi = import_helper.import_module('_testcapi')
|
|
|
|
def target():
|
|
pass
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
target.__module__ = random_string()
|
|
target.__module__ = object()
|
|
target.__module__ = None
|
|
|
|
def reader():
|
|
for _ in range(ITERS):
|
|
for get_module in (_testcapi.function_get_module,
|
|
_testcapi.function_get_module_macro):
|
|
value = get_module(target)
|
|
self.assertIn(type(value), (str, object, type(None)))
|
|
|
|
threading_helper.run_concurrently(
|
|
[writer, reader] * (NUM_THREADS // 2))
|
|
|
|
def test_c_api_read_while_replacing_name(self):
|
|
from test.support import import_helper
|
|
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
|
|
|
|
def target():
|
|
pass
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
# Non-ASCII names exercise lazy UTF-8 conversion.
|
|
target.__name__ = 'name_\u03bb' + random_string()
|
|
|
|
def reader():
|
|
for _ in range(ITERS):
|
|
name = _testlimitedcapi.eval_get_func_name(target)
|
|
self.assertIsInstance(name, str)
|
|
|
|
threading_helper.run_concurrently(
|
|
[writer, reader] * (NUM_THREADS // 2))
|
|
|
|
def test_argument_errors_while_replacing_qualname(self):
|
|
def target(x, *, y):
|
|
pass
|
|
|
|
def posonly(x, /):
|
|
pass
|
|
|
|
calls = (
|
|
lambda: target(),
|
|
lambda: target(1),
|
|
lambda: target(1, 2),
|
|
lambda: target(1, x=2),
|
|
lambda: target(1, extra=2),
|
|
lambda: posonly(x=1),
|
|
)
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
target.__qualname__ = random_string()
|
|
posonly.__qualname__ = random_string()
|
|
|
|
def reader():
|
|
for _ in range(ITERS):
|
|
for call in calls:
|
|
with self.assertRaises(TypeError):
|
|
call()
|
|
|
|
threading_helper.run_concurrently(
|
|
[writer, reader] * (NUM_THREADS // 2))
|
|
|
|
def test_call_while_replacing_defaults(self):
|
|
# The eval loop reads __defaults__ and __kwdefaults__ without holding
|
|
# a lock while pushing a frame.
|
|
def target(x="init", *, y="init"):
|
|
return x, y
|
|
|
|
def writer():
|
|
for _ in range(ITERS):
|
|
target.__defaults__ = (random_string(),)
|
|
target.__kwdefaults__ = {"y": random_string()}
|
|
|
|
def caller():
|
|
for _ in range(ITERS):
|
|
target()
|
|
|
|
threading_helper.run_concurrently(
|
|
[writer, caller] * (NUM_THREADS // 2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|