From ca76cb7bb054a4e65eebb26693be89e19f6a1c6c Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" <69878+youknowone@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:53:41 +0900 Subject: [PATCH] thread_inherit_context, upgrade threading & contextvar (#6727) * thread_inherit_context * Update contextvar, threading from CPython 3.14.2 * partially patch test_sys to 3.14.2 --------- Co-authored-by: CPython Devleopers <> --- Lib/contextvars.py | 4 +++ Lib/test/test_sys.py | 5 +-- Lib/threading.py | 72 ++++++++++++++++++++++++++++--------- crates/vm/src/stdlib/sys.rs | 3 ++ crates/vm/src/vm/setting.rs | 4 +++ src/settings.rs | 28 +++++++++++++++ 6 files changed, 98 insertions(+), 18 deletions(-) diff --git a/Lib/contextvars.py b/Lib/contextvars.py index d78c80dfe..14514f185 100644 --- a/Lib/contextvars.py +++ b/Lib/contextvars.py @@ -1,4 +1,8 @@ +import _collections_abc from _contextvars import Context, ContextVar, Token, copy_context __all__ = ('Context', 'ContextVar', 'Token', 'copy_context') + + +_collections_abc.Mapping.register(Context) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index e5f88b325..b65e1291b 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -889,10 +889,11 @@ class SysModuleTest(unittest.TestCase): "dont_write_bytecode", "no_user_site", "no_site", "ignore_environment", "verbose", "bytes_warning", "quiet", "hash_randomization", "isolated", "dev_mode", "utf8_mode", - "warn_default_encoding", "safe_path", "int_max_str_digits") + "warn_default_encoding", "safe_path", "int_max_str_digits", + "thread_inherit_context") for attr in attrs: self.assertTrue(hasattr(sys.flags, attr), attr) - attr_type = bool if attr in ("dev_mode", "safe_path") else int + attr_type = bool if attr in ("dev_mode", "safe_path", "thread_inherit_context") else int self.assertEqual(type(getattr(sys.flags, attr)), attr_type, attr) self.assertTrue(repr(sys.flags)) self.assertEqual(len(sys.flags), len(attrs)) diff --git a/Lib/threading.py b/Lib/threading.py index 15bf786a6..c03b0b537 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -3,7 +3,7 @@ import os as _os import sys as _sys import _thread -import warnings +import _contextvars from time import monotonic as _time from _weakrefset import WeakSet @@ -48,6 +48,10 @@ try: __all__.append('get_native_id') except AttributeError: _HAVE_THREAD_NATIVE_ID = False +try: + _set_name = _thread.set_name +except AttributeError: + _set_name = None ThreadError = _thread.error try: _CRLock = _thread.RLock @@ -129,6 +133,7 @@ def RLock(*args, **kwargs): """ if args or kwargs: + import warnings warnings.warn( 'Passing arguments to RLock is deprecated and will be removed in 3.15', DeprecationWarning, @@ -160,7 +165,7 @@ class _RLock: except KeyError: pass return "<%s %s.%s object owner=%r count=%d at %s>" % ( - "locked" if self._block.locked() else "unlocked", + "locked" if self.locked() else "unlocked", self.__class__.__module__, self.__class__.__qualname__, owner, @@ -237,6 +242,10 @@ class _RLock: def __exit__(self, t, v, tb): self.release() + def locked(self): + """Return whether this object is locked.""" + return self._block.locked() + # Internal methods used by condition variables def _acquire_restore(self, state): @@ -282,9 +291,10 @@ class Condition: if lock is None: lock = RLock() self._lock = lock - # Export the lock's acquire() and release() methods + # Export the lock's acquire(), release(), and locked() methods self.acquire = lock.acquire self.release = lock.release + self.locked = lock.locked # If the lock defines _release_save() and/or _acquire_restore(), # these override the default implementations (which just call # release() and acquire() on the lock). Ditto for _is_owned(). @@ -868,7 +878,7 @@ class Thread: _initialized = False def __init__(self, group=None, target=None, name=None, - args=(), kwargs=None, *, daemon=None): + args=(), kwargs=None, *, daemon=None, context=None): """This constructor should always be called with keyword arguments. Arguments are: *group* should be None; reserved for future extension when a ThreadGroup @@ -885,6 +895,14 @@ class Thread: *kwargs* is a dictionary of keyword arguments for the target invocation. Defaults to {}. + *context* is the contextvars.Context value to use for the thread. + The default value is None, which means to check + sys.flags.thread_inherit_context. If that flag is true, use a copy + of the context of the caller. If false, use an empty context. To + explicitly start with an empty context, pass a new instance of + contextvars.Context(). To explicitly start with a copy of the current + context, pass the value from contextvars.copy_context(). + If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread. @@ -914,10 +932,11 @@ class Thread: self._daemonic = daemon else: self._daemonic = current_thread().daemon + self._context = context self._ident = None if _HAVE_THREAD_NATIVE_ID: self._native_id = None - self._handle = _ThreadHandle() + self._os_thread_handle = _ThreadHandle() self._started = Event() self._initialized = True # Copy of sys.stderr used by self._invoke_excepthook() @@ -932,7 +951,7 @@ class Thread: if new_ident is not None: # This thread is alive. self._ident = new_ident - assert self._handle.ident == new_ident + assert self._os_thread_handle.ident == new_ident if _HAVE_THREAD_NATIVE_ID: self._set_native_id() else: @@ -945,7 +964,7 @@ class Thread: status = "initial" if self._started.is_set(): status = "started" - if self._handle.is_done(): + if self._os_thread_handle.is_done(): status = "stopped" if self._daemonic: status += " daemon" @@ -971,9 +990,19 @@ class Thread: with _active_limbo_lock: _limbo[self] = self + + if self._context is None: + # 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() + try: # Start joinable thread - _start_joinable_thread(self._bootstrap, handle=self._handle, + _start_joinable_thread(self._bootstrap, handle=self._os_thread_handle, daemon=self.daemon) except Exception: with _active_limbo_lock: @@ -1025,11 +1054,20 @@ class Thread: def _set_native_id(self): self._native_id = get_native_id() + def _set_os_name(self): + if _set_name is None or not self._name: + return + try: + _set_name(self._name) + except OSError: + pass + def _bootstrap_inner(self): try: self._set_ident() if _HAVE_THREAD_NATIVE_ID: self._set_native_id() + self._set_os_name() self._started.set() with _active_limbo_lock: _active[self._ident] = self @@ -1041,7 +1079,7 @@ class Thread: _sys.setprofile(_profile_hook) try: - self.run() + self._context.run(self.run) except: self._invoke_excepthook(self) finally: @@ -1092,7 +1130,7 @@ class Thread: if timeout is not None: timeout = max(timeout, 0) - self._handle.join(timeout) + self._os_thread_handle.join(timeout) @property def name(self): @@ -1109,6 +1147,8 @@ class Thread: def name(self, name): assert self._initialized, "Thread.__init__() not called" self._name = str(name) + if get_ident() == self._ident: + self._set_os_name() @property def ident(self): @@ -1143,7 +1183,7 @@ class Thread: """ assert self._initialized, "Thread.__init__() not called" - return self._started.is_set() and not self._handle.is_done() + return self._started.is_set() and not self._os_thread_handle.is_done() @property def daemon(self): @@ -1354,7 +1394,7 @@ class _MainThread(Thread): Thread.__init__(self, name="MainThread", daemon=False) self._started.set() self._ident = _get_main_thread_ident() - self._handle = _make_thread_handle(self._ident) + self._os_thread_handle = _make_thread_handle(self._ident) if _HAVE_THREAD_NATIVE_ID: self._set_native_id() with _active_limbo_lock: @@ -1402,7 +1442,7 @@ class _DummyThread(Thread): daemon=_daemon_threads_allowed()) self._started.set() self._set_ident() - self._handle = _make_thread_handle(self._ident) + self._os_thread_handle = _make_thread_handle(self._ident) if _HAVE_THREAD_NATIVE_ID: self._set_native_id() with _active_limbo_lock: @@ -1410,7 +1450,7 @@ class _DummyThread(Thread): _DeleteDummyThreadOnDel(self) def is_alive(self): - if not self._handle.is_done() and self._started.is_set(): + if not self._os_thread_handle.is_done() and self._started.is_set(): return True raise RuntimeError("thread is not alive") @@ -1524,7 +1564,7 @@ def _shutdown(): # dubious, but some code does it. We can't wait for it to be marked as done # normally - that won't happen until the interpreter is nearly dead. So # mark it done here. - if _main_thread._handle.is_done() and _is_main_interpreter(): + if _main_thread._os_thread_handle.is_done() and _is_main_interpreter(): # _shutdown() was already called return @@ -1537,7 +1577,7 @@ def _shutdown(): atexit_call() if _is_main_interpreter(): - _main_thread._handle._set_done() + _main_thread._os_thread_handle._set_done() # Wait for all non-daemon threads to exit. _thread_shutdown() diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 8a65a926c..66a43427f 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1301,6 +1301,8 @@ mod sys { safe_path: bool, /// -X warn_default_encoding, PYTHONWARNDEFAULTENCODING warn_default_encoding: u8, + /// -X thread_inherit_context, whether new threads inherit context from parent + thread_inherit_context: bool, } impl FlagsData { @@ -1324,6 +1326,7 @@ mod sys { int_max_str_digits: settings.int_max_str_digits, safe_path: settings.safe_path, warn_default_encoding: settings.warn_default_encoding as u8, + thread_inherit_context: settings.thread_inherit_context, } } } diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 9be21b448..a7779156f 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -88,6 +88,9 @@ pub struct Settings { /// -X warn_default_encoding, PYTHONWARNDEFAULTENCODING pub warn_default_encoding: bool, + /// -X thread_inherit_context, whether new threads inherit context from parent + pub thread_inherit_context: bool, + /// -i pub inspect: bool, @@ -190,6 +193,7 @@ impl Default for Settings { isolated: false, dev_mode: false, warn_default_encoding: false, + thread_inherit_context: false, warnoptions: vec![], path_list: vec![], argv: vec![], diff --git a/src/settings.rs b/src/settings.rs index 7dd3c1a77..71a9c5a3f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -285,6 +285,20 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { } }; } + "thread_inherit_context" => { + settings.thread_inherit_context = match value { + Some("1") => true, + Some("0") => false, + _ => { + error!( + "Fatal Python error: config_init_thread_inherit_context: \ + -X thread_inherit_context=n: n is missing or invalid\n\ + Python runtime state: preinitialized" + ); + std::process::exit(1); + } + }; + } _ => {} } (name, value.map(str::to_owned)) @@ -297,6 +311,20 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { if env_bool("PYTHONNODEBUGRANGES") { settings.code_debug_ranges = false; } + if let Some(val) = get_env("PYTHON_THREAD_INHERIT_CONTEXT") { + settings.thread_inherit_context = match val.to_str() { + Some("1") => true, + Some("0") => false, + _ => { + error!( + "Fatal Python error: config_init_thread_inherit_context: \ + PYTHON_THREAD_INHERIT_CONTEXT=N: N is missing or invalid\n\ + Python runtime state: preinitialized" + ); + std::process::exit(1); + } + }; + } // Parse PYTHONIOENCODING=encoding[:errors] if let Some(val) = get_env("PYTHONIOENCODING")