From c29fef5f9f77c42078ad19cdb0ccb8c6ab62fc85 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 22 Jul 2026 18:32:13 -0700 Subject: [PATCH] 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