diff --git a/Lib/test/_test_atexit.py b/Lib/test/_test_atexit.py index db4edd72c51..2e961d6a485 100644 --- a/Lib/test/_test_atexit.py +++ b/Lib/test/_test_atexit.py @@ -47,7 +47,6 @@ def func2(*args, **kwargs): ('func2', (), {}), ('func1', (1, 2), {})]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_badargs(self): def func(): pass @@ -55,14 +54,12 @@ def func(): # func() has no parameter, but it's called with 2 parameters self.assert_raises_unraisable(TypeError, func, 1 ,2) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_raise(self): def raise_type_error(): raise TypeError self.assert_raises_unraisable(TypeError, raise_type_error) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_raise_unnormalized(self): # bpo-10756: Make sure that an unnormalized exception is handled # properly. @@ -71,7 +68,6 @@ def div_zero(): self.assert_raises_unraisable(ZeroDivisionError, div_zero) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_exit(self): self.assert_raises_unraisable(SystemExit, sys.exit) @@ -122,7 +118,6 @@ def test_bound_methods(self): atexit._run_exitfuncs() self.assertEqual(l, [5]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_atexit_with_unregistered_function(self): # See bpo-46025 for more info def func(): @@ -140,7 +135,6 @@ def func(): finally: atexit.unregister(func) - @unittest.skip("TODO: RUSTPYTHON; Hangs") def test_eq_unregister_clear(self): # Issue #112127: callback's __eq__ may call unregister or _clear class Evil: @@ -154,7 +148,6 @@ def __eq__(self, other): atexit.unregister(Evil()) atexit._clear() - @unittest.skip("TODO: RUSTPYTHON; Hangs") def test_eq_unregister(self): # Issue #112127: callback's __eq__ may call unregister def f1(): diff --git a/crates/vm/src/stdlib/atexit.rs b/crates/vm/src/stdlib/atexit.rs index 338fae3b2b7..638927fe90f 100644 --- a/crates/vm/src/stdlib/atexit.rs +++ b/crates/vm/src/stdlib/atexit.rs @@ -7,7 +7,11 @@ mod atexit { #[pyfunction] fn register(func: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyObjectRef { - vm.state.atexit_funcs.lock().push((func.clone(), args)); + // Callbacks go in LIFO order (insert at front) + vm.state + .atexit_funcs + .lock() + .insert(0, Box::new((func.clone(), args))); func } @@ -18,27 +22,62 @@ mod atexit { #[pyfunction] fn unregister(func: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let mut funcs = vm.state.atexit_funcs.lock(); - - let mut i = 0; - while i < funcs.len() { - if vm.bool_eq(&funcs[i].0, &func)? { - funcs.remove(i); - } else { - i += 1; + // Iterate backward (oldest to newest in LIFO list). + // Release the lock during comparison so __eq__ can call atexit functions. + let mut i = { + let funcs = vm.state.atexit_funcs.lock(); + funcs.len() as isize - 1 + }; + while i >= 0 { + let (cb, entry_ptr) = { + let funcs = vm.state.atexit_funcs.lock(); + if i as usize >= funcs.len() { + i = funcs.len() as isize; + i -= 1; + continue; + } + let entry = &funcs[i as usize]; + (entry.0.clone(), &**entry as *const (PyObjectRef, FuncArgs)) + }; + // Lock released: __eq__ can safely call atexit functions + let eq = vm.bool_eq(&func, &cb)?; + if eq { + // The entry may have moved during __eq__. Search backward by identity. + let mut funcs = vm.state.atexit_funcs.lock(); + let mut j = (funcs.len() as isize - 1).min(i); + while j >= 0 { + if core::ptr::eq(&**funcs.get(j as usize).unwrap(), entry_ptr) { + funcs.remove(j as usize); + i = j; + break; + } + j -= 1; + } } + { + let funcs = vm.state.atexit_funcs.lock(); + if i as usize >= funcs.len() { + i = funcs.len() as isize; + } + } + i -= 1; } - Ok(()) } #[pyfunction] pub fn _run_exitfuncs(vm: &VirtualMachine) { let funcs: Vec<_> = core::mem::take(&mut *vm.state.atexit_funcs.lock()); - for (func, args) in funcs.into_iter().rev() { + // Callbacks stored in LIFO order, iterate forward + for entry in funcs.into_iter() { + let (func, args) = *entry; if let Err(e) = func.call(args, vm) { let exit = e.fast_isinstance(vm.ctx.exceptions.system_exit); - vm.run_unraisable(e, Some("Error in atexit._run_exitfuncs".to_owned()), func); + let msg = func + .repr(vm) + .ok() + .map(|r| format!("Exception ignored in atexit callback {}", r.as_wtf8())); + vm.run_unraisable(e, msg, vm.ctx.none()); if exit { break; } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 88f73a7c963..05210eb09d7 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -585,7 +585,7 @@ pub struct PyGlobalState { pub stacksize: AtomicCell, pub thread_count: AtomicCell, pub hash_secret: HashSecret, - pub atexit_funcs: PyMutex>, + pub atexit_funcs: PyMutex>>, pub codec_registry: CodecsRegistry, pub finalizing: AtomicBool, pub warnings: WarningsState,