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 <>
This commit is contained in:
Jeong, YunWonandCPython Devleopers authored and GitHub committed 2026-01-18 08:53:41 +09:00
1 parent 11c9b0e783
commit ca76cb7bb0
6 files changed
+98 -18

No files matched your search

+4
View File
@@ -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)
+3 -2
View File
@@ -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))
+56 -16
View File
@@ -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()
+3
View File
@@ -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,
}
}
}
+4
View File
@@ -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![],
+28
View File
@@ -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")