From ba99f2e455f29a5b3bd2f12f85e0b8985ec49bb2 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Sat, 28 Dec 2024 13:00:59 -0800 Subject: [PATCH 01/39] Make _contextvars a builtin module. --- Makefile.pre.in | 1 + Modules/Setup | 1 - Modules/Setup.stdlib.in | 1 - Modules/config.c.in | 4 ++++ PCbuild/pythoncore.vcxproj | 2 +- PCbuild/pythoncore.vcxproj.filters | 6 +++--- Modules/_contextvarsmodule.c => Python/_contextvars.c | 2 +- .../clinic/_contextvars.c.h | 0 configure.ac | 1 - 9 files changed, 10 insertions(+), 8 deletions(-) rename Modules/_contextvarsmodule.c => Python/_contextvars.c (97%) rename Modules/clinic/_contextvarsmodule.c.h => Python/clinic/_contextvars.c.h (100%) diff --git a/Makefile.pre.in b/Makefile.pre.in index 67acf0fc520087..18484a42abb6e8 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -420,6 +420,7 @@ PARSER_HEADERS= \ # Python PYTHON_OBJS= \ + Python/_contextvars.o \ Python/_warnings.o \ Python/Python-ast.o \ Python/Python-tokenize.o \ diff --git a/Modules/Setup b/Modules/Setup index ddf39e0b966610..e01c7bb1a8a45e 100644 --- a/Modules/Setup +++ b/Modules/Setup @@ -132,7 +132,6 @@ PYTHONPATH=$(COREPYTHONPATH) #_asyncio _asynciomodule.c #_bisect _bisectmodule.c -#_contextvars _contextvarsmodule.c #_csv _csv.c #_datetime _datetimemodule.c #_decimal _decimal/_decimal.c diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 6bb05a06a3465d..174e8339083f7a 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -31,7 +31,6 @@ @MODULE_ARRAY_TRUE@array arraymodule.c @MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c @MODULE__BISECT_TRUE@_bisect _bisectmodule.c -@MODULE__CONTEXTVARS_TRUE@_contextvars _contextvarsmodule.c @MODULE__CSV_TRUE@_csv _csv.c @MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c @MODULE__JSON_TRUE@_json _json.c diff --git a/Modules/config.c.in b/Modules/config.c.in index c578cd103dc629..704f58506048a3 100644 --- a/Modules/config.c.in +++ b/Modules/config.c.in @@ -19,6 +19,7 @@ extern PyObject* PyInit__imp(void); extern PyObject* PyInit_gc(void); extern PyObject* PyInit__ast(void); extern PyObject* PyInit__tokenize(void); +extern PyObject* PyInit__contextvars(void); extern PyObject* _PyWarnings_Init(void); extern PyObject* PyInit__string(void); @@ -45,6 +46,9 @@ struct _inittab _PyImport_Inittab[] = { /* This lives in gcmodule.c */ {"gc", PyInit_gc}, + /* This lives in Python/_contextvars.c */ + {"_contextvars", PyInit__contextvars}, + /* This lives in _warnings.c */ {"_warnings", _PyWarnings_Init}, diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 9ebf58ae8a9bc4..ef6dbf9f8e4222 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -423,7 +423,6 @@ - @@ -570,6 +569,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 6c76a6ab592a84..b661aad2019454 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -1262,6 +1262,9 @@ PC + + Python + Python @@ -1526,9 +1529,6 @@ Objects - - Modules - Modules\zlib diff --git a/Modules/_contextvarsmodule.c b/Python/_contextvars.c similarity index 97% rename from Modules/_contextvarsmodule.c rename to Python/_contextvars.c index 3f96f07909b69a..0f8b8004c1af22 100644 --- a/Modules/_contextvarsmodule.c +++ b/Python/_contextvars.c @@ -1,6 +1,6 @@ #include "Python.h" -#include "clinic/_contextvarsmodule.c.h" +#include "clinic/_contextvars.c.h" /*[clinic input] module _contextvars diff --git a/Modules/clinic/_contextvarsmodule.c.h b/Python/clinic/_contextvars.c.h similarity index 100% rename from Modules/clinic/_contextvarsmodule.c.h rename to Python/clinic/_contextvars.c.h diff --git a/configure.ac b/configure.ac index cf16e77f0a1503..ce10d518f24c6c 100644 --- a/configure.ac +++ b/configure.ac @@ -7775,7 +7775,6 @@ dnl always enabled extension modules PY_STDLIB_MOD_SIMPLE([array]) PY_STDLIB_MOD_SIMPLE([_asyncio]) PY_STDLIB_MOD_SIMPLE([_bisect]) -PY_STDLIB_MOD_SIMPLE([_contextvars]) PY_STDLIB_MOD_SIMPLE([_csv]) PY_STDLIB_MOD_SIMPLE([_heapq]) PY_STDLIB_MOD_SIMPLE([_json]) From 6d00c2aeec9a0e222d8df6383acde2f707f33bf6 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 20 Dec 2024 16:23:16 -0800 Subject: [PATCH 02/39] Add 'context' parameter to Thread. * Add ``sys.flags.inherit_context``. * Add ``-X inherit_context`` and :envvar:`PYTHON_INHERIT_CONTEXT` --- Doc/library/sys.rst | 15 ++++- Doc/library/threading.rst | 16 +++++- Doc/using/cmdline.rst | 19 +++++++ Include/cpython/initconfig.h | 1 + Lib/test/test_capi/test_config.py | 10 +++- Lib/test/test_context.py | 55 +++++++++++++++++++ Lib/test/test_decimal.py | 7 ++- Lib/test/test_embed.py | 7 ++- Lib/test/test_sys.py | 5 +- Lib/threading.py | 24 +++++++- ...-01-06-10-55-41.gh-issue-128555.tAK_AY.rst | 16 ++++++ Python/initconfig.c | 51 +++++++++++++++++ Python/sysmodule.c | 2 + 13 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 855237e0984972..43bda09323f557 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -535,7 +535,8 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. data:: flags The :term:`named tuple` *flags* exposes the status of command line - flags. The attributes are read only. + flags. Flags should only be accessed only by name and not by index. The + attributes are read only. .. list-table:: @@ -594,6 +595,12 @@ always available. Unless explicitly noted otherwise, all variables are read-only * - .. attribute:: flags.warn_default_encoding - :option:`-X warn_default_encoding <-X>` + * - .. attribute:: flags.gil + - :option:`-X gil <-X>` and :envvar:`PYTHON_GIL` + + * - .. attribute:: flags.inherit_context + - :option:`-X inherit_context <-X>` and :envvar:`PYTHON_INHERIT_CONTEXT` + .. versionchanged:: 3.2 Added ``quiet`` attribute for the new :option:`-q` flag. @@ -620,6 +627,12 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. versionchanged:: 3.11 Added the ``int_max_str_digits`` attribute. + .. versionchanged:: 3.13 + Added the ``gil`` attribute. + + .. versionchanged:: 3.14 + Added the ``inherit_context`` attribute. + .. data:: float_info diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 00511df32e4388..65e498aabb8016 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -334,7 +334,7 @@ since it is impossible to detect the termination of alien threads. .. class:: Thread(group=None, target=None, name=None, args=(), kwargs={}, *, \ - daemon=None) + daemon=None, context=None) This constructor should always be called with keyword arguments. Arguments are: @@ -359,6 +359,17 @@ since it is impossible to detect the termination of alien threads. If ``None`` (the default), the daemonic property is inherited from the current thread. + *context* is the :class:`~contextvars.Context` value to use when starting + the thread. The default value is ``None`` which indicates that the + :data:`sys.flags.inherit_context` flag controls the behaviour. If + the flag is true, threads will start with a copy of the context of the + caller of :meth:`~Thread.start`. If false, they will start with + an empty context. To explicitly start with an empty context, + pass a new instance of :class:`~contextvars.Context()`. To explicitly + start with a copy of the current context, pass the value from + :func:`~contextvars.copy_context()`. The flag defaults true on + free-threaded builds and false otherwise. + If the subclass overrides the constructor, it must make sure to invoke the base class constructor (``Thread.__init__()``) before doing anything else to the thread. @@ -369,6 +380,9 @@ since it is impossible to detect the termination of alien threads. .. versionchanged:: 3.10 Use the *target* name if *name* argument is omitted. + .. versionchanged:: 3.14 + Added the *context* parameter. + .. method:: start() Start the thread's activity. diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 2a59cf3f62d4c5..be5816f7c8d2b0 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -628,6 +628,15 @@ Miscellaneous options .. versionadded:: 3.13 + * :samp:`-X inherit_context={0,1}` causes :class:`~threading.Thread` + to, by default, use a copy of context of of the caller of + ``Thread.start()`` when starting. Otherwise, threads will start + with an empty context. If unset, the value of this option defaults + to ``1`` on free-threaded builds and to ``0`` otherwise. See also + :envvar:`PYTHON_INHERIT_CONTEXT`. + + .. versionadded:: 3.14 + It also allows passing arbitrary values and retrieving them through the :data:`sys._xoptions` dictionary. @@ -1221,6 +1230,16 @@ conflict. .. versionadded:: 3.13 +.. envvar:: PYTHON_INHERIT_CONTEXT + + If this variable is set to ``1`` then :class:`~threading.Thread` will, + by default, use a copy of context of of the caller of ``Thread.start()`` + when starting. Otherwise, new threads will start with an empty context. + If unset, this variable defaults to ``1`` on free-threaded builds and to + ``0`` otherwise. See also :option:`-X inherit_context<-X>`. + + .. versionadded:: 3.14 + Debug-mode variables ~~~~~~~~~~~~~~~~~~~~ diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 8ef19f677066c2..d8edfa973b96f4 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -179,6 +179,7 @@ typedef struct PyConfig { int use_frozen_modules; int safe_path; int int_max_str_digits; + int inherit_context; #ifdef __APPLE__ int use_system_logger; #endif diff --git a/Lib/test/test_capi/test_config.py b/Lib/test/test_capi/test_config.py index a3179efe4a8235..a54c11377993a8 100644 --- a/Lib/test/test_capi/test_config.py +++ b/Lib/test/test_capi/test_config.py @@ -55,6 +55,7 @@ def test_config_get(self): ("filesystem_errors", str, None), ("hash_seed", int, None), ("home", str | None, None), + ("inherit_context", int, None), ("import_time", bool, None), ("inspect", bool, None), ("install_signal_handlers", bool, None), @@ -98,7 +99,7 @@ def test_config_get(self): ] if support.Py_DEBUG: options.append(("run_presite", str | None, None)) - if sysconfig.get_config_var('Py_GIL_DISABLED'): + if support.Py_GIL_DISABLED: options.append(("enable_gil", int, None)) options.append(("tlbc_enabled", int, None)) if support.MS_WINDOWS: @@ -170,7 +171,7 @@ def test_config_get_sys_flags(self): ("warn_default_encoding", "warn_default_encoding", False), ("safe_path", "safe_path", False), ("int_max_str_digits", "int_max_str_digits", False), - # "gil" is tested below + # "gil" and "inherit_context" are tested below ): with self.subTest(flag=flag, name=name, negate=negate): value = config_get(name) @@ -182,11 +183,14 @@ def test_config_get_sys_flags(self): config_get('use_hash_seed') == 0 or config_get('hash_seed') != 0) - if sysconfig.get_config_var('Py_GIL_DISABLED'): + if support.Py_GIL_DISABLED: value = config_get('enable_gil') expected = (value if value != -1 else None) self.assertEqual(sys.flags.gil, expected) + expected_inherit_context = 1 if support.Py_GIL_DISABLED else 0 + self.assertEqual(sys.flags.inherit_context, expected_inherit_context) + def test_config_get_non_existent(self): # Test PyConfig_Get() on non-existent option name config_get = _testcapi.config_get diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index 82d1797ab3b79e..53ae7b65e1dfbf 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -1,3 +1,4 @@ +import sys import collections.abc import concurrent.futures import contextvars @@ -383,6 +384,60 @@ def sub(num): tp.shutdown() self.assertEqual(results, list(range(10))) + @isolated_context + @threading_helper.requires_working_threading() + def test_context_thread_inherit(self): + import threading + + cvar = contextvars.ContextVar('cvar') + + def run_context_none(): + if sys.flags.inherit_context: + expected = 1 + else: + expected = None + self.assertEqual(cvar.get(None), expected) + + # By default, context is inherited based on the + # sys.flags.inherit_context option. + cvar.set(1) + thread = threading.Thread(target=run_context_none) + thread.start() + thread.join() + + # Passing 'None' explicitly should have same behaviour as not + # passing parameter. + thread = threading.Thread(target=run_context_none, context=None) + thread.start() + thread.join() + + # An explicit Context value can also be passed + custom_ctx = contextvars.Context() + custom_var = None + + def setup_context(): + nonlocal custom_var + custom_var = contextvars.ContextVar('custom') + custom_var.set(2) + + custom_ctx.run(setup_context) + + def run_custom(): + self.assertEqual(custom_var.get(), 2) + + thread = threading.Thread(target=run_custom, context=custom_ctx) + thread.start() + thread.join() + + # You can also pass a new Context() object to start with an empty context + def run_empty(): + with self.assertRaises(LookupError): + cvar.get() + + thread = threading.Thread(target=run_empty, context=contextvars.Context()) + thread.start() + thread.join() + # HAMT Tests diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 02d3fa985e75b9..48a31e5098c1d1 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -44,6 +44,7 @@ import random import inspect import threading +import contextvars if sys.platform == 'darwin': @@ -1725,8 +1726,10 @@ def test_threading(self): self.finish1 = threading.Event() self.finish2 = threading.Event() - th1 = threading.Thread(target=thfunc1, args=(self,)) - th2 = threading.Thread(target=thfunc2, args=(self,)) + th1 = threading.Thread(target=thfunc1, args=(self,), + context=contextvars.Context()) + th2 = threading.Thread(target=thfunc2, args=(self,), + context=contextvars.Context()) th1.start() th2.start() diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index cd65496cafb04d..739e112073d036 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -50,7 +50,7 @@ INIT_LOOPS = 4 MAX_HASH_SEED = 4294967295 -ABI_THREAD = 't' if sysconfig.get_config_var('Py_GIL_DISABLED') else '' +ABI_THREAD = 't' if support.Py_GIL_DISABLED else '' # PLATSTDLIB_LANDMARK copied from Modules/getpath.py if os.name == 'nt': PLATSTDLIB_LANDMARK = f'{sys.platlibdir}' @@ -60,6 +60,10 @@ PLATSTDLIB_LANDMARK = (f'{sys.platlibdir}/python{VERSION_MAJOR}.' f'{VERSION_MINOR}{ABI_THREAD}/lib-dynload') +if support.Py_GIL_DISABLED: + DEFAULT_INHERIT_CONTEXT = 1 +else: + DEFAULT_INHERIT_CONTEXT = 0 # If we are running from a build dir, but the stdlib has been installed, # some tests need to expect different results. @@ -586,6 +590,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'tracemalloc': 0, 'perf_profiling': 0, 'import_time': False, + 'inherit_context': DEFAULT_INHERIT_CONTEXT, 'code_debug_ranges': True, 'show_ref_count': False, 'dump_refs': False, diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 39857445a02255..d782874fbe80dd 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1845,8 +1845,9 @@ def test_pythontypes(self): # symtable entry # XXX # sys.flags - # FIXME: The +1 will not be necessary once gh-122575 is fixed - check(sys.flags, vsize('') + self.P * (1 + len(sys.flags))) + # FIXME: The +2 is for the 'gil' and 'inherit_context' flags and + # will not be necessary once gh-122575 is fixed + check(sys.flags, vsize('') + self.P * (2 + len(sys.flags))) def test_asyncgen_hooks(self): old = sys.get_asyncgen_hooks() diff --git a/Lib/threading.py b/Lib/threading.py index da9cdf0b09d83c..eec76a175bfa1e 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -3,6 +3,7 @@ import os as _os import sys as _sys import _thread +import _contextvars from time import monotonic as _time from _weakrefset import WeakSet @@ -871,7 +872,7 @@ class Thread: _initialized = False def __init__(self, group=None, target=None, name=None, - args=(), kwargs=None, *, daemon=None): + args=(), kwargs=None, *, daemon=None, context=None): """This constructor should always be called with keyword arguments. Arguments are: *group* should be None; reserved for future extension when a ThreadGroup @@ -888,6 +889,14 @@ class is implemented. *kwargs* is a dictionary of keyword arguments for the target invocation. Defaults to {}. + *context* is the contextvars.Context value to use for the thread. + The default value is None, which means to check + sys.flags.inherit_context. If that flag is true, use a copy of + the context of the caller. If false, use an empty context. To + explicitly start with an empty context, pass a new instance of + contextvars.Context(). To explicitly start with a copy of the + current context, pass the value from contextvars.copy_context(). + If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread. @@ -917,6 +926,7 @@ class is implemented. self._daemonic = daemon else: self._daemonic = current_thread().daemon + self._context = context self._ident = None if _HAVE_THREAD_NATIVE_ID: self._native_id = None @@ -972,6 +982,16 @@ def start(self): with _active_limbo_lock: _limbo[self] = self + + if self._context is None: + # No context provided + if _sys.flags.inherit_context: + # start with a copy of the context of the caller + self._context = _contextvars.copy_context() + else: + # start with an empty context + self._context = _contextvars.Context() + try: # Start joinable thread _start_joinable_thread(self._bootstrap, handle=self._handle, @@ -1051,7 +1071,7 @@ def _bootstrap_inner(self): _sys.setprofile(_profile_hook) try: - self.run() + self._context.run(self.run) except: self._invoke_excepthook(self) finally: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst new file mode 100644 index 00000000000000..fd48a047a6d185 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst @@ -0,0 +1,16 @@ +Add the :data:`sys.flags.inherit_context` flag. + +* This flag is set to true by default on the free-threaded build + and false otherwise. If the flag is true, starting a new thread using + :class:`threading.Thread` will, by default, use a copy of the + :class:`contextvars.Context` from the caller of + :meth:`threading.Thread.start` rather than using an empty context. + +* Add the :option:`-X inherit_context <-X>` command-line option and + :envvar:`PYTHON_INHERIT_CONTEXT` environment variable, which set the + :data:`~sys.flags.inherit_context` flag. + +* Add the ``context`` keyword parameter to :class:`~threading.Thread`. It can + be used to explicitly pass a context value to be used by a new thread. + +* Make the :mod:`_contextvars` module built-in. diff --git a/Python/initconfig.c b/Python/initconfig.c index 4db77ef47d2362..187796b6c16c1a 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -141,6 +141,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(filesystem_errors, WSTR, READ_ONLY, NO_SYS), SPEC(hash_seed, ULONG, READ_ONLY, NO_SYS), SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS), + SPEC(inherit_context, INT, READ_ONLY, NO_SYS), SPEC(import_time, BOOL, READ_ONLY, NO_SYS), SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS), SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated @@ -302,6 +303,9 @@ The following implementation-specific options are available:\n\ -X importtime: show how long each import takes; also PYTHONPROFILEIMPORTTIME\n\ -X int_max_str_digits=N: limit the size of int<->str conversions;\n\ 0 disables the limit; also PYTHONINTMAXSTRDIGITS\n\ +-X inherit_context=[0|1]: enable (1) or disable (0) threads inheriting context\n\ + vars by default; enabled by default in the free-threaded build and\n\ + disabled otherwise; also PYTHON_INHERIT_CONTEXT\n\ -X no_debug_ranges: don't include extra location information in code objects;\n\ also PYTHONNODEBUGRANGES\n\ -X perf: support the Linux \"perf\" profiler; also PYTHONPERFSUPPORT=1\n\ @@ -887,6 +891,7 @@ config_check_consistency(const PyConfig *config) assert(config->cpu_count != 0); // config->use_frozen_modules is initialized later // by _PyConfig_InitImportConfig(). + assert(config->inherit_context >= 0); #ifdef __APPLE__ assert(config->use_system_logger >= 0); #endif @@ -992,6 +997,11 @@ _PyConfig_InitCompatConfig(PyConfig *config) config->_is_python_build = 0; config->code_debug_ranges = 1; config->cpu_count = -1; +#ifdef Py_GIL_DISABLED + config->inherit_context = 1; +#else + config->inherit_context = 0; +#endif #ifdef __APPLE__ config->use_system_logger = 0; #endif @@ -1024,6 +1034,11 @@ config_init_defaults(PyConfig *config) #ifdef MS_WINDOWS config->legacy_windows_stdio = 0; #endif +#ifdef Py_GIL_DISABLED + config->inherit_context = 1; +#else + config->inherit_context = 0; +#endif #ifdef __APPLE__ config->use_system_logger = 0; #endif @@ -1058,6 +1073,11 @@ PyConfig_InitIsolatedConfig(PyConfig *config) config->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS; config->safe_path = 1; config->pathconfig_warnings = 0; +#ifdef Py_GIL_DISABLED + config->inherit_context = 1; +#else + config->inherit_context = 0; +#endif #ifdef MS_WINDOWS config->legacy_windows_stdio = 0; #endif @@ -1887,6 +1907,32 @@ config_init_cpu_count(PyConfig *config) "n must be greater than 0"); } +static PyStatus +config_init_inherit_context(PyConfig *config) +{ + const char *env = config_get_env(config, "PYTHON_INHERIT_CONTEXT"); + if (env) { + int enabled; + if (_Py_str_to_int(env, &enabled) < 0 || (enabled < 0) || (enabled > 1)) { + return _PyStatus_ERR( + "PYTHON_INHERIT_CONTEXT=N: N is missing or invalid"); + } + config->inherit_context = enabled; + } + + const wchar_t *xoption = config_get_xoption(config, L"inherit_context"); + if (xoption) { + int enabled; + const wchar_t *sep = wcschr(xoption, L'='); + if (!sep || (config_wstr_to_int(sep + 1, &enabled) < 0) || (enabled < 0) || (enabled > 1)) { + return _PyStatus_ERR( + "-X inherit_context=n: n is missing or invalid"); + } + config->inherit_context = enabled; + } + return _PyStatus_OK(); +} + static PyStatus config_init_tlbc(PyConfig *config) { @@ -2166,6 +2212,11 @@ config_read_complex_options(PyConfig *config) } #endif + status = config_init_inherit_context(config); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + status = config_init_tlbc(config); if (_PyStatus_EXCEPTION(status)) { return status; diff --git a/Python/sysmodule.c b/Python/sysmodule.c index d5cb448eb618e8..6ab9d2b17a8453 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3141,6 +3141,7 @@ static PyStructSequence_Field flags_fields[] = { {"safe_path", "-P"}, {"int_max_str_digits", "-X int_max_str_digits"}, {"gil", "-X gil"}, + {"inherit_context", "-X inherit_context"}, {0} }; @@ -3244,6 +3245,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags) #else SetFlagObj(PyLong_FromLong(1)); #endif + SetFlag(config->inherit_context); #undef SetFlagObj #undef SetFlag return 0; From a868fe97356f97f56468d43b3ca23b4b95636f62 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 6 Feb 2025 16:10:34 -0800 Subject: [PATCH 03/39] Tweak blurb markup. --- .../2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst index fd48a047a6d185..22c7399bee2204 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst @@ -13,4 +13,4 @@ Add the :data:`sys.flags.inherit_context` flag. * Add the ``context`` keyword parameter to :class:`~threading.Thread`. It can be used to explicitly pass a context value to be used by a new thread. -* Make the :mod:`_contextvars` module built-in. +* Make the ``_contextvars`` module built-in. From 16fa2c3891e143eca728a84b6942670ff810276d Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 6 Feb 2025 16:13:00 -0800 Subject: [PATCH 04/39] Doc markup fix. --- Doc/library/threading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 65e498aabb8016..d333a094f5883e 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -367,7 +367,7 @@ since it is impossible to detect the termination of alien threads. an empty context. To explicitly start with an empty context, pass a new instance of :class:`~contextvars.Context()`. To explicitly start with a copy of the current context, pass the value from - :func:`~contextvars.copy_context()`. The flag defaults true on + :func:`~contextvars.copy_context`. The flag defaults true on free-threaded builds and false otherwise. If the subclass overrides the constructor, it must make sure to invoke the From f0ccc8d21ae6c99ed4d706dcfbbfb4f1a0deca63 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 27 Dec 2024 13:31:18 -0800 Subject: [PATCH 05/39] Use contextvar for catch_warnings(). --- Doc/library/warnings.rst | 79 ++++++-- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 4 + Lib/test/test_warnings/__init__.py | 56 ++++-- Lib/warnings.py | 110 ++++++++-- Python/_warnings.c | 190 ++++++++++++++---- 8 files changed, 359 insertions(+), 83 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 0c7e8543f331db..39f31dedecdca6 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -324,11 +324,13 @@ the warning using the :class:`catch_warnings` context manager:: While within the context manager all warnings will simply be ignored. This allows you to use known-deprecated code without having to see the warning while not suppressing the warning for other code that might not be aware of its use -of deprecated code. Note: this can only be guaranteed in a single-threaded -application. If two or more threads use the :class:`catch_warnings` context -manager at the same time, the behavior is undefined. +of deprecated code. + .. note:: + See :ref:`warning-thread-safe` for details on the thread-safety of the + :class:`catch_warnings` context manager when used in multi-threaded + programs. .. _warning-testing: @@ -364,10 +366,13 @@ the warning has been cleared. Once the context manager exits, the warnings filter is restored to its state when the context was entered. This prevents tests from changing the warnings filter in unexpected ways between tests and leading to indeterminate test -results. The :func:`showwarning` function in the module is also restored to -its original value. Note: this can only be guaranteed in a single-threaded -application. If two or more threads use the :class:`catch_warnings` context -manager at the same time, the behavior is undefined. +results. + + .. note:: + + See :ref:`warning-thread-safe` for details on the thread-safety of the + :class:`catch_warnings` context manager when used in multi-threaded + programs. When testing multiple operations that raise the same kind of warning, it is important to test them in a manner that confirms each operation is raising @@ -615,12 +620,62 @@ Available Context Managers .. note:: - The :class:`catch_warnings` manager works by replacing and - then later restoring the module's - :func:`showwarning` function and internal list of filter - specifications. This means the context manager is modifying - global state and therefore is not thread-safe. + See :ref:`warning-thread-safe` for details on the thread-safety of the + :class:`catch_warnings` context manager when used in multi-threaded + programs. + .. versionchanged:: 3.11 Added the *action*, *category*, *lineno*, and *append* parameters. + + +.. _warning-thread-safe: + +Thread-safety of Context Managers +--------------------------------- + +The behavior of :class:`catch_warnings` context manager depends on the value +of the :data:`sys.flags.inherit_context` flag. Being thread-safe means that +behavior is predictable in a multi-threaded program. For free-threaded +builds, the flag defaults to true, and false otherwise. + +If the :data:`~sys.flags.inherit_context` flag is false, then +:class:`catch_warnings` will manipulate the global attributes of the +:mod:`warnings` module. This is not thread-safe. If two or more threads use +the context manager at the same time, the behavior is undefined. + +If the flag is true, :class:`catch_warnings` will not manipulate global +attributes and will instead use a :class:`~contextvars.ContextVar` to +store the newly established warning filtering state. A context variable +provides thread-local storage and it makes the use of :class:`catch_warnings` +thread-safe. + +The *record* parameter of the context handler also behaves differently +depending on the value of the flag. When *record* is true and the flag is +false, the context manager works by replacing and then later restoring the +module's :func:`showwarning` function. This is not thread-safe. + +When *record* is true and the flag is false, the :func:`showwarning` function +is not replaced. The recording status is instead indicated by an internal +property in the context variable. In this case, the :func:`showwarning` +function will not be restored when exiting the context handler. + +The :data:`~sys.flags.inherit_context` flag can be set the :option:`-X +inherit_context <-X>` command-line option or by the +:envvar:`PYTHON_INHERIT_CONTEXT` environment variable. + + .. note:: + + When the :data:`~sys.flags.inherit_context` flag true, it also causes + threads created by :class:`threading.Thread` to start with a copy of + the context variables from the thread starting it. This means that + context established by using :class:`catch_warnings` in one thread + will also apply to new threads started by it. If the new thread creates + a new context with :class:`catch_warnings`, that context only applies to + that thread. + +.. versionchanged:: 3.14 + + Added the :data:`sys.flags.inherit_context` flag and the use of a context + variable for :class:`catch_warnings` if the flag is true. diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 90214a314031d1..5172f4f63d2c55 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -776,6 +776,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_type_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_uninitialized_submodules)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_warn_unawaited_coroutine)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_warnings_context)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_xoptions)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(abs_tol)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(access)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 97a75d0c46c867..433fe58714ed72 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -265,6 +265,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(_type_) STRUCT_FOR_ID(_uninitialized_submodules) STRUCT_FOR_ID(_warn_unawaited_coroutine) + STRUCT_FOR_ID(_warnings_context) STRUCT_FOR_ID(_xoptions) STRUCT_FOR_ID(abs_tol) STRUCT_FOR_ID(access) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 4f928cc050bf8e..cf32fe451b73e5 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -774,6 +774,7 @@ extern "C" { INIT_ID(_type_), \ INIT_ID(_uninitialized_submodules), \ INIT_ID(_warn_unawaited_coroutine), \ + INIT_ID(_warnings_context), \ INIT_ID(_xoptions), \ INIT_ID(abs_tol), \ INIT_ID(access), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 5b78d038fc1192..de8d27f1892b5b 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -856,6 +856,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_warnings_context); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_xoptions); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 4bd164b8a9a82b..e981f9f81f71a1 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -43,15 +43,21 @@ def warnings_state(module): except NameError: pass original_warnings = warning_tests.warnings - original_filters = module.filters - try: + if module._use_context: + saved_context, context = module._new_context() + else: + original_filters = module.filters module.filters = original_filters[:] + try: module.simplefilter("once") warning_tests.warnings = module yield finally: warning_tests.warnings = original_warnings - module.filters = original_filters + if module._use_context: + module._set_context(saved_context) + else: + module.filters = original_filters class TestWarning(Warning): @@ -336,15 +342,15 @@ def test_filterwarnings_duplicate_filters(self): with original_warnings.catch_warnings(module=self.module): self.module.resetwarnings() self.module.filterwarnings("error", category=UserWarning) - self.assertEqual(len(self.module.filters), 1) + self.assertEqual(len(self.module._get_filters()), 1) self.module.filterwarnings("ignore", category=UserWarning) self.module.filterwarnings("error", category=UserWarning) self.assertEqual( - len(self.module.filters), 2, + len(self.module._get_filters()), 2, "filterwarnings inserted duplicate filter" ) self.assertEqual( - self.module.filters[0][0], "error", + self.module._get_filters()[0][0], "error", "filterwarnings did not promote filter to " "the beginning of list" ) @@ -353,15 +359,15 @@ def test_simplefilter_duplicate_filters(self): with original_warnings.catch_warnings(module=self.module): self.module.resetwarnings() self.module.simplefilter("error", category=UserWarning) - self.assertEqual(len(self.module.filters), 1) + self.assertEqual(len(self.module._get_filters()), 1) self.module.simplefilter("ignore", category=UserWarning) self.module.simplefilter("error", category=UserWarning) self.assertEqual( - len(self.module.filters), 2, + len(self.module._get_filters()), 2, "simplefilter inserted duplicate filter" ) self.assertEqual( - self.module.filters[0][0], "error", + self.module._get_filters()[0][0], "error", "simplefilter did not promote filter to the beginning of list" ) @@ -373,7 +379,7 @@ def test_append_duplicate(self): self.module.simplefilter("error", append=True) self.module.simplefilter("ignore", append=True) self.module.warn("test_append_duplicate", category=UserWarning) - self.assertEqual(len(self.module.filters), 2, + self.assertEqual(len(self.module._get_filters()), 2, "simplefilter inserted duplicate filter" ) self.assertEqual(len(w), 0, @@ -906,6 +912,10 @@ def test_default_action(self): def test_showwarning_missing(self): # Test that showwarning() missing is okay. + if self.module._use_context: + # If _use_context is true, the warnings module does not + # override/restore showwarning() + return text = 'del showwarning test' with original_warnings.catch_warnings(module=self.module): self.module.filterwarnings("always", category=UserWarning) @@ -1049,11 +1059,11 @@ def test_issue31416(self): # bad warnings.filters or warnings.defaultaction. wmod = self.module with original_warnings.catch_warnings(module=wmod): - wmod.filters = [(None, None, Warning, None, 0)] + wmod._get_filters()[:] = [(None, None, Warning, None, 0)] with self.assertRaises(TypeError): wmod.warn_explicit('foo', Warning, 'bar', 1) - wmod.filters = [] + wmod._get_filters()[:] = [] with support.swap_attr(wmod, 'defaultaction', None), \ self.assertRaises(TypeError): wmod.warn_explicit('foo', Warning, 'bar', 1) @@ -1190,6 +1200,8 @@ class CatchWarningTests(BaseTest): """Test catch_warnings().""" def test_catch_warnings_restore(self): + if self.module._use_context: + return # test disabled if using context vars wmod = self.module orig_filters = wmod.filters orig_showwarning = wmod.showwarning @@ -1240,25 +1252,29 @@ def test_catch_warnings_reentry_guard(self): def test_catch_warnings_defaults(self): wmod = self.module - orig_filters = wmod.filters + orig_filters = wmod._get_filters() orig_showwarning = wmod.showwarning # Ensure default behaviour is not to record warnings with wmod.catch_warnings(module=wmod) as w: self.assertIsNone(w) self.assertIs(wmod.showwarning, orig_showwarning) - self.assertIsNot(wmod.filters, orig_filters) - self.assertIs(wmod.filters, orig_filters) + self.assertIsNot(wmod._get_filters(), orig_filters) + self.assertIs(wmod._get_filters(), orig_filters) if wmod is sys.modules['warnings']: # Ensure the default module is this one with wmod.catch_warnings() as w: self.assertIsNone(w) self.assertIs(wmod.showwarning, orig_showwarning) - self.assertIsNot(wmod.filters, orig_filters) - self.assertIs(wmod.filters, orig_filters) + self.assertIsNot(wmod._get_filters(), orig_filters) + self.assertIs(wmod._get_filters(), orig_filters) def test_record_override_showwarning_before(self): # Issue #28835: If warnings.showwarning() was overridden, make sure # that catch_warnings(record=True) overrides it again. + if self.module._use_context: + # If _use_context is true, the warnings module does not restore + # showwarning() + return text = "This is a warning" wmod = self.module my_log = [] @@ -1284,6 +1300,10 @@ def my_logger(message, category, filename, lineno, file=None, line=None): def test_record_override_showwarning_inside(self): # Issue #28835: It is possible to override warnings.showwarning() # in the catch_warnings(record=True) context manager. + if self.module._use_context: + # If _use_context is true, the warnings module does not restore + # showwarning() + return text = "This is a warning" wmod = self.module my_log = [] @@ -1406,7 +1426,7 @@ def test_default_filter_configuration(self): code = "import sys; sys.modules.pop('warnings', None); sys.modules['_warnings'] = None; " else: code = "" - code += "import warnings; [print(f) for f in warnings.filters]" + code += "import warnings; [print(f) for f in warnings._get_filters()]" rc, stdout, stderr = assert_python_ok("-c", code, __isolated=True) stdout_lines = [line.strip() for line in stdout.splitlines()] diff --git a/Lib/warnings.py b/Lib/warnings.py index f20b01372dd7a4..dacbdf0006d0cc 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -1,12 +1,77 @@ """Python part of the warnings subsystem.""" import sys +import _contextvars __all__ = ["warn", "warn_explicit", "showwarning", "formatwarning", "filterwarnings", "simplefilter", "resetwarnings", "catch_warnings", "deprecated"] +# If true, catch_warnings() will use a context var to hold the modified +# filters list. Otherwise, catch_warnings() will operate on the 'filters' +# global of the warnings module. +_use_context = sys.flags.inherit_context + +class _Context: + def __init__(self, filters): + self._filters = filters + self.log = None # if set to a list, logging is enabled + + def copy(self): + context = _Context(self._filters[:]) + if self.log is not None: + context.log = self.log + return context + + def _record_warning(self, msg): + self.log.append(msg) + + +class _GlobalContext(_Context): + def __init__(self): + self.log = None + + @property + def _filters(self): + # Since there is quite a lot of code that assigns to + # warnings.filters, this needs to return the current value of + # the module global. + try: + return filters + except NameError: + # 'filters' global was deleted. Do we need to actually handle this case? + return [] + + +_global_context = _GlobalContext() + +_warnings_context = _contextvars.ContextVar('warnings_context') + +def _get_context(): + if not _use_context: + return _global_context + try: + return _warnings_context.get() + except LookupError: + return _global_context + +def _set_context(context): + assert _use_context + _warnings_context.set(context) + +def _new_context(): + assert _use_context + old_context = _get_context() + new_context = old_context.copy() + _set_context(new_context) + return old_context, new_context + +def _get_filters(): + """Return the current list of filters. This is a non-public API used by + module functions and by the unit tests.""" + return _get_context()._filters + def showwarning(message, category, filename, lineno, file=None, line=None): """Hook to write a warning to a file; replace if you like.""" msg = WarningMessage(message, category, filename, lineno, file, line) @@ -18,6 +83,10 @@ def formatwarning(message, category, filename, lineno, line=None): return _formatwarnmsg_impl(msg) def _showwarnmsg_impl(msg): + context = _get_context() + if context.log is not None: + context._record_warning(msg) + return file = msg.file if file is None: file = sys.stderr @@ -193,6 +262,7 @@ def _filters_mutated(): def _add_filter(*item, append): with _lock: + filters = _get_filters() if not append: # Remove possible duplicate filters, so new one will be placed # in correct place. If append=True and duplicate exists, do nothing. @@ -209,7 +279,7 @@ def _add_filter(*item, append): def resetwarnings(): """Clear the list of warning filters, so that no filters are active.""" with _lock: - filters[:] = [] + del _get_filters()[:] _filters_mutated_lock_held() class _OptionError(Exception): @@ -378,7 +448,7 @@ def warn_explicit(message, category, filename, lineno, if registry.get(key): return # Search the filters - for item in filters: + for item in _get_filters(): action, msg, cat, mod, ln = item if ((msg is None or msg.match(text)) and issubclass(category, cat) and @@ -499,31 +569,41 @@ def __enter__(self): raise RuntimeError("Cannot enter %r twice" % self) self._entered = True with _lock: - self._filters = self._module.filters - self._module.filters = self._filters[:] + if _use_context: + self._saved_context, context = self._module._new_context() + else: + context = None + self._filters = self._module.filters + self._module.filters = self._filters[:] + self._showwarning = self._module.showwarning + self._showwarnmsg_impl = self._module._showwarnmsg_impl self._module._filters_mutated_lock_held() - self._showwarning = self._module.showwarning - self._showwarnmsg_impl = self._module._showwarnmsg_impl if self._record: - log = [] - self._module._showwarnmsg_impl = log.append - # Reset showwarning() to the default implementation to make sure - # that _showwarnmsg() calls _showwarnmsg_impl() - self._module.showwarning = self._module._showwarning_orig + if _use_context: + context.log = log = [] + else: + log = [] + self._module._showwarnmsg_impl = log.append + # Reset showwarning() to the default implementation to make sure + # that _showwarnmsg() calls _showwarnmsg_impl() + self._module.showwarning = self._module._showwarning_orig else: log = None if self._filter is not None: - simplefilter(*self._filter) + self._module.simplefilter(*self._filter) return log def __exit__(self, *exc_info): if not self._entered: raise RuntimeError("Cannot exit %r without entering first" % self) with _lock: - self._module.filters = self._filters + if _use_context: + self._module._warnings_context.set(self._saved_context) + else: + self._module.filters = self._filters + self._module.showwarning = self._showwarning + self._module._showwarnmsg_impl = self._showwarnmsg_impl self._module._filters_mutated_lock_held() - self._module.showwarning = self._showwarning - self._module._showwarnmsg_impl = self._showwarnmsg_impl class deprecated: diff --git a/Python/_warnings.c b/Python/_warnings.c index bb195da9512caf..874d6efdadafe8 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -254,6 +254,87 @@ warnings_lock_held(WarningsState *st) return PyMutex_IsLocked(&st->lock.mutex); } +static PyObject * +get_warnings_context(PyInterpreterState *interp) +{ + PyObject *ctx_var = GET_WARNINGS_ATTR(interp, _warnings_context, 0); + if (ctx_var == NULL) { + if (!PyErr_Occurred()) { + // likely that the 'warnings' module doesn't exist anymore + Py_RETURN_NONE; + } + else { + return NULL; + } + } + if (!PyContextVar_CheckExact(ctx_var)) { + PyErr_Format(PyExc_TypeError, + MODULE_NAME "._warnings_context must be a ContextVar, " + "not '%.200s'", + Py_TYPE(ctx_var)->tp_name); + Py_DECREF(ctx_var); + return NULL; + } + PyObject *ctx; + if (PyContextVar_Get(ctx_var, NULL, &ctx) < 0) { + Py_DECREF(ctx_var); + return NULL; + } + Py_DECREF(ctx_var); + if (ctx == NULL) { + Py_RETURN_NONE; + } + return ctx; +} + +static PyObject * +get_warnings_context_filters(PyInterpreterState *interp) +{ + PyObject *ctx = get_warnings_context(interp); + if (ctx == NULL) { + return NULL; + } + if (ctx == Py_None) { + Py_DECREF(ctx); + Py_RETURN_NONE; + } + PyObject *context_filters = PyObject_GetAttrString(ctx, "_filters"); + Py_DECREF(ctx); + if (context_filters == NULL) { + return NULL; + } + if (!PyList_Check(context_filters)) { + PyErr_SetString(PyExc_ValueError, + "warnings._warnings_context _filters must be a list"); + Py_DECREF(context_filters); + return NULL; + } + return context_filters; +} + +// Returns a borrowed reference to the list. +static PyObject * +get_warnings_filters(PyInterpreterState *interp) +{ + WarningsState *st = warnings_get_state(interp); + PyObject *warnings_filters = GET_WARNINGS_ATTR(interp, filters, 0); + if (warnings_filters == NULL) { + if (PyErr_Occurred()) + return NULL; + } + else { + Py_SETREF(st->filters, warnings_filters); + } + + PyObject *filters = st->filters; + if (filters == NULL || !PyList_Check(filters)) { + PyErr_SetString(PyExc_ValueError, + MODULE_NAME ".filters must be a list"); + return NULL; + } + return filters; +} + /*[clinic input] _acquire_lock as warnings_acquire_lock @@ -344,35 +425,14 @@ get_default_action(PyInterpreterState *interp) return default_action; } - -/* The item is a new reference. */ -static PyObject* -get_filter(PyInterpreterState *interp, PyObject *category, - PyObject *text, Py_ssize_t lineno, - PyObject *module, PyObject **item) -{ - WarningsState *st = warnings_get_state(interp); - assert(st != NULL); - - assert(warnings_lock_held(st)); - - PyObject *warnings_filters = GET_WARNINGS_ATTR(interp, filters, 0); - if (warnings_filters == NULL) { - if (PyErr_Occurred()) - return NULL; - } - else { - Py_SETREF(st->filters, warnings_filters); - } - - PyObject *filters = st->filters; - if (filters == NULL || !PyList_Check(filters)) { - PyErr_SetString(PyExc_ValueError, - MODULE_NAME ".filters must be a list"); - return NULL; - } - - /* WarningsState.filters could change while we are iterating over it. */ +/* Search filters list of match, returns false on error. If no match + * then 'matched_action' is NULL. */ +static bool +filter_search(PyInterpreterState *interp, PyObject *category, + PyObject *text, Py_ssize_t lineno, + PyObject *module, char *list_name, PyObject *filters, + PyObject **item, PyObject **matched_action) { + /* filters list could change while we are iterating over it. */ for (Py_ssize_t i = 0; i < PyList_GET_SIZE(filters); i++) { PyObject *tmp_item, *action, *msg, *cat, *mod, *ln_obj; Py_ssize_t ln; @@ -381,8 +441,8 @@ get_filter(PyInterpreterState *interp, PyObject *category, tmp_item = PyList_GET_ITEM(filters, i); if (!PyTuple_Check(tmp_item) || PyTuple_GET_SIZE(tmp_item) != 5) { PyErr_Format(PyExc_ValueError, - MODULE_NAME ".filters item %zd isn't a 5-tuple", i); - return NULL; + "warnings.%s item %zd isn't a 5-tuple", list_name, i); + return false; } /* Python code: action, msg, cat, mod, ln = item */ @@ -398,42 +458,96 @@ get_filter(PyInterpreterState *interp, PyObject *category, "action must be a string, not '%.200s'", Py_TYPE(action)->tp_name); Py_DECREF(tmp_item); - return NULL; + return false; } good_msg = check_matched(interp, msg, text); if (good_msg == -1) { Py_DECREF(tmp_item); - return NULL; + return false; } good_mod = check_matched(interp, mod, module); if (good_mod == -1) { Py_DECREF(tmp_item); - return NULL; + return false; } is_subclass = PyObject_IsSubclass(category, cat); if (is_subclass == -1) { Py_DECREF(tmp_item); - return NULL; + return false; } ln = PyLong_AsSsize_t(ln_obj); if (ln == -1 && PyErr_Occurred()) { Py_DECREF(tmp_item); - return NULL; + return false; } if (good_msg && is_subclass && good_mod && (ln == 0 || lineno == ln)) { *item = tmp_item; - return action; + *matched_action = action; + return true; } Py_DECREF(tmp_item); } + *matched_action = NULL; + return true; +} + +/* The item is a new reference. */ +static PyObject* +get_filter(PyInterpreterState *interp, PyObject *category, + PyObject *text, Py_ssize_t lineno, + PyObject *module, PyObject **item) +{ + WarningsState *st = warnings_get_state(interp); + assert(st != NULL); + + assert(warnings_lock_held(st)); + + /* check _warning_context _filters list */ + PyObject *context_filters = get_warnings_context_filters(interp); + bool use_global_filters = false; + if (context_filters == NULL) { + return NULL; + } + if (context_filters == Py_None) { + use_global_filters = true; + Py_DECREF(context_filters); + } else { + PyObject *context_action = NULL; + if (!filter_search(interp, category, text, lineno, module, "_warnings_context _filters", + context_filters, item, &context_action)) { + Py_DECREF(context_filters); + return NULL; + } + Py_DECREF(context_filters); + if (context_action != NULL) { + return context_action; + } + } + + PyObject *action; + + if (use_global_filters) { + /* check warnings.filters list */ + PyObject *filters = get_warnings_filters(interp); + if (filters == NULL) { + return NULL; + } + if (!filter_search(interp, category, text, lineno, module, "filters", + filters, item, &action)) { + return NULL; + } + if (action != NULL) { + return action; + } + } - PyObject *action = get_default_action(interp); + action = get_default_action(interp); if (action != NULL) { *item = Py_NewRef(Py_None); return action; From bcadd20b918e16f730d085b3e550e38ed2a49a6c Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 10:22:15 -0800 Subject: [PATCH 06/39] Add blurb. --- .../Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst diff --git a/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst b/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst new file mode 100644 index 00000000000000..dd4a0fe7d77cde --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst @@ -0,0 +1,7 @@ +Make :class:`warnings.catch_warnings` use a context variable for holding the +warning filtering state if the :data:`sys.flags.inherit_context` flag is set +to true. This makes using the context manager thread-safe in multi-threaded +programs. The flag is true by default in free-threaded builds and is +otherwise false. The default value of the flag can be overridden by the the +:option:`-X inherit_context <-X>` command-line option or by the +:envvar:`PYTHON_INHERIT_CONTEXT` environment variable. From 0cfe578fbf8912b131b0762e7e013ab8bb583869 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 10:35:39 -0800 Subject: [PATCH 07/39] Add "_warnings_context" as identifier. --- Tools/build/generate_global_objects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/build/generate_global_objects.py b/Tools/build/generate_global_objects.py index b5b6de0e7dc2dc..1c580e1bf18f67 100644 --- a/Tools/build/generate_global_objects.py +++ b/Tools/build/generate_global_objects.py @@ -29,6 +29,7 @@ 'defaultaction', 'filters', 'onceregistry', + '_warnings_context', # from WRAP_METHOD() in Objects/weakrefobject.c '__bytes__', From 928c2dfcca6dee85bbfc38ee8a3fa79028c676d5 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 11:07:05 -0800 Subject: [PATCH 08/39] Fix test_support for context var filters. Since the unittest/runner.py establishes a new warnings context with `catch_warnings`, these tests must use `warnings._get_filters()` to retrieve the current list of filters, not the `warnings.filters` global. --- Lib/test/support/__init__.py | 7 ++++--- Lib/test/test_support.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index f31d98bf731d67..ee4f1f88fc4eb1 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2359,8 +2359,9 @@ def clear_ignored_deprecations(*tokens: object) -> None: raise ValueError("Provide token or tokens returned by ignore_deprecations_from") new_filters = [] + old_filters = warnings._get_filters() endswith = tuple(rf"(?#support{id(token)})" for token in tokens) - for action, message, category, module, lineno in warnings.filters: + for action, message, category, module, lineno in old_filters: if action == "ignore" and category is DeprecationWarning: if isinstance(message, re.Pattern): msg = message.pattern @@ -2369,8 +2370,8 @@ def clear_ignored_deprecations(*tokens: object) -> None: if msg.endswith(endswith): continue new_filters.append((action, message, category, module, lineno)) - if warnings.filters != new_filters: - warnings.filters[:] = new_filters + if old_filters != new_filters: + old_filters[:] = new_filters warnings._filters_mutated() diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index d900db546ada8d..de2581f45df0ac 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -27,23 +27,23 @@ class TestSupport(unittest.TestCase): @classmethod def setUpClass(cls): - orig_filter_len = len(warnings.filters) + orig_filter_len = len(warnings._get_filters()) cls._warnings_helper_token = support.ignore_deprecations_from( "test.support.warnings_helper", like=".*used in test_support.*" ) cls._test_support_token = support.ignore_deprecations_from( __name__, like=".*You should NOT be seeing this.*" ) - assert len(warnings.filters) == orig_filter_len + 2 + assert len(warnings._get_filters()) == orig_filter_len + 2 @classmethod def tearDownClass(cls): - orig_filter_len = len(warnings.filters) + orig_filter_len = len(warnings._get_filters()) support.clear_ignored_deprecations( cls._warnings_helper_token, cls._test_support_token, ) - assert len(warnings.filters) == orig_filter_len - 2 + assert len(warnings._get_filters()) == orig_filter_len - 2 def test_ignored_deprecations_are_silent(self): """Test support.ignore_deprecations_from() silences warnings""" From 65751d9f7c09bcf26b6d1c279f8f039abe4ecc34 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 12:41:50 -0800 Subject: [PATCH 09/39] Regenerate 'configure' script. --- configure | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/configure b/configure index 22456959add174..1508ae8fb032b7 100755 --- a/configure +++ b/configure @@ -801,8 +801,6 @@ MODULE__HEAPQ_FALSE MODULE__HEAPQ_TRUE MODULE__CSV_FALSE MODULE__CSV_TRUE -MODULE__CONTEXTVARS_FALSE -MODULE__CONTEXTVARS_TRUE MODULE__BISECT_FALSE MODULE__BISECT_TRUE MODULE__ASYNCIO_FALSE @@ -30724,28 +30722,6 @@ then : -fi - - - if test "$py_cv_module__contextvars" != "n/a" -then : - py_cv_module__contextvars=yes -fi - if test "$py_cv_module__contextvars" = yes; then - MODULE__CONTEXTVARS_TRUE= - MODULE__CONTEXTVARS_FALSE='#' -else - MODULE__CONTEXTVARS_TRUE='#' - MODULE__CONTEXTVARS_FALSE= -fi - - as_fn_append MODULE_BLOCK "MODULE__CONTEXTVARS_STATE=$py_cv_module__contextvars$as_nl" - if test "x$py_cv_module__contextvars" = xyes -then : - - - - fi @@ -33596,10 +33572,6 @@ if test -z "${MODULE__BISECT_TRUE}" && test -z "${MODULE__BISECT_FALSE}"; then as_fn_error $? "conditional \"MODULE__BISECT\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi -if test -z "${MODULE__CONTEXTVARS_TRUE}" && test -z "${MODULE__CONTEXTVARS_FALSE}"; then - as_fn_error $? "conditional \"MODULE__CONTEXTVARS\" was never defined. -Usually this means the macro was only invoked conditionally." "$LINENO" 5 -fi if test -z "${MODULE__CSV_TRUE}" && test -z "${MODULE__CSV_FALSE}"; then as_fn_error $? "conditional \"MODULE__CSV\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 From 09e72b8d76ec25b1b23477ff7b8beb4218ee26a4 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 15:23:37 -0800 Subject: [PATCH 10/39] Rename flag to `thread_inherit_context`. --- Doc/library/sys.rst | 7 ++-- Doc/library/threading.rst | 13 +++--- Doc/using/cmdline.rst | 8 ++-- Include/cpython/initconfig.h | 2 +- Lib/test/test_capi/test_config.py | 6 +-- Lib/test/test_context.py | 4 +- Lib/test/test_embed.py | 6 +-- Lib/test/test_sys.py | 2 +- Lib/threading.py | 10 ++--- ...-01-06-10-55-41.gh-issue-128555.tAK_AY.rst | 8 ++-- Python/initconfig.c | 40 ++++++++++--------- Python/sysmodule.c | 4 +- 12 files changed, 56 insertions(+), 54 deletions(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 43bda09323f557..779b179988b935 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -598,8 +598,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only * - .. attribute:: flags.gil - :option:`-X gil <-X>` and :envvar:`PYTHON_GIL` - * - .. attribute:: flags.inherit_context - - :option:`-X inherit_context <-X>` and :envvar:`PYTHON_INHERIT_CONTEXT` + * - .. attribute:: flags.thread_inherit_context + - :option:`-X thread_inherit_context <-X>` and + :envvar:`PYTHON_THREAD_INHERIT_CONTEXT` .. versionchanged:: 3.2 Added ``quiet`` attribute for the new :option:`-q` flag. @@ -631,7 +632,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only Added the ``gil`` attribute. .. versionchanged:: 3.14 - Added the ``inherit_context`` attribute. + Added the ``thread_inherit_context`` attribute. .. data:: float_info diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index d333a094f5883e..8049f2979f768a 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -361,14 +361,13 @@ since it is impossible to detect the termination of alien threads. *context* is the :class:`~contextvars.Context` value to use when starting the thread. The default value is ``None`` which indicates that the - :data:`sys.flags.inherit_context` flag controls the behaviour. If + :data:`sys.flags.thread_inherit_context` flag controls the behaviour. If the flag is true, threads will start with a copy of the context of the - caller of :meth:`~Thread.start`. If false, they will start with - an empty context. To explicitly start with an empty context, - pass a new instance of :class:`~contextvars.Context()`. To explicitly - start with a copy of the current context, pass the value from - :func:`~contextvars.copy_context`. The flag defaults true on - free-threaded builds and false otherwise. + caller of :meth:`~Thread.start`. If false, they will start with an empty + context. To explicitly start with an empty context, pass a new instance of + :class:`~contextvars.Context()`. To explicitly start with a copy of the + current context, pass the value from :func:`~contextvars.copy_context`. The + flag defaults true on free-threaded builds and false otherwise. If the subclass overrides the constructor, it must make sure to invoke the base class constructor (``Thread.__init__()``) before doing anything else to diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index be5816f7c8d2b0..8a4d4fbcd94433 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -628,12 +628,12 @@ Miscellaneous options .. versionadded:: 3.13 - * :samp:`-X inherit_context={0,1}` causes :class:`~threading.Thread` + * :samp:`-X thread_inherit_context={0,1}` causes :class:`~threading.Thread` to, by default, use a copy of context of of the caller of ``Thread.start()`` when starting. Otherwise, threads will start with an empty context. If unset, the value of this option defaults to ``1`` on free-threaded builds and to ``0`` otherwise. See also - :envvar:`PYTHON_INHERIT_CONTEXT`. + :envvar:`PYTHON_THREAD_INHERIT_CONTEXT`. .. versionadded:: 3.14 @@ -1230,13 +1230,13 @@ conflict. .. versionadded:: 3.13 -.. envvar:: PYTHON_INHERIT_CONTEXT +.. envvar:: PYTHON_THREAD_INHERIT_CONTEXT If this variable is set to ``1`` then :class:`~threading.Thread` will, by default, use a copy of context of of the caller of ``Thread.start()`` when starting. Otherwise, new threads will start with an empty context. If unset, this variable defaults to ``1`` on free-threaded builds and to - ``0`` otherwise. See also :option:`-X inherit_context<-X>`. + ``0`` otherwise. See also :option:`-X thread_inherit_context<-X>`. .. versionadded:: 3.14 diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index d8edfa973b96f4..202f4a8964736e 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -179,7 +179,7 @@ typedef struct PyConfig { int use_frozen_modules; int safe_path; int int_max_str_digits; - int inherit_context; + int thread_inherit_context; #ifdef __APPLE__ int use_system_logger; #endif diff --git a/Lib/test/test_capi/test_config.py b/Lib/test/test_capi/test_config.py index a54c11377993a8..21c66a7b613335 100644 --- a/Lib/test/test_capi/test_config.py +++ b/Lib/test/test_capi/test_config.py @@ -55,7 +55,7 @@ def test_config_get(self): ("filesystem_errors", str, None), ("hash_seed", int, None), ("home", str | None, None), - ("inherit_context", int, None), + ("thread_inherit_context", int, None), ("import_time", bool, None), ("inspect", bool, None), ("install_signal_handlers", bool, None), @@ -171,7 +171,7 @@ def test_config_get_sys_flags(self): ("warn_default_encoding", "warn_default_encoding", False), ("safe_path", "safe_path", False), ("int_max_str_digits", "int_max_str_digits", False), - # "gil" and "inherit_context" are tested below + # "gil" and "thread_inherit_context" are tested below ): with self.subTest(flag=flag, name=name, negate=negate): value = config_get(name) @@ -189,7 +189,7 @@ def test_config_get_sys_flags(self): self.assertEqual(sys.flags.gil, expected) expected_inherit_context = 1 if support.Py_GIL_DISABLED else 0 - self.assertEqual(sys.flags.inherit_context, expected_inherit_context) + self.assertEqual(sys.flags.thread_inherit_context, expected_inherit_context) def test_config_get_non_existent(self): # Test PyConfig_Get() on non-existent option name diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index 53ae7b65e1dfbf..b026f5cb64a571 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -392,14 +392,14 @@ def test_context_thread_inherit(self): cvar = contextvars.ContextVar('cvar') def run_context_none(): - if sys.flags.inherit_context: + if sys.flags.thread_inherit_context: expected = 1 else: expected = None self.assertEqual(cvar.get(None), expected) # By default, context is inherited based on the - # sys.flags.inherit_context option. + # sys.flags.thread_inherit_context option. cvar.set(1) thread = threading.Thread(target=run_context_none) thread.start() diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 739e112073d036..772d62320a5ba9 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -61,9 +61,9 @@ f'{VERSION_MINOR}{ABI_THREAD}/lib-dynload') if support.Py_GIL_DISABLED: - DEFAULT_INHERIT_CONTEXT = 1 + DEFAULT_THREAD_INHERIT_CONTEXT = 1 else: - DEFAULT_INHERIT_CONTEXT = 0 + DEFAULT_THREAD_INHERIT_CONTEXT = 0 # If we are running from a build dir, but the stdlib has been installed, # some tests need to expect different results. @@ -590,7 +590,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'tracemalloc': 0, 'perf_profiling': 0, 'import_time': False, - 'inherit_context': DEFAULT_INHERIT_CONTEXT, + 'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT, 'code_debug_ranges': True, 'show_ref_count': False, 'dump_refs': False, diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index d782874fbe80dd..b58355700f2c6d 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1845,7 +1845,7 @@ def test_pythontypes(self): # symtable entry # XXX # sys.flags - # FIXME: The +2 is for the 'gil' and 'inherit_context' flags and + # FIXME: The +2 is for the 'gil' and 'thread_inherit_context' flags and # will not be necessary once gh-122575 is fixed check(sys.flags, vsize('') + self.P * (2 + len(sys.flags))) diff --git a/Lib/threading.py b/Lib/threading.py index eec76a175bfa1e..fc27de3f7a2d79 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -891,11 +891,11 @@ class is implemented. *context* is the contextvars.Context value to use for the thread. The default value is None, which means to check - sys.flags.inherit_context. If that flag is true, use a copy of - the context of the caller. If false, use an empty context. To + sys.flags.thread_inherit_context. If that flag is true, use a copy + of the context of the caller. If false, use an empty context. To explicitly start with an empty context, pass a new instance of - contextvars.Context(). To explicitly start with a copy of the - current context, pass the value from contextvars.copy_context(). + contextvars.Context(). To explicitly start with a copy of the current + context, pass the value from contextvars.copy_context(). If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything @@ -985,7 +985,7 @@ def start(self): if self._context is None: # No context provided - if _sys.flags.inherit_context: + if _sys.flags.thread_inherit_context: # start with a copy of the context of the caller self._context = _contextvars.copy_context() else: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst index 22c7399bee2204..e0b468e76a062b 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst @@ -1,4 +1,4 @@ -Add the :data:`sys.flags.inherit_context` flag. +Add the :data:`sys.flags.thread_inherit_context` flag. * This flag is set to true by default on the free-threaded build and false otherwise. If the flag is true, starting a new thread using @@ -6,9 +6,9 @@ Add the :data:`sys.flags.inherit_context` flag. :class:`contextvars.Context` from the caller of :meth:`threading.Thread.start` rather than using an empty context. -* Add the :option:`-X inherit_context <-X>` command-line option and - :envvar:`PYTHON_INHERIT_CONTEXT` environment variable, which set the - :data:`~sys.flags.inherit_context` flag. +* Add the :option:`-X thread_inherit_context <-X>` command-line option and + :envvar:`PYTHON_THREAD_INHERIT_CONTEXT` environment variable, which set the + :data:`~sys.flags.thread_inherit_context` flag. * Add the ``context`` keyword parameter to :class:`~threading.Thread`. It can be used to explicitly pass a context value to be used by a new thread. diff --git a/Python/initconfig.c b/Python/initconfig.c index 187796b6c16c1a..02d43c0236c28e 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -141,7 +141,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(filesystem_errors, WSTR, READ_ONLY, NO_SYS), SPEC(hash_seed, ULONG, READ_ONLY, NO_SYS), SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS), - SPEC(inherit_context, INT, READ_ONLY, NO_SYS), + SPEC(thread_inherit_context, INT, READ_ONLY, NO_SYS), SPEC(import_time, BOOL, READ_ONLY, NO_SYS), SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS), SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated @@ -303,9 +303,6 @@ The following implementation-specific options are available:\n\ -X importtime: show how long each import takes; also PYTHONPROFILEIMPORTTIME\n\ -X int_max_str_digits=N: limit the size of int<->str conversions;\n\ 0 disables the limit; also PYTHONINTMAXSTRDIGITS\n\ --X inherit_context=[0|1]: enable (1) or disable (0) threads inheriting context\n\ - vars by default; enabled by default in the free-threaded build and\n\ - disabled otherwise; also PYTHON_INHERIT_CONTEXT\n\ -X no_debug_ranges: don't include extra location information in code objects;\n\ also PYTHONNODEBUGRANGES\n\ -X perf: support the Linux \"perf\" profiler; also PYTHONPERFSUPPORT=1\n\ @@ -329,6 +326,9 @@ The following implementation-specific options are available:\n\ PYTHON_TLBC\n" #endif "\ +-X thread_inherit_context=[0|1]: enable (1) or disable (0) threads inheriting\n\ + context vars by default; enabled by default in the free-threaded\n\ + build and disabled otherwise; also PYTHON_THREAD_INHERIT_CONTEXT\n\ -X tracemalloc[=N]: trace Python memory allocations; N sets a traceback limit\n \ of N frames (default: 1); also PYTHONTRACEMALLOC=N\n\ -X utf8[=0|1]: enable (1) or disable (0) UTF-8 mode; also PYTHONUTF8\n\ @@ -416,6 +416,8 @@ static const char usage_envvars[] = #ifdef Py_GIL_DISABLED "PYTHON_TLBC : when set to 0, disables thread-local bytecode (-X tlbc)\n" #endif +"PYTHON_THREAD_INHERIT_CONTEXT: threads inherit context vars if 1\n" +" (-X thread_inherit_context)\n" "PYTHONTRACEMALLOC: trace Python memory allocations (-X tracemalloc)\n" "PYTHONUNBUFFERED: disable stdout/stderr buffering (-u)\n" "PYTHONUTF8 : control the UTF-8 mode (-X utf8)\n" @@ -891,7 +893,7 @@ config_check_consistency(const PyConfig *config) assert(config->cpu_count != 0); // config->use_frozen_modules is initialized later // by _PyConfig_InitImportConfig(). - assert(config->inherit_context >= 0); + assert(config->thread_inherit_context >= 0); #ifdef __APPLE__ assert(config->use_system_logger >= 0); #endif @@ -998,9 +1000,9 @@ _PyConfig_InitCompatConfig(PyConfig *config) config->code_debug_ranges = 1; config->cpu_count = -1; #ifdef Py_GIL_DISABLED - config->inherit_context = 1; + config->thread_inherit_context = 1; #else - config->inherit_context = 0; + config->thread_inherit_context = 0; #endif #ifdef __APPLE__ config->use_system_logger = 0; @@ -1035,9 +1037,9 @@ config_init_defaults(PyConfig *config) config->legacy_windows_stdio = 0; #endif #ifdef Py_GIL_DISABLED - config->inherit_context = 1; + config->thread_inherit_context = 1; #else - config->inherit_context = 0; + config->thread_inherit_context = 0; #endif #ifdef __APPLE__ config->use_system_logger = 0; @@ -1074,9 +1076,9 @@ PyConfig_InitIsolatedConfig(PyConfig *config) config->safe_path = 1; config->pathconfig_warnings = 0; #ifdef Py_GIL_DISABLED - config->inherit_context = 1; + config->thread_inherit_context = 1; #else - config->inherit_context = 0; + config->thread_inherit_context = 0; #endif #ifdef MS_WINDOWS config->legacy_windows_stdio = 0; @@ -1908,27 +1910,27 @@ config_init_cpu_count(PyConfig *config) } static PyStatus -config_init_inherit_context(PyConfig *config) +config_init_thread_inherit_context(PyConfig *config) { - const char *env = config_get_env(config, "PYTHON_INHERIT_CONTEXT"); + const char *env = config_get_env(config, "PYTHON_THREAD_INHERIT_CONTEXT"); if (env) { int enabled; if (_Py_str_to_int(env, &enabled) < 0 || (enabled < 0) || (enabled > 1)) { return _PyStatus_ERR( - "PYTHON_INHERIT_CONTEXT=N: N is missing or invalid"); + "PYTHON_THREAD_INHERIT_CONTEXT=N: N is missing or invalid"); } - config->inherit_context = enabled; + config->thread_inherit_context = enabled; } - const wchar_t *xoption = config_get_xoption(config, L"inherit_context"); + const wchar_t *xoption = config_get_xoption(config, L"thread_inherit_context"); if (xoption) { int enabled; const wchar_t *sep = wcschr(xoption, L'='); if (!sep || (config_wstr_to_int(sep + 1, &enabled) < 0) || (enabled < 0) || (enabled > 1)) { return _PyStatus_ERR( - "-X inherit_context=n: n is missing or invalid"); + "-X thread_inherit_context=n: n is missing or invalid"); } - config->inherit_context = enabled; + config->thread_inherit_context = enabled; } return _PyStatus_OK(); } @@ -2212,7 +2214,7 @@ config_read_complex_options(PyConfig *config) } #endif - status = config_init_inherit_context(config); + status = config_init_thread_inherit_context(config); if (_PyStatus_EXCEPTION(status)) { return status; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 6ab9d2b17a8453..71285c074be066 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3141,7 +3141,7 @@ static PyStructSequence_Field flags_fields[] = { {"safe_path", "-P"}, {"int_max_str_digits", "-X int_max_str_digits"}, {"gil", "-X gil"}, - {"inherit_context", "-X inherit_context"}, + {"thread_inherit_context", "-X thread_inherit_context"}, {0} }; @@ -3245,7 +3245,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags) #else SetFlagObj(PyLong_FromLong(1)); #endif - SetFlag(config->inherit_context); + SetFlag(config->thread_inherit_context); #undef SetFlagObj #undef SetFlag return 0; From 872920d525f3938bd22c623d4e0041f012688aba Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 12:41:50 -0800 Subject: [PATCH 11/39] Regenerate 'configure' script. --- configure | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/configure b/configure index d46bc563a67245..260a49fc83149b 100755 --- a/configure +++ b/configure @@ -801,8 +801,6 @@ MODULE__HEAPQ_FALSE MODULE__HEAPQ_TRUE MODULE__CSV_FALSE MODULE__CSV_TRUE -MODULE__CONTEXTVARS_FALSE -MODULE__CONTEXTVARS_TRUE MODULE__BISECT_FALSE MODULE__BISECT_TRUE MODULE__ASYNCIO_FALSE @@ -30727,28 +30725,6 @@ then : -fi - - - if test "$py_cv_module__contextvars" != "n/a" -then : - py_cv_module__contextvars=yes -fi - if test "$py_cv_module__contextvars" = yes; then - MODULE__CONTEXTVARS_TRUE= - MODULE__CONTEXTVARS_FALSE='#' -else - MODULE__CONTEXTVARS_TRUE='#' - MODULE__CONTEXTVARS_FALSE= -fi - - as_fn_append MODULE_BLOCK "MODULE__CONTEXTVARS_STATE=$py_cv_module__contextvars$as_nl" - if test "x$py_cv_module__contextvars" = xyes -then : - - - - fi @@ -33599,10 +33575,6 @@ if test -z "${MODULE__BISECT_TRUE}" && test -z "${MODULE__BISECT_FALSE}"; then as_fn_error $? "conditional \"MODULE__BISECT\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi -if test -z "${MODULE__CONTEXTVARS_TRUE}" && test -z "${MODULE__CONTEXTVARS_FALSE}"; then - as_fn_error $? "conditional \"MODULE__CONTEXTVARS\" was never defined. -Usually this means the macro was only invoked conditionally." "$LINENO" 5 -fi if test -z "${MODULE__CSV_TRUE}" && test -z "${MODULE__CSV_FALSE}"; then as_fn_error $? "conditional \"MODULE__CSV\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 From 9e955dc723d60d1f0d1017781fa098579f83d1bb Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Feb 2025 19:10:47 -0800 Subject: [PATCH 12/39] Use separate flag `thread_safe_warnings`. --- Doc/library/warnings.rst | 41 +++++++++------- Doc/using/cmdline.rst | 18 +++++++ Include/cpython/initconfig.h | 1 + Lib/test/test_capi/test_config.py | 6 ++- Lib/test/test_embed.py | 7 ++- Lib/test/test_sys.py | 7 +-- Lib/warnings.py | 2 +- ...-02-11-10-22-11.gh-issue-128384.jyWEkA.rst | 14 +++--- Python/initconfig.c | 47 ++++++++++++++++++- Python/sysmodule.c | 2 + 10 files changed, 110 insertions(+), 35 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 39f31dedecdca6..c5d6555458e608 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -636,16 +636,17 @@ Thread-safety of Context Managers --------------------------------- The behavior of :class:`catch_warnings` context manager depends on the value -of the :data:`sys.flags.inherit_context` flag. Being thread-safe means that -behavior is predictable in a multi-threaded program. For free-threaded -builds, the flag defaults to true, and false otherwise. +of the :data:`sys.flags.thread_safe_warnings` flag. If the flag is true, the +context manager behaves in a thread-safe fashion and otherwise not. Being +thread-safe means that behavior is predictable in a multi-threaded program. +For free-threaded builds, the flag defaults to true, and false otherwise. -If the :data:`~sys.flags.inherit_context` flag is false, then -:class:`catch_warnings` will manipulate the global attributes of the +If the :data:`~sys.flags.thread_safe_warnings` flag is false, then +:class:`catch_warnings` will modify the global attributes of the :mod:`warnings` module. This is not thread-safe. If two or more threads use the context manager at the same time, the behavior is undefined. -If the flag is true, :class:`catch_warnings` will not manipulate global +If the flag is true, :class:`catch_warnings` will not modify global attributes and will instead use a :class:`~contextvars.ContextVar` to store the newly established warning filtering state. A context variable provides thread-local storage and it makes the use of :class:`catch_warnings` @@ -661,21 +662,25 @@ is not replaced. The recording status is instead indicated by an internal property in the context variable. In this case, the :func:`showwarning` function will not be restored when exiting the context handler. -The :data:`~sys.flags.inherit_context` flag can be set the :option:`-X -inherit_context <-X>` command-line option or by the -:envvar:`PYTHON_INHERIT_CONTEXT` environment variable. +The :data:`~sys.flags.thread_safe_warnings` flag can be set the :option:`-X +thread_safe_warnings<-X>` command-line option or by the +:envvar:`PYTHON_THREAD_SAFE_WARNINGS` environment variable. .. note:: - When the :data:`~sys.flags.inherit_context` flag true, it also causes - threads created by :class:`threading.Thread` to start with a copy of - the context variables from the thread starting it. This means that - context established by using :class:`catch_warnings` in one thread - will also apply to new threads started by it. If the new thread creates - a new context with :class:`catch_warnings`, that context only applies to - that thread. + It is likely that most programs that desire thread-safe + behaviour of the warnings module will also want to set the + :data:`~sys.flags.thread_inherit_context` flag to true. That flag + causes threads created by :class:`threading.Thread` to start + with a copy of the context variables from the thread starting + it. When true, the context established by :class:`catch_warnings` + in one thread will also apply to new threads started by it. If false, + new threads will start with an empty warnings context variable, + meaning that only the filters in :data:`warnings.filters` will be + active. .. versionchanged:: 3.14 - Added the :data:`sys.flags.inherit_context` flag and the use of a context - variable for :class:`catch_warnings` if the flag is true. + Added the :data:`sys.flags.thread_safe_warnings` flag and the use of a + context variable for :class:`catch_warnings` if the flag is true. Previous + versions of Python acted as if the flag was always set to false. diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 8a4d4fbcd94433..38587bdfed38f0 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -637,6 +637,14 @@ Miscellaneous options .. versionadded:: 3.14 + * :samp:`-X thread_safe_warnings={0,1}` causes the + :class:`warnings.catch_warnings` context manager to use a + :class:`~contextvars.ContextVar` to store warnings filter state. If + unset, the value of this option defaults to ``1`` on free-threaded builds + and to ``0`` otherwise. See also :envvar:`PYTHON_THREAD_SAFE_WARNINGS`. + + .. versionadded:: 3.14 + It also allows passing arbitrary values and retrieving them through the :data:`sys._xoptions` dictionary. @@ -1240,6 +1248,16 @@ conflict. .. versionadded:: 3.14 +.. envvar:: PYTHON_THREAD_SAFE_WARNINGS + + If set to ``1`` then the :class:`warnings.catch_warnings` context + manager will use a :class:`~contextvars.ContextVar` to store warnings + filter state. If unset, this variable defaults to ``1`` on + free-threaded builds and to ``0`` otherwise. See :option:`-X + thread_safe_warnings<-X>`. + + .. versionadded:: 3.14 + Debug-mode variables ~~~~~~~~~~~~~~~~~~~~ diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 202f4a8964736e..a30f166256d594 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -180,6 +180,7 @@ typedef struct PyConfig { int safe_path; int int_max_str_digits; int thread_inherit_context; + int thread_safe_warnings; #ifdef __APPLE__ int use_system_logger; #endif diff --git a/Lib/test/test_capi/test_config.py b/Lib/test/test_capi/test_config.py index 21c66a7b613335..f3169cf397b98b 100644 --- a/Lib/test/test_capi/test_config.py +++ b/Lib/test/test_capi/test_config.py @@ -56,6 +56,7 @@ def test_config_get(self): ("hash_seed", int, None), ("home", str | None, None), ("thread_inherit_context", int, None), + ("thread_safe_warnings", int, None), ("import_time", bool, None), ("inspect", bool, None), ("install_signal_handlers", bool, None), @@ -171,7 +172,7 @@ def test_config_get_sys_flags(self): ("warn_default_encoding", "warn_default_encoding", False), ("safe_path", "safe_path", False), ("int_max_str_digits", "int_max_str_digits", False), - # "gil" and "thread_inherit_context" are tested below + # "gil", "thread_inherit_context" and "thread_safe_warnings" are tested below ): with self.subTest(flag=flag, name=name, negate=negate): value = config_get(name) @@ -191,6 +192,9 @@ def test_config_get_sys_flags(self): expected_inherit_context = 1 if support.Py_GIL_DISABLED else 0 self.assertEqual(sys.flags.thread_inherit_context, expected_inherit_context) + expected_safe_warnings = 1 if support.Py_GIL_DISABLED else 0 + self.assertEqual(sys.flags.thread_safe_warnings, expected_safe_warnings) + def test_config_get_non_existent(self): # Test PyConfig_Get() on non-existent option name config_get = _testcapi.config_get diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 772d62320a5ba9..0ea1fcf034bc88 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -60,10 +60,8 @@ PLATSTDLIB_LANDMARK = (f'{sys.platlibdir}/python{VERSION_MAJOR}.' f'{VERSION_MINOR}{ABI_THREAD}/lib-dynload') -if support.Py_GIL_DISABLED: - DEFAULT_THREAD_INHERIT_CONTEXT = 1 -else: - DEFAULT_THREAD_INHERIT_CONTEXT = 0 +DEFAULT_THREAD_INHERIT_CONTEXT = 1 if support.Py_GIL_DISABLED else 0 +DEFAULT_THREAD_SAFE_WARNINGS = 1 if support.Py_GIL_DISABLED else 0 # If we are running from a build dir, but the stdlib has been installed, # some tests need to expect different results. @@ -591,6 +589,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'perf_profiling': 0, 'import_time': False, 'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT, + 'thread_safe_warnings': DEFAULT_THREAD_SAFE_WARNINGS, 'code_debug_ranges': True, 'show_ref_count': False, 'dump_refs': False, diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index b58355700f2c6d..bfc47427743987 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1845,9 +1845,10 @@ def test_pythontypes(self): # symtable entry # XXX # sys.flags - # FIXME: The +2 is for the 'gil' and 'thread_inherit_context' flags and - # will not be necessary once gh-122575 is fixed - check(sys.flags, vsize('') + self.P * (2 + len(sys.flags))) + # FIXME: The +3 is for the 'gil', 'thread_inherit_context' and + # 'thread_safe_warnings' flags and will not be necessary once + # gh-122575 is fixed + check(sys.flags, vsize('') + self.P * (3 + len(sys.flags))) def test_asyncgen_hooks(self): old = sys.get_asyncgen_hooks() diff --git a/Lib/warnings.py b/Lib/warnings.py index 061445b09590b8..1cc28f0bfc5734 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -11,7 +11,7 @@ # If true, catch_warnings() will use a context var to hold the modified # filters list. Otherwise, catch_warnings() will operate on the 'filters' # global of the warnings module. -_use_context = sys.flags.inherit_context +_use_context = sys.flags.thread_safe_warnings class _Context: def __init__(self, filters): diff --git a/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst b/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst index dd4a0fe7d77cde..3d48febb6d3430 100644 --- a/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst +++ b/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst @@ -1,7 +1,7 @@ -Make :class:`warnings.catch_warnings` use a context variable for holding the -warning filtering state if the :data:`sys.flags.inherit_context` flag is set -to true. This makes using the context manager thread-safe in multi-threaded -programs. The flag is true by default in free-threaded builds and is -otherwise false. The default value of the flag can be overridden by the the -:option:`-X inherit_context <-X>` command-line option or by the -:envvar:`PYTHON_INHERIT_CONTEXT` environment variable. +Make :class:`warnings.catch_warnings` use a context variable for holding +the warning filtering state if the :data:`sys.flags.thread_safe_warnings` +flag is set to true. This makes using the context manager thread-safe in +multi-threaded programs. The flag is true by default in free-threaded builds +and is otherwise false. The value of the flag can be overridden by the +the :option:`-X thread_safe_warnings <-X>` command-line option or by the +:envvar:`PYTHON_THREAD_SAFE_WARNINGS` environment variable. diff --git a/Python/initconfig.c b/Python/initconfig.c index 02d43c0236c28e..d3004f91c10325 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -142,6 +142,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(hash_seed, ULONG, READ_ONLY, NO_SYS), SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS), SPEC(thread_inherit_context, INT, READ_ONLY, NO_SYS), + SPEC(thread_safe_warnings, INT, READ_ONLY, NO_SYS), SPEC(import_time, BOOL, READ_ONLY, NO_SYS), SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS), SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated @@ -329,6 +330,12 @@ The following implementation-specific options are available:\n\ -X thread_inherit_context=[0|1]: enable (1) or disable (0) threads inheriting\n\ context vars by default; enabled by default in the free-threaded\n\ build and disabled otherwise; also PYTHON_THREAD_INHERIT_CONTEXT\n\ +-X thread_safe_warnings=[0|1]: if true (1) then the warnings module will\n\ + use a context variable to store warnings filtering state, making it\n\ + safe to use in multi-threaded programs; if false (0) then the\n\ + warnings module will use module globals, which is not thread-safe;\n\ + set to true for free-threaded builds and false otherwise; also\n\ + PYTHON_THREAD_SAFE_WARNINGS\n\ -X tracemalloc[=N]: trace Python memory allocations; N sets a traceback limit\n \ of N frames (default: 1); also PYTHONTRACEMALLOC=N\n\ -X utf8[=0|1]: enable (1) or disable (0) UTF-8 mode; also PYTHONUTF8\n\ @@ -416,8 +423,10 @@ static const char usage_envvars[] = #ifdef Py_GIL_DISABLED "PYTHON_TLBC : when set to 0, disables thread-local bytecode (-X tlbc)\n" #endif -"PYTHON_THREAD_INHERIT_CONTEXT: threads inherit context vars if 1\n" +"PYTHON_THREAD_INHERIT_CONTEXT: if true (1), threads inherit context vars\n" " (-X thread_inherit_context)\n" +"PYTHON_THREAD_SAFE_WARNINGS: if true (1), enable thread-safe warnings module\n" +" behaviour (-X thread_safe_warnings)\n" "PYTHONTRACEMALLOC: trace Python memory allocations (-X tracemalloc)\n" "PYTHONUNBUFFERED: disable stdout/stderr buffering (-u)\n" "PYTHONUTF8 : control the UTF-8 mode (-X utf8)\n" @@ -894,6 +903,7 @@ config_check_consistency(const PyConfig *config) // config->use_frozen_modules is initialized later // by _PyConfig_InitImportConfig(). assert(config->thread_inherit_context >= 0); + assert(config->thread_safe_warnings >= 0); #ifdef __APPLE__ assert(config->use_system_logger >= 0); #endif @@ -1001,8 +1011,10 @@ _PyConfig_InitCompatConfig(PyConfig *config) config->cpu_count = -1; #ifdef Py_GIL_DISABLED config->thread_inherit_context = 1; + config->thread_safe_warnings = 1; #else config->thread_inherit_context = 0; + config->thread_safe_warnings = 0; #endif #ifdef __APPLE__ config->use_system_logger = 0; @@ -1038,8 +1050,10 @@ config_init_defaults(PyConfig *config) #endif #ifdef Py_GIL_DISABLED config->thread_inherit_context = 1; + config->thread_safe_warnings = 1; #else config->thread_inherit_context = 0; + config->thread_safe_warnings = 0; #endif #ifdef __APPLE__ config->use_system_logger = 0; @@ -1935,6 +1949,32 @@ config_init_thread_inherit_context(PyConfig *config) return _PyStatus_OK(); } +static PyStatus +config_init_thread_safe_warnings(PyConfig *config) +{ + const char *env = config_get_env(config, "PYTHON_THREAD_SAFE_WARNINGS"); + if (env) { + int enabled; + if (_Py_str_to_int(env, &enabled) < 0 || (enabled < 0) || (enabled > 1)) { + return _PyStatus_ERR( + "PYTHON_THREAD_SAFE_WARNINGS=N: N is missing or invalid"); + } + config->thread_safe_warnings = enabled; + } + + const wchar_t *xoption = config_get_xoption(config, L"thread_safe_warnings"); + if (xoption) { + int enabled; + const wchar_t *sep = wcschr(xoption, L'='); + if (!sep || (config_wstr_to_int(sep + 1, &enabled) < 0) || (enabled < 0) || (enabled > 1)) { + return _PyStatus_ERR( + "-X thread_safe_warnings=n: n is missing or invalid"); + } + config->thread_safe_warnings = enabled; + } + return _PyStatus_OK(); +} + static PyStatus config_init_tlbc(PyConfig *config) { @@ -2219,6 +2259,11 @@ config_read_complex_options(PyConfig *config) return status; } + status = config_init_thread_safe_warnings(config); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + status = config_init_tlbc(config); if (_PyStatus_EXCEPTION(status)) { return status; diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 71285c074be066..b2d6171a1355cb 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3142,6 +3142,7 @@ static PyStructSequence_Field flags_fields[] = { {"int_max_str_digits", "-X int_max_str_digits"}, {"gil", "-X gil"}, {"thread_inherit_context", "-X thread_inherit_context"}, + {"thread_safe_warnings", "-X thread_safe_warnings"}, {0} }; @@ -3246,6 +3247,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags) SetFlagObj(PyLong_FromLong(1)); #endif SetFlag(config->thread_inherit_context); + SetFlag(config->thread_safe_warnings); #undef SetFlagObj #undef SetFlag return 0; From 3a56c64d90f8926d130362a2556a0a4a2e3c7345 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 12 Feb 2025 11:16:18 -0800 Subject: [PATCH 13/39] Add doc for ``thread_safe_warnings`` flag. --- Doc/library/sys.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index c8f44ad7a4d644..9f1ca89b1c4275 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -602,6 +602,11 @@ always available. Unless explicitly noted otherwise, all variables are read-only - :option:`-X thread_inherit_context <-X>` and :envvar:`PYTHON_THREAD_INHERIT_CONTEXT` + * - .. attribute:: flags.thread_safe_warnings + - :option:`-X thread_inherit_context <-X>` and + :envvar:`PYTHON_THREAD_SAFE_WARNINGS` + + .. versionchanged:: 3.2 Added ``quiet`` attribute for the new :option:`-q` flag. @@ -634,6 +639,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. versionchanged:: 3.14 Added the ``thread_inherit_context`` attribute. + .. versionchanged:: 3.14 + Added the ``thread_safe_warnings`` attribute. + .. data:: float_info From daa3d52245b8ec527425219960933b8474c285f9 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 08:11:51 -0800 Subject: [PATCH 14/39] Create _py_warnings.py for Python implementation. --- Lib/{warnings.py => _py_warnings.py} | 250 ++++++++++++++------------- Lib/test/support/warnings_helper.py | 11 +- Lib/test/test_warnings/__init__.py | 11 +- 3 files changed, 147 insertions(+), 125 deletions(-) rename Lib/{warnings.py => _py_warnings.py} (86%) diff --git a/Lib/warnings.py b/Lib/_py_warnings.py similarity index 86% rename from Lib/warnings.py rename to Lib/_py_warnings.py index 1cc28f0bfc5734..de1d39bd601511 100644 --- a/Lib/warnings.py +++ b/Lib/_py_warnings.py @@ -2,17 +2,53 @@ import sys import _contextvars +import _thread __all__ = ["warn", "warn_explicit", "showwarning", "formatwarning", "filterwarnings", "simplefilter", "resetwarnings", "catch_warnings", "deprecated"] + +# Normally '_wm' is sys.modules['warnings'] but for unit tests it can be +# a different module. User code is allowed to reassign global attributes +# of the 'warnings' module, commonly 'filters' or 'showwarning'. So we +# need to lookup these global attributes dynamically on the '_wn' object, +# rather than binding them earlier. The code in this module consistently uses +# '_wn.' rather than using the globals of this module. If the +# '_warnings' C extension is in use, some globals are replaced by functions +# and variables defined in that extension. +_wm = None + + +def _set_module(module): + global _wm + _wm = module + + +# filters contains a sequence of filter 5-tuples +# The components of the 5-tuple are: +# - an action: error, ignore, always, all, default, module, or once +# - a compiled regex that must match the warning message +# - a class representing the warning category +# - a compiled regex that must match the module that is being warned +# - a line number for the line being warning, or 0 to mean any line +# If either if the compiled regexs are None, match anything. +filters = [] + + +defaultaction = "default" +onceregistry = {} +_lock = _thread.RLock() +_filters_version = 1 + + # If true, catch_warnings() will use a context var to hold the modified # filters list. Otherwise, catch_warnings() will operate on the 'filters' # global of the warnings module. _use_context = sys.flags.thread_safe_warnings + class _Context: def __init__(self, filters): self._filters = filters @@ -38,52 +74,64 @@ def _filters(self): # warnings.filters, this needs to return the current value of # the module global. try: - return filters - except NameError: + return _wm.filters + except AttributeError: # 'filters' global was deleted. Do we need to actually handle this case? return [] _global_context = _GlobalContext() + _warnings_context = _contextvars.ContextVar('warnings_context') + def _get_context(): if not _use_context: return _global_context try: - return _warnings_context.get() + return _wm._warnings_context.get() except LookupError: return _global_context + def _set_context(context): assert _use_context - _warnings_context.set(context) + _wm._warnings_context.set(context) + def _new_context(): assert _use_context - old_context = _get_context() + old_context = _wm._get_context() new_context = old_context.copy() - _set_context(new_context) + _wm._set_context(new_context) return old_context, new_context + def _get_filters(): """Return the current list of filters. This is a non-public API used by module functions and by the unit tests.""" - return _get_context()._filters + return _wm._get_context()._filters + + +def _filters_mutated_lock_held(): + _wm._filters_version += 1 + def showwarning(message, category, filename, lineno, file=None, line=None): """Hook to write a warning to a file; replace if you like.""" - msg = WarningMessage(message, category, filename, lineno, file, line) - _showwarnmsg_impl(msg) + msg = _wm.WarningMessage(message, category, filename, lineno, file, line) + _wm._showwarnmsg_impl(msg) + def formatwarning(message, category, filename, lineno, line=None): """Function to format a warning the standard way.""" - msg = WarningMessage(message, category, filename, lineno, None, line) - return _formatwarnmsg_impl(msg) + msg = _wm.WarningMessage(message, category, filename, lineno, None, line) + return _wm._formatwarnmsg_impl(msg) + def _showwarnmsg_impl(msg): - context = _get_context() + context = _wm._get_context() if context.log is not None: context._record_warning(msg) return @@ -94,13 +142,14 @@ def _showwarnmsg_impl(msg): # sys.stderr is None when run with pythonw.exe: # warnings get lost return - text = _formatwarnmsg(msg) + text = _wm._formatwarnmsg(msg) try: file.write(text) except OSError: # the file (probably stderr) is invalid - this warning gets lost. pass + def _formatwarnmsg_impl(msg): category = msg.category.__name__ s = f"{msg.filename}:{msg.lineno}: {category}: {msg.message}\n" @@ -160,14 +209,16 @@ def _formatwarnmsg_impl(msg): f'allocation traceback\n') return s + # Keep a reference to check if the function was replaced _showwarning_orig = showwarning + def _showwarnmsg(msg): """Hook to write a warning to a file; replace if you like.""" try: - sw = showwarning - except NameError: + sw = _wm.showwarning + except AttributeError: pass else: if sw is not _showwarning_orig: @@ -179,23 +230,26 @@ def _showwarnmsg(msg): sw(msg.message, msg.category, msg.filename, msg.lineno, msg.file, msg.line) return - _showwarnmsg_impl(msg) + _wm._showwarnmsg_impl(msg) + # Keep a reference to check if the function was replaced _formatwarning_orig = formatwarning + def _formatwarnmsg(msg): """Function to format a warning the standard way.""" try: - fw = formatwarning - except NameError: + fw = _wm.formatwarning + except AttributeError: pass else: if fw is not _formatwarning_orig: # warnings.formatwarning() was replaced return fw(msg.message, msg.category, msg.filename, msg.lineno, msg.line) - return _formatwarnmsg_impl(msg) + return _wm._formatwarnmsg_impl(msg) + def filterwarnings(action, message="", category=Warning, module="", lineno=0, append=False): @@ -234,7 +288,8 @@ def filterwarnings(action, message="", category=Warning, module="", lineno=0, else: module = None - _add_filter(action, message, category, module, lineno, append=append) + _wm._add_filter(action, message, category, module, lineno, append=append) + def simplefilter(action, category=Warning, lineno=0, append=False): """Insert a simple entry into the list of warnings filters (at the front). @@ -252,17 +307,19 @@ def simplefilter(action, category=Warning, lineno=0, append=False): raise TypeError("lineno must be an int") if lineno < 0: raise ValueError("lineno must be an int >= 0") - _add_filter(action, None, category, None, lineno, append=append) + _wm._add_filter(action, None, category, None, lineno, append=append) + def _filters_mutated(): # Even though this function is not part of the public API, it's used by # a fair amount of user code. - with _lock: - _filters_mutated_lock_held() + with _wm._lock: + _wm._filters_mutated_lock_held() + def _add_filter(*item, append): - with _lock: - filters = _get_filters() + with _wm._lock: + filters = _wm._get_filters() if not append: # Remove possible duplicate filters, so new one will be placed # in correct place. If append=True and duplicate exists, do nothing. @@ -274,37 +331,41 @@ def _add_filter(*item, append): else: if item not in filters: filters.append(item) - _filters_mutated_lock_held() + _wm._filters_mutated_lock_held() + def resetwarnings(): """Clear the list of warning filters, so that no filters are active.""" - with _lock: - del _get_filters()[:] - _filters_mutated_lock_held() + with _wm._lock: + del _wm._get_filters()[:] + _wm._filters_mutated_lock_held() + class _OptionError(Exception): """Exception used by option processing helpers.""" pass + # Helper to process -W options passed via sys.warnoptions def _processoptions(args): for arg in args: try: - _setoption(arg) - except _OptionError as msg: + _wm._setoption(arg) + except _wm._OptionError as msg: print("Invalid -W option ignored:", msg, file=sys.stderr) + # Helper for _processoptions() def _setoption(arg): parts = arg.split(':') if len(parts) > 5: - raise _OptionError("too many fields (max 5): %r" % (arg,)) + raise _wm._OptionError("too many fields (max 5): %r" % (arg,)) while len(parts) < 5: parts.append('') action, message, category, module, lineno = [s.strip() for s in parts] - action = _getaction(action) - category = _getcategory(category) + action = _wm._getaction(action) + category = _wm._getcategory(category) if message or module: import re if message: @@ -317,10 +378,11 @@ def _setoption(arg): if lineno < 0: raise ValueError except (ValueError, OverflowError): - raise _OptionError("invalid lineno %r" % (lineno,)) from None + raise _wm._OptionError("invalid lineno %r" % (lineno,)) from None else: lineno = 0 - filterwarnings(action, message, category, module, lineno) + _wm.filterwarnings(action, message, category, module, lineno) + # Helper for _setoption() def _getaction(action): @@ -329,7 +391,8 @@ def _getaction(action): for a in ('default', 'always', 'all', 'ignore', 'module', 'once', 'error'): if a.startswith(action): return a - raise _OptionError("invalid action: %r" % (action,)) + raise _wm._OptionError("invalid action: %r" % (action,)) + # Helper for _setoption() def _getcategory(category): @@ -343,13 +406,13 @@ def _getcategory(category): try: m = __import__(module, None, None, [klass]) except ImportError: - raise _OptionError("invalid module name: %r" % (module,)) from None + raise _wm._OptionError("invalid module name: %r" % (module,)) from None try: cat = getattr(m, klass) except AttributeError: - raise _OptionError("unknown warning category: %r" % (category,)) from None + raise _wm._OptionError("unknown warning category: %r" % (category,)) from None if not issubclass(cat, Warning): - raise _OptionError("invalid warning category: %r" % (category,)) + raise _wm._OptionError("invalid warning category: %r" % (category,)) return cat @@ -420,8 +483,10 @@ def warn(message, category=None, stacklevel=1, source=None, else: module = "" registry = globals.setdefault("__warningregistry__", {}) - warn_explicit(message, category, filename, lineno, module, registry, - globals, source) + _wm.warn_explicit( + message, category, filename, lineno, module, registry, globals, source + ) + def warn_explicit(message, category, filename, lineno, module=None, registry=None, module_globals=None, @@ -438,17 +503,17 @@ def warn_explicit(message, category, filename, lineno, text = message message = category(message) key = (text, category, lineno) - with _lock: + with _wm._lock: if registry is None: registry = {} - if registry.get('version', 0) != _filters_version: + if registry.get('version', 0) != _wm._filters_version: registry.clear() - registry['version'] = _filters_version + registry['version'] = _wm._filters_version # Quick test for common case if registry.get(key): return # Search the filters - for item in _get_filters(): + for item in _wm._get_filters(): action, msg, cat, mod, ln = item if ((msg is None or msg.match(text)) and issubclass(category, cat) and @@ -456,7 +521,7 @@ def warn_explicit(message, category, filename, lineno, (ln == 0 or lineno == ln)): break else: - action = defaultaction + action = _wm.defaultaction # Early exit actions if action == "ignore": return @@ -467,9 +532,9 @@ def warn_explicit(message, category, filename, lineno, if action == "once": registry[key] = 1 oncekey = (text, category) - if onceregistry.get(oncekey): + if _wm.onceregistry.get(oncekey): return - onceregistry[oncekey] = 1 + _wm.onceregistry[oncekey] = 1 elif action in {"always", "all"}: pass elif action == "module": @@ -492,8 +557,8 @@ def warn_explicit(message, category, filename, lineno, linecache.getlines(filename, module_globals) # Print message and context - msg = WarningMessage(message, category, filename, lineno, source) - _showwarnmsg(msg) + msg = _wm.WarningMessage(message, category, filename, lineno, source) + _wm._showwarnmsg(msg) class WarningMessage(object): @@ -565,7 +630,7 @@ def __enter__(self): if self._entered: raise RuntimeError("Cannot enter %r twice" % self) self._entered = True - with _lock: + with _wm._lock: if _use_context: self._saved_context, context = self._module._new_context() else: @@ -593,7 +658,7 @@ def __enter__(self): def __exit__(self, *exc_info): if not self._entered: raise RuntimeError("Cannot exit %r without entering first" % self) - with _lock: + with _wm._lock: if _use_context: self._module._warnings_context.set(self._saved_context) else: @@ -679,7 +744,7 @@ def __call__(self, arg, /): @functools.wraps(original_new) def __new__(cls, *args, **kwargs): if cls is arg: - warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1) if original_new is not object.__new__: return original_new(cls, *args, **kwargs) # Mirrors a similar check in object.__new__. @@ -698,7 +763,7 @@ def __new__(cls, *args, **kwargs): @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): - warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1) return original_init_subclass(*args, **kwargs) arg.__init_subclass__ = classmethod(__init_subclass__) @@ -707,7 +772,7 @@ def __init_subclass__(*args, **kwargs): else: @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): - warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1) return original_init_subclass(*args, **kwargs) arg.__init_subclass__ = __init_subclass__ @@ -721,7 +786,7 @@ def __init_subclass__(*args, **kwargs): @functools.wraps(arg) def wrapper(*args, **kwargs): - warn(msg, category=category, stacklevel=stacklevel + 1) + _wm.warn(msg, category=category, stacklevel=stacklevel + 1) return arg(*args, **kwargs) if inspect.iscoroutinefunction(arg): @@ -738,6 +803,7 @@ def wrapper(*args, **kwargs): _DEPRECATED_MSG = "{name!r} is deprecated and slated for removal in Python {remove}" + def _deprecated(name, message=_DEPRECATED_MSG, *, remove, _version=sys.version_info): """Warn that *name* is deprecated or should be removed. @@ -754,7 +820,7 @@ def _deprecated(name, message=_DEPRECATED_MSG, *, remove, _version=sys.version_i raise RuntimeError(msg) else: msg = message.format(name=name, remove=remove_formatted) - warn(msg, DeprecationWarning, stacklevel=3) + _wm.warn(msg, DeprecationWarning, stacklevel=3) # Private utility function called by _PyErr_WarnUnawaitedCoroutine @@ -777,65 +843,17 @@ def extract(): # coroutine origin tracking *and* tracemalloc enabled, they'll get two # partially-redundant tracebacks. If we wanted to be clever we could # probably detect this case and avoid it, but for now we don't bother. - warn(msg, category=RuntimeWarning, stacklevel=2, source=coro) - - -# filters contains a sequence of filter 5-tuples -# The components of the 5-tuple are: -# - an action: error, ignore, always, all, default, module, or once -# - a compiled regex that must match the warning message -# - a class representing the warning category -# - a compiled regex that must match the module that is being warned -# - a line number for the line being warning, or 0 to mean any line -# If either if the compiled regexs are None, match anything. -try: - from _warnings import (filters, _defaultaction, _onceregistry, - warn, warn_explicit, - _filters_mutated_lock_held, - _acquire_lock, _release_lock, + _wm.warn( + msg, category=RuntimeWarning, stacklevel=2, source=coro ) - defaultaction = _defaultaction - onceregistry = _onceregistry - _warnings_defaults = True - - class _Lock: - def __enter__(self): - _acquire_lock() - return self - - def __exit__(self, *args): - _release_lock() - _lock = _Lock() -except ImportError: - filters = [] - defaultaction = "default" - onceregistry = {} - - import _thread - - _lock = _thread.RLock() - - _filters_version = 1 - - def _filters_mutated_lock_held(): - global _filters_version - _filters_version += 1 - - _warnings_defaults = False - - -# Module initialization -_processoptions(sys.warnoptions) -if not _warnings_defaults: +def _setup_defaults(): # Several warning categories are ignored by default in regular builds - if not hasattr(sys, 'gettotalrefcount'): - filterwarnings("default", category=DeprecationWarning, - module="__main__", append=1) - simplefilter("ignore", category=DeprecationWarning, append=1) - simplefilter("ignore", category=PendingDeprecationWarning, append=1) - simplefilter("ignore", category=ImportWarning, append=1) - simplefilter("ignore", category=ResourceWarning, append=1) - -del _warnings_defaults + if hasattr(sys, 'gettotalrefcount'): + return + _wm.filterwarnings("default", category=DeprecationWarning, module="__main__", append=1) + _wm.simplefilter("ignore", category=DeprecationWarning, append=1) + _wm.simplefilter("ignore", category=PendingDeprecationWarning, append=1) + _wm.simplefilter("ignore", category=ImportWarning, append=1) + _wm.simplefilter("ignore", category=ResourceWarning, append=1) diff --git a/Lib/test/support/warnings_helper.py b/Lib/test/support/warnings_helper.py index c1bf0562300678..a6e43dff2003b7 100644 --- a/Lib/test/support/warnings_helper.py +++ b/Lib/test/support/warnings_helper.py @@ -160,11 +160,12 @@ def _filterwarnings(filters, quiet=False): registry = frame.f_globals.get('__warningregistry__') if registry: registry.clear() - with warnings.catch_warnings(record=True) as w: - # Set filter "always" to record all warnings. Because - # test_warnings swap the module, we need to look up in - # the sys.modules dictionary. - sys.modules['warnings'].simplefilter("always") + # Because test_warnings swap the module, we need to look up in the + # sys.modules dictionary. + wmod = sys.modules['warnings'] + with wmod.catch_warnings(record=True) as w: + # Set filter "always" to record all warnings. + wmod.simplefilter("always") yield WarningsRecorder(w) # Filter the recorded warnings reraise = list(w) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 9916c6e799325c..ff9479d2677c77 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -24,10 +24,13 @@ from warnings import deprecated -py_warnings = import_helper.import_fresh_module('warnings', - blocked=['_warnings']) -c_warnings = import_helper.import_fresh_module('warnings', - fresh=['_warnings']) +py_warnings = import_helper.import_fresh_module('_py_warnings') +py_warnings._set_module(py_warnings) + +c_warnings = import_helper.import_fresh_module( + "warnings", fresh=["_warnings", "_py_warnings"] +) +c_warnings._set_module(c_warnings) @contextmanager def warnings_state(module): From 83419e462b9d9e009b17ac4fe3230cec8a779903 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 12:25:58 -0800 Subject: [PATCH 15/39] Add _warnings_context to _warnings module. --- Include/internal/pycore_warnings.h | 1 + Python/_warnings.c | 35 ++++++++++++------------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Include/internal/pycore_warnings.h b/Include/internal/pycore_warnings.h index 672228cd6fbd19..c6d98e2d664a5e 100644 --- a/Include/internal/pycore_warnings.h +++ b/Include/internal/pycore_warnings.h @@ -16,6 +16,7 @@ struct _warnings_runtime_state { PyObject *default_action; /* String */ _PyRecursiveMutex lock; long filters_version; + PyObject *context; }; extern int _PyWarnings_InitState(PyInterpreterState *interp); diff --git a/Python/_warnings.c b/Python/_warnings.c index c23ba756e8b04e..24792d53119525 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -67,6 +67,7 @@ warnings_clear_state(WarningsState *st) Py_CLEAR(st->filters); Py_CLEAR(st->once_registry); Py_CLEAR(st->default_action); + Py_CLEAR(st->context); } #ifndef Py_DEBUG @@ -154,6 +155,13 @@ _PyWarnings_InitState(PyInterpreterState *interp) } } + if (st->context == NULL) { + st->context = PyContextVar_New("_warnings_context", NULL); + if (st->context == NULL) { + return -1; + } + } + st->filters_version = 0; return 0; } @@ -257,30 +265,12 @@ warnings_lock_held(WarningsState *st) static PyObject * get_warnings_context(PyInterpreterState *interp) { - PyObject *ctx_var = GET_WARNINGS_ATTR(interp, _warnings_context, 0); - if (ctx_var == NULL) { - if (!PyErr_Occurred()) { - // likely that the 'warnings' module doesn't exist anymore - Py_RETURN_NONE; - } - else { - return NULL; - } - } - if (!PyContextVar_CheckExact(ctx_var)) { - PyErr_Format(PyExc_TypeError, - MODULE_NAME "._warnings_context must be a ContextVar, " - "not '%.200s'", - Py_TYPE(ctx_var)->tp_name); - Py_DECREF(ctx_var); - return NULL; - } + WarningsState *st = warnings_get_state(interp); + assert(PyContextVar_CheckExact(st->context)); PyObject *ctx; - if (PyContextVar_Get(ctx_var, NULL, &ctx) < 0) { - Py_DECREF(ctx_var); + if (PyContextVar_Get(st->context, NULL, &ctx) < 0) { return NULL; } - Py_DECREF(ctx_var); if (ctx == NULL) { Py_RETURN_NONE; } @@ -1652,6 +1642,9 @@ warnings_module_exec(PyObject *module) if (PyModule_AddObjectRef(module, "_defaultaction", st->default_action) < 0) { return -1; } + if (PyModule_AddObjectRef(module, "_warnings_context", st->context) < 0) { + return -1; + } return 0; } From 15443e826e072d793b165bd6492ce9c28a361437 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 12:27:28 -0800 Subject: [PATCH 16/39] Remove '_warnings_context' global string. --- Include/internal/pycore_global_objects_fini_generated.h | 1 - Include/internal/pycore_global_strings.h | 1 - Include/internal/pycore_runtime_init_generated.h | 1 - Include/internal/pycore_unicodeobject_generated.h | 4 ---- Tools/build/generate_global_objects.py | 1 - 5 files changed, 8 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 5172f4f63d2c55..90214a314031d1 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -776,7 +776,6 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_type_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_uninitialized_submodules)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_warn_unawaited_coroutine)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_warnings_context)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_xoptions)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(abs_tol)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(access)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 433fe58714ed72..97a75d0c46c867 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -265,7 +265,6 @@ struct _Py_global_strings { STRUCT_FOR_ID(_type_) STRUCT_FOR_ID(_uninitialized_submodules) STRUCT_FOR_ID(_warn_unawaited_coroutine) - STRUCT_FOR_ID(_warnings_context) STRUCT_FOR_ID(_xoptions) STRUCT_FOR_ID(abs_tol) STRUCT_FOR_ID(access) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index cf32fe451b73e5..4f928cc050bf8e 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -774,7 +774,6 @@ extern "C" { INIT_ID(_type_), \ INIT_ID(_uninitialized_submodules), \ INIT_ID(_warn_unawaited_coroutine), \ - INIT_ID(_warnings_context), \ INIT_ID(_xoptions), \ INIT_ID(abs_tol), \ INIT_ID(access), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index de8d27f1892b5b..5b78d038fc1192 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -856,10 +856,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(_warnings_context); - _PyUnicode_InternStatic(interp, &string); - assert(_PyUnicode_CheckConsistency(string, 1)); - assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_xoptions); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Tools/build/generate_global_objects.py b/Tools/build/generate_global_objects.py index 1c580e1bf18f67..b5b6de0e7dc2dc 100644 --- a/Tools/build/generate_global_objects.py +++ b/Tools/build/generate_global_objects.py @@ -29,7 +29,6 @@ 'defaultaction', 'filters', 'onceregistry', - '_warnings_context', # from WRAP_METHOD() in Objects/weakrefobject.c '__bytes__', From 983e7ee7272244de3ff8d4e2415ee7e37dccd5a2 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 13:27:57 -0800 Subject: [PATCH 17/39] Rename flag to 'context_aware_warnings'. --- Doc/library/sys.rst | 6 +-- Doc/library/warnings.rst | 54 ++++++++++--------- Doc/using/cmdline.rst | 8 +-- Include/cpython/initconfig.h | 2 +- Lib/_py_warnings.py | 2 +- Lib/test/test_capi/test_config.py | 6 +-- Lib/test/test_embed.py | 4 +- Lib/test/test_sys.py | 2 +- ...-02-11-10-22-11.gh-issue-128384.jyWEkA.rst | 6 +-- Python/initconfig.c | 43 ++++++++------- Python/sysmodule.c | 4 +- 11 files changed, 70 insertions(+), 67 deletions(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 9f1ca89b1c4275..94525c86b3f646 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -602,9 +602,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only - :option:`-X thread_inherit_context <-X>` and :envvar:`PYTHON_THREAD_INHERIT_CONTEXT` - * - .. attribute:: flags.thread_safe_warnings + * - .. attribute:: flags.context_aware_warnings - :option:`-X thread_inherit_context <-X>` and - :envvar:`PYTHON_THREAD_SAFE_WARNINGS` + :envvar:`PYTHON_CONTEXT_AWARE_WARNINGS` .. versionchanged:: 3.2 @@ -640,7 +640,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only Added the ``thread_inherit_context`` attribute. .. versionchanged:: 3.14 - Added the ``thread_safe_warnings`` attribute. + Added the ``context_aware_warnings`` attribute. .. data:: float_info diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index c5d6555458e608..ba52f735a25f31 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -328,9 +328,9 @@ of deprecated code. .. note:: - See :ref:`warning-thread-safe` for details on the thread-safety of the - :class:`catch_warnings` context manager when used in multi-threaded - programs. + See :ref:`warning-concurrent-safe` for details on the + concurrency-safety of the :class:`catch_warnings` context manager when + used in programs using multiple threads or async functions. .. _warning-testing: @@ -370,9 +370,9 @@ results. .. note:: - See :ref:`warning-thread-safe` for details on the thread-safety of the - :class:`catch_warnings` context manager when used in multi-threaded - programs. + See :ref:`warning-concurrent-safe` for details on the + concurrency-safety of the :class:`catch_warnings` context manager when + used in programs using multiple threads or async functions. When testing multiple operations that raise the same kind of warning, it is important to test them in a manner that confirms each operation is raising @@ -620,9 +620,9 @@ Available Context Managers .. note:: - See :ref:`warning-thread-safe` for details on the thread-safety of the - :class:`catch_warnings` context manager when used in multi-threaded - programs. + See :ref:`warning-concurrent-safe` for details on the + concurrency-safety of the :class:`catch_warnings` context manager when + used in programs using multiple threads or async functions. .. versionchanged:: 3.11 @@ -630,21 +630,25 @@ Available Context Managers Added the *action*, *category*, *lineno*, and *append* parameters. -.. _warning-thread-safe: +.. _warning-concurrent-safe: -Thread-safety of Context Managers ---------------------------------- +Concurrent safety of Context Managers +------------------------------------- -The behavior of :class:`catch_warnings` context manager depends on the value -of the :data:`sys.flags.thread_safe_warnings` flag. If the flag is true, the -context manager behaves in a thread-safe fashion and otherwise not. Being -thread-safe means that behavior is predictable in a multi-threaded program. -For free-threaded builds, the flag defaults to true, and false otherwise. +The behavior of :class:`catch_warnings` context manager depends on the +:data:`sys.flags.context_aware_warnings` flag. If the flag is true, the +context manager behaves in a concurrent-safe fashion and otherwise not. +Concurrent-safe means that it is both thread-safe and safe to use within +:ref:`asyncio coroutines ` and tasks. Being thread-safe means +that behavior is predictable in a multi-threaded program. The flag defaults +to true for free-threaded builds and false otherwise. -If the :data:`~sys.flags.thread_safe_warnings` flag is false, then +If the :data:`~sys.flags.context_aware_warnings` flag is false, then :class:`catch_warnings` will modify the global attributes of the -:mod:`warnings` module. This is not thread-safe. If two or more threads use -the context manager at the same time, the behavior is undefined. +:mod:`warnings` module. This is not safe if used within a concurrent program +(using multiple threads or using asyncio coroutines). For example, if two +or more threads use the :class:`catch_warnings` class at the same time, the +behavior is undefined. If the flag is true, :class:`catch_warnings` will not modify global attributes and will instead use a :class:`~contextvars.ContextVar` to @@ -655,16 +659,16 @@ thread-safe. The *record* parameter of the context handler also behaves differently depending on the value of the flag. When *record* is true and the flag is false, the context manager works by replacing and then later restoring the -module's :func:`showwarning` function. This is not thread-safe. +module's :func:`showwarning` function. That is not concurrent-safe. When *record* is true and the flag is false, the :func:`showwarning` function is not replaced. The recording status is instead indicated by an internal property in the context variable. In this case, the :func:`showwarning` function will not be restored when exiting the context handler. -The :data:`~sys.flags.thread_safe_warnings` flag can be set the :option:`-X -thread_safe_warnings<-X>` command-line option or by the -:envvar:`PYTHON_THREAD_SAFE_WARNINGS` environment variable. +The :data:`~sys.flags.context_aware_warnings` flag can be set the :option:`-X +context_aware_warnings<-X>` command-line option or by the +:envvar:`PYTHON_CONTEXT_AWARE_WARNINGS` environment variable. .. note:: @@ -681,6 +685,6 @@ thread_safe_warnings<-X>` command-line option or by the .. versionchanged:: 3.14 - Added the :data:`sys.flags.thread_safe_warnings` flag and the use of a + Added the :data:`sys.flags.context_aware_warnings` flag and the use of a context variable for :class:`catch_warnings` if the flag is true. Previous versions of Python acted as if the flag was always set to false. diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 38587bdfed38f0..29b92b74a0cb57 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -637,11 +637,11 @@ Miscellaneous options .. versionadded:: 3.14 - * :samp:`-X thread_safe_warnings={0,1}` causes the + * :samp:`-X context_aware_warnings={0,1}` causes the :class:`warnings.catch_warnings` context manager to use a :class:`~contextvars.ContextVar` to store warnings filter state. If unset, the value of this option defaults to ``1`` on free-threaded builds - and to ``0`` otherwise. See also :envvar:`PYTHON_THREAD_SAFE_WARNINGS`. + and to ``0`` otherwise. See also :envvar:`PYTHON_CONTEXT_AWARE_WARNINGS`. .. versionadded:: 3.14 @@ -1248,13 +1248,13 @@ conflict. .. versionadded:: 3.14 -.. envvar:: PYTHON_THREAD_SAFE_WARNINGS +.. envvar:: PYTHON_CONTEXT_AWARE_WARNINGS If set to ``1`` then the :class:`warnings.catch_warnings` context manager will use a :class:`~contextvars.ContextVar` to store warnings filter state. If unset, this variable defaults to ``1`` on free-threaded builds and to ``0`` otherwise. See :option:`-X - thread_safe_warnings<-X>`. + context_aware_warnings<-X>`. .. versionadded:: 3.14 diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index a30f166256d594..4874aab80dedba 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -180,7 +180,7 @@ typedef struct PyConfig { int safe_path; int int_max_str_digits; int thread_inherit_context; - int thread_safe_warnings; + int context_aware_warnings; #ifdef __APPLE__ int use_system_logger; #endif diff --git a/Lib/_py_warnings.py b/Lib/_py_warnings.py index de1d39bd601511..6c16d213be34bd 100644 --- a/Lib/_py_warnings.py +++ b/Lib/_py_warnings.py @@ -46,7 +46,7 @@ def _set_module(module): # If true, catch_warnings() will use a context var to hold the modified # filters list. Otherwise, catch_warnings() will operate on the 'filters' # global of the warnings module. -_use_context = sys.flags.thread_safe_warnings +_use_context = sys.flags.context_aware_warnings class _Context: diff --git a/Lib/test/test_capi/test_config.py b/Lib/test/test_capi/test_config.py index f3169cf397b98b..19add846f233ec 100644 --- a/Lib/test/test_capi/test_config.py +++ b/Lib/test/test_capi/test_config.py @@ -56,7 +56,7 @@ def test_config_get(self): ("hash_seed", int, None), ("home", str | None, None), ("thread_inherit_context", int, None), - ("thread_safe_warnings", int, None), + ("context_aware_warnings", int, None), ("import_time", bool, None), ("inspect", bool, None), ("install_signal_handlers", bool, None), @@ -172,7 +172,7 @@ def test_config_get_sys_flags(self): ("warn_default_encoding", "warn_default_encoding", False), ("safe_path", "safe_path", False), ("int_max_str_digits", "int_max_str_digits", False), - # "gil", "thread_inherit_context" and "thread_safe_warnings" are tested below + # "gil", "thread_inherit_context" and "context_aware_warnings" are tested below ): with self.subTest(flag=flag, name=name, negate=negate): value = config_get(name) @@ -193,7 +193,7 @@ def test_config_get_sys_flags(self): self.assertEqual(sys.flags.thread_inherit_context, expected_inherit_context) expected_safe_warnings = 1 if support.Py_GIL_DISABLED else 0 - self.assertEqual(sys.flags.thread_safe_warnings, expected_safe_warnings) + self.assertEqual(sys.flags.context_aware_warnings, expected_safe_warnings) def test_config_get_non_existent(self): # Test PyConfig_Get() on non-existent option name diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 0ea1fcf034bc88..b5344a931bb97b 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -61,7 +61,7 @@ f'{VERSION_MINOR}{ABI_THREAD}/lib-dynload') DEFAULT_THREAD_INHERIT_CONTEXT = 1 if support.Py_GIL_DISABLED else 0 -DEFAULT_THREAD_SAFE_WARNINGS = 1 if support.Py_GIL_DISABLED else 0 +DEFAULT_CONTEXT_AWARE_WARNINGS = 1 if support.Py_GIL_DISABLED else 0 # If we are running from a build dir, but the stdlib has been installed, # some tests need to expect different results. @@ -589,7 +589,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'perf_profiling': 0, 'import_time': False, 'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT, - 'thread_safe_warnings': DEFAULT_THREAD_SAFE_WARNINGS, + 'context_aware_warnings': DEFAULT_CONTEXT_AWARE_WARNINGS, 'code_debug_ranges': True, 'show_ref_count': False, 'dump_refs': False, diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index bfc47427743987..214aab0185ee25 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1846,7 +1846,7 @@ def test_pythontypes(self): # XXX # sys.flags # FIXME: The +3 is for the 'gil', 'thread_inherit_context' and - # 'thread_safe_warnings' flags and will not be necessary once + # 'context_aware_warnings' flags and will not be necessary once # gh-122575 is fixed check(sys.flags, vsize('') + self.P * (3 + len(sys.flags))) diff --git a/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst b/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst index 3d48febb6d3430..011e25d89262bf 100644 --- a/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst +++ b/Misc/NEWS.d/next/Library/2025-02-11-10-22-11.gh-issue-128384.jyWEkA.rst @@ -1,7 +1,7 @@ Make :class:`warnings.catch_warnings` use a context variable for holding -the warning filtering state if the :data:`sys.flags.thread_safe_warnings` +the warning filtering state if the :data:`sys.flags.context_aware_warnings` flag is set to true. This makes using the context manager thread-safe in multi-threaded programs. The flag is true by default in free-threaded builds and is otherwise false. The value of the flag can be overridden by the -the :option:`-X thread_safe_warnings <-X>` command-line option or by the -:envvar:`PYTHON_THREAD_SAFE_WARNINGS` environment variable. +the :option:`-X context_aware_warnings <-X>` command-line option or by the +:envvar:`PYTHON_CONTEXT_AWARE_WARNINGS` environment variable. diff --git a/Python/initconfig.c b/Python/initconfig.c index d3004f91c10325..ddd1565ae059df 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -142,7 +142,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(hash_seed, ULONG, READ_ONLY, NO_SYS), SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS), SPEC(thread_inherit_context, INT, READ_ONLY, NO_SYS), - SPEC(thread_safe_warnings, INT, READ_ONLY, NO_SYS), + SPEC(context_aware_warnings, INT, READ_ONLY, NO_SYS), SPEC(import_time, BOOL, READ_ONLY, NO_SYS), SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS), SPEC(isolated, BOOL, READ_ONLY, NO_SYS), // sys.flags.isolated @@ -330,12 +330,11 @@ The following implementation-specific options are available:\n\ -X thread_inherit_context=[0|1]: enable (1) or disable (0) threads inheriting\n\ context vars by default; enabled by default in the free-threaded\n\ build and disabled otherwise; also PYTHON_THREAD_INHERIT_CONTEXT\n\ --X thread_safe_warnings=[0|1]: if true (1) then the warnings module will\n\ - use a context variable to store warnings filtering state, making it\n\ - safe to use in multi-threaded programs; if false (0) then the\n\ - warnings module will use module globals, which is not thread-safe;\n\ - set to true for free-threaded builds and false otherwise; also\n\ - PYTHON_THREAD_SAFE_WARNINGS\n\ +-X context_aware_warnings=[0|1]: if true (1) then the warnings module will\n\ + use a context variables; if false (0) then the warnings module will\n\ + use module globals, which is not concurrent-safe; set to true for\n\ + free-threaded builds and false otherwise; also\n\ + PYTHON_CONTEXT_AWARE_WARNINGS\n\ -X tracemalloc[=N]: trace Python memory allocations; N sets a traceback limit\n \ of N frames (default: 1); also PYTHONTRACEMALLOC=N\n\ -X utf8[=0|1]: enable (1) or disable (0) UTF-8 mode; also PYTHONUTF8\n\ @@ -425,8 +424,8 @@ static const char usage_envvars[] = #endif "PYTHON_THREAD_INHERIT_CONTEXT: if true (1), threads inherit context vars\n" " (-X thread_inherit_context)\n" -"PYTHON_THREAD_SAFE_WARNINGS: if true (1), enable thread-safe warnings module\n" -" behaviour (-X thread_safe_warnings)\n" +"PYTHON_CONTEXT_AWARE_WARNINGS: if true (1), enable thread-safe warnings module\n" +" behaviour (-X context_aware_warnings)\n" "PYTHONTRACEMALLOC: trace Python memory allocations (-X tracemalloc)\n" "PYTHONUNBUFFERED: disable stdout/stderr buffering (-u)\n" "PYTHONUTF8 : control the UTF-8 mode (-X utf8)\n" @@ -903,7 +902,7 @@ config_check_consistency(const PyConfig *config) // config->use_frozen_modules is initialized later // by _PyConfig_InitImportConfig(). assert(config->thread_inherit_context >= 0); - assert(config->thread_safe_warnings >= 0); + assert(config->context_aware_warnings >= 0); #ifdef __APPLE__ assert(config->use_system_logger >= 0); #endif @@ -1011,10 +1010,10 @@ _PyConfig_InitCompatConfig(PyConfig *config) config->cpu_count = -1; #ifdef Py_GIL_DISABLED config->thread_inherit_context = 1; - config->thread_safe_warnings = 1; + config->context_aware_warnings = 1; #else config->thread_inherit_context = 0; - config->thread_safe_warnings = 0; + config->context_aware_warnings = 0; #endif #ifdef __APPLE__ config->use_system_logger = 0; @@ -1050,10 +1049,10 @@ config_init_defaults(PyConfig *config) #endif #ifdef Py_GIL_DISABLED config->thread_inherit_context = 1; - config->thread_safe_warnings = 1; + config->context_aware_warnings = 1; #else config->thread_inherit_context = 0; - config->thread_safe_warnings = 0; + config->context_aware_warnings = 0; #endif #ifdef __APPLE__ config->use_system_logger = 0; @@ -1950,27 +1949,27 @@ config_init_thread_inherit_context(PyConfig *config) } static PyStatus -config_init_thread_safe_warnings(PyConfig *config) +config_init_context_aware_warnings(PyConfig *config) { - const char *env = config_get_env(config, "PYTHON_THREAD_SAFE_WARNINGS"); + const char *env = config_get_env(config, "PYTHON_CONTEXT_AWARE_WARNINGS"); if (env) { int enabled; if (_Py_str_to_int(env, &enabled) < 0 || (enabled < 0) || (enabled > 1)) { return _PyStatus_ERR( - "PYTHON_THREAD_SAFE_WARNINGS=N: N is missing or invalid"); + "PYTHON_CONTEXT_AWARE_WARNINGS=N: N is missing or invalid"); } - config->thread_safe_warnings = enabled; + config->context_aware_warnings = enabled; } - const wchar_t *xoption = config_get_xoption(config, L"thread_safe_warnings"); + const wchar_t *xoption = config_get_xoption(config, L"context_aware_warnings"); if (xoption) { int enabled; const wchar_t *sep = wcschr(xoption, L'='); if (!sep || (config_wstr_to_int(sep + 1, &enabled) < 0) || (enabled < 0) || (enabled > 1)) { return _PyStatus_ERR( - "-X thread_safe_warnings=n: n is missing or invalid"); + "-X context_aware_warnings=n: n is missing or invalid"); } - config->thread_safe_warnings = enabled; + config->context_aware_warnings = enabled; } return _PyStatus_OK(); } @@ -2259,7 +2258,7 @@ config_read_complex_options(PyConfig *config) return status; } - status = config_init_thread_safe_warnings(config); + status = config_init_context_aware_warnings(config); if (_PyStatus_EXCEPTION(status)) { return status; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index b2d6171a1355cb..87cfef955eb0b8 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3142,7 +3142,7 @@ static PyStructSequence_Field flags_fields[] = { {"int_max_str_digits", "-X int_max_str_digits"}, {"gil", "-X gil"}, {"thread_inherit_context", "-X thread_inherit_context"}, - {"thread_safe_warnings", "-X thread_safe_warnings"}, + {"context_aware_warnings", "-X context_aware_warnings"}, {0} }; @@ -3247,7 +3247,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags) SetFlagObj(PyLong_FromLong(1)); #endif SetFlag(config->thread_inherit_context); - SetFlag(config->thread_safe_warnings); + SetFlag(config->context_aware_warnings); #undef SetFlagObj #undef SetFlag return 0; From e5e660c50ad10b435c62df065e6240a86c0f72b3 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 13:39:58 -0800 Subject: [PATCH 18/39] Use catch_warnings() for module under test. This actually doesn't make a difference for behaviour because everything uses sys.modules['warnings'] anyhow (which is swapped out) but it's less confusing this way. --- Lib/test/test_warnings/__init__.py | 96 +++++++++++++++--------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index ff9479d2677c77..556af7d1771c4d 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -120,14 +120,14 @@ class FilterTests(BaseTest): """Testing the filtering functionality.""" def test_error(self): - with original_warnings.catch_warnings(module=self.module) as w: + with self.module.catch_warnings(module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("error", category=UserWarning) self.assertRaises(UserWarning, self.module.warn, "FilterTests.test_error") def test_error_after_default(self): - with original_warnings.catch_warnings(module=self.module) as w: + with self.module.catch_warnings(module=self.module) as w: self.module.resetwarnings() message = "FilterTests.test_ignore_after_default" def f(): @@ -145,7 +145,7 @@ def f(): self.assertRaises(UserWarning, f) def test_ignore(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("ignore", category=UserWarning) @@ -154,7 +154,7 @@ def test_ignore(self): self.assertEqual(list(__warningregistry__), ['version']) def test_ignore_after_default(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() message = "FilterTests.test_ignore_after_default" @@ -168,7 +168,7 @@ def f(): def test_always_and_all(self): for mode in {"always", "all"}: - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings(mode, category=UserWarning) @@ -184,7 +184,7 @@ def f(): def test_always_and_all_after_default(self): for mode in {"always", "all"}: - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() message = "FilterTests.test_always_and_all_after_ignore" @@ -204,7 +204,7 @@ def f(): self.assertEqual(w[-1].message.args[0], message) def test_default(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("default", category=UserWarning) @@ -220,7 +220,7 @@ def test_default(self): raise ValueError("loop variant unhandled") def test_module(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("module", category=UserWarning) @@ -232,7 +232,7 @@ def test_module(self): self.assertEqual(len(w), 0) def test_once(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("once", category=UserWarning) @@ -249,7 +249,7 @@ def test_once(self): self.assertEqual(len(w), 0) def test_module_globals(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.simplefilter("always", UserWarning) @@ -270,14 +270,14 @@ def test_module_globals(self): self.assertEqual(len(w), 2) def test_inheritance(self): - with original_warnings.catch_warnings(module=self.module) as w: + with self.module.catch_warnings(module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("error", category=Warning) self.assertRaises(UserWarning, self.module.warn, "FilterTests.test_inheritance", UserWarning) def test_ordering(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("ignore", category=UserWarning) @@ -293,7 +293,7 @@ def test_ordering(self): def test_filterwarnings(self): # Test filterwarnings(). # Implicitly also tests resetwarnings(). - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.filterwarnings("error", "", Warning, "", 0) self.assertRaises(UserWarning, self.module.warn, 'convert to error') @@ -318,7 +318,7 @@ def test_filterwarnings(self): self.assertIs(w[-1].category, UserWarning) def test_message_matching(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.simplefilter("ignore", UserWarning) self.module.filterwarnings("error", "match", UserWarning) @@ -335,14 +335,14 @@ def match(self, a): L[:] = [] L = [("default",X(),UserWarning,X(),0) for i in range(2)] - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.filters = L self.module.warn_explicit(UserWarning("b"), None, "f.py", 42) self.assertEqual(str(w[-1].message), "b") def test_filterwarnings_duplicate_filters(self): - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.resetwarnings() self.module.filterwarnings("error", category=UserWarning) self.assertEqual(len(self.module._get_filters()), 1) @@ -359,7 +359,7 @@ def test_filterwarnings_duplicate_filters(self): ) def test_simplefilter_duplicate_filters(self): - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.resetwarnings() self.module.simplefilter("error", category=UserWarning) self.assertEqual(len(self.module._get_filters()), 1) @@ -375,7 +375,7 @@ def test_simplefilter_duplicate_filters(self): ) def test_append_duplicate(self): - with original_warnings.catch_warnings(module=self.module, + with self.module.catch_warnings(module=self.module, record=True) as w: self.module.resetwarnings() self.module.simplefilter("ignore") @@ -412,7 +412,7 @@ def test_argument_validation(self): self.module.simplefilter('ignore', lineno=-1) def test_catchwarnings_with_simplefilter_ignore(self): - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.resetwarnings() self.module.simplefilter("error") with self.module.catch_warnings( @@ -421,7 +421,7 @@ def test_catchwarnings_with_simplefilter_ignore(self): self.module.warn("This will be ignored") def test_catchwarnings_with_simplefilter_error(self): - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.resetwarnings() with self.module.catch_warnings( module=self.module, action="error", category=FutureWarning @@ -446,7 +446,7 @@ class WarnTests(BaseTest): """Test warnings.warn() and warnings.warn_explicit().""" def test_message(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.simplefilter("once") for i in range(4): @@ -459,7 +459,7 @@ def test_message(self): def test_warn_nonstandard_types(self): # warn() should handle non-standard types without issue. for ob in (Warning, None, 42): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.simplefilter("once") self.module.warn(ob) @@ -469,7 +469,7 @@ def test_warn_nonstandard_types(self): def test_filename(self): with warnings_state(self.module): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: warning_tests.inner("spam1") self.assertEqual(os.path.basename(w[-1].filename), @@ -482,7 +482,7 @@ def test_stacklevel(self): # Test stacklevel argument # make sure all messages are different, so the warning won't be skipped with warnings_state(self.module): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: warning_tests.inner("spam3", stacklevel=1) self.assertEqual(os.path.basename(w[-1].filename), @@ -509,7 +509,7 @@ def test_stacklevel_import(self): # Issue #24305: With stacklevel=2, module-level warnings should work. import_helper.unload('test.test_warnings.data.import_warning') with warnings_state(self.module): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.simplefilter('always') import test.test_warnings.data.import_warning # noqa: F401 @@ -518,7 +518,7 @@ def test_stacklevel_import(self): def test_skip_file_prefixes(self): with warnings_state(self.module): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.simplefilter('always') @@ -546,7 +546,7 @@ def test_skip_file_prefixes_file_path(self): # see: gh-126209 with warnings_state(self.module): skipped = warning_tests.__file__ - with original_warnings.catch_warnings( + with self.module.catch_warnings( record=True, module=self.module, ) as w: warning_tests.outer("msg", skip_file_prefixes=(skipped,)) @@ -569,13 +569,13 @@ def test_exec_filename(self): codeobj = compile(("import warnings\n" "warnings.warn('hello', UserWarning)"), filename, "exec") - with original_warnings.catch_warnings(record=True) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter("always", category=UserWarning) exec(codeobj) self.assertEqual(w[0].filename, filename) def test_warn_explicit_non_ascii_filename(self): - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("always", category=UserWarning) @@ -646,7 +646,7 @@ class NonWarningSubclass: self.assertIn('category must be a Warning subclass, not ', str(cm.exception)) - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.resetwarnings() self.module.filterwarnings('default') with self.assertWarns(MyWarningClass) as cm: @@ -662,7 +662,7 @@ class NonWarningSubclass: self.assertIsInstance(cm.warning, Warning) def check_module_globals(self, module_globals): - with original_warnings.catch_warnings(module=self.module, record=True) as w: + with self.module.catch_warnings(module=self.module, record=True) as w: self.module.filterwarnings('default') self.module.warn_explicit( 'eggs', UserWarning, 'bar', 1, @@ -675,7 +675,7 @@ def check_module_globals_error(self, module_globals, errmsg, errtype=ValueError) if self.module is py_warnings: self.check_module_globals(module_globals) return - with original_warnings.catch_warnings(module=self.module, record=True) as w: + with self.module.catch_warnings(module=self.module, record=True) as w: self.module.filterwarnings('always') with self.assertRaisesRegex(errtype, re.escape(errmsg)): self.module.warn_explicit( @@ -687,7 +687,7 @@ def check_module_globals_deprecated(self, module_globals, msg): if self.module is py_warnings: self.check_module_globals(module_globals) return - with original_warnings.catch_warnings(module=self.module, record=True) as w: + with self.module.catch_warnings(module=self.module, record=True) as w: self.module.filterwarnings('always') self.module.warn_explicit( 'eggs', UserWarning, 'bar', 1, @@ -776,7 +776,7 @@ class WCmdLineTests(BaseTest): def test_improper_input(self): # Uses the private _setoption() function to test the parsing # of command-line warning arguments - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.assertRaises(self.module._OptionError, self.module._setoption, '1:2:3:4:5:6') self.assertRaises(self.module._OptionError, @@ -795,7 +795,7 @@ def test_improper_input(self): self.assertRaises(UserWarning, self.module.warn, 'convert to error') def test_import_from_module(self): - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module._setoption('ignore::Warning') with self.assertRaises(self.module._OptionError): self.module._setoption('ignore::TestWarning') @@ -838,7 +838,7 @@ class _WarningsTests(BaseTest, unittest.TestCase): def test_filter(self): # Everything should function even if 'filters' is not in warnings. - with original_warnings.catch_warnings(module=self.module) as w: + with self.module.catch_warnings(module=self.module) as w: self.module.filterwarnings("error", "", Warning, "", 0) self.assertRaises(UserWarning, self.module.warn, 'convert to error') @@ -853,7 +853,7 @@ def test_onceregistry(self): try: original_registry = self.module.onceregistry __warningregistry__ = {} - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() self.module.filterwarnings("once", category=UserWarning) @@ -881,7 +881,7 @@ def test_default_action(self): message = UserWarning("defaultaction test") original = self.module.defaultaction try: - with original_warnings.catch_warnings(record=True, + with self.module.catch_warnings(record=True, module=self.module) as w: self.module.resetwarnings() registry = {} @@ -920,7 +920,7 @@ def test_showwarning_missing(self): # override/restore showwarning() return text = 'del showwarning test' - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.filterwarnings("always", category=UserWarning) del self.module.showwarning with support.captured_output('stderr') as stream: @@ -931,7 +931,7 @@ def test_showwarning_missing(self): def test_showwarnmsg_missing(self): # Test that _showwarnmsg() missing is okay. text = 'del _showwarnmsg test' - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.filterwarnings("always", category=UserWarning) show = self.module._showwarnmsg @@ -945,7 +945,7 @@ def test_showwarnmsg_missing(self): self.assertIn(text, result) def test_showwarning_not_callable(self): - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.filterwarnings("always", category=UserWarning) self.module.showwarning = print with support.captured_output('stdout'): @@ -956,7 +956,7 @@ def test_showwarning_not_callable(self): def test_show_warning_output(self): # With showwarning() missing, make sure that output is okay. text = 'test show_warning' - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.filterwarnings("always", category=UserWarning) del self.module.showwarning with support.captured_output('stderr') as stream: @@ -981,12 +981,12 @@ def test_filename_none(self): globals_dict = globals() oldfile = globals_dict['__file__'] try: - catch = original_warnings.catch_warnings(record=True, + catch = self.module.catch_warnings(record=True, module=self.module) with catch as w: self.module.filterwarnings("always", category=UserWarning) globals_dict['__file__'] = None - original_warnings.warn('test', UserWarning) + self.module.warn('test', UserWarning) self.assertTrue(len(w)) finally: globals_dict['__file__'] = oldfile @@ -1023,7 +1023,7 @@ def get_source(self, fullname): wmod = self.module - with original_warnings.catch_warnings(module=wmod): + with wmod.catch_warnings(module=wmod): wmod.filterwarnings('default', category=UserWarning) linecache.clearcache() @@ -1050,7 +1050,7 @@ def test_issue31411(self): # warn_explicit() shouldn't raise a SystemError in case # warnings.onceregistry isn't a dictionary. wmod = self.module - with original_warnings.catch_warnings(module=wmod): + with wmod.catch_warnings(module=wmod): wmod.filterwarnings('once') with support.swap_attr(wmod, 'onceregistry', None): with self.assertRaises(TypeError): @@ -1061,7 +1061,7 @@ def test_issue31416(self): # warn_explicit() shouldn't cause an assertion failure in case of a # bad warnings.filters or warnings.defaultaction. wmod = self.module - with original_warnings.catch_warnings(module=wmod): + with wmod.catch_warnings(module=wmod): wmod._get_filters()[:] = [(None, None, Warning, None, 0)] with self.assertRaises(TypeError): wmod.warn_explicit('foo', Warning, 'bar', 1) @@ -1075,7 +1075,7 @@ def test_issue31416(self): def test_issue31566(self): # warn() shouldn't cause an assertion failure in case of a bad # __name__ global. - with original_warnings.catch_warnings(module=self.module): + with self.module.catch_warnings(module=self.module): self.module.filterwarnings('error', category=UserWarning) with support.swap_item(globals(), '__name__', b'foo'), \ support.swap_item(globals(), '__file__', None): From e054a57c20f19b2ac309431b9a3d13fe1774ebb7 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 13:48:03 -0800 Subject: [PATCH 19/39] Don't pass 'module' to catch_warnings(). This doesn't actually do anything given that sys.modules['warnings'] has already been swapped out. Simplify the unit test code by not passing it. --- Lib/test/test_warnings/__init__.py | 139 ++++++++++++----------------- 1 file changed, 56 insertions(+), 83 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 556af7d1771c4d..64fb4153c68e71 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -120,14 +120,14 @@ class FilterTests(BaseTest): """Testing the filtering functionality.""" def test_error(self): - with self.module.catch_warnings(module=self.module) as w: + with self.module.catch_warnings() as w: self.module.resetwarnings() self.module.filterwarnings("error", category=UserWarning) self.assertRaises(UserWarning, self.module.warn, "FilterTests.test_error") def test_error_after_default(self): - with self.module.catch_warnings(module=self.module) as w: + with self.module.catch_warnings() as w: self.module.resetwarnings() message = "FilterTests.test_ignore_after_default" def f(): @@ -145,8 +145,7 @@ def f(): self.assertRaises(UserWarning, f) def test_ignore(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("ignore", category=UserWarning) self.module.warn("FilterTests.test_ignore", UserWarning) @@ -154,8 +153,7 @@ def test_ignore(self): self.assertEqual(list(__warningregistry__), ['version']) def test_ignore_after_default(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() message = "FilterTests.test_ignore_after_default" def f(): @@ -168,8 +166,7 @@ def f(): def test_always_and_all(self): for mode in {"always", "all"}: - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings(mode, category=UserWarning) message = "FilterTests.test_always_and_all" @@ -184,8 +181,7 @@ def f(): def test_always_and_all_after_default(self): for mode in {"always", "all"}: - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() message = "FilterTests.test_always_and_all_after_ignore" def f(): @@ -204,8 +200,7 @@ def f(): self.assertEqual(w[-1].message.args[0], message) def test_default(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("default", category=UserWarning) message = UserWarning("FilterTests.test_default") @@ -220,8 +215,7 @@ def test_default(self): raise ValueError("loop variant unhandled") def test_module(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("module", category=UserWarning) message = UserWarning("FilterTests.test_module") @@ -232,8 +226,7 @@ def test_module(self): self.assertEqual(len(w), 0) def test_once(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("once", category=UserWarning) message = UserWarning("FilterTests.test_once") @@ -249,8 +242,7 @@ def test_once(self): self.assertEqual(len(w), 0) def test_module_globals(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter("always", UserWarning) # bpo-33509: module_globals=None must not crash @@ -270,15 +262,14 @@ def test_module_globals(self): self.assertEqual(len(w), 2) def test_inheritance(self): - with self.module.catch_warnings(module=self.module) as w: + with self.module.catch_warnings() as w: self.module.resetwarnings() self.module.filterwarnings("error", category=Warning) self.assertRaises(UserWarning, self.module.warn, "FilterTests.test_inheritance", UserWarning) def test_ordering(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("ignore", category=UserWarning) self.module.filterwarnings("error", category=UserWarning, @@ -293,8 +284,7 @@ def test_ordering(self): def test_filterwarnings(self): # Test filterwarnings(). # Implicitly also tests resetwarnings(). - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.filterwarnings("error", "", Warning, "", 0) self.assertRaises(UserWarning, self.module.warn, 'convert to error') @@ -318,8 +308,7 @@ def test_filterwarnings(self): self.assertIs(w[-1].category, UserWarning) def test_message_matching(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter("ignore", UserWarning) self.module.filterwarnings("error", "match", UserWarning) self.assertRaises(UserWarning, self.module.warn, "match") @@ -335,14 +324,13 @@ def match(self, a): L[:] = [] L = [("default",X(),UserWarning,X(),0) for i in range(2)] - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.filters = L self.module.warn_explicit(UserWarning("b"), None, "f.py", 42) self.assertEqual(str(w[-1].message), "b") def test_filterwarnings_duplicate_filters(self): - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.resetwarnings() self.module.filterwarnings("error", category=UserWarning) self.assertEqual(len(self.module._get_filters()), 1) @@ -359,7 +347,7 @@ def test_filterwarnings_duplicate_filters(self): ) def test_simplefilter_duplicate_filters(self): - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.resetwarnings() self.module.simplefilter("error", category=UserWarning) self.assertEqual(len(self.module._get_filters()), 1) @@ -375,8 +363,7 @@ def test_simplefilter_duplicate_filters(self): ) def test_append_duplicate(self): - with self.module.catch_warnings(module=self.module, - record=True) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.simplefilter("ignore") self.module.simplefilter("error", append=True) @@ -415,16 +402,14 @@ def test_catchwarnings_with_simplefilter_ignore(self): with self.module.catch_warnings(module=self.module): self.module.resetwarnings() self.module.simplefilter("error") - with self.module.catch_warnings( - module=self.module, action="ignore" - ): + with self.module.catch_warnings(action="ignore"): self.module.warn("This will be ignored") def test_catchwarnings_with_simplefilter_error(self): - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.resetwarnings() with self.module.catch_warnings( - module=self.module, action="error", category=FutureWarning + action="error", category=FutureWarning ): with support.captured_stderr() as stderr: error_msg = "Other types of warnings are not errors" @@ -446,8 +431,7 @@ class WarnTests(BaseTest): """Test warnings.warn() and warnings.warn_explicit().""" def test_message(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter("once") for i in range(4): text = 'multi %d' %i # Different text on each call. @@ -459,8 +443,7 @@ def test_message(self): def test_warn_nonstandard_types(self): # warn() should handle non-standard types without issue. for ob in (Warning, None, 42): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter("once") self.module.warn(ob) # Don't directly compare objects since @@ -469,8 +452,7 @@ def test_warn_nonstandard_types(self): def test_filename(self): with warnings_state(self.module): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: warning_tests.inner("spam1") self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") @@ -482,8 +464,7 @@ def test_stacklevel(self): # Test stacklevel argument # make sure all messages are different, so the warning won't be skipped with warnings_state(self.module): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: warning_tests.inner("spam3", stacklevel=1) self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") @@ -509,8 +490,7 @@ def test_stacklevel_import(self): # Issue #24305: With stacklevel=2, module-level warnings should work. import_helper.unload('test.test_warnings.data.import_warning') with warnings_state(self.module): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter('always') import test.test_warnings.data.import_warning # noqa: F401 self.assertEqual(len(w), 1) @@ -518,8 +498,7 @@ def test_stacklevel_import(self): def test_skip_file_prefixes(self): with warnings_state(self.module): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.simplefilter('always') # Warning never attributed to the data/ package. @@ -546,9 +525,7 @@ def test_skip_file_prefixes_file_path(self): # see: gh-126209 with warnings_state(self.module): skipped = warning_tests.__file__ - with self.module.catch_warnings( - record=True, module=self.module, - ) as w: + with self.module.catch_warnings(record=True) as w: warning_tests.outer("msg", skip_file_prefixes=(skipped,)) self.assertEqual(len(w), 1) @@ -575,8 +552,7 @@ def test_exec_filename(self): self.assertEqual(w[0].filename, filename) def test_warn_explicit_non_ascii_filename(self): - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("always", category=UserWarning) filenames = ["nonascii\xe9\u20ac"] @@ -646,7 +622,7 @@ class NonWarningSubclass: self.assertIn('category must be a Warning subclass, not ', str(cm.exception)) - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.resetwarnings() self.module.filterwarnings('default') with self.assertWarns(MyWarningClass) as cm: @@ -662,7 +638,7 @@ class NonWarningSubclass: self.assertIsInstance(cm.warning, Warning) def check_module_globals(self, module_globals): - with self.module.catch_warnings(module=self.module, record=True) as w: + with self.module.catch_warnings(record=True) as w: self.module.filterwarnings('default') self.module.warn_explicit( 'eggs', UserWarning, 'bar', 1, @@ -675,7 +651,7 @@ def check_module_globals_error(self, module_globals, errmsg, errtype=ValueError) if self.module is py_warnings: self.check_module_globals(module_globals) return - with self.module.catch_warnings(module=self.module, record=True) as w: + with self.module.catch_warnings(record=True) as w: self.module.filterwarnings('always') with self.assertRaisesRegex(errtype, re.escape(errmsg)): self.module.warn_explicit( @@ -687,7 +663,7 @@ def check_module_globals_deprecated(self, module_globals, msg): if self.module is py_warnings: self.check_module_globals(module_globals) return - with self.module.catch_warnings(module=self.module, record=True) as w: + with self.module.catch_warnings(record=True) as w: self.module.filterwarnings('always') self.module.warn_explicit( 'eggs', UserWarning, 'bar', 1, @@ -776,7 +752,7 @@ class WCmdLineTests(BaseTest): def test_improper_input(self): # Uses the private _setoption() function to test the parsing # of command-line warning arguments - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.assertRaises(self.module._OptionError, self.module._setoption, '1:2:3:4:5:6') self.assertRaises(self.module._OptionError, @@ -795,7 +771,7 @@ def test_improper_input(self): self.assertRaises(UserWarning, self.module.warn, 'convert to error') def test_import_from_module(self): - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module._setoption('ignore::Warning') with self.assertRaises(self.module._OptionError): self.module._setoption('ignore::TestWarning') @@ -838,7 +814,7 @@ class _WarningsTests(BaseTest, unittest.TestCase): def test_filter(self): # Everything should function even if 'filters' is not in warnings. - with self.module.catch_warnings(module=self.module) as w: + with self.module.catch_warnings() as w: self.module.filterwarnings("error", "", Warning, "", 0) self.assertRaises(UserWarning, self.module.warn, 'convert to error') @@ -853,8 +829,7 @@ def test_onceregistry(self): try: original_registry = self.module.onceregistry __warningregistry__ = {} - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() self.module.filterwarnings("once", category=UserWarning) self.module.warn_explicit(message, UserWarning, "file", 42) @@ -881,8 +856,7 @@ def test_default_action(self): message = UserWarning("defaultaction test") original = self.module.defaultaction try: - with self.module.catch_warnings(record=True, - module=self.module) as w: + with self.module.catch_warnings(record=True) as w: self.module.resetwarnings() registry = {} self.module.warn_explicit(message, UserWarning, "", 42, @@ -920,7 +894,7 @@ def test_showwarning_missing(self): # override/restore showwarning() return text = 'del showwarning test' - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.filterwarnings("always", category=UserWarning) del self.module.showwarning with support.captured_output('stderr') as stream: @@ -931,7 +905,7 @@ def test_showwarning_missing(self): def test_showwarnmsg_missing(self): # Test that _showwarnmsg() missing is okay. text = 'del _showwarnmsg test' - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.filterwarnings("always", category=UserWarning) show = self.module._showwarnmsg @@ -945,7 +919,7 @@ def test_showwarnmsg_missing(self): self.assertIn(text, result) def test_showwarning_not_callable(self): - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.filterwarnings("always", category=UserWarning) self.module.showwarning = print with support.captured_output('stdout'): @@ -956,7 +930,7 @@ def test_showwarning_not_callable(self): def test_show_warning_output(self): # With showwarning() missing, make sure that output is okay. text = 'test show_warning' - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.filterwarnings("always", category=UserWarning) del self.module.showwarning with support.captured_output('stderr') as stream: @@ -981,8 +955,7 @@ def test_filename_none(self): globals_dict = globals() oldfile = globals_dict['__file__'] try: - catch = self.module.catch_warnings(record=True, - module=self.module) + catch = self.module.catch_warnings(record=True) with catch as w: self.module.filterwarnings("always", category=UserWarning) globals_dict['__file__'] = None @@ -1023,7 +996,7 @@ def get_source(self, fullname): wmod = self.module - with wmod.catch_warnings(module=wmod): + with wmod.catch_warnings(): wmod.filterwarnings('default', category=UserWarning) linecache.clearcache() @@ -1050,7 +1023,7 @@ def test_issue31411(self): # warn_explicit() shouldn't raise a SystemError in case # warnings.onceregistry isn't a dictionary. wmod = self.module - with wmod.catch_warnings(module=wmod): + with wmod.catch_warnings(): wmod.filterwarnings('once') with support.swap_attr(wmod, 'onceregistry', None): with self.assertRaises(TypeError): @@ -1061,7 +1034,7 @@ def test_issue31416(self): # warn_explicit() shouldn't cause an assertion failure in case of a # bad warnings.filters or warnings.defaultaction. wmod = self.module - with wmod.catch_warnings(module=wmod): + with wmod.catch_warnings(): wmod._get_filters()[:] = [(None, None, Warning, None, 0)] with self.assertRaises(TypeError): wmod.warn_explicit('foo', Warning, 'bar', 1) @@ -1075,7 +1048,7 @@ def test_issue31416(self): def test_issue31566(self): # warn() shouldn't cause an assertion failure in case of a bad # __name__ global. - with self.module.catch_warnings(module=self.module): + with self.module.catch_warnings(): self.module.filterwarnings('error', category=UserWarning) with support.swap_item(globals(), '__name__', b'foo'), \ support.swap_item(globals(), '__file__', None): @@ -1209,12 +1182,12 @@ def test_catch_warnings_restore(self): orig_filters = wmod.filters orig_showwarning = wmod.showwarning # Ensure both showwarning and filters are restored when recording - with wmod.catch_warnings(module=wmod, record=True): + with wmod.catch_warnings(record=True): wmod.filters = wmod.showwarning = object() self.assertIs(wmod.filters, orig_filters) self.assertIs(wmod.showwarning, orig_showwarning) # Same test, but with recording disabled - with wmod.catch_warnings(module=wmod, record=False): + with wmod.catch_warnings(record=False): wmod.filters = wmod.showwarning = object() self.assertIs(wmod.filters, orig_filters) self.assertIs(wmod.showwarning, orig_showwarning) @@ -1222,7 +1195,7 @@ def test_catch_warnings_restore(self): def test_catch_warnings_recording(self): wmod = self.module # Ensure warnings are recorded when requested - with wmod.catch_warnings(module=wmod, record=True) as w: + with wmod.catch_warnings(record=True) as w: self.assertEqual(w, []) self.assertIs(type(w), list) wmod.simplefilter("always") @@ -1236,19 +1209,19 @@ def test_catch_warnings_recording(self): self.assertEqual(w, []) # Ensure warnings are not recorded when not requested orig_showwarning = wmod.showwarning - with wmod.catch_warnings(module=wmod, record=False) as w: + with wmod.catch_warnings(record=False) as w: self.assertIsNone(w) self.assertIs(wmod.showwarning, orig_showwarning) def test_catch_warnings_reentry_guard(self): wmod = self.module # Ensure catch_warnings is protected against incorrect usage - x = wmod.catch_warnings(module=wmod, record=True) + x = wmod.catch_warnings(record=True) self.assertRaises(RuntimeError, x.__exit__) with x: self.assertRaises(RuntimeError, x.__enter__) # Same test, but with recording disabled - x = wmod.catch_warnings(module=wmod, record=False) + x = wmod.catch_warnings(record=False) self.assertRaises(RuntimeError, x.__exit__) with x: self.assertRaises(RuntimeError, x.__enter__) @@ -1258,7 +1231,7 @@ def test_catch_warnings_defaults(self): orig_filters = wmod._get_filters() orig_showwarning = wmod.showwarning # Ensure default behaviour is not to record warnings - with wmod.catch_warnings(module=wmod) as w: + with wmod.catch_warnings() as w: self.assertIsNone(w) self.assertIs(wmod.showwarning, orig_showwarning) self.assertIsNot(wmod._get_filters(), orig_filters) @@ -1288,7 +1261,7 @@ def my_logger(message, category, filename, lineno, file=None, line=None): # Override warnings.showwarning() before calling catch_warnings() with support.swap_attr(wmod, 'showwarning', my_logger): - with wmod.catch_warnings(module=wmod, record=True) as log: + with wmod.catch_warnings(record=True) as log: self.assertIsNot(wmod.showwarning, my_logger) wmod.simplefilter("always") @@ -1315,7 +1288,7 @@ def my_logger(message, category, filename, lineno, file=None, line=None): nonlocal my_log my_log.append(message) - with wmod.catch_warnings(module=wmod, record=True) as log: + with wmod.catch_warnings(record=True) as log: wmod.simplefilter("always") wmod.showwarning = my_logger wmod.warn(text) From 2466cec58123ab198aef91f9b42da4c88d0a30b8 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 18 Feb 2025 21:36:05 -0800 Subject: [PATCH 20/39] Correct error in warnings module docs. --- Doc/library/warnings.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index ba52f735a25f31..7caa60b840edc2 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -661,8 +661,8 @@ depending on the value of the flag. When *record* is true and the flag is false, the context manager works by replacing and then later restoring the module's :func:`showwarning` function. That is not concurrent-safe. -When *record* is true and the flag is false, the :func:`showwarning` function -is not replaced. The recording status is instead indicated by an internal +When *record* is true and the flag is true, the :func:`showwarning` function +is not replaced. Instead, the recording status is indicated by an internal property in the context variable. In this case, the :func:`showwarning` function will not be restored when exiting the context handler. From 165a57359d6085513ecf95faa2b76c68064553a3 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 19 Feb 2025 09:47:27 -0800 Subject: [PATCH 21/39] Use PyObject_GetAttr. --- Include/internal/pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + Include/internal/pycore_runtime_init_generated.h | 1 + Include/internal/pycore_unicodeobject_generated.h | 4 ++++ Python/_warnings.c | 2 +- 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 90214a314031d1..99f31ea169ddba 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -749,6 +749,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_feature_version)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_field_types)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_fields_)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_filters)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_finalizing)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_find_and_load)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_fix_up_module)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 97a75d0c46c867..14beb890fe8384 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -238,6 +238,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(_feature_version) STRUCT_FOR_ID(_field_types) STRUCT_FOR_ID(_fields_) + STRUCT_FOR_ID(_filters) STRUCT_FOR_ID(_finalizing) STRUCT_FOR_ID(_find_and_load) STRUCT_FOR_ID(_fix_up_module) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 4f928cc050bf8e..c66cfc66fd6c13 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -747,6 +747,7 @@ extern "C" { INIT_ID(_feature_version), \ INIT_ID(_field_types), \ INIT_ID(_fields_), \ + INIT_ID(_filters), \ INIT_ID(_finalizing), \ INIT_ID(_find_and_load), \ INIT_ID(_fix_up_module), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 5b78d038fc1192..8ef8dac2056c4f 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -748,6 +748,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_filters); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_finalizing); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Python/_warnings.c b/Python/_warnings.c index 24792d53119525..0231997d0466d3 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -288,7 +288,7 @@ get_warnings_context_filters(PyInterpreterState *interp) Py_DECREF(ctx); Py_RETURN_NONE; } - PyObject *context_filters = PyObject_GetAttrString(ctx, "_filters"); + PyObject *context_filters = PyObject_GetAttr(ctx, &_Py_ID(_filters)); Py_DECREF(ctx); if (context_filters == NULL) { return NULL; From af8728db16d5ffcc1c4d4995814a0770a1537b52 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 19 Feb 2025 09:48:33 -0800 Subject: [PATCH 22/39] Typo fix for docstring. --- Lib/_py_warnings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_py_warnings.py b/Lib/_py_warnings.py index 6c16d213be34bd..c568fd774649f0 100644 --- a/Lib/_py_warnings.py +++ b/Lib/_py_warnings.py @@ -13,9 +13,9 @@ # Normally '_wm' is sys.modules['warnings'] but for unit tests it can be # a different module. User code is allowed to reassign global attributes # of the 'warnings' module, commonly 'filters' or 'showwarning'. So we -# need to lookup these global attributes dynamically on the '_wn' object, +# need to lookup these global attributes dynamically on the '_wm' object, # rather than binding them earlier. The code in this module consistently uses -# '_wn.' rather than using the globals of this module. If the +# '_wm.' rather than using the globals of this module. If the # '_warnings' C extension is in use, some globals are replaced by functions # and variables defined in that extension. _wm = None From 1c02b7e7f82d7f52d1daee2d9ac8a7b746b2f659 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 19 Feb 2025 09:48:51 -0800 Subject: [PATCH 23/39] Avoid DECREF calls on None. --- Python/_warnings.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Python/_warnings.c b/Python/_warnings.c index 0231997d0466d3..eb4204d68d5fcc 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -285,7 +285,6 @@ get_warnings_context_filters(PyInterpreterState *interp) return NULL; } if (ctx == Py_None) { - Py_DECREF(ctx); Py_RETURN_NONE; } PyObject *context_filters = PyObject_GetAttr(ctx, &_Py_ID(_filters)); @@ -509,7 +508,6 @@ get_filter(PyInterpreterState *interp, PyObject *category, } if (context_filters == Py_None) { use_global_filters = true; - Py_DECREF(context_filters); } else { PyObject *context_action = NULL; if (!filter_search(interp, category, text, lineno, module, "_warnings_context _filters", From dba89e0d1a4dd96cfdc07afb58e9d49cbd6495fa Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 19 Feb 2025 09:50:46 -0800 Subject: [PATCH 24/39] Add warnings.py module (missed in previous commit). --- Lib/warnings.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Lib/warnings.py diff --git a/Lib/warnings.py b/Lib/warnings.py new file mode 100644 index 00000000000000..6759857d909399 --- /dev/null +++ b/Lib/warnings.py @@ -0,0 +1,99 @@ +import sys + +__all__ = [ + "warn", + "warn_explicit", + "showwarning", + "formatwarning", + "filterwarnings", + "simplefilter", + "resetwarnings", + "catch_warnings", + "deprecated", +] + +from _py_warnings import ( + WarningMessage, + _DEPRECATED_MSG, + _OptionError, + _add_filter, + _deprecated, + _filters_mutated, + _filters_mutated_lock_held, + _filters_version, + _formatwarning_orig, + _formatwarnmsg, + _formatwarnmsg_impl, + _get_context, + _get_filters, + _getaction, + _getcategory, + _is_filename_to_skip, + _is_internal_filename, + _is_internal_frame, + _lock, + _new_context, + _next_external_frame, + _processoptions, + _set_context, + _set_module, + _setoption, + _setup_defaults, + _showwarning_orig, + _showwarnmsg, + _showwarnmsg_impl, + _use_context, + _warn_unawaited_coroutine, + _warnings_context, + catch_warnings, + defaultaction, + deprecated, + filters, + filterwarnings, + formatwarning, + onceregistry, + resetwarnings, + showwarning, + simplefilter, + warn, + warn_explicit, +) + +try: + # Try to use the C extension, this will replace some parts of the + # _py_warnings implementation imported above. + from _warnings import ( + _acquire_lock, + _defaultaction as defaultaction, + _filters_mutated_lock_held, + _onceregistry as onceregistry, + _release_lock, + _warnings_context, + filters, + warn, + warn_explicit, + ) + + _warnings_defaults = True + + class _Lock: + def __enter__(self): + _acquire_lock() + return self + + def __exit__(self, *args): + _release_lock() + + _lock = _Lock() +except ImportError: + _warnings_defaults = False + + +# Module initialization +_set_module(sys.modules[__name__]) +_processoptions(sys.warnoptions) +if not _warnings_defaults: + _setup_defaults() + +del _warnings_defaults +del _setup_defaults From 2c48fc5122522a8aaefa358c1b7e860bc0d268de Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 19 Feb 2025 09:53:11 -0800 Subject: [PATCH 25/39] Add comment about why 'context' is passed in test. --- Lib/test/test_decimal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 48a31e5098c1d1..884b272482e72d 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1726,6 +1726,9 @@ def test_threading(self): self.finish1 = threading.Event() self.finish2 = threading.Event() + # This test wants to start threads with an empty context, no matter + # the setting of sys.flags.thread_inherit_context. We pass the + # 'context' argument explicitly with an empty context instance. th1 = threading.Thread(target=thfunc1, args=(self,), context=contextvars.Context()) th2 = threading.Thread(target=thfunc2, args=(self,), From 53eb72d5511ab0f6e2cd6c0c8b8ea6c52db45c6b Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 21 Feb 2025 12:07:59 -0800 Subject: [PATCH 26/39] Revise "decimal' docs, adding note about flag. --- Doc/library/decimal.rst | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index 9318af60b60f95..2111ff67edaa3d 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -1884,13 +1884,20 @@ the current thread. If :func:`setcontext` has not been called before :func:`getcontext`, then :func:`getcontext` will automatically create a new context for use in the -current thread. - -The new context is copied from a prototype context called *DefaultContext*. To -control the defaults so that each thread will use the same values throughout the -application, directly modify the *DefaultContext* object. This should be done -*before* any threads are started so that there won't be a race condition between -threads calling :func:`getcontext`. For example:: +current thread. New context objects have default values set from the +:data:`decimal.DefaultContext` object. + +The :data:`sys.flags.thread_inherit_context` flag affects the context for +new threads. If the flag is false, new threads will start with an empty +context. In this case, :func:`getcontext` will create a new context object +when called and use the default values from *DefaultContext*. If the flag +is true, new threads will start with a copy of context from the caller of +:meth:`Thread.start`. + +To control the defaults so that each thread will use the same values throughout +the application, directly modify the *DefaultContext* object. This should be +done *before* any threads are started so that there won't be a race condition +between threads calling :func:`getcontext`. For example:: # Set applicationwide defaults for all threads about to be launched DefaultContext.prec = 12 From f79daaa392c854880ce8af16ee0d0f17503f3a56 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 25 Feb 2025 15:57:22 -0800 Subject: [PATCH 27/39] Fix race in filter_search(). Using the critical section seems the easiest fix for this. Add a unit test that fails TSAN check if the fix is not applied. --- Lib/test/test_free_threading/test_races.py | 32 ++++++++++++++++++++++ Python/_warnings.c | 30 +++++++++++++------- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_free_threading/test_races.py b/Lib/test/test_free_threading/test_races.py index 85aa69c8cd494f..b2eccb5822cbca 100644 --- a/Lib/test/test_free_threading/test_races.py +++ b/Lib/test/test_free_threading/test_races.py @@ -5,6 +5,7 @@ import time import unittest import _testinternalcapi +import warnings from test.support import threading_helper @@ -286,5 +287,36 @@ def set_recursion_limit(): do_race(something_recursive, set_recursion_limit) +@threading_helper.requires_working_threading() +class TestWarningsRaces(TestBase): + def setUp(self): + self.saved_filters = warnings.filters[:] + # Add multiple filters to the list to increase odds of race. + for lineno in range(20): + warnings.filterwarnings('ignore', message='not matched', category=Warning, lineno=lineno) + # Override showwarning() so that we don't actually show warnings. + def showwarning(*args): + pass + warnings.showwarning = showwarning + + def tearDown(self): + warnings.filters[:] = self.saved_filters + warnings.showwarning = warnings._showwarning_orig + + def test_racing_warnings_filter(self): + # Modifying the warnings.filters list while another thread is using + # warn() should not crash or race. + def modify_filters(): + time.sleep(0) + warnings.filters[:] = [('ignore', None, UserWarning, None, 0)] + time.sleep(0) + warnings.filters[:] = self.saved_filters + + def emit_warning(): + warnings.warn('dummy message', category=UserWarning) + + do_race(modify_filters, emit_warning) + + if __name__ == "__main__": unittest.main() diff --git a/Python/_warnings.c b/Python/_warnings.c index eb4204d68d5fcc..af3eaf62e74f0a 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -424,7 +424,10 @@ filter_search(PyInterpreterState *interp, PyObject *category, PyObject *text, Py_ssize_t lineno, PyObject *module, char *list_name, PyObject *filters, PyObject **item, PyObject **matched_action) { - /* filters list could change while we are iterating over it. */ + bool result = true; + *matched_action = NULL; + /* Avoid the filters list changing while we iterate over it. */ + Py_BEGIN_CRITICAL_SECTION(filters); for (Py_ssize_t i = 0; i < PyList_GET_SIZE(filters); i++) { PyObject *tmp_item, *action, *msg, *cat, *mod, *ln_obj; Py_ssize_t ln; @@ -434,7 +437,8 @@ filter_search(PyInterpreterState *interp, PyObject *category, if (!PyTuple_Check(tmp_item) || PyTuple_GET_SIZE(tmp_item) != 5) { PyErr_Format(PyExc_ValueError, "warnings.%s item %zd isn't a 5-tuple", list_name, i); - return false; + result = false; + break; } /* Python code: action, msg, cat, mod, ln = item */ @@ -450,43 +454,49 @@ filter_search(PyInterpreterState *interp, PyObject *category, "action must be a string, not '%.200s'", Py_TYPE(action)->tp_name); Py_DECREF(tmp_item); - return false; + result = false; + break; } good_msg = check_matched(interp, msg, text); if (good_msg == -1) { Py_DECREF(tmp_item); - return false; + result = false; + break; } good_mod = check_matched(interp, mod, module); if (good_mod == -1) { Py_DECREF(tmp_item); - return false; + result = false; + break; } is_subclass = PyObject_IsSubclass(category, cat); if (is_subclass == -1) { Py_DECREF(tmp_item); - return false; + result = false; + break; } ln = PyLong_AsSsize_t(ln_obj); if (ln == -1 && PyErr_Occurred()) { Py_DECREF(tmp_item); - return false; + result = false; + break; } if (good_msg && is_subclass && good_mod && (ln == 0 || lineno == ln)) { *item = tmp_item; *matched_action = action; - return true; + result = true; + break; } Py_DECREF(tmp_item); } - *matched_action = NULL; - return true; + Py_END_CRITICAL_SECTION(); + return result; } /* The item is a new reference. */ From 5ca1d39dc3431c219bc46d875d151dc219d048fd Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Mar 2025 15:56:06 -0700 Subject: [PATCH 28/39] Add note to free-threading howto. --- Doc/howto/free-threading-python.rst | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Doc/howto/free-threading-python.rst b/Doc/howto/free-threading-python.rst index cd920553a3a461..e3de98e1683176 100644 --- a/Doc/howto/free-threading-python.rst +++ b/Doc/howto/free-threading-python.rst @@ -152,3 +152,33 @@ to re-enable it in a thread-safe way in the 3.14 release. This overhead is expected to be reduced in upcoming Python release. We are aiming for an overhead of 10% or less on the pyperformance suite compared to the default GIL-enabled build. + + +Behavioral changes +================== + +This section describes CPython behavioural changes with the free-threaded +build. + + +Context variables +----------------- + +In the free-threaded build, the flag :data:`~sys.flags.thread_inherit_context` +is set to true by default. In the default GIL-enabled build, the flag +defaults to false. This will cause threads created with +:class:`threading.Thread` to start with a copy of the +:class:`~contextvars.Context()` of the caller of +:meth:`~threading.Thread.start`. If the flag is false, threads start with an +empty :class:`~contextvars.Context()`. + + +Warning filters +--------------- + +In the free-threaded build, the flag :data:`~sys.flags.context_aware_warnings` +is set to true by default. In the default GIL-enabled build, the flag defaults +to false. If the flag is true then the :class:`warnings.catch_warnings` +context manager uses a context variable for warning filters. If the flag is +false then :class:`~warnings.catch_warnings` modifies the global filters list, +which is not thread-safe. See the :mod:`warnings` module for more details. From bad0fdba3b37d4d491c6b96214aca1a01a13a4e4 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Mar 2025 17:21:12 -0700 Subject: [PATCH 29/39] Doc fixes for missing refs. --- Doc/library/decimal.rst | 2 +- Doc/library/warnings.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index 2111ff67edaa3d..1b334b0aa5f56c 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -1892,7 +1892,7 @@ new threads. If the flag is false, new threads will start with an empty context. In this case, :func:`getcontext` will create a new context object when called and use the default values from *DefaultContext*. If the flag is true, new threads will start with a copy of context from the caller of -:meth:`Thread.start`. +:meth:`threading.Thread.start`. To control the defaults so that each thread will use the same values throughout the application, directly modify the *DefaultContext* object. This should be diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 7caa60b840edc2..00bafd1be4bd0c 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -680,8 +680,8 @@ context_aware_warnings<-X>` command-line option or by the it. When true, the context established by :class:`catch_warnings` in one thread will also apply to new threads started by it. If false, new threads will start with an empty warnings context variable, - meaning that only the filters in :data:`warnings.filters` will be - active. + meaning that any filtering that was established by a + :class:`catch_warnings` context manager will no longer be active. .. versionchanged:: 3.14 From bb35c2e5d59e9ad17beaadf557468e70617fad62 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Mar 2025 18:03:03 -0700 Subject: [PATCH 30/39] Add "_py_warnings" to stdlib_module_names.h. --- Python/stdlib_module_names.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index 584b050fc4bb6e..4b6b0a89701dd7 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -62,6 +62,7 @@ static const char* _Py_stdlib_module_names[] = { "_posixshmem", "_posixsubprocess", "_py_abc", +"_py_warnings", "_pydatetime", "_pydecimal", "_pyio", From adf32cb66b38d950b63dc633ae81b27c31c4a23c Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 17 Mar 2025 10:15:19 -0700 Subject: [PATCH 31/39] Improve error text in Python/_warnings.c Co-authored-by: Kumar Aditya --- Python/_warnings.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/_warnings.c b/Python/_warnings.c index 10d42741623183..329947612595fd 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -294,7 +294,7 @@ get_warnings_context_filters(PyInterpreterState *interp) } if (!PyList_Check(context_filters)) { PyErr_SetString(PyExc_ValueError, - "warnings._warnings_context _filters must be a list"); + "_filters of warnings._warnings_context must be a list"); Py_DECREF(context_filters); return NULL; } From 47d0b6f3d3a0f40e610ac68b4274411fbb110184 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 17 Mar 2025 10:25:05 -0700 Subject: [PATCH 32/39] Avoid unused-variable warning in _warnings.c. --- Python/_warnings.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/_warnings.c b/Python/_warnings.c index 329947612595fd..28c3ad9302e13f 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -505,10 +505,11 @@ get_filter(PyInterpreterState *interp, PyObject *category, PyObject *text, Py_ssize_t lineno, PyObject *module, PyObject **item) { +#ifdef Py_DEBUG WarningsState *st = warnings_get_state(interp); assert(st != NULL); - assert(warnings_lock_held(st)); +#endif /* check _warning_context _filters list */ PyObject *context_filters = get_warnings_context_filters(interp); From b880dd1f5923c5480d214d1dea48ac4973909930 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 17 Mar 2025 13:48:51 -0700 Subject: [PATCH 33/39] Minor code improvement to _warnings.c. --- Python/_warnings.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/_warnings.c b/Python/_warnings.c index 28c3ad9302e13f..d2a3f780008d4f 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -513,10 +513,10 @@ get_filter(PyInterpreterState *interp, PyObject *category, /* check _warning_context _filters list */ PyObject *context_filters = get_warnings_context_filters(interp); - bool use_global_filters = false; if (context_filters == NULL) { return NULL; } + bool use_global_filters = false; if (context_filters == Py_None) { use_global_filters = true; } else { From 9220223ebfc51110a0cddd4a684c3cae9c28a647 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 26 Mar 2025 17:55:00 -0700 Subject: [PATCH 34/39] Add extra unit tests. Check behaviour of the catch_warnings() context manager when used with asyncio co-routines and threads. --- Lib/test/test_warnings/__init__.py | 113 +++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 64fb4153c68e71..7b1482617713cc 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1528,6 +1528,119 @@ def test_late_resource_warning(self): self.assertTrue(err.startswith(expected), ascii(err)) +class AsyncTests(BaseTest): + """Verifies that the catch_warnings() context manager behaves + as expected when used inside async co-routines. This requires + that the context_aware_warnings flag is enabled, so that + the context manager uses a context variable. + """ + + @unittest.skipIf(not sys.flags.context_aware_warnings, + "requires context aware warnings") + def test_async_context(self): + import asyncio + + async def run_a(): + with self.module.catch_warnings(record=True) as w: + await asyncio.sleep(0) + # The warning emitted here should be caught be the enclosing + # context manager. + self.module.warn('run_a warning', UserWarning) + await asyncio.sleep(0) + self.assertEqual(len(w), 1) + self.assertEqual(w[0].message.args[0], 'run_a warning') + + async def run_b(): + with self.module.catch_warnings(record=True) as w: + await asyncio.sleep(0) + # The warning emitted here should be caught be the enclosing + # context manager. + self.module.warn('run_b warning', UserWarning) + await asyncio.sleep(0) + self.assertEqual(len(w), 1) + self.assertEqual(w[0].message.args[0], 'run_b warning') + + async def run_tasks(): + task_a = asyncio.create_task(run_a()) + task_b = asyncio.create_task(run_b()) + await asyncio.gather(task_a, task_b) + + asyncio.run(run_tasks()) + + +class CAsyncTests(AsyncTests, unittest.TestCase): + module = c_warnings + + +class PyAsyncTests(AsyncTests, unittest.TestCase): + module = py_warnings + + +class ThreadTests(BaseTest): + """Verifies that the catch_warnings() context manager behaves as + expected when used within threads. This requires that both the + context_aware_warnings flag and thread_inherit_context flags are enabled. + """ + + ENABLE_THREAD_TESTS = (sys.flags.context_aware_warnings and + sys.flags.thread_inherit_context) + + @unittest.skipIf(not ENABLE_THREAD_TESTS, + "requires thread-safe warnings flags") + def test_threaded_context(self): + import threading + + barrier = threading.Barrier(2) + + def run_a(): + with self.module.catch_warnings(record=True) as w: + barrier.wait() + # The warning emitted here should be caught be the enclosing + # context manager. + self.module.warn('run_a warning', UserWarning) + barrier.wait() + self.assertEqual(len(w), 1) + self.assertEqual(w[0].message.args[0], 'run_a warning') + # Should be caught be the catch_warnings() context manager of run_threads() + self.module.warn('main warning', UserWarning) + + def run_b(): + with self.module.catch_warnings(record=True) as w: + barrier.wait() + # The warning emitted here should be caught be the enclosing + # context manager. + barrier.wait() + self.module.warn('run_b warning', UserWarning) + self.assertEqual(len(w), 1) + self.assertEqual(w[0].message.args[0], 'run_b warning') + # Should be caught be the catch_warnings() context manager of run_threads() + self.module.warn('main warning', UserWarning) + + def run_threads(): + threads = [ + threading.Thread(target=run_a), + threading.Thread(target=run_b), + ] + with self.module.catch_warnings(record=True) as w: + for thread in threads: + thread.start() + for thread in threads: + thread.join() + self.assertEqual(len(w), 2) + self.assertEqual(w[0].message.args[0], 'main warning') + self.assertEqual(w[1].message.args[0], 'main warning') + + run_threads() + + +class CThreadTests(ThreadTests, unittest.TestCase): + module = c_warnings + + +class PyThreadTests(ThreadTests, unittest.TestCase): + module = py_warnings + + class DeprecatedTests(PyPublicAPITests): def test_dunder_deprecated(self): @deprecated("A will go away soon") From c2c90cb5014566120c5ad3a9266139bb6972aa59 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 27 Mar 2025 10:21:32 -0700 Subject: [PATCH 35/39] Use asyncio events to get deterministic execution. --- Lib/test/test_warnings/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 7b1482617713cc..25db2d8313d9e8 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1540,23 +1540,33 @@ class AsyncTests(BaseTest): def test_async_context(self): import asyncio + # Events to force the execution interleaving we want. + step_a1 = asyncio.Event() + step_a2 = asyncio.Event() + step_b1 = asyncio.Event() + step_b2 = asyncio.Event() + async def run_a(): with self.module.catch_warnings(record=True) as w: - await asyncio.sleep(0) + await step_a1.wait() # The warning emitted here should be caught be the enclosing # context manager. self.module.warn('run_a warning', UserWarning) - await asyncio.sleep(0) + step_b1.set() + await step_a2.wait() self.assertEqual(len(w), 1) self.assertEqual(w[0].message.args[0], 'run_a warning') + step_b2.set() async def run_b(): with self.module.catch_warnings(record=True) as w: - await asyncio.sleep(0) + step_a1.set() + await step_b1.wait() # The warning emitted here should be caught be the enclosing # context manager. self.module.warn('run_b warning', UserWarning) - await asyncio.sleep(0) + step_a2.set() + await step_b2.wait() self.assertEqual(len(w), 1) self.assertEqual(w[0].message.args[0], 'run_b warning') From 4e461d9227200014bb0addbaca6c91a6e59da927 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 9 Apr 2025 12:36:21 -0700 Subject: [PATCH 36/39] Add unit test for asyncio tasks. --- Lib/test/test_warnings/__init__.py | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 9669fd9a1a6c4e..94e69ad17c03b4 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1577,6 +1577,42 @@ async def run_tasks(): asyncio.run(run_tasks()) + @unittest.skipIf(not sys.flags.context_aware_warnings, + "requires context aware warnings") + def test_async_task_inherit(self): + """Check that a new asyncio task inherits warnings context from the + coroutine that spawns it. + """ + import asyncio + + step1 = asyncio.Event() + step2 = asyncio.Event() + + async def run_child1(): + await step1.wait() + # This should be recorded by the run_parent() catch_warnings + # context. + self.module.warn('child warning', UserWarning) + step2.set() + + async def run_child2(): + # This establishes a new catch_warnings() context. The + # run_child1() task should still be using the context from + # run_parent() if context-aware warnings are enabled. + with self.module.catch_warnings(record=True) as w: + step1.set() + await step2.wait() + + async def run_parent(): + with self.module.catch_warnings(record=True) as w: + child1_task = asyncio.create_task(run_child1()) + child2_task = asyncio.create_task(run_child2()) + await step2.wait() + self.assertEqual(len(w), 1) + self.assertEqual(w[0].message.args[0], 'child warning') + + asyncio.run(run_parent()) + class CAsyncTests(AsyncTests, unittest.TestCase): module = c_warnings From 543927b7b3cb80cb23b06e63f01f6fcc5c926752 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 9 Apr 2025 10:11:28 -0700 Subject: [PATCH 37/39] Update Doc/howto/free-threading-python.rst Co-authored-by: Kumar Aditya --- Doc/howto/free-threading-python.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/howto/free-threading-python.rst b/Doc/howto/free-threading-python.rst index e3de98e1683176..f7a894ac2cd78e 100644 --- a/Doc/howto/free-threading-python.rst +++ b/Doc/howto/free-threading-python.rst @@ -165,11 +165,11 @@ Context variables ----------------- In the free-threaded build, the flag :data:`~sys.flags.thread_inherit_context` -is set to true by default. In the default GIL-enabled build, the flag -defaults to false. This will cause threads created with +is set to true by default which causes threads created with :class:`threading.Thread` to start with a copy of the :class:`~contextvars.Context()` of the caller of -:meth:`~threading.Thread.start`. If the flag is false, threads start with an +:meth:`~threading.Thread.start`. In the default GIL-enabled build, the flag +defaults to false so threads start with an empty :class:`~contextvars.Context()`. From 4f1191019a1a1ce6f7a2c77889e7d08373a69493 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 9 Apr 2025 13:53:35 -0700 Subject: [PATCH 38/39] Use asyncio.gather() rather than create_task(). This seems a bit simpler since gather() will create tasks as needed. --- Lib/test/test_warnings/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 94e69ad17c03b4..d1e79c1d61a5ed 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1571,9 +1571,7 @@ async def run_b(): self.assertEqual(w[0].message.args[0], 'run_b warning') async def run_tasks(): - task_a = asyncio.create_task(run_a()) - task_b = asyncio.create_task(run_b()) - await asyncio.gather(task_a, task_b) + await asyncio.gather(run_a(), run_b()) asyncio.run(run_tasks()) @@ -1605,9 +1603,7 @@ async def run_child2(): async def run_parent(): with self.module.catch_warnings(record=True) as w: - child1_task = asyncio.create_task(run_child1()) - child2_task = asyncio.create_task(run_child2()) - await step2.wait() + await asyncio.gather(run_child1(), run_child2()) self.assertEqual(len(w), 1) self.assertEqual(w[0].message.args[0], 'child warning') From 42d157beabc196862b3819941d7ba05f6c73ce72 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 9 Apr 2025 15:17:54 -0700 Subject: [PATCH 39/39] Call resetwarnings() in a few places. This is needed because unit tests are now run with "-W error" on the command line in CI (GH-128770). We want to start with an empty list of filters to avoid supurious errors. --- Lib/test/test_free_threading/test_races.py | 1 + Lib/test/test_warnings/__init__.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Lib/test/test_free_threading/test_races.py b/Lib/test/test_free_threading/test_races.py index b2eccb5822cbca..23b48c76195408 100644 --- a/Lib/test/test_free_threading/test_races.py +++ b/Lib/test/test_free_threading/test_races.py @@ -291,6 +291,7 @@ def set_recursion_limit(): class TestWarningsRaces(TestBase): def setUp(self): self.saved_filters = warnings.filters[:] + warnings.resetwarnings() # Add multiple filters to the list to increase odds of race. for lineno in range(20): warnings.filterwarnings('ignore', message='not matched', category=Warning, lineno=lineno) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index d1e79c1d61a5ed..1716acb46b93b0 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1535,6 +1535,10 @@ class AsyncTests(BaseTest): the context manager uses a context variable. """ + def setUp(self): + super().setUp() + self.module.resetwarnings() + @unittest.skipIf(not sys.flags.context_aware_warnings, "requires context aware warnings") def test_async_context(self): @@ -1627,6 +1631,10 @@ class ThreadTests(BaseTest): ENABLE_THREAD_TESTS = (sys.flags.context_aware_warnings and sys.flags.thread_inherit_context) + def setUp(self): + super().setUp() + self.module.resetwarnings() + @unittest.skipIf(not ENABLE_THREAD_TESTS, "requires thread-safe warnings flags") def test_threaded_context(self):