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"^$") 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()