Skip to content

Commit f4b395a

Browse files
committed
Implement sys.call_tracing, sys._current_exceptions
- Add sys.call_tracing as func(*args) dispatch - Add sys._current_exceptions with per-thread exception tracking via thread_exceptions registry synced on push/pop/set_exception - Fix f_back to use previous_frame pointer and cross-thread lookup - Add module="sys" to float_info struct sequence - Fix sys.exit to unpack tuple args - Set default stdio_errors to surrogateescape - Remove noisy __del__ warning in object core
1 parent cdcbf0a commit f4b395a

File tree

8 files changed

+130
-23
lines changed

8 files changed

+130
-23
lines changed

Lib/test/test_sys.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@ def test_getwindowsversion(self):
431431
# still has 5 elements
432432
maj, min, buildno, plat, csd = sys.getwindowsversion()
433433

434-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'sys' has no attribute 'call_tracing'
435434
def test_call_tracing(self):
436435
self.assertRaises(TypeError, sys.call_tracing, type, 2)
437436

@@ -497,7 +496,6 @@ def test_getframemodulename(self):
497496
self.assertIsNone(sys._getframemodulename(i))
498497

499498
# sys._current_frames() is a CPython-only gimmick.
500-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: didn't find f123() on thread's call stack
501499
@threading_helper.reap_threads
502500
@threading_helper.requires_working_threading()
503501
def test_current_frames(self):
@@ -565,7 +563,6 @@ def g456():
565563
leave_g.set()
566564
t.join()
567565

568-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'sys' has no attribute '_current_exceptions'
569566
@threading_helper.reap_threads
570567
@threading_helper.requires_working_threading()
571568
def test_current_exceptions(self):
@@ -1188,7 +1185,6 @@ def __del__(self):
11881185
rc, stdout, stderr = assert_python_ok('-c', code)
11891186
self.assertEqual(stdout.rstrip(), b'True')
11901187

1191-
@unittest.expectedFailure # TODO: RUSTPYTHON; IndexError: list index out of range
11921188
def test_issue20602(self):
11931189
# sys.flags and sys.float_info were wiped during shutdown.
11941190
code = """if 1:

crates/vm/src/builtins/frame.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use super::{PyCode, PyDictRef, PyIntRef, PyStrRef};
66
use crate::{
7-
AsObject, Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
7+
Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
88
class::PyClassImpl,
99
frame::{Frame, FrameOwner, FrameRef},
1010
function::PySetterValue,
@@ -192,16 +192,42 @@ impl Py<Frame> {
192192

193193
#[pygetset]
194194
pub fn f_back(&self, vm: &VirtualMachine) -> Option<PyRef<Frame>> {
195-
// TODO: actually store f_back inside Frame struct
195+
let previous = self.previous_frame();
196+
if previous.is_null() {
197+
return None;
198+
}
196199

197-
// get the frame in the frame stack that appears before this one.
198-
// won't work if this frame isn't in the frame stack, hence the todo above
199-
vm.frames
200+
if let Some(frame) = vm
201+
.frames
200202
.borrow()
201203
.iter()
202-
.rev()
203-
.skip_while(|p| !p.is(self.as_object()))
204-
.nth(1)
204+
.find(|f| {
205+
let ptr: *const Frame = &****f;
206+
core::ptr::eq(ptr, previous)
207+
})
205208
.cloned()
209+
{
210+
return Some(frame);
211+
}
212+
213+
#[cfg(feature = "threading")]
214+
{
215+
let registry = vm.state.thread_frames.lock();
216+
for slot in registry.values() {
217+
if let Some(frame) = slot
218+
.lock()
219+
.iter()
220+
.find(|f| {
221+
let ptr: *const Frame = &****f;
222+
core::ptr::eq(ptr, previous)
223+
})
224+
.cloned()
225+
{
226+
return Some(frame);
227+
}
228+
}
229+
}
230+
231+
None
206232
}
207233
}

crates/vm/src/object/core.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,10 +1078,7 @@ impl PyObject {
10781078
Some(true) => Ok(()),
10791079
// we've been resurrected by __del__
10801080
Some(false) => Err(()),
1081-
None => {
1082-
warn!("couldn't run __del__ method for object");
1083-
Ok(())
1084-
}
1081+
None => Ok(()),
10851082
}
10861083
}
10871084

crates/vm/src/stdlib/sys.rs

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ mod sys {
3333
use crate::{
3434
AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult,
3535
builtins::{
36-
PyBaseExceptionRef, PyDictRef, PyFrozenSet, PyNamespace, PyStr, PyStrRef, PyTupleRef,
37-
PyTypeRef,
36+
PyBaseExceptionRef, PyDictRef, PyFrozenSet, PyNamespace, PyStr, PyStrRef, PyTuple,
37+
PyTupleRef, PyTypeRef,
3838
},
3939
common::{
4040
ascii,
@@ -789,8 +789,22 @@ mod sys {
789789

790790
#[pyfunction]
791791
fn exit(code: OptionalArg<PyObjectRef>, vm: &VirtualMachine) -> PyResult {
792-
let code = code.unwrap_or_none(vm);
793-
Err(vm.new_exception(vm.ctx.exceptions.system_exit.to_owned(), vec![code]))
792+
let status = code.unwrap_or_none(vm);
793+
let args = if let Some(status_tuple) = status.downcast_ref::<PyTuple>() {
794+
status_tuple.as_slice().to_vec()
795+
} else {
796+
vec![status]
797+
};
798+
let exc = vm.invoke_exception(vm.ctx.exceptions.system_exit.to_owned(), args)?;
799+
Err(exc)
800+
}
801+
802+
#[pyfunction]
803+
fn call_tracing(func: PyObjectRef, args: PyTupleRef, vm: &VirtualMachine) -> PyResult {
804+
// CPython temporarily enables tracing state around this call.
805+
// RustPython does not currently model the full C-level tracing toggles,
806+
// but call semantics (func(*args)) are matched.
807+
func.call(PosArgs::new(args.as_slice().to_vec()), vm)
794808
}
795809

796810
#[pyfunction]
@@ -1024,6 +1038,36 @@ mod sys {
10241038
Ok(dict)
10251039
}
10261040

1041+
/// Return a dictionary mapping each thread's identifier to its currently
1042+
/// active exception, or None if no exception is active.
1043+
#[cfg(feature = "threading")]
1044+
#[pyfunction]
1045+
fn _current_exceptions(vm: &VirtualMachine) -> PyResult<PyDictRef> {
1046+
use crate::AsObject;
1047+
1048+
let dict = vm.ctx.new_dict();
1049+
let exceptions = vm.state.thread_exceptions.lock();
1050+
1051+
for (thread_id, exc) in exceptions.iter() {
1052+
let key = vm.ctx.new_int(*thread_id);
1053+
let value = exc
1054+
.as_ref()
1055+
.map_or_else(|| vm.ctx.none(), |e| e.clone().into());
1056+
dict.set_item(key.as_object(), value, vm)?;
1057+
}
1058+
1059+
Ok(dict)
1060+
}
1061+
1062+
#[cfg(not(feature = "threading"))]
1063+
#[pyfunction]
1064+
fn _current_exceptions(vm: &VirtualMachine) -> PyResult<PyDictRef> {
1065+
let dict = vm.ctx.new_dict();
1066+
let key = vm.ctx.new_int(0);
1067+
dict.set_item(key.as_object(), vm.topmost_exception().to_pyobject(vm), vm)?;
1068+
Ok(dict)
1069+
}
1070+
10271071
/// Stub for non-threading builds - returns empty dict
10281072
#[cfg(not(feature = "threading"))]
10291073
#[pyfunction]
@@ -1619,7 +1663,7 @@ mod sys {
16191663
};
16201664
}
16211665

1622-
#[pystruct_sequence(name = "float_info", data = "FloatInfoData", no_attr)]
1666+
#[pystruct_sequence(name = "float_info", module = "sys", data = "FloatInfoData", no_attr)]
16231667
pub(super) struct PyFloatInfo;
16241668

16251669
#[pyclass(with(PyStructSequence))]

crates/vm/src/stdlib/thread.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,11 @@ pub(crate) mod _thread {
607607
pub fn init_main_thread_ident(vm: &VirtualMachine) {
608608
let ident = get_ident();
609609
vm.state.main_thread_ident.store(ident);
610+
vm.state
611+
.thread_exceptions
612+
.lock()
613+
.entry(ident)
614+
.or_insert(None);
610615
}
611616

612617
/// ExceptHookArgs - simple class to hold exception hook arguments

crates/vm/src/vm/interpreter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ where
121121
#[cfg(feature = "threading")]
122122
thread_frames: parking_lot::Mutex::new(std::collections::HashMap::new()),
123123
#[cfg(feature = "threading")]
124+
thread_exceptions: parking_lot::Mutex::new(std::collections::HashMap::new()),
125+
#[cfg(feature = "threading")]
124126
thread_handles: parking_lot::Mutex::new(Vec::new()),
125127
#[cfg(feature = "threading")]
126128
shutdown_handles: parking_lot::Mutex::new(Vec::new()),

crates/vm/src/vm/mod.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ pub struct PyGlobalState {
132132
/// Registry of all threads' current frames for sys._current_frames()
133133
#[cfg(feature = "threading")]
134134
pub thread_frames: parking_lot::Mutex<HashMap<u64, stdlib::thread::CurrentFrameSlot>>,
135+
/// Registry of all threads' current active exceptions for sys._current_exceptions()
136+
#[cfg(feature = "threading")]
137+
pub thread_exceptions: parking_lot::Mutex<HashMap<u64, Option<PyBaseExceptionRef>>>,
135138
/// Registry of all ThreadHandles for fork cleanup
136139
#[cfg(feature = "threading")]
137140
pub thread_handles: parking_lot::Mutex<Vec<stdlib::thread::HandleEntry>>,
@@ -380,7 +383,12 @@ impl VirtualMachine {
380383
let errors = if fd == 2 {
381384
Some("backslashreplace")
382385
} else {
383-
self.state.config.settings.stdio_errors.as_deref()
386+
self.state
387+
.config
388+
.settings
389+
.stdio_errors
390+
.as_deref()
391+
.or(Some("surrogateescape"))
384392
};
385393

386394
let stdio = self.call_method(
@@ -1333,13 +1341,19 @@ impl VirtualMachine {
13331341
let mut excs = self.exceptions.borrow_mut();
13341342
let prev = core::mem::take(&mut *excs);
13351343
excs.prev = Some(Box::new(prev));
1336-
excs.exc = exc
1344+
excs.exc = exc;
1345+
drop(excs);
1346+
#[cfg(feature = "threading")]
1347+
self.sync_current_thread_exception_state();
13371348
}
13381349

13391350
pub(crate) fn pop_exception(&self) -> Option<PyBaseExceptionRef> {
13401351
let mut excs = self.exceptions.borrow_mut();
13411352
let cur = core::mem::take(&mut *excs);
13421353
*excs = *cur.prev.expect("pop_exception() without nested exc stack");
1354+
drop(excs);
1355+
#[cfg(feature = "threading")]
1356+
self.sync_current_thread_exception_state();
13431357
cur.exc
13441358
}
13451359

@@ -1351,6 +1365,8 @@ impl VirtualMachine {
13511365
// don't be holding the RefCell guard while __del__ is called
13521366
let prev = core::mem::replace(&mut self.exceptions.borrow_mut().exc, exc);
13531367
drop(prev);
1368+
#[cfg(feature = "threading")]
1369+
self.sync_current_thread_exception_state();
13541370
}
13551371

13561372
pub(crate) fn contextualize_exception(&self, exception: &Py<PyBaseException>) {
@@ -1392,6 +1408,13 @@ impl VirtualMachine {
13921408
}
13931409
}
13941410

1411+
#[cfg(feature = "threading")]
1412+
fn sync_current_thread_exception_state(&self) {
1413+
let ident = crate::stdlib::thread::get_ident();
1414+
let exc = self.topmost_exception();
1415+
self.state.thread_exceptions.lock().insert(ident, exc);
1416+
}
1417+
13951418
pub fn handle_exit_exception(&self, exc: PyBaseExceptionRef) -> u32 {
13961419
if exc.fast_isinstance(self.ctx.exceptions.system_exit) {
13971420
let args = exc.args();

crates/vm/src/vm/thread.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ fn init_frame_slot_if_needed(vm: &VirtualMachine) {
7070
.thread_frames
7171
.lock()
7272
.insert(thread_id, new_slot.clone());
73+
vm.state.thread_exceptions.lock().insert(thread_id, None);
7374
*slot.borrow_mut() = Some(new_slot);
7475
}
7576
});
@@ -114,6 +115,7 @@ pub fn get_current_frame() -> *const Frame {
114115
pub fn cleanup_current_thread_frames(vm: &VirtualMachine) {
115116
let thread_id = crate::stdlib::thread::get_ident();
116117
vm.state.thread_frames.lock().remove(&thread_id);
118+
vm.state.thread_exceptions.lock().remove(&thread_id);
117119
CURRENT_FRAME_SLOT.with(|s| {
118120
*s.borrow_mut() = None;
119121
});
@@ -144,6 +146,18 @@ pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) {
144146
registry.insert(current_ident, new_slot.clone());
145147
drop(registry);
146148

149+
let mut exc_registry = match vm.state.thread_exceptions.try_lock() {
150+
Some(guard) => guard,
151+
None => {
152+
unsafe { vm.state.thread_exceptions.force_unlock() };
153+
vm.state.thread_exceptions.lock()
154+
}
155+
};
156+
let current_exc = vm.topmost_exception();
157+
exc_registry.clear();
158+
exc_registry.insert(current_ident, current_exc);
159+
drop(exc_registry);
160+
147161
// Update thread-local to point to the new slot
148162
CURRENT_FRAME_SLOT.with(|s| {
149163
*s.borrow_mut() = Some(new_slot);

0 commit comments

Comments
 (0)