diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 123d51b77ea..a8d39dc55c6 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -834,8 +834,6 @@ class CWCmdLineTests(WCmdLineTests, unittest.TestCase): class PyWCmdLineTests(WCmdLineTests, unittest.TestCase): module = py_warnings - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_improper_option(self): # Same as above, but check that the message is printed out when # the interpreter is executed. This also checks that options are diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index 6faef040a0e..567bc40a2c3 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -110,10 +110,11 @@ impl Interpreter { /// Finalize vm and turns an exception to exit code. /// - /// Finalization steps including 5 steps: + /// Finalization steps (matching Py_FinalizeEx): /// 1. Flush stdout and stderr. /// 1. Handle exit exception and turn it to exit code. - /// 1. Wait for non-daemon threads (threading._shutdown). + /// 1. Wait for thread shutdown (call threading._shutdown). + /// 1. Mark vm as finalizing. /// 1. Run atexit exit functions. /// 1. Mark vm as finalized. /// @@ -129,13 +130,26 @@ impl Interpreter { 0 }; - // Wait for non-daemon threads (wait_for_thread_shutdown) - wait_for_thread_shutdown(vm); + // Wait for thread shutdown - call threading._shutdown() if available. + // This waits for all non-daemon threads to complete. + // threading module may not be imported, so ignore import errors. + if let Ok(threading) = vm.import("threading", 0) + && let Ok(shutdown) = threading.get_attr("_shutdown", vm) + && let Err(e) = shutdown.call((), vm) + { + vm.run_unraisable( + e, + Some("Exception ignored in threading shutdown".to_owned()), + threading, + ); + } + + // Mark as finalizing AFTER thread shutdown + vm.state.finalizing.store(true, Ordering::Release); + // Run atexit exit functions atexit::_run_exitfuncs(vm); - vm.state.finalizing.store(true, Ordering::Release); - vm.flush_std(); exit_code @@ -143,35 +157,6 @@ impl Interpreter { } } -/// Wait until threading._shutdown completes, provided -/// the threading module was imported in the first place. -/// The shutdown routine will wait until all non-daemon -/// "threading" threads have completed. -fn wait_for_thread_shutdown(vm: &VirtualMachine) { - // Try to get the threading module if it was already imported - // Use sys.modules.get("threading") like PyImport_GetModule - let threading = match (|| -> PyResult<_> { - let sys_modules = vm.sys_module.get_attr("modules", vm)?; - let threading = sys_modules.get_item("threading", vm)?; - Ok(threading) - })() { - Ok(module) => module, - Err(_) => { - // threading not imported, nothing to do - return; - } - }; - - // Call threading._shutdown() - if let Err(e) = vm.call_method(&threading, "_shutdown", ()) { - vm.run_unraisable( - e, - Some("Exception ignored on threading shutdown".to_owned()), - threading, - ); - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 27b70e321cf..0c29d34ef88 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -550,6 +550,18 @@ impl VirtualMachine { #[cold] pub fn run_unraisable(&self, e: PyBaseExceptionRef, msg: Option, object: PyObjectRef) { + // During interpreter finalization, sys.unraisablehook may not be available, + // but we still need to report exceptions (especially from atexit callbacks). + // Write directly to stderr like PyErr_FormatUnraisable. + if self + .state + .finalizing + .load(std::sync::atomic::Ordering::Acquire) + { + self.write_unraisable_to_stderr(&e, msg.as_deref(), &object); + return; + } + let sys_module = self.import("sys", 0).unwrap(); let unraisablehook = sys_module.get_attr("unraisablehook", self).unwrap(); @@ -568,6 +580,57 @@ impl VirtualMachine { } } + /// Write unraisable exception to stderr during finalization. + /// Similar to _PyErr_WriteUnraisableDefaultHook in CPython. + fn write_unraisable_to_stderr( + &self, + e: &PyBaseExceptionRef, + msg: Option<&str>, + object: &PyObjectRef, + ) { + // Get stderr once and reuse it + let stderr = crate::stdlib::sys::get_stderr(self).ok(); + + let write_to_stderr = |s: &str, stderr: &Option, vm: &VirtualMachine| { + if let Some(stderr) = stderr { + let _ = vm.call_method(stderr, "write", (s.to_owned(),)); + } else { + eprint!("{}", s); + } + }; + + // Format: "Exception ignored {msg} {object_repr}\n" + if let Some(msg) = msg { + write_to_stderr(&format!("Exception ignored {}", msg), &stderr, self); + } else { + write_to_stderr("Exception ignored in: ", &stderr, self); + } + + if let Ok(repr) = object.repr(self) { + write_to_stderr(&format!("{}\n", repr.as_str()), &stderr, self); + } else { + write_to_stderr("\n", &stderr, self); + } + + // Write exception type and message + let exc_type_name = e.class().name(); + if let Ok(exc_str) = e.as_object().str(self) { + let exc_str = exc_str.as_str(); + if exc_str.is_empty() { + write_to_stderr(&format!("{}\n", exc_type_name), &stderr, self); + } else { + write_to_stderr(&format!("{}: {}\n", exc_type_name, exc_str), &stderr, self); + } + } else { + write_to_stderr(&format!("{}\n", exc_type_name), &stderr, self); + } + + // Flush stderr to ensure output is visible + if let Some(ref stderr) = stderr { + let _ = self.call_method(stderr, "flush", ()); + } + } + #[inline(always)] pub fn run_frame(&self, frame: FrameRef) -> PyResult { match self.with_frame(frame, |f| f.run(self))? {