Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Lib/contextvars.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 3 additions & 2 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,10 +889,11 @@ def test_sys_flags(self):
"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))
Expand Down
72 changes: 56 additions & 16 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,6 +48,10 @@
__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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -160,7 +165,7 @@ def __repr__(self):
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,
Expand Down Expand Up @@ -237,6 +242,10 @@ def release(self):
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):
Expand Down Expand Up @@ -282,9 +291,10 @@ def __init__(self, lock=None):
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().
Expand Down Expand Up @@ -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
Expand All @@ -885,6 +895,14 @@ class is implemented.
*kwargs* is a dictionary of keyword arguments for the target
invocation. Defaults to {}.

*context* is the contextvars.Context value to use for the thread.
The default value is None, which means to check
sys.flags.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.
Expand Down Expand Up @@ -914,10 +932,11 @@ class is implemented.
self._daemonic = daemon
else:
self._daemonic = current_thread().daemon
self._context = context
self._ident = None
if _HAVE_THREAD_NATIVE_ID:
self._native_id = None
self._handle = _ThreadHandle()
self._os_thread_handle = _ThreadHandle()
self._started = Event()
self._initialized = True
# Copy of sys.stderr used by self._invoke_excepthook()
Expand All @@ -932,7 +951,7 @@ def _after_fork(self, new_ident=None):
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:
Expand All @@ -945,7 +964,7 @@ def __repr__(self):
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"
Expand All @@ -971,9 +990,19 @@ def start(self):

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:
Expand Down Expand Up @@ -1025,11 +1054,20 @@ def _set_ident(self):
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
Expand All @@ -1041,7 +1079,7 @@ def _bootstrap_inner(self):
_sys.setprofile(_profile_hook)

try:
self.run()
self._context.run(self.run)
except:
self._invoke_excepthook(self)
finally:
Expand Down Expand Up @@ -1092,7 +1130,7 @@ def join(self, timeout=None):
if timeout is not None:
timeout = max(timeout, 0)

self._handle.join(timeout)
self._os_thread_handle.join(timeout)

@property
def name(self):
Expand All @@ -1109,6 +1147,8 @@ def name(self):
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):
Expand Down Expand Up @@ -1143,7 +1183,7 @@ def is_alive(self):

"""
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):
Expand Down Expand Up @@ -1354,7 +1394,7 @@ def __init__(self):
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:
Expand Down Expand Up @@ -1402,15 +1442,15 @@ def __init__(self):
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:
_active[self._ident] = self
_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")

Expand Down Expand Up @@ -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

Expand All @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions crates/vm/src/stdlib/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/vm/src/vm/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// -i
pub inspect: bool,

Expand Down Expand Up @@ -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![],
Expand Down
28 changes: 28 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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);
}
};
}
Comment on lines +314 to +327

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PYTHONINTMAXSTRDIGITS and int_max_str_digits handling to compare precedence

# Look for PYTHONINTMAXSTRDIGITS handling
rg -n -B5 -A10 "PYTHONINTMAXSTRDIGITS" src/settings.rs

# Check int_max_str_digits processing order
echo "---"
rg -n "int_max_str_digits" src/settings.rs

Repository: RustPython/RustPython

Length of output: 1647


🏁 Script executed:

#!/bin/bash
# Get detailed view of lines 288-327 to see the exact order of processing

sed -n '280,330p' src/settings.rs | cat -n

Repository: RustPython/RustPython

Length of output: 2419


Fix precedence: X-option should override environment variable, not vice versa.

The environment variable PYTHON_THREAD_INHERIT_CONTEXT (lines 315-328) is processed after the -X thread_inherit_context option (lines 289-301), causing it to override the X-option. This is inconsistent with the handling of int_max_str_digits, where the X-option is processed last and takes precedence. Reorder to process the environment variable before the X-option block to ensure command-line options take precedence, or document this as intentional behavior if it differs from Python's design.

🤖 Prompt for AI Agents
In `@src/settings.rs` around lines 314 - 327, The PYTHON_THREAD_INHERIT_CONTEXT
env handling currently runs after the -X thread_inherit_context option and thus
overrides the X-option; move the shown get_env("PYTHON_THREAD_INHERIT_CONTEXT")
block so it executes before the code that parses/handles the -X
thread_inherit_context option (the block that sets
settings.thread_inherit_context from the -X flag), ensuring the command-line -X
thread_inherit_context takes precedence (consistent with int_max_str_digits
behavior).


// Parse PYTHONIOENCODING=encoding[:errors]
if let Some(val) = get_env("PYTHONIOENCODING")
Expand Down
Loading