From c29fef5f9f77c42078ad19cdb0ccb8c6ab62fc85 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 22 Jul 2026 18:32:13 -0700 Subject: [PATCH 1/3] gh-154562: Add ContextVar.thread_inheritable(). Context variables created this way will automatically be inherited by the context for new threads, regardless of the setting of `thread_inherit_context`. --- Doc/howto/free-threading-python.rst | 5 +- Doc/library/contextvars.rst | 66 ++++ Doc/library/threading.rst | 6 +- Doc/using/cmdline.rst | 21 +- Include/internal/pycore_context.h | 7 + Lib/test/test_context.py | 297 +++++++++++++++++- Lib/threading.py | 19 +- ...-07-23-13-19-38.gh-issue-154562.WBaAJV.rst | 6 + Python/_contextvars.c | 14 + Python/clinic/_contextvars.c.h | 19 +- Python/clinic/context.c.h | 77 ++++- Python/context.c | 110 ++++++- 12 files changed, 617 insertions(+), 30 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst diff --git a/Doc/howto/free-threading-python.rst b/Doc/howto/free-threading-python.rst index 53bea1db191d76..51e24d13cc660f 100644 --- a/Doc/howto/free-threading-python.rst +++ b/Doc/howto/free-threading-python.rst @@ -152,8 +152,9 @@ 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`. In the default GIL-enabled build, the flag -defaults to false so threads start with an -empty :class:`~contextvars.Context()`. +defaults to false, so threads start with a context containing only the caller's +current bindings for context variables created by +:meth:`~contextvars.ContextVar.thread_inheritable`. Warning filters diff --git a/Doc/library/contextvars.rst b/Doc/library/contextvars.rst index b0cc0be8e911bf..eedbca31fc11fa 100644 --- a/Doc/library/contextvars.rst +++ b/Doc/library/contextvars.rst @@ -45,6 +45,72 @@ Context Variables :class:`!ContextVar`\s are :ref:`generic ` over the type of their contained value. + .. classmethod:: ContextVar.thread_inheritable(name, [*, default]) + + Return a new context variable whose binding is inherited by new + :class:`threading.Thread` instances. When + :meth:`threading.Thread.start` would otherwise start the thread with + an empty context (that is, when + :data:`sys.flags.thread_inherit_context` is false and no explicit + :class:`Context` was supplied), the new thread's context is initialized + with the caller's current bindings for all thread-inheritable variables. + This allows individual context variables to opt in to thread inheritance + on a per-variable basis. + + These are ordinary bindings in the new thread's context: they are + visible to :func:`copy_context`, may be changed independently with + :meth:`ContextVar.set`, and are in turn inherited by threads started + from the new thread. Threads started with an explicitly supplied + context are unaffected, as are threads started by means other than + :meth:`threading.Thread.start`, such as + :func:`_thread.start_new_thread` or the C API. + + The *name* and *default* parameters have the same meaning as for the + :class:`ContextVar` constructor. When + :data:`sys.flags.thread_inherit_context` is true, a variable created + with this method behaves identically to one created with + :class:`ContextVar`, so it is safe to use unconditionally. + + Libraries that also support Python versions without this method must + choose how to degrade on those versions. For state with a safe + default in a new thread (the behavior that :class:`threading.local` + based state has always had), fall back to an ordinary context + variable:: + + _new_var = getattr(ContextVar, "thread_inheritable", ContextVar) + var = _new_var("var") + + With this fallback, on older versions new threads see the variable's + default rather than the starting thread's binding, unless the + application enables :option:`-X thread_inherit_context <-X>`. + + For state that was previously a module-level global, reverting to the + default in new threads may break existing threaded code, since such + code has always seen the current global value. Those libraries can + instead pick the best semantics each interpreter supports:: + + if hasattr(ContextVar, "thread_inheritable"): + # Context-local and inherited by new threads. + _new_var = ContextVar.thread_inheritable + elif getattr(sys.flags, "thread_inherit_context", False): + # Threads inherit the whole context, so an ordinary + # context variable is inherited too. + _new_var = ContextVar + else: + # Keep the library's historical global semantics. + _new_var = _GlobalVar + + Here ``_GlobalVar`` is a class provided by the library that stores a + single process-wide value. To be substitutable for + :class:`ContextVar` it must accept the same constructor arguments + (*name* positional, keyword-only *default*), distinguish a missing + *default* from ``default=None``, and provide ``get()``, ``set()`` + returning a token, and ``reset()``. This case gives up task-local + isolation: concurrent tasks and threads share one value, exactly as + they did before the library adopted context variables. + + .. versionadded:: 3.16 + .. attribute:: ContextVar.name The name of the variable. This is a read-only property. diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 5d9a7b6314b166..391dd6b8acdebb 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -539,8 +539,10 @@ since it is impossible to detect the termination of alien threads. the thread. The default value is ``None`` which indicates that the :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 + caller of :meth:`~Thread.start`. If false, they will start with a + context containing only the caller's current bindings for context variables + created by :meth:`~contextvars.ContextVar.thread_inheritable`. 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. diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 677fbbae3f4219..6b1426da110afa 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -675,11 +675,12 @@ Miscellaneous options .. versionadded:: 3.13 * :samp:`-X thread_inherit_context={0,1}` causes :class:`~threading.Thread` - to, by default, use a copy of context 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_THREAD_INHERIT_CONTEXT`. + to, by default, use a copy of the context of the caller of + ``Thread.start()`` when starting. Otherwise, threads start with a context + containing only the caller's current bindings for context variables + created by :meth:`~contextvars.ContextVar.thread_inheritable`. If unset, the + value of this option defaults to ``1`` on free-threaded builds and to ``0`` + otherwise. See also :envvar:`PYTHON_THREAD_INHERIT_CONTEXT`. .. versionadded:: 3.14 @@ -1367,10 +1368,12 @@ conflict. .. 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 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 thread_inherit_context<-X>`. + by default, use a copy of the context of the caller of ``Thread.start()`` + when starting. Otherwise, new threads start with a context containing only + the caller's current bindings for context variables created by + :meth:`~contextvars.ContextVar.thread_inheritable`. If unset, this variable + defaults to ``1`` on free-threaded builds and to ``0`` otherwise. See also + :option:`-X thread_inherit_context<-X>`. .. versionadded:: 3.14 diff --git a/Include/internal/pycore_context.h b/Include/internal/pycore_context.h index a833f790a621b1..40d3d0d7ef8768 100644 --- a/Include/internal/pycore_context.h +++ b/Include/internal/pycore_context.h @@ -26,6 +26,11 @@ struct _pycontextobject { PyHamtObject *ctx_vars; PyObject *ctx_weakreflist; int ctx_entered; + // Redundant subset of ctx_vars holding only the bindings of + // thread-inheritable context variables (see + // ContextVar.thread_inheritable()). Used to efficiently create the + // starting context of a new thread. + PyHamtObject *ctx_thread_inheritable_vars; }; @@ -33,6 +38,7 @@ struct _pycontextvarobject { PyObject_HEAD PyObject *var_name; PyObject *var_default; + char var_thread_inheritable; #ifndef Py_GIL_DISABLED PyObject *var_cached; uint64_t var_cached_tsid; @@ -54,6 +60,7 @@ struct _pycontexttokenobject { // _testinternalcapi.hamt() used by tests. // Export for '_testcapi' shared extension PyAPI_FUNC(PyObject*) _PyContext_NewHamtForTests(void); +PyAPI_FUNC(PyObject*) _PyContext_NewThreadStartContext(void); PyAPI_FUNC(int) _PyContext_Enter(PyThreadState *ts, PyObject *octx); PyAPI_FUNC(int) _PyContext_Exit(PyThreadState *ts, PyObject *octx); diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index ef20495dcc01ea..b6baa801336ac3 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -9,7 +9,7 @@ import unittest import weakref from test import support -from test.support import threading_helper +from test.support import script_helper, threading_helper try: from _testinternalcapi import hamt @@ -40,8 +40,47 @@ def test_context_var_new_1(self): with self.assertRaises(AttributeError): c.name = 'bbb' + inheritable = contextvars.ContextVar.thread_inheritable('inheritable') + self.assertIs(type(inheritable), contextvars.ContextVar) + self.assertEqual(inheritable.name, 'inheritable') + + inheritable_with_default = ( + contextvars.ContextVar.thread_inheritable( + 'inheritable_with_default', default=42, + ) + ) + self.assertEqual(inheritable_with_default.get(), 42) + + with self.assertRaisesRegex(TypeError, 'must be a str'): + contextvars.ContextVar.thread_inheritable(1) + with self.assertRaises(TypeError): + contextvars.ContextVar.thread_inheritable('var', None) + with self.assertRaises(TypeError): + contextvars.ContextVar('var', inherit=True) + self.assertNotEqual(hash(c), hash('aaa')) + def test_thread_inheritable_context_var_gc(self): + class Value: + pass + + def make_cycle(): + var = contextvars.ContextVar.thread_inheritable('var') + ctx = contextvars.Context() + value = Value() + value_ref = weakref.ref(value) + + def bind(): + var.set(value) + value.context = contextvars.copy_context() + + ctx.run(bind) + return value_ref + + value_ref = make_cycle() + support.gc_collect() + self.assertIsNone(value_ref()) + @isolated_context def test_context_var_repr_1(self): c = contextvars.ContextVar('a') @@ -587,6 +626,262 @@ def __eq__(self, other): ctx1 == ctx2 +@threading_helper.requires_working_threading() +class ThreadInheritableVarTest(unittest.TestCase): + # These tests run in a subprocess with -X thread_inherit_context pinned, + # since its default depends on the build (true on free-threaded builds). + + def run_with_flag(self, flag, source): + _, _, stderr = script_helper.assert_python_ok( + '-X', f'thread_inherit_context={flag}', '-c', source) + self.assertEqual(stderr, b'') + + def test_thread_inheritance(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import ContextVar, copy_context + + inh = ContextVar.thread_inheritable('inh', default='default') + plain = ContextVar('plain') + inh.set('inherited') + plain.set('not inherited') + + def child(): + # The binding is a real binding in the thread's context: + # visible to get(), copy_context() and Context methods. + assert inh.get() == 'inherited' + ctx = copy_context() + assert inh in ctx + assert ctx[inh] == 'inherited' + assert ctx.run(inh.get) == 'inherited' + # Non-inheritable vars are not visible. + assert plain not in ctx + try: + plain.get() + except LookupError: + pass + else: + raise AssertionError('plain was inherited') + + t = threading.Thread(target=child) + t.start() + t.join() + """) + + def test_inheritance_captured_at_start_and_context_copy(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import Context, ContextVar + + inh = ContextVar.thread_inheritable('inh') + values = [] + + # The binding is captured by start(), not by Thread(). + t = threading.Thread(target=lambda: values.append(inh.get())) + inh.set('at start') + t.start() + t.join() + + def start_and_join(): + t = threading.Thread( + target=lambda: values.append(inh.get())) + t.start() + t.join() + + # Context.run() and Context.copy() preserve the inheritable subset. + ctx = Context() + ctx.run(inh.set, 'context') + ctx.run(start_and_join) + ctx_copy = ctx.copy() + ctx_copy.run(inh.set, 'copy') + ctx_copy.run(start_and_join) + + assert values == ['at start', 'context', 'copy'] + """) + + def test_thread_start_retry_recaptures_context(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import ContextVar + + inh = ContextVar.thread_inheritable('inh') + inh.set('first attempt') + values = [] + t = threading.Thread(target=lambda: values.append(inh.get())) + + start_joinable_thread = threading._start_joinable_thread + + def fail_start(*args, **kwargs): + raise threading.ThreadError + + threading._start_joinable_thread = fail_start + try: + try: + t.start() + except threading.ThreadError: + pass + else: + raise AssertionError('thread start did not fail') + finally: + threading._start_joinable_thread = start_joinable_thread + + inh.set('retry') + t.start() + t.join() + assert values == ['retry'] + """) + + def test_thread_set_and_reset(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import ContextVar + + inh = ContextVar.thread_inheritable('inh') + inh.set('inherited') + + def child(): + token = inh.set('child value') + assert inh.get() == 'child value' + inh.reset(token) + assert inh.get() == 'inherited' + + t = threading.Thread(target=child) + t.start() + t.join() + # The thread's set() does not affect the parent. + assert inh.get() == 'inherited' + """) + + def test_thread_inheritance_transitive(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import ContextVar + + inh = ContextVar.thread_inheritable('inh') + inh.set('inherited') + + def grandchild(): + assert inh.get() == 'inherited' + + def child(): + # The child never sets the var; the binding must still + # propagate to threads it starts. + t = threading.Thread(target=grandchild) + t.start() + t.join() + + t = threading.Thread(target=child) + t.start() + t.join() + """) + + def test_thread_inheritance_unset_or_deleted(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import ContextVar + + unset = ContextVar.thread_inheritable('unset', default='default') + deleted = ContextVar.thread_inheritable('deleted') + token = deleted.set('inherited') + deleted.reset(token) + + def child(): + assert unset.get() == 'default' + try: + deleted.get() + except LookupError: + pass + else: + raise AssertionError('deleted binding was inherited') + + t = threading.Thread(target=child) + t.start() + t.join() + """) + + def test_thread_explicit_context(self): + self.run_with_flag(0, """if True: + import threading + from contextvars import ContextVar, Context + + inh = ContextVar.thread_inheritable('inh', default='default') + inh.set('inherited') + + def child(): + assert inh.get() == 'default' + + t = threading.Thread(target=child, context=Context()) + t.start() + t.join() + """) + + def test_thread_inheritance_asyncio(self): + self.run_with_flag(0, """if True: + import asyncio + import threading + from contextvars import ContextVar + + inh = ContextVar.thread_inheritable('inh') + plain = ContextVar('plain') + inh.set('inherited') + plain.set('not inherited') + + def check_plain_not_set(): + try: + plain.get() + except LookupError: + pass + else: + raise AssertionError('plain was inherited') + + async def task(): + # Tasks run in a copy of the thread's context, which + # includes the inherited binding. + assert inh.get() == 'inherited' + check_plain_not_set() + # A set() inside the task is confined to the task. + inh.set('task value') + + async def main(): + assert inh.get() == 'inherited' + await asyncio.gather(task(), task()) + assert inh.get() == 'inherited' + # Callbacks also run in a copy of the current context. + loop = asyncio.get_running_loop() + fut = loop.create_future() + loop.call_soon( + lambda: fut.set_result((inh.get(), plain.get(None)))) + assert await fut == ('inherited', None) + + def child(): + asyncio.run(main()) + + t = threading.Thread(target=child) + t.start() + t.join() + """) + + def test_thread_inherit_context_flag_true(self): + self.run_with_flag(1, """if True: + import threading + from contextvars import ContextVar + + inh = ContextVar.thread_inheritable('inh') + plain = ContextVar('plain') + inh.set('inherited') + plain.set('also inherited') + + def child(): + # With the flag set, the full context is copied. + assert inh.get() == 'inherited' + assert plain.get() == 'also inherited' + + t = threading.Thread(target=child) + t.start() + t.join() + """) + + # HAMT Tests diff --git a/Lib/threading.py b/Lib/threading.py index abac31e25886fa..8f50449d5667c6 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1034,8 +1034,9 @@ 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.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 + of the context of the caller. If false, use a context containing only + the bindings of thread-inheritable context variables. 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(). @@ -1127,14 +1128,17 @@ def start(self): with _active_limbo_lock: _limbo[self] = self - if self._context is None: + context_is_implicit = self._context is None + if context_is_implicit: # No context provided if _sys.flags.thread_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() + # Start with a context containing only the bindings of + # thread-inheritable context variables (see + # ContextVar.thread_inheritable); usually empty. + self._context = _contextvars._thread_start_context() try: # Start joinable thread @@ -1143,6 +1147,9 @@ def start(self): except Exception: with _active_limbo_lock: del _limbo[self] + if context_is_implicit: + # Capture the caller's context again if start() is retried. + self._context = None raise self._started.wait() # Will set ident and native_id @@ -1219,6 +1226,8 @@ def _bootstrap_inner(self): except: self._invoke_excepthook(self) finally: + # Break references held by the context after any bootstrap path. + self._context = None self._delete() def _delete(self): diff --git a/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst b/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst new file mode 100644 index 00000000000000..08def726565d89 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst @@ -0,0 +1,6 @@ +Add :meth:`contextvars.ContextVar.thread_inheritable`, an alternative +constructor for context variables whose bindings are inherited by new +:class:`threading.Thread` instances even when +:data:`sys.flags.thread_inherit_context` is false. This lets libraries opt +individual context variables in to thread inheritance without requiring the +application to enable the flag process-wide. diff --git a/Python/_contextvars.c b/Python/_contextvars.c index 86acc94fbc79cd..2d722c01c031be 100644 --- a/Python/_contextvars.c +++ b/Python/_contextvars.c @@ -1,4 +1,5 @@ #include "Python.h" +#include "pycore_context.h" // _PyContext_NewThreadStartContext() #include "clinic/_contextvars.c.h" @@ -20,10 +21,23 @@ _contextvars_copy_context_impl(PyObject *module) } +/*[clinic input] +_contextvars._thread_start_context +[clinic start generated code]*/ + +static PyObject * +_contextvars__thread_start_context_impl(PyObject *module) +/*[clinic end generated code: output=7e656d156c385a65 input=4575c5d2223de58d]*/ +{ + return _PyContext_NewThreadStartContext(); +} + + PyDoc_STRVAR(module_doc, "Context Variables"); static PyMethodDef _contextvars_methods[] = { _CONTEXTVARS_COPY_CONTEXT_METHODDEF + _CONTEXTVARS__THREAD_START_CONTEXT_METHODDEF {NULL, NULL} }; diff --git a/Python/clinic/_contextvars.c.h b/Python/clinic/_contextvars.c.h index b1885e41c355d2..381d8c159beb4b 100644 --- a/Python/clinic/_contextvars.c.h +++ b/Python/clinic/_contextvars.c.h @@ -18,4 +18,21 @@ _contextvars_copy_context(PyObject *module, PyObject *Py_UNUSED(ignored)) { return _contextvars_copy_context_impl(module); } -/*[clinic end generated code: output=26e07024451baf52 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(_contextvars__thread_start_context__doc__, +"_thread_start_context($module, /)\n" +"--\n" +"\n"); + +#define _CONTEXTVARS__THREAD_START_CONTEXT_METHODDEF \ + {"_thread_start_context", (PyCFunction)_contextvars__thread_start_context, METH_NOARGS, _contextvars__thread_start_context__doc__}, + +static PyObject * +_contextvars__thread_start_context_impl(PyObject *module); + +static PyObject * +_contextvars__thread_start_context(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _contextvars__thread_start_context_impl(module); +} +/*[clinic end generated code: output=2c6c9d89faff4312 input=a9049054013a1b77]*/ diff --git a/Python/clinic/context.c.h b/Python/clinic/context.c.h index ece7341d65d5fb..f8a27bf5b8ab64 100644 --- a/Python/clinic/context.c.h +++ b/Python/clinic/context.c.h @@ -2,6 +2,10 @@ preserve [clinic start generated code]*/ +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif #include "pycore_modsupport.h" // _PyArg_CheckPositional() PyDoc_STRVAR(_contextvars_Context_get__doc__, @@ -116,6 +120,77 @@ _contextvars_Context_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) return _contextvars_Context_copy_impl((PyContext *)self); } +PyDoc_STRVAR(_contextvars_ContextVar_thread_inheritable__doc__, +"thread_inheritable($type, name, /, *, default=)\n" +"--\n" +"\n" +"Create a context variable whose binding is inherited by new threads.\n" +"\n" +"The bindings of such variables are copied into the context of a new\n" +"thread by threading.Thread.start() when the thread would otherwise\n" +"start with an empty context."); + +#define _CONTEXTVARS_CONTEXTVAR_THREAD_INHERITABLE_METHODDEF \ + {"thread_inheritable", _PyCFunction_CAST(_contextvars_ContextVar_thread_inheritable), METH_FASTCALL|METH_KEYWORDS|METH_CLASS, _contextvars_ContextVar_thread_inheritable__doc__}, + +static PyObject * +_contextvars_ContextVar_thread_inheritable_impl(PyTypeObject *type, + PyObject *name, + PyObject *default_value); + +static PyObject * +_contextvars_ContextVar_thread_inheritable(PyObject *type, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(default), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "default", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "thread_inheritable", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + PyObject *name; + PyObject *default_value = NULL; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + name = args[0]; + if (!noptargs) { + goto skip_optional_kwonly; + } + default_value = args[1]; +skip_optional_kwonly: + return_value = _contextvars_ContextVar_thread_inheritable_impl((PyTypeObject *)type, name, default_value); + +exit: + return return_value; +} + PyDoc_STRVAR(_contextvars_ContextVar_get__doc__, "get($self, default=, /)\n" "--\n" @@ -259,4 +334,4 @@ token_exit(PyObject *self, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=90ec3e4375804e9b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d8a9c933c7fcf8c3 input=a9049054013a1b77]*/ diff --git a/Python/context.c b/Python/context.c index 4678054ff3ad74..226d239a0fdc36 100644 --- a/Python/context.c +++ b/Python/context.c @@ -47,7 +47,8 @@ static PyContext * context_new_empty(void); static PyContext * -context_new_from_vars(PyHamtObject *vars); +context_new_from_vars(PyHamtObject *vars, + PyHamtObject *thread_inheritable_vars); static inline PyContext * context_get(void); @@ -56,7 +57,7 @@ static PyContextToken * token_new(PyContext *ctx, PyContextVar *var, PyObject *val); static PyContextVar * -contextvar_new(PyObject *name, PyObject *def); +contextvar_new(PyObject *name, PyObject *def, int thread_inheritable); static int contextvar_set(PyContextVar *var, PyObject *val); @@ -79,12 +80,29 @@ PyContext_New(void) } +PyObject * +_PyContext_NewThreadStartContext(void) +{ + PyContext *ctx = context_get(); + if (ctx == NULL) { + return NULL; + } + + // The thread-inheritable subset becomes the new context's full vars map. + // Every entry in it is thread-inheritable, so it is also its own subset. + return (PyObject *)context_new_from_vars( + ctx->ctx_thread_inheritable_vars, + ctx->ctx_thread_inheritable_vars); +} + + PyObject * PyContext_Copy(PyObject * octx) { ENSURE_Context(octx, NULL) PyContext *ctx = (PyContext *)octx; - return (PyObject *)context_new_from_vars(ctx->ctx_vars); + return (PyObject *)context_new_from_vars( + ctx->ctx_vars, ctx->ctx_thread_inheritable_vars); } @@ -96,7 +114,8 @@ PyContext_CopyCurrent(void) return NULL; } - return (PyObject *)context_new_from_vars(ctx->ctx_vars); + return (PyObject *)context_new_from_vars( + ctx->ctx_vars, ctx->ctx_thread_inheritable_vars); } static const char * @@ -269,7 +288,7 @@ PyContextVar_New(const char *name, PyObject *def) if (pyname == NULL) { return NULL; } - PyContextVar *var = contextvar_new(pyname, def); + PyContextVar *var = contextvar_new(pyname, def, 0); Py_DECREF(pyname); return (PyObject *)var; } @@ -437,6 +456,7 @@ _context_alloc(void) ctx->ctx_vars = NULL; ctx->ctx_prev = NULL; + ctx->ctx_thread_inheritable_vars = NULL; ctx->ctx_entered = 0; ctx->ctx_weakreflist = NULL; @@ -458,13 +478,20 @@ context_new_empty(void) return NULL; } + ctx->ctx_thread_inheritable_vars = _PyHamt_New(); + if (ctx->ctx_thread_inheritable_vars == NULL) { + Py_DECREF(ctx); + return NULL; + } + _PyObject_GC_TRACK(ctx); return ctx; } static PyContext * -context_new_from_vars(PyHamtObject *vars) +context_new_from_vars(PyHamtObject *vars, + PyHamtObject *thread_inheritable_vars) { PyContext *ctx = _context_alloc(); if (ctx == NULL) { @@ -472,6 +499,8 @@ context_new_from_vars(PyHamtObject *vars) } ctx->ctx_vars = (PyHamtObject*)Py_NewRef(vars); + ctx->ctx_thread_inheritable_vars = + (PyHamtObject*)Py_NewRef(thread_inheritable_vars); _PyObject_GC_TRACK(ctx); return ctx; @@ -523,6 +552,7 @@ context_tp_clear(PyObject *op) PyContext *self = _PyContext_CAST(op); Py_CLEAR(self->ctx_prev); Py_CLEAR(self->ctx_vars); + Py_CLEAR(self->ctx_thread_inheritable_vars); return 0; } @@ -532,6 +562,7 @@ context_tp_traverse(PyObject *op, visitproc visit, void *arg) PyContext *self = _PyContext_CAST(op); Py_VISIT(self->ctx_prev); Py_VISIT(self->ctx_vars); + Py_VISIT(self->ctx_thread_inheritable_vars); return 0; } @@ -708,7 +739,8 @@ static PyObject * _contextvars_Context_copy_impl(PyContext *self) /*[clinic end generated code: output=30ba8896c4707a15 input=ebafdbdd9c72d592]*/ { - return (PyObject *)context_new_from_vars(self->ctx_vars); + return (PyObject *)context_new_from_vars( + self->ctx_vars, self->ctx_thread_inheritable_vars); } @@ -801,7 +833,23 @@ contextvar_set(PyContextVar *var, PyObject *val) return -1; } + // Compute both new maps before installing either so that an error + // leaves the context unchanged. + PyHamtObject *new_thread_inheritable_vars = NULL; + if (var->var_thread_inheritable) { + new_thread_inheritable_vars = _PyHamt_Assoc( + ctx->ctx_thread_inheritable_vars, (PyObject *)var, val); + if (new_thread_inheritable_vars == NULL) { + Py_DECREF(new_vars); + return -1; + } + } + Py_SETREF(ctx->ctx_vars, new_vars); + if (new_thread_inheritable_vars != NULL) { + Py_SETREF(ctx->ctx_thread_inheritable_vars, + new_thread_inheritable_vars); + } #ifndef Py_GIL_DISABLED var->var_cached = val; /* borrow */ @@ -835,7 +883,23 @@ contextvar_del(PyContextVar *var) return -1; } + // Compute both new maps before installing either so that an error + // leaves the context unchanged. + PyHamtObject *new_thread_inheritable_vars = NULL; + if (var->var_thread_inheritable) { + new_thread_inheritable_vars = _PyHamt_Without( + ctx->ctx_thread_inheritable_vars, (PyObject *)var); + if (new_thread_inheritable_vars == NULL) { + Py_DECREF(new_vars); + return -1; + } + } + Py_SETREF(ctx->ctx_vars, new_vars); + if (new_thread_inheritable_vars != NULL) { + Py_SETREF(ctx->ctx_thread_inheritable_vars, + new_thread_inheritable_vars); + } return 0; } @@ -868,7 +932,7 @@ contextvar_generate_hash(void *addr, PyObject *name) } static PyContextVar * -contextvar_new(PyObject *name, PyObject *def) +contextvar_new(PyObject *name, PyObject *def, int thread_inheritable) { if (!PyUnicode_Check(name)) { PyErr_SetString(PyExc_TypeError, @@ -883,6 +947,7 @@ contextvar_new(PyObject *name, PyObject *def) var->var_name = Py_NewRef(name); var->var_default = Py_XNewRef(def); + var->var_thread_inheritable = thread_inheritable; #ifndef Py_GIL_DISABLED var->var_cached = NULL; @@ -927,7 +992,7 @@ contextvar_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return NULL; } - return (PyObject *)contextvar_new(name, def); + return (PyObject *)contextvar_new(name, def, 0); } static int @@ -1009,6 +1074,32 @@ contextvar_tp_repr(PyObject *op) } +/*[clinic input] +@classmethod +_contextvars.ContextVar.thread_inheritable + name: object + / + * + default: object = NULL + +Create a context variable whose binding is inherited by new threads. + +The bindings of such variables are copied into the context of a new +thread by threading.Thread.start() when the thread would otherwise +start with an empty context. +[clinic start generated code]*/ + +static PyObject * +_contextvars_ContextVar_thread_inheritable_impl(PyTypeObject *type, + PyObject *name, + PyObject *default_value) +/*[clinic end generated code: output=a890265ff610a979 input=f211f3eedeb507b8]*/ +{ + assert(type == &PyContextVar_Type); + return (PyObject *)contextvar_new(name, default_value, 1); +} + + /*[clinic input] _contextvars.ContextVar.get default: object = NULL @@ -1099,6 +1190,7 @@ static PyMemberDef PyContextVar_members[] = { }; static PyMethodDef PyContextVar_methods[] = { + _CONTEXTVARS_CONTEXTVAR_THREAD_INHERITABLE_METHODDEF _CONTEXTVARS_CONTEXTVAR_GET_METHODDEF _CONTEXTVARS_CONTEXTVAR_SET_METHODDEF _CONTEXTVARS_CONTEXTVAR_RESET_METHODDEF From f38695acf14e9504fe6bf5fc705498452f4172fa Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 24 Jul 2026 10:42:48 -0700 Subject: [PATCH 2/3] Add PyContextVar_NewThreadInheritable(). --- Doc/c-api/contextvars.rst | 8 ++++++++ Include/cpython/context.h | 7 +++++++ Python/context.c | 19 ++++++++++++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst index b7c6550ff34aac..487962800bc6dc 100644 --- a/Doc/c-api/contextvars.rst +++ b/Doc/c-api/contextvars.rst @@ -157,6 +157,14 @@ Context variable functions: a default value for the context variable, or ``NULL`` for no default. If an error has occurred, this function returns ``NULL``. +.. c:function:: PyObject *PyContextVar_NewThreadInheritable(const char *name, PyObject *def) + + Create a new ``ContextVar`` object whose bindings are inherited by new + :class:`threading.Thread` instances. The parameters and return value are + the same as for :c:func:`PyContextVar_New`. + + .. versionadded:: 3.16 + .. c:function:: int PyContextVar_Get(PyObject *var, PyObject *default_value, PyObject **value) Get the value of a context variable. Returns ``-1`` if an error has diff --git a/Include/cpython/context.h b/Include/cpython/context.h index 3a7a4b459c09ad..70c33c45e962e4 100644 --- a/Include/cpython/context.h +++ b/Include/cpython/context.h @@ -68,6 +68,13 @@ PyAPI_FUNC(int) PyContext_ClearWatcher(int watcher_id); PyAPI_FUNC(PyObject *) PyContextVar_New( const char *name, PyObject *default_value); +/* Create a new thread-inheritable context variable. + + default_value can be NULL. +*/ +PyAPI_FUNC(PyObject *) PyContextVar_NewThreadInheritable( + const char *name, PyObject *default_value); + /* Get a value for the variable. diff --git a/Python/context.c b/Python/context.c index 226d239a0fdc36..b912907fbd587f 100644 --- a/Python/context.c +++ b/Python/context.c @@ -281,18 +281,31 @@ PyContext_Exit(PyObject *octx) } -PyObject * -PyContextVar_New(const char *name, PyObject *def) +static PyObject * +contextvar_new_from_utf8(const char *name, PyObject *def, + int thread_inheritable) { PyObject *pyname = PyUnicode_FromString(name); if (pyname == NULL) { return NULL; } - PyContextVar *var = contextvar_new(pyname, def, 0); + PyContextVar *var = contextvar_new(pyname, def, thread_inheritable); Py_DECREF(pyname); return (PyObject *)var; } +PyObject * +PyContextVar_New(const char *name, PyObject *def) +{ + return contextvar_new_from_utf8(name, def, 0); +} + +PyObject * +PyContextVar_NewThreadInheritable(const char *name, PyObject *def) +{ + return contextvar_new_from_utf8(name, def, 1); +} + int PyContextVar_Get(PyObject *ovar, PyObject *def, PyObject **val) From 942f31290dbe898ff2723c6334ba42a6b50d50fd Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 27 Jul 2026 11:35:05 -0700 Subject: [PATCH 3/3] Use tri-value `thread_inheritable` keyword arg. Use a keyword arg so that `ContextVar(thread_inheritable=False)` is supported. The default is `thread_inheritable=None`, which means to use the value of `thread_inherit_context`. This allows libraries to explicitly specify if they want a variable to be inherited by new threads. Add C function `PyContextVar_NewWithFlags` for creating context variables which override automatic thread context inheritance. --- Doc/c-api/contextvars.rst | 32 +- Doc/data/refcounts.dat | 5 + Doc/howto/free-threading-python.rst | 6 +- Doc/library/contextvars.rst | 107 +++---- Doc/library/threading.rst | 15 +- Doc/using/cmdline.rst | 26 +- Include/cpython/context.h | 14 +- Include/internal/pycore_context.h | 23 +- Lib/test/test_context.py | 273 +++++++++++++++-- Lib/threading.py | 19 +- ...-07-23-13-19-38.gh-issue-154562.WBaAJV.rst | 13 +- Modules/Setup.stdlib.in | 2 +- Modules/_testcapi/context.c | 62 ++++ Modules/_testcapi/parts.h | 1 + Modules/_testcapimodule.c | 3 + PCbuild/_testcapi.vcxproj | 1 + PCbuild/_testcapi.vcxproj.filters | 3 + Python/_contextvars.c | 12 +- Python/clinic/_contextvars.c.h | 16 +- Python/clinic/context.c.h | 77 +---- Python/context.c | 281 ++++++++++++------ 21 files changed, 659 insertions(+), 332 deletions(-) create mode 100644 Modules/_testcapi/context.c diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst index 487962800bc6dc..7489ea7bf28810 100644 --- a/Doc/c-api/contextvars.rst +++ b/Doc/c-api/contextvars.rst @@ -155,13 +155,35 @@ Context variable functions: Create a new ``ContextVar`` object. The *name* parameter is used for introspection and debug purposes. The *def* parameter specifies a default value for the context variable, or ``NULL`` for no default. - If an error has occurred, this function returns ``NULL``. + Automatic inheritance of the variable's bindings by + :class:`threading.Thread` follows + :data:`sys.flags.thread_inherit_context`. If an error has occurred, this + function returns ``NULL``. -.. c:function:: PyObject *PyContextVar_NewThreadInheritable(const char *name, PyObject *def) +.. c:function:: PyObject *PyContextVar_NewWithFlags(const char *name, PyObject *def, int flags) - Create a new ``ContextVar`` object whose bindings are inherited by new - :class:`threading.Thread` instances. The parameters and return value are - the same as for :c:func:`PyContextVar_New`. + Create a new ``ContextVar`` object with the specified *flags*. The *name*, + *def*, and return value are the same as for :c:func:`PyContextVar_New`. + *flags* must be one of the following values: + + .. c:macro:: Py_CONTEXTVAR_INHERIT_THREAD_DEFAULT + + Follow :data:`sys.flags.thread_inherit_context` when deciding whether a + new :class:`threading.Thread` inherits the variable's binding. This is + equivalent to :c:func:`PyContextVar_New`. + + .. c:macro:: Py_CONTEXTVAR_INHERIT_THREAD_NEVER + + Do not automatically inherit the variable's binding, regardless of + :data:`sys.flags.thread_inherit_context`. + + .. c:macro:: Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS + + Automatically inherit the variable's binding, regardless of + :data:`sys.flags.thread_inherit_context`. + + Passing any other value causes the function to return ``NULL`` with + :exc:`ValueError` set. .. versionadded:: 3.16 diff --git a/Doc/data/refcounts.dat b/Doc/data/refcounts.dat index 60c02aabeb89c5..7b8d5fc2335f21 100644 --- a/Doc/data/refcounts.dat +++ b/Doc/data/refcounts.dat @@ -392,6 +392,11 @@ PyContextVar_New:PyObject*::+1: PyContextVar_New:const char*:name:: PyContextVar_New:PyObject*:def:+1: +PyContextVar_NewWithFlags:PyObject*::+1: +PyContextVar_NewWithFlags:const char*:name:: +PyContextVar_NewWithFlags:PyObject*:def:+1: +PyContextVar_NewWithFlags:int:flags:: + PyContextVar_Set:PyObject*::+1: PyContextVar_Set:PyObject*:var:0: PyContextVar_Set:PyObject*:value:+1: diff --git a/Doc/howto/free-threading-python.rst b/Doc/howto/free-threading-python.rst index 51e24d13cc660f..91a7a4359cc253 100644 --- a/Doc/howto/free-threading-python.rst +++ b/Doc/howto/free-threading-python.rst @@ -152,9 +152,9 @@ 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`. In the default GIL-enabled build, the flag -defaults to false, so threads start with a context containing only the caller's -current bindings for context variables created by -:meth:`~contextvars.ContextVar.thread_inheritable`. +defaults to false, so threads do not inherit context variable bindings by +default. Individual variables can override either setting with the +:class:`~contextvars.ContextVar` constructor's *thread_inheritable* parameter. Warning filters diff --git a/Doc/library/contextvars.rst b/Doc/library/contextvars.rst index eedbca31fc11fa..4280312c797dc1 100644 --- a/Doc/library/contextvars.rst +++ b/Doc/library/contextvars.rst @@ -24,7 +24,7 @@ See also :pep:`567` for additional details. Context Variables ----------------- -.. class:: ContextVar(name, [*, default]) +.. class:: ContextVar(name, [*, default, thread_inheritable]) This class is used to declare a new Context Variable, e.g.:: @@ -37,6 +37,38 @@ Context Variables :meth:`ContextVar.get` when no value for the variable is found in the current context. + The optional keyword-only *thread_inheritable* parameter controls whether + the variable's current binding is automatically inherited by a + :class:`threading.Thread` when no explicit :class:`Context` is supplied. + If ``None`` (the default), :data:`sys.flags.thread_inherit_context` + determines whether the binding is inherited. If true, the binding is + inherited regardless of that flag; if false, it is not inherited regardless + of the flag. + + This parameter only affects implicit context inheritance by + :meth:`threading.Thread.start`. The bindings are captured from the caller + of :meth:`~threading.Thread.start`, not the creator of the + :class:`~threading.Thread` object. It does not affect threads started + directly through :mod:`_thread` or by extension modules. + + An explicitly supplied thread context is not filtered, and neither are + contexts copied for other purposes, including asynchronous tasks and + :func:`asyncio.to_thread`. An inherited binding is an ordinary binding in + the new thread's context: it is visible to :func:`copy_context`, may be + changed independently, and may in turn be inherited by threads started from + that thread. Only the binding is copied; the bound object itself is not + copied. A mutable bound object can therefore be accessed concurrently by + the starting and new threads. + + For a thread pool, inheritance occurs when each worker thread starts, not + each time work is submitted. Consequently, + :class:`~concurrent.futures.ThreadPoolExecutor` does not propagate the + submitter's current bindings to an existing worker merely because a variable + is thread-inheritable. + + .. versionchanged:: 3.16 + Added the *thread_inheritable* parameter. + **Important:** Context Variables should be created at the top module level and never in closures. :class:`Context` objects hold strong references to context variables which prevents context variables @@ -45,78 +77,19 @@ Context Variables :class:`!ContextVar`\s are :ref:`generic ` over the type of their contained value. - .. classmethod:: ContextVar.thread_inheritable(name, [*, default]) - - Return a new context variable whose binding is inherited by new - :class:`threading.Thread` instances. When - :meth:`threading.Thread.start` would otherwise start the thread with - an empty context (that is, when - :data:`sys.flags.thread_inherit_context` is false and no explicit - :class:`Context` was supplied), the new thread's context is initialized - with the caller's current bindings for all thread-inheritable variables. - This allows individual context variables to opt in to thread inheritance - on a per-variable basis. - - These are ordinary bindings in the new thread's context: they are - visible to :func:`copy_context`, may be changed independently with - :meth:`ContextVar.set`, and are in turn inherited by threads started - from the new thread. Threads started with an explicitly supplied - context are unaffected, as are threads started by means other than - :meth:`threading.Thread.start`, such as - :func:`_thread.start_new_thread` or the C API. - - The *name* and *default* parameters have the same meaning as for the - :class:`ContextVar` constructor. When - :data:`sys.flags.thread_inherit_context` is true, a variable created - with this method behaves identically to one created with - :class:`ContextVar`, so it is safe to use unconditionally. - - Libraries that also support Python versions without this method must - choose how to degrade on those versions. For state with a safe - default in a new thread (the behavior that :class:`threading.local` - based state has always had), fall back to an ordinary context - variable:: - - _new_var = getattr(ContextVar, "thread_inheritable", ContextVar) - var = _new_var("var") - - With this fallback, on older versions new threads see the variable's - default rather than the starting thread's binding, unless the - application enables :option:`-X thread_inherit_context <-X>`. - - For state that was previously a module-level global, reverting to the - default in new threads may break existing threaded code, since such - code has always seen the current global value. Those libraries can - instead pick the best semantics each interpreter supports:: - - if hasattr(ContextVar, "thread_inheritable"): - # Context-local and inherited by new threads. - _new_var = ContextVar.thread_inheritable - elif getattr(sys.flags, "thread_inherit_context", False): - # Threads inherit the whole context, so an ordinary - # context variable is inherited too. - _new_var = ContextVar - else: - # Keep the library's historical global semantics. - _new_var = _GlobalVar - - Here ``_GlobalVar`` is a class provided by the library that stores a - single process-wide value. To be substitutable for - :class:`ContextVar` it must accept the same constructor arguments - (*name* positional, keyword-only *default*), distinguish a missing - *default* from ``default=None``, and provide ``get()``, ``set()`` - returning a token, and ``reset()``. This case gives up task-local - isolation: concurrent tasks and threads share one value, exactly as - they did before the library adopted context variables. - - .. versionadded:: 3.16 - .. attribute:: ContextVar.name The name of the variable. This is a read-only property. .. versionadded:: 3.7.1 + .. attribute:: ContextVar.thread_inheritable + + The value of the *thread_inheritable* constructor parameter: ``None``, + ``True``, or ``False``. This is a read-only property. + + .. versionadded:: 3.16 + .. method:: get([default]) Return a value for the context variable for the current context. diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 391dd6b8acdebb..627e412673e51d 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -539,13 +539,14 @@ since it is impossible to detect the termination of alien threads. the thread. The default value is ``None`` which indicates that the :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 a - context containing only the caller's current bindings for context variables - created by :meth:`~contextvars.ContextVar.thread_inheritable`. 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 not inherit bindings by + default. The :class:`~contextvars.ContextVar` constructor's + *thread_inheritable* parameter can override the flag for an individual + variable. 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`. + Explicit contexts are not filtered by per-variable settings. 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 6b1426da110afa..11f8e139c0bec4 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -674,13 +674,13 @@ Miscellaneous options .. versionadded:: 3.13 - * :samp:`-X thread_inherit_context={0,1}` causes :class:`~threading.Thread` - to, by default, use a copy of the context of the caller of - ``Thread.start()`` when starting. Otherwise, threads start with a context - containing only the caller's current bindings for context variables - created by :meth:`~contextvars.ContextVar.thread_inheritable`. If unset, the - value of this option defaults to ``1`` on free-threaded builds and to ``0`` - otherwise. See also :envvar:`PYTHON_THREAD_INHERIT_CONTEXT`. + * :samp:`-X thread_inherit_context={0,1}` controls whether + :class:`~threading.Thread` inherits context variable bindings from the + caller of ``Thread.start()`` by default. Individual context variables can + override this setting with the :class:`~contextvars.ContextVar` + constructor's *thread_inheritable* parameter. If unset, the value of this + option defaults to ``1`` on free-threaded builds and to ``0`` otherwise. + See also :envvar:`PYTHON_THREAD_INHERIT_CONTEXT`. .. versionadded:: 3.14 @@ -1367,12 +1367,12 @@ conflict. .. envvar:: PYTHON_THREAD_INHERIT_CONTEXT - If this variable is set to ``1`` then :class:`~threading.Thread` will, - by default, use a copy of the context of the caller of ``Thread.start()`` - when starting. Otherwise, new threads start with a context containing only - the caller's current bindings for context variables created by - :meth:`~contextvars.ContextVar.thread_inheritable`. If unset, this variable - defaults to ``1`` on free-threaded builds and to ``0`` otherwise. See also + This variable controls whether :class:`~threading.Thread` inherits context + variable bindings from the caller of ``Thread.start()`` by default. + Individual context variables can override it with the + :class:`~contextvars.ContextVar` constructor's *thread_inheritable* + parameter. If unset, this variable defaults to ``1`` on free-threaded + builds and to ``0`` otherwise. See also :option:`-X thread_inherit_context<-X>`. .. versionadded:: 3.14 diff --git a/Include/cpython/context.h b/Include/cpython/context.h index 70c33c45e962e4..471d4ad6c2b8a7 100644 --- a/Include/cpython/context.h +++ b/Include/cpython/context.h @@ -68,12 +68,18 @@ PyAPI_FUNC(int) PyContext_ClearWatcher(int watcher_id); PyAPI_FUNC(PyObject *) PyContextVar_New( const char *name, PyObject *default_value); -/* Create a new thread-inheritable context variable. +/* Flags for PyContextVar_NewWithFlags(). */ +#define Py_CONTEXTVAR_INHERIT_THREAD_DEFAULT 0 +#define Py_CONTEXTVAR_INHERIT_THREAD_NEVER 1 +#define Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS 2 - default_value can be NULL. +/* Create a new context variable with the specified flags. + + default_value can be NULL. flags must be one of the + Py_CONTEXTVAR_INHERIT_THREAD_* values. */ -PyAPI_FUNC(PyObject *) PyContextVar_NewThreadInheritable( - const char *name, PyObject *default_value); +PyAPI_FUNC(PyObject *) PyContextVar_NewWithFlags( + const char *name, PyObject *default_value, int flags); /* Get a value for the variable. diff --git a/Include/internal/pycore_context.h b/Include/internal/pycore_context.h index 40d3d0d7ef8768..eb9be65666d0ba 100644 --- a/Include/internal/pycore_context.h +++ b/Include/internal/pycore_context.h @@ -26,19 +26,28 @@ struct _pycontextobject { PyHamtObject *ctx_vars; PyObject *ctx_weakreflist; int ctx_entered; - // Redundant subset of ctx_vars holding only the bindings of - // thread-inheritable context variables (see - // ContextVar.thread_inheritable()). Used to efficiently create the - // starting context of a new thread. - PyHamtObject *ctx_thread_inheritable_vars; + // Redundant subset of ctx_vars holding exactly the bindings inherited by + // an implicitly created child thread. Since the HAMT is immutable, it can + // be shared directly with the child's context. While this and ctx_vars + // are the same object, one HAMT update serves both fields; adding a + // non-inheritable binding makes them diverge. This relies on both + // var_thread_inherit and thread_inherit_context remaining immutable. + PyHamtObject *ctx_inheritable_vars; }; +typedef enum { + _Py_CONTEXTVAR_INHERIT_DEFAULT = 0, + _Py_CONTEXTVAR_INHERIT_FALSE = 1, + _Py_CONTEXTVAR_INHERIT_TRUE = 2, +} _PyContextVarInherit; + + struct _pycontextvarobject { PyObject_HEAD PyObject *var_name; PyObject *var_default; - char var_thread_inheritable; + _PyContextVarInherit var_thread_inherit; #ifndef Py_GIL_DISABLED PyObject *var_cached; uint64_t var_cached_tsid; @@ -60,7 +69,7 @@ struct _pycontexttokenobject { // _testinternalcapi.hamt() used by tests. // Export for '_testcapi' shared extension PyAPI_FUNC(PyObject*) _PyContext_NewHamtForTests(void); -PyAPI_FUNC(PyObject*) _PyContext_NewThreadStartContext(void); +PyAPI_FUNC(PyObject*) _PyContext_NewThreadContext(void); PyAPI_FUNC(int) _PyContext_Enter(PyThreadState *ts, PyObject *octx); PyAPI_FUNC(int) _PyContext_Exit(PyThreadState *ts, PyObject *octx); diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index b6baa801336ac3..ae9cee626a4335 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -16,6 +16,11 @@ except ImportError: hamt = None +try: + import _testcapi +except ImportError: + _testcapi = None + def isolated_context(func): """Needed to make reftracking test mode work.""" @@ -36,36 +41,53 @@ def test_context_var_new_1(self): c = contextvars.ContextVar('aaa') self.assertEqual(c.name, 'aaa') + self.assertIsNone(c.thread_inheritable) with self.assertRaises(AttributeError): c.name = 'bbb' + with self.assertRaises(AttributeError): + c.thread_inheritable = True - inheritable = contextvars.ContextVar.thread_inheritable('inheritable') + inheritable = contextvars.ContextVar( + 'inheritable', thread_inheritable=True) self.assertIs(type(inheritable), contextvars.ContextVar) self.assertEqual(inheritable.name, 'inheritable') + self.assertIs(inheritable.thread_inheritable, True) - inheritable_with_default = ( - contextvars.ContextVar.thread_inheritable( - 'inheritable_with_default', default=42, - ) + inheritable_with_default = contextvars.ContextVar( + 'inheritable_with_default', default=42, + thread_inheritable=True, ) self.assertEqual(inheritable_with_default.get(), 42) - with self.assertRaisesRegex(TypeError, 'must be a str'): - contextvars.ContextVar.thread_inheritable(1) + # None and omission both select the interpreter-wide default. + default_policy = contextvars.ContextVar( + 'default_policy', thread_inheritable=None) + non_inheritable = contextvars.ContextVar( + 'non_inheritable', thread_inheritable=False) + self.assertIsNone(default_policy.thread_inheritable) + self.assertIs(non_inheritable.thread_inheritable, False) + with self.assertRaisesRegex( + TypeError, + 'thread_inheritable must be a bool or None, not int'): + contextvars.ContextVar('var', thread_inheritable=1) with self.assertRaises(TypeError): - contextvars.ContextVar.thread_inheritable('var', None) + contextvars.ContextVar('var', None) with self.assertRaises(TypeError): contextvars.ContextVar('var', inherit=True) + # thread_inheritable is a read-only attribute, not a constructor. + with self.assertRaises(TypeError): + contextvars.ContextVar.thread_inheritable('var') self.assertNotEqual(hash(c), hash('aaa')) - def test_thread_inheritable_context_var_gc(self): + def test_context_var_thread_inheritable_gc(self): class Value: pass def make_cycle(): - var = contextvars.ContextVar.thread_inheritable('var') + var = contextvars.ContextVar( + 'var', thread_inheritable=True) ctx = contextvars.Context() value = Value() value_ref = weakref.ref(value) @@ -101,6 +123,25 @@ def test_context_var_repr_1(self): c.reset(t) self.assertIn(' used ', repr(t)) + @isolated_context + def test_context_var_repr_thread_inheritable(self): + # The policy is only shown when it overrides the flag. + c = contextvars.ContextVar('a') + self.assertNotIn('thread_inheritable', repr(c)) + c = contextvars.ContextVar('a', thread_inheritable=None) + self.assertNotIn('thread_inheritable', repr(c)) + + c = contextvars.ContextVar('a', thread_inheritable=True) + self.assertRegex( + repr(c), + r"^$") + c = contextvars.ContextVar('a', default=123, thread_inheritable=False) + self.assertRegex( + repr(c), + r"^$") + @isolated_context def test_token_repr_1(self): c = contextvars.ContextVar('a') @@ -641,10 +682,15 @@ def test_thread_inheritance(self): import threading from contextvars import ContextVar, copy_context - inh = ContextVar.thread_inheritable('inh', default='default') + inh = ContextVar('inh', default='default', thread_inheritable=True) plain = ContextVar('plain') + default_policy = ContextVar( + 'default_policy', thread_inheritable=None) + excluded = ContextVar('excluded', thread_inheritable=False) inh.set('inherited') plain.set('not inherited') + default_policy.set('not inherited either') + excluded.set('explicitly excluded') def child(): # The binding is a real binding in the thread's context: @@ -656,12 +702,15 @@ def child(): assert ctx.run(inh.get) == 'inherited' # Non-inheritable vars are not visible. assert plain not in ctx - try: - plain.get() - except LookupError: - pass - else: - raise AssertionError('plain was inherited') + assert default_policy not in ctx + assert excluded not in ctx + for var in (plain, default_policy, excluded): + try: + var.get() + except LookupError: + pass + else: + raise AssertionError(f'{var.name} was inherited') t = threading.Thread(target=child) t.start() @@ -673,7 +722,7 @@ def test_inheritance_captured_at_start_and_context_copy(self): import threading from contextvars import Context, ContextVar - inh = ContextVar.thread_inheritable('inh') + inh = ContextVar('inh', thread_inheritable=True) values = [] # The binding is captured by start(), not by Thread(). @@ -704,7 +753,7 @@ def test_thread_start_retry_recaptures_context(self): import threading from contextvars import ContextVar - inh = ContextVar.thread_inheritable('inh') + inh = ContextVar('inh', thread_inheritable=True) inh.set('first attempt') values = [] t = threading.Thread(target=lambda: values.append(inh.get())) @@ -736,7 +785,7 @@ def test_thread_set_and_reset(self): import threading from contextvars import ContextVar - inh = ContextVar.thread_inheritable('inh') + inh = ContextVar('inh', thread_inheritable=True) inh.set('inherited') def child(): @@ -757,7 +806,7 @@ def test_thread_inheritance_transitive(self): import threading from contextvars import ContextVar - inh = ContextVar.thread_inheritable('inh') + inh = ContextVar('inh', thread_inheritable=True) inh.set('inherited') def grandchild(): @@ -775,13 +824,53 @@ def child(): t.join() """) + def test_thread_non_inheritable_not_retained(self): + # An excluded binding must be dropped from the new thread's internal + # subset of thread-inheritable bindings, not just from its visible + # bindings. Otherwise the excluded value stays reachable through the + # context of every descendant thread. + source = """if True: + import gc + import threading + import weakref + from contextvars import ContextVar, copy_context + + excluded = ContextVar('excluded', thread_inheritable=False) + descendant = [] + + class Value: + pass + + def chain(depth): + if depth: + t = threading.Thread(target=chain, args=(depth - 1,)) + t.start() + t.join() + else: + descendant.append(copy_context()) + + value = Value() + ref = weakref.ref(value) + excluded.set(value) + chain(4) + del value + excluded.set(None) + gc.collect() + + assert excluded not in descendant[0] + assert ref() is None, 'excluded value was retained by a thread' + """ + for flag in (0, 1): + with self.subTest(thread_inherit_context=flag): + self.run_with_flag(flag, source) + def test_thread_inheritance_unset_or_deleted(self): self.run_with_flag(0, """if True: import threading from contextvars import ContextVar - unset = ContextVar.thread_inheritable('unset', default='default') - deleted = ContextVar.thread_inheritable('deleted') + unset = ContextVar('unset', default='default', thread_inheritable=True) + deleted = ContextVar('deleted', thread_inheritable=True) token = deleted.set('inherited') deleted.reset(token) @@ -804,7 +893,7 @@ def test_thread_explicit_context(self): import threading from contextvars import ContextVar, Context - inh = ContextVar.thread_inheritable('inh', default='default') + inh = ContextVar('inh', default='default', thread_inheritable=True) inh.set('inherited') def child(): @@ -821,7 +910,7 @@ def test_thread_inheritance_asyncio(self): import threading from contextvars import ContextVar - inh = ContextVar.thread_inheritable('inh') + inh = ContextVar('inh', thread_inheritable=True) plain = ContextVar('plain') inh.set('inherited') plain.set('not inherited') @@ -864,23 +953,151 @@ def child(): def test_thread_inherit_context_flag_true(self): self.run_with_flag(1, """if True: import threading - from contextvars import ContextVar + from contextvars import ContextVar, copy_context - inh = ContextVar.thread_inheritable('inh') + inh = ContextVar('inh', thread_inheritable=True) plain = ContextVar('plain') + default_policy = ContextVar( + 'default_policy', thread_inheritable=None) + excluded = ContextVar('excluded', thread_inheritable=False) inh.set('inherited') plain.set('also inherited') + default_policy.set('also inherited by default') + excluded.set('not inherited') def child(): - # With the flag set, the full context is copied. assert inh.get() == 'inherited' assert plain.get() == 'also inherited' + assert default_policy.get() == 'also inherited by default' + assert excluded.get(None) is None + assert excluded not in copy_context() t = threading.Thread(target=child) t.start() t.join() + + # A supplied context is authoritative and is not filtered. + values = [] + t = threading.Thread( + target=lambda: values.append(excluded.get()), + context=copy_context()) + t.start() + t.join() + assert values == ['not inherited'] """) + @unittest.skipIf(_testcapi is None, 'requires _testcapi') + def test_capi_thread_inheritance(self): + # The C API flags must override the global flag the same way the + # thread_inheritable keyword argument does. + source = """if True: + import threading + import _testcapi + from contextvars import copy_context + + new_with_flags = _testcapi.contextvar_new_with_flags + always = _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS + never = _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_NEVER + inh = new_with_flags('inh', always) + excl = new_with_flags('excl', never) + plain = _testcapi.contextvar_new('plain') + inh.set('inh value') + excl.set('excl value') + plain.set('plain value') + + values = [] + + def child(): + values.append( + (inh.get(None), excl.get(None), plain.get(None))) + assert excl not in copy_context() + + t = threading.Thread(target=child) + t.start() + t.join() + assert values == [('inh value', None, expected_plain)], values + """ + for flag, expected_plain in ((0, None), (1, 'plain value')): + with self.subTest(thread_inherit_context=flag): + self.run_with_flag( + flag, + f'expected_plain = {expected_plain!r}\n{source}') + + +@unittest.skipIf(_testcapi is None, 'requires _testcapi') +class CAPIContextVarTest(unittest.TestCase): + + def constructors(self): + with_flags = _testcapi.contextvar_new_with_flags + return ( + ('new', _testcapi.contextvar_new), + ('flags_default', lambda name, *default: with_flags( + name, _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_DEFAULT, + *default)), + ('flags_never', lambda name, *default: with_flags( + name, _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_NEVER, + *default)), + ('flags_always', lambda name, *default: with_flags( + name, _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS, + *default)), + ) + + def test_new(self): + var = _testcapi.contextvar_new('plain') + self.assertIs(type(var), contextvars.ContextVar) + self.assertEqual(var.name, 'plain') + self.assertIsNone(var.thread_inheritable) + + def test_new_with_flags(self): + cases = ( + (_testcapi.Py_CONTEXTVAR_INHERIT_THREAD_DEFAULT, None), + (_testcapi.Py_CONTEXTVAR_INHERIT_THREAD_NEVER, False), + (_testcapi.Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS, True), + ) + for flags, expected in cases: + with self.subTest(flags=flags): + var = _testcapi.contextvar_new_with_flags('var', flags) + self.assertIs(type(var), contextvars.ContextVar) + self.assertEqual(var.name, 'var') + self.assertIs(var.thread_inheritable, expected) + + def test_new_with_invalid_flags(self): + always = _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS + never = _testcapi.Py_CONTEXTVAR_INHERIT_THREAD_NEVER + for flags in (-1, always | never, 4): + with self.subTest(flags=flags): + with self.assertRaisesRegex( + ValueError, 'invalid ContextVar flags'): + _testcapi.contextvar_new_with_flags('var', flags) + + def test_no_default(self): + # A NULL default is not the same as a None default. + for name, new in self.constructors(): + with self.subTest(new=name): + var = new('var') + with self.assertRaises(LookupError): + var.get() + self.assertIsNone(var.get(None)) + + def test_default(self): + for name, new in self.constructors(): + with self.subTest(new=name): + self.assertIsNone(new('var', None).get()) + self.assertEqual(new('var', 42).get(), 42) + + def test_default_refcount(self): + # Doc/data/refcounts.dat documents def as a +1 argument. + for name, new in self.constructors(): + with self.subTest(new=name): + default = ['default'] + before = sys.getrefcount(default) + var = new('var', default) + self.assertEqual(sys.getrefcount(default), before + 1) + self.assertIs(var.get(), default) + del var + support.gc_collect() + self.assertEqual(sys.getrefcount(default), before) + # HAMT Tests diff --git a/Lib/threading.py b/Lib/threading.py index 8f50449d5667c6..8ede0ecca78c1d 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1033,10 +1033,9 @@ 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.thread_inherit_context. If that flag is true, use a copy - of the context of the caller. If false, use a context containing only - the bindings of thread-inheritable context variables. To explicitly - start with an empty context, pass a new instance of + sys.flags.thread_inherit_context. Variables can override that flag + with the ContextVar constructor's thread_inheritable parameter. 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(). @@ -1130,15 +1129,9 @@ def start(self): context_is_implicit = self._context is None if context_is_implicit: - # No context provided - if _sys.flags.thread_inherit_context: - # start with a copy of the context of the caller - self._context = _contextvars.copy_context() - else: - # Start with a context containing only the bindings of - # thread-inheritable context variables (see - # ContextVar.thread_inheritable); usually empty. - self._context = _contextvars._thread_start_context() + # Capture the caller's bindings selected for automatic thread + # inheritance by the global flag and per-variable overrides. + self._context = _contextvars._new_thread_context() try: # Start joinable thread diff --git a/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst b/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst index 08def726565d89..0df0e397fe282d 100644 --- a/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst +++ b/Misc/NEWS.d/next/Library/2026-07-23-13-19-38.gh-issue-154562.WBaAJV.rst @@ -1,6 +1,7 @@ -Add :meth:`contextvars.ContextVar.thread_inheritable`, an alternative -constructor for context variables whose bindings are inherited by new -:class:`threading.Thread` instances even when -:data:`sys.flags.thread_inherit_context` is false. This lets libraries opt -individual context variables in to thread inheritance without requiring the -application to enable the flag process-wide. +Add the *thread_inheritable* keyword-only parameter and corresponding +read-only attribute to :class:`contextvars.ContextVar`. They let an +individual variable force its bindings to be included in or excluded from +implicit :class:`threading.Thread` context inheritance, independently of +:data:`sys.flags.thread_inherit_context`. Add +:c:func:`PyContextVar_NewWithFlags` for creating context variables which +override automatic thread context inheritance. diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 0f88dddf03b9f6..439792a1f8ae45 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -173,7 +173,7 @@ @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c -@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c +@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/context.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/context.c b/Modules/_testcapi/context.c new file mode 100644 index 00000000000000..b56d28bc52c778 --- /dev/null +++ b/Modules/_testcapi/context.c @@ -0,0 +1,62 @@ +#include "parts.h" +#include "util.h" + + +typedef PyObject *(*contextvar_ctor)(const char *, PyObject *); + +static PyObject * +call_contextvar_ctor(contextvar_ctor ctor, PyObject *args) +{ + const char *name; + PyObject *def = NULL; + // Omitting the default passes NULL, which is not the same as Py_None. + if (!PyArg_ParseTuple(args, "s|O", &name, &def)) { + return NULL; + } + return ctor(name, def); +} + +static PyObject * +contextvar_new(PyObject *Py_UNUSED(module), PyObject *args) +{ + return call_contextvar_ctor(PyContextVar_New, args); +} + +static PyObject * +contextvar_new_with_flags(PyObject *Py_UNUSED(module), PyObject *args) +{ + const char *name; + int flags; + PyObject *def = NULL; + // Put flags before the optional default so omitting the default passes + // NULL, which is not the same as Py_None. + if (!PyArg_ParseTuple(args, "si|O", &name, &flags, &def)) { + return NULL; + } + return PyContextVar_NewWithFlags(name, def, flags); +} + + +static PyMethodDef test_methods[] = { + {"contextvar_new", contextvar_new, METH_VARARGS}, + {"contextvar_new_with_flags", contextvar_new_with_flags, METH_VARARGS}, + {NULL}, +}; + +int +_PyTestCapi_Init_Context(PyObject *m) +{ + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } + if (PyModule_AddIntMacro(m, Py_CONTEXTVAR_INHERIT_THREAD_DEFAULT) < 0) { + return -1; + } + if (PyModule_AddIntMacro(m, Py_CONTEXTVAR_INHERIT_THREAD_NEVER) < 0) { + return -1; + } + if (PyModule_AddIntMacro(m, Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS) < 0) { + return -1; + } + return 0; +} diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index 98b5dd47accde3..3e3a9f831a81a9 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -68,5 +68,6 @@ int _PyTestCapi_Init_Type(PyObject *mod); int _PyTestCapi_Init_Function(PyObject *mod); int _PyTestCapi_Init_Module(PyObject *mod); int _PyTestCapi_Init_Weakref(PyObject *mod); +int _PyTestCapi_Init_Context(PyObject *mod); #endif // Py_TESTCAPI_PARTS_H diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index fb18a866e62812..bf7c408f1d0852 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3943,6 +3943,9 @@ _testcapi_exec(PyObject *m) if (_PyTestCapi_Init_Weakref(m) < 0) { return -1; } + if (_PyTestCapi_Init_Context(m) < 0) { + return -1; + } return 0; } diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index 64e50b67be4656..7caaff38265860 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -134,6 +134,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index a3b62e1df663e0..a5a5cc0100edbc 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -135,6 +135,9 @@ Source Files + + Source Files + diff --git a/Python/_contextvars.c b/Python/_contextvars.c index 2d722c01c031be..a65e30abf61e03 100644 --- a/Python/_contextvars.c +++ b/Python/_contextvars.c @@ -1,5 +1,5 @@ #include "Python.h" -#include "pycore_context.h" // _PyContext_NewThreadStartContext() +#include "pycore_context.h" // _PyContext_NewThreadContext() #include "clinic/_contextvars.c.h" @@ -22,14 +22,14 @@ _contextvars_copy_context_impl(PyObject *module) /*[clinic input] -_contextvars._thread_start_context +_contextvars._new_thread_context [clinic start generated code]*/ static PyObject * -_contextvars__thread_start_context_impl(PyObject *module) -/*[clinic end generated code: output=7e656d156c385a65 input=4575c5d2223de58d]*/ +_contextvars__new_thread_context_impl(PyObject *module) +/*[clinic end generated code: output=2d108487bd5c4a49 input=bc928642a19055eb]*/ { - return _PyContext_NewThreadStartContext(); + return _PyContext_NewThreadContext(); } @@ -37,7 +37,7 @@ PyDoc_STRVAR(module_doc, "Context Variables"); static PyMethodDef _contextvars_methods[] = { _CONTEXTVARS_COPY_CONTEXT_METHODDEF - _CONTEXTVARS__THREAD_START_CONTEXT_METHODDEF + _CONTEXTVARS__NEW_THREAD_CONTEXT_METHODDEF {NULL, NULL} }; diff --git a/Python/clinic/_contextvars.c.h b/Python/clinic/_contextvars.c.h index 381d8c159beb4b..dc709782129622 100644 --- a/Python/clinic/_contextvars.c.h +++ b/Python/clinic/_contextvars.c.h @@ -19,20 +19,20 @@ _contextvars_copy_context(PyObject *module, PyObject *Py_UNUSED(ignored)) return _contextvars_copy_context_impl(module); } -PyDoc_STRVAR(_contextvars__thread_start_context__doc__, -"_thread_start_context($module, /)\n" +PyDoc_STRVAR(_contextvars__new_thread_context__doc__, +"_new_thread_context($module, /)\n" "--\n" "\n"); -#define _CONTEXTVARS__THREAD_START_CONTEXT_METHODDEF \ - {"_thread_start_context", (PyCFunction)_contextvars__thread_start_context, METH_NOARGS, _contextvars__thread_start_context__doc__}, +#define _CONTEXTVARS__NEW_THREAD_CONTEXT_METHODDEF \ + {"_new_thread_context", (PyCFunction)_contextvars__new_thread_context, METH_NOARGS, _contextvars__new_thread_context__doc__}, static PyObject * -_contextvars__thread_start_context_impl(PyObject *module); +_contextvars__new_thread_context_impl(PyObject *module); static PyObject * -_contextvars__thread_start_context(PyObject *module, PyObject *Py_UNUSED(ignored)) +_contextvars__new_thread_context(PyObject *module, PyObject *Py_UNUSED(ignored)) { - return _contextvars__thread_start_context_impl(module); + return _contextvars__new_thread_context_impl(module); } -/*[clinic end generated code: output=2c6c9d89faff4312 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c2d927c634f05b08 input=a9049054013a1b77]*/ diff --git a/Python/clinic/context.c.h b/Python/clinic/context.c.h index f8a27bf5b8ab64..ece7341d65d5fb 100644 --- a/Python/clinic/context.c.h +++ b/Python/clinic/context.c.h @@ -2,10 +2,6 @@ preserve [clinic start generated code]*/ -#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) -# include "pycore_gc.h" // PyGC_Head -# include "pycore_runtime.h" // _Py_ID() -#endif #include "pycore_modsupport.h" // _PyArg_CheckPositional() PyDoc_STRVAR(_contextvars_Context_get__doc__, @@ -120,77 +116,6 @@ _contextvars_Context_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) return _contextvars_Context_copy_impl((PyContext *)self); } -PyDoc_STRVAR(_contextvars_ContextVar_thread_inheritable__doc__, -"thread_inheritable($type, name, /, *, default=)\n" -"--\n" -"\n" -"Create a context variable whose binding is inherited by new threads.\n" -"\n" -"The bindings of such variables are copied into the context of a new\n" -"thread by threading.Thread.start() when the thread would otherwise\n" -"start with an empty context."); - -#define _CONTEXTVARS_CONTEXTVAR_THREAD_INHERITABLE_METHODDEF \ - {"thread_inheritable", _PyCFunction_CAST(_contextvars_ContextVar_thread_inheritable), METH_FASTCALL|METH_KEYWORDS|METH_CLASS, _contextvars_ContextVar_thread_inheritable__doc__}, - -static PyObject * -_contextvars_ContextVar_thread_inheritable_impl(PyTypeObject *type, - PyObject *name, - PyObject *default_value); - -static PyObject * -_contextvars_ContextVar_thread_inheritable(PyObject *type, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - Py_hash_t ob_hash; - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_hash = -1, - .ob_item = { &_Py_ID(default), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"", "default", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "thread_inheritable", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[2]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; - PyObject *name; - PyObject *default_value = NULL; - - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - name = args[0]; - if (!noptargs) { - goto skip_optional_kwonly; - } - default_value = args[1]; -skip_optional_kwonly: - return_value = _contextvars_ContextVar_thread_inheritable_impl((PyTypeObject *)type, name, default_value); - -exit: - return return_value; -} - PyDoc_STRVAR(_contextvars_ContextVar_get__doc__, "get($self, default=, /)\n" "--\n" @@ -334,4 +259,4 @@ token_exit(PyObject *self, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=d8a9c933c7fcf8c3 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=90ec3e4375804e9b input=a9049054013a1b77]*/ diff --git a/Python/context.c b/Python/context.c index b912907fbd587f..73f6c2562c2e0c 100644 --- a/Python/context.c +++ b/Python/context.c @@ -48,7 +48,7 @@ context_new_empty(void); static PyContext * context_new_from_vars(PyHamtObject *vars, - PyHamtObject *thread_inheritable_vars); + PyHamtObject *inheritable_vars); static inline PyContext * context_get(void); @@ -57,7 +57,8 @@ static PyContextToken * token_new(PyContext *ctx, PyContextVar *var, PyObject *val); static PyContextVar * -contextvar_new(PyObject *name, PyObject *def, int thread_inheritable); +contextvar_new(PyObject *name, PyObject *def, + _PyContextVarInherit thread_inherit); static int contextvar_set(PyContextVar *var, PyObject *val); @@ -81,18 +82,19 @@ PyContext_New(void) PyObject * -_PyContext_NewThreadStartContext(void) +_PyContext_NewThreadContext(void) { - PyContext *ctx = context_get(); - if (ctx == NULL) { + PyContext *starter_ctx = context_get(); + if (starter_ctx == NULL) { return NULL; } - // The thread-inheritable subset becomes the new context's full vars map. - // Every entry in it is thread-inheritable, so it is also its own subset. + // The inheritable subset becomes both the child's full bindings map and + // its inheritable subset. HAMTs are immutable, so both fields can share + // the starter's map without filtering or copying it. return (PyObject *)context_new_from_vars( - ctx->ctx_thread_inheritable_vars, - ctx->ctx_thread_inheritable_vars); + starter_ctx->ctx_inheritable_vars, + starter_ctx->ctx_inheritable_vars); } @@ -102,7 +104,7 @@ PyContext_Copy(PyObject * octx) ENSURE_Context(octx, NULL) PyContext *ctx = (PyContext *)octx; return (PyObject *)context_new_from_vars( - ctx->ctx_vars, ctx->ctx_thread_inheritable_vars); + ctx->ctx_vars, ctx->ctx_inheritable_vars); } @@ -115,7 +117,7 @@ PyContext_CopyCurrent(void) } return (PyObject *)context_new_from_vars( - ctx->ctx_vars, ctx->ctx_thread_inheritable_vars); + ctx->ctx_vars, ctx->ctx_inheritable_vars); } static const char * @@ -283,13 +285,13 @@ PyContext_Exit(PyObject *octx) static PyObject * contextvar_new_from_utf8(const char *name, PyObject *def, - int thread_inheritable) + _PyContextVarInherit thread_inherit) { PyObject *pyname = PyUnicode_FromString(name); if (pyname == NULL) { return NULL; } - PyContextVar *var = contextvar_new(pyname, def, thread_inheritable); + PyContextVar *var = contextvar_new(pyname, def, thread_inherit); Py_DECREF(pyname); return (PyObject *)var; } @@ -297,13 +299,31 @@ contextvar_new_from_utf8(const char *name, PyObject *def, PyObject * PyContextVar_New(const char *name, PyObject *def) { - return contextvar_new_from_utf8(name, def, 0); + return contextvar_new_from_utf8( + name, def, _Py_CONTEXTVAR_INHERIT_DEFAULT); } PyObject * -PyContextVar_NewThreadInheritable(const char *name, PyObject *def) -{ - return contextvar_new_from_utf8(name, def, 1); +PyContextVar_NewWithFlags(const char *name, PyObject *def, int flags) +{ + _PyContextVarInherit thread_inherit; + switch (flags) { + case Py_CONTEXTVAR_INHERIT_THREAD_DEFAULT: + thread_inherit = _Py_CONTEXTVAR_INHERIT_DEFAULT; + break; + case Py_CONTEXTVAR_INHERIT_THREAD_NEVER: + thread_inherit = _Py_CONTEXTVAR_INHERIT_FALSE; + break; + case Py_CONTEXTVAR_INHERIT_THREAD_ALWAYS: + thread_inherit = _Py_CONTEXTVAR_INHERIT_TRUE; + break; + default: + PyErr_Format(PyExc_ValueError, + "invalid ContextVar flags: 0x%x", + (unsigned int)flags); + return NULL; + } + return contextvar_new_from_utf8(name, def, thread_inherit); } @@ -469,7 +489,7 @@ _context_alloc(void) ctx->ctx_vars = NULL; ctx->ctx_prev = NULL; - ctx->ctx_thread_inheritable_vars = NULL; + ctx->ctx_inheritable_vars = NULL; ctx->ctx_entered = 0; ctx->ctx_weakreflist = NULL; @@ -491,11 +511,11 @@ context_new_empty(void) return NULL; } - ctx->ctx_thread_inheritable_vars = _PyHamt_New(); - if (ctx->ctx_thread_inheritable_vars == NULL) { - Py_DECREF(ctx); - return NULL; - } + // A fresh context has no excluded bindings, so its full and inheritable + // maps are the same object. Preserve this identity to let one HAMT update + // serve both fields until a non-inheritable binding is added. + ctx->ctx_inheritable_vars = + (PyHamtObject *)Py_NewRef(ctx->ctx_vars); _PyObject_GC_TRACK(ctx); return ctx; @@ -504,7 +524,7 @@ context_new_empty(void) static PyContext * context_new_from_vars(PyHamtObject *vars, - PyHamtObject *thread_inheritable_vars) + PyHamtObject *inheritable_vars) { PyContext *ctx = _context_alloc(); if (ctx == NULL) { @@ -512,8 +532,8 @@ context_new_from_vars(PyHamtObject *vars, } ctx->ctx_vars = (PyHamtObject*)Py_NewRef(vars); - ctx->ctx_thread_inheritable_vars = - (PyHamtObject*)Py_NewRef(thread_inheritable_vars); + ctx->ctx_inheritable_vars = + (PyHamtObject*)Py_NewRef(inheritable_vars); _PyObject_GC_TRACK(ctx); return ctx; @@ -565,7 +585,7 @@ context_tp_clear(PyObject *op) PyContext *self = _PyContext_CAST(op); Py_CLEAR(self->ctx_prev); Py_CLEAR(self->ctx_vars); - Py_CLEAR(self->ctx_thread_inheritable_vars); + Py_CLEAR(self->ctx_inheritable_vars); return 0; } @@ -575,7 +595,7 @@ context_tp_traverse(PyObject *op, visitproc visit, void *arg) PyContext *self = _PyContext_CAST(op); Py_VISIT(self->ctx_prev); Py_VISIT(self->ctx_vars); - Py_VISIT(self->ctx_thread_inheritable_vars); + Py_VISIT(self->ctx_inheritable_vars); return 0; } @@ -753,7 +773,7 @@ _contextvars_Context_copy_impl(PyContext *self) /*[clinic end generated code: output=30ba8896c4707a15 input=ebafdbdd9c72d592]*/ { return (PyObject *)context_new_from_vars( - self->ctx_vars, self->ctx_thread_inheritable_vars); + self->ctx_vars, self->ctx_inheritable_vars); } @@ -827,6 +847,22 @@ PyTypeObject PyContext_Type = { /////////////////////////// ContextVar +static int +contextvar_is_thread_inheritable(PyContextVar *var) +{ + switch (var->var_thread_inherit) { + case _Py_CONTEXTVAR_INHERIT_DEFAULT: + return _PyInterpreterState_GET()->config.thread_inherit_context; + case _Py_CONTEXTVAR_INHERIT_FALSE: + return 0; + case _Py_CONTEXTVAR_INHERIT_TRUE: + return 1; + } + // No default case so the compiler warns about unhandled policies. + Py_UNREACHABLE(); +} + + static int contextvar_set(PyContextVar *var, PyObject *val) { @@ -840,35 +876,49 @@ contextvar_set(PyContextVar *var, PyObject *val) return -1; } + PyHamtObject *old_vars = ctx->ctx_vars; PyHamtObject *new_vars = _PyHamt_Assoc( - ctx->ctx_vars, (PyObject *)var, val); + old_vars, (PyObject *)var, val); if (new_vars == NULL) { return -1; } // Compute both new maps before installing either so that an error // leaves the context unchanged. - PyHamtObject *new_thread_inheritable_vars = NULL; - if (var->var_thread_inheritable) { - new_thread_inheritable_vars = _PyHamt_Assoc( - ctx->ctx_thread_inheritable_vars, (PyObject *)var, val); - if (new_thread_inheritable_vars == NULL) { - Py_DECREF(new_vars); - return -1; + PyHamtObject *old_inheritable_vars = NULL; + PyHamtObject *new_inheritable_vars = NULL; + if (contextvar_is_thread_inheritable(var)) { + old_inheritable_vars = ctx->ctx_inheritable_vars; + if (old_inheritable_vars == old_vars) { + // All current bindings are inheritable. One update serves both + // fields and preserves sharing for subsequent updates. + new_inheritable_vars = + (PyHamtObject *)Py_NewRef(new_vars); + } + else { + new_inheritable_vars = _PyHamt_Assoc( + old_inheritable_vars, (PyObject *)var, val); + if (new_inheritable_vars == NULL) { + Py_DECREF(new_vars); + return -1; + } } } - Py_SETREF(ctx->ctx_vars, new_vars); - if (new_thread_inheritable_vars != NULL) { - Py_SETREF(ctx->ctx_thread_inheritable_vars, - new_thread_inheritable_vars); + // Install both maps before either old map is decref'ed. Besides keeping + // their subset relationship atomic with respect to finalizers, updating + // the cache first lets a re-entrant set() performed by a finalizer win. + ctx->ctx_vars = new_vars; + if (new_inheritable_vars != NULL) { + ctx->ctx_inheritable_vars = new_inheritable_vars; } - #ifndef Py_GIL_DISABLED var->var_cached = val; /* borrow */ var->var_cached_tsid = ts->id; var->var_cached_tsver = ts->context_ver; #endif + Py_DECREF(old_vars); + Py_XDECREF(old_inheritable_vars); return 0; } @@ -884,13 +934,13 @@ contextvar_del(PyContextVar *var) return -1; } - PyHamtObject *vars = ctx->ctx_vars; - PyHamtObject *new_vars = _PyHamt_Without(vars, (PyObject *)var); + PyHamtObject *old_vars = ctx->ctx_vars; + PyHamtObject *new_vars = _PyHamt_Without(old_vars, (PyObject *)var); if (new_vars == NULL) { return -1; } - if (vars == new_vars) { + if (old_vars == new_vars) { Py_DECREF(new_vars); PyErr_SetObject(PyExc_LookupError, (PyObject *)var); return -1; @@ -898,21 +948,34 @@ contextvar_del(PyContextVar *var) // Compute both new maps before installing either so that an error // leaves the context unchanged. - PyHamtObject *new_thread_inheritable_vars = NULL; - if (var->var_thread_inheritable) { - new_thread_inheritable_vars = _PyHamt_Without( - ctx->ctx_thread_inheritable_vars, (PyObject *)var); - if (new_thread_inheritable_vars == NULL) { - Py_DECREF(new_vars); - return -1; + PyHamtObject *old_inheritable_vars = NULL; + PyHamtObject *new_inheritable_vars = NULL; + if (contextvar_is_thread_inheritable(var)) { + old_inheritable_vars = ctx->ctx_inheritable_vars; + if (old_inheritable_vars == old_vars) { + // Both fields contain the same bindings, so the result of the + // full-map deletion is also the new inheritable map. + new_inheritable_vars = + (PyHamtObject *)Py_NewRef(new_vars); + } + else { + new_inheritable_vars = _PyHamt_Without( + old_inheritable_vars, (PyObject *)var); + if (new_inheritable_vars == NULL) { + Py_DECREF(new_vars); + return -1; + } } } - Py_SETREF(ctx->ctx_vars, new_vars); - if (new_thread_inheritable_vars != NULL) { - Py_SETREF(ctx->ctx_thread_inheritable_vars, - new_thread_inheritable_vars); + // Keep the full map and its inheritable subset in sync before decrefing + // either displaced map. + ctx->ctx_vars = new_vars; + if (new_inheritable_vars != NULL) { + ctx->ctx_inheritable_vars = new_inheritable_vars; } + Py_DECREF(old_vars); + Py_XDECREF(old_inheritable_vars); return 0; } @@ -945,7 +1008,8 @@ contextvar_generate_hash(void *addr, PyObject *name) } static PyContextVar * -contextvar_new(PyObject *name, PyObject *def, int thread_inheritable) +contextvar_new(PyObject *name, PyObject *def, + _PyContextVarInherit thread_inherit) { if (!PyUnicode_Check(name)) { PyErr_SetString(PyExc_TypeError, @@ -960,7 +1024,7 @@ contextvar_new(PyObject *name, PyObject *def, int thread_inheritable) var->var_name = Py_NewRef(name); var->var_default = Py_XNewRef(def); - var->var_thread_inheritable = thread_inheritable; + var->var_thread_inherit = thread_inherit; #ifndef Py_GIL_DISABLED var->var_cached = NULL; @@ -995,17 +1059,43 @@ class _contextvars.ContextVar "PyContextVar *" "&PyContextVar_Type" static PyObject * contextvar_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"", "default", NULL}; + static char *kwlist[] = { + "", "default", "thread_inheritable", NULL, + }; PyObject *name; PyObject *def = NULL; + PyObject *thread_inheritable = NULL; if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|$O:ContextVar", kwlist, &name, &def)) + args, kwds, "O|$OO:ContextVar", kwlist, + &name, &def, &thread_inheritable)) { return NULL; } - return (PyObject *)contextvar_new(name, def, 0); + if (thread_inheritable != NULL && + thread_inheritable != Py_None && + thread_inheritable != Py_True && + thread_inheritable != Py_False) { + PyErr_Format( + PyExc_TypeError, + "thread_inheritable must be a bool or None, not %T", + thread_inheritable); + return NULL; + } + + _PyContextVarInherit thread_inherit; + if (thread_inheritable == Py_True) { + thread_inherit = _Py_CONTEXTVAR_INHERIT_TRUE; + } + else if (thread_inheritable == Py_False) { + thread_inherit = _Py_CONTEXTVAR_INHERIT_FALSE; + } + else { + thread_inherit = _Py_CONTEXTVAR_INHERIT_DEFAULT; + } + + return (PyObject *)contextvar_new(name, def, thread_inherit); } static int @@ -1050,11 +1140,22 @@ static PyObject * contextvar_tp_repr(PyObject *op) { PyContextVar *self = _PyContextVar_CAST(op); + const char *thread_inherit_repr = NULL; + if (self->var_thread_inherit == _Py_CONTEXTVAR_INHERIT_TRUE) { + thread_inherit_repr = " thread_inheritable=True"; + } + else if (self->var_thread_inherit == _Py_CONTEXTVAR_INHERIT_FALSE) { + thread_inherit_repr = " thread_inheritable=False"; + } + Py_ssize_t thread_inherit_repr_len = thread_inherit_repr == NULL + ? 0 : (Py_ssize_t)strlen(thread_inherit_repr); + // Estimation based on the shortest name and default value, // but maximize the pointer size. // "" // "" Py_ssize_t estimate = self->var_default ? 53 : 43; + estimate += thread_inherit_repr_len; PyUnicodeWriter *writer = PyUnicodeWriter_Create(estimate); if (writer == NULL) { return NULL; @@ -1076,6 +1177,14 @@ contextvar_tp_repr(PyObject *op) } } + // Only shown when set, so that the common case is unchanged. + if (thread_inherit_repr != NULL && + PyUnicodeWriter_WriteASCII( + writer, thread_inherit_repr, thread_inherit_repr_len) < 0) + { + goto error; + } + if (PyUnicodeWriter_Format(writer, " at %p>", self) < 0) { goto error; } @@ -1087,32 +1196,6 @@ contextvar_tp_repr(PyObject *op) } -/*[clinic input] -@classmethod -_contextvars.ContextVar.thread_inheritable - name: object - / - * - default: object = NULL - -Create a context variable whose binding is inherited by new threads. - -The bindings of such variables are copied into the context of a new -thread by threading.Thread.start() when the thread would otherwise -start with an empty context. -[clinic start generated code]*/ - -static PyObject * -_contextvars_ContextVar_thread_inheritable_impl(PyTypeObject *type, - PyObject *name, - PyObject *default_value) -/*[clinic end generated code: output=a890265ff610a979 input=f211f3eedeb507b8]*/ -{ - assert(type == &PyContextVar_Type); - return (PyObject *)contextvar_new(name, default_value, 1); -} - - /*[clinic input] _contextvars.ContextVar.get default: object = NULL @@ -1197,13 +1280,34 @@ _contextvars_ContextVar_reset_impl(PyContextVar *self, PyObject *token) } +static PyObject * +contextvar_get_thread_inheritable(PyObject *op, void *Py_UNUSED(ignored)) +{ + PyContextVar *self = _PyContextVar_CAST(op); + switch (self->var_thread_inherit) { + case _Py_CONTEXTVAR_INHERIT_DEFAULT: + Py_RETURN_NONE; + case _Py_CONTEXTVAR_INHERIT_FALSE: + Py_RETURN_FALSE; + case _Py_CONTEXTVAR_INHERIT_TRUE: + Py_RETURN_TRUE; + } + // No default case so the compiler warns about unhandled policies. + Py_UNREACHABLE(); +} + static PyMemberDef PyContextVar_members[] = { {"name", _Py_T_OBJECT, offsetof(PyContextVar, var_name), Py_READONLY}, {NULL} }; +static PyGetSetDef PyContextVar_getsetlist[] = { + {"thread_inheritable", contextvar_get_thread_inheritable, NULL, + PyDoc_STR("the automatic thread-inheritance policy: None, True, or False")}, + {NULL} +}; + static PyMethodDef PyContextVar_methods[] = { - _CONTEXTVARS_CONTEXTVAR_THREAD_INHERITABLE_METHODDEF _CONTEXTVARS_CONTEXTVAR_GET_METHODDEF _CONTEXTVARS_CONTEXTVAR_SET_METHODDEF _CONTEXTVARS_CONTEXTVAR_RESET_METHODDEF @@ -1219,6 +1323,7 @@ PyTypeObject PyContextVar_Type = { sizeof(PyContextVar), .tp_methods = PyContextVar_methods, .tp_members = PyContextVar_members, + .tp_getset = PyContextVar_getsetlist, .tp_dealloc = contextvar_tp_dealloc, .tp_getattro = PyObject_GenericGetAttr, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,