Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
generator is borrowed
  • Loading branch information
youknowone committed Feb 1, 2026
commit 7258a4ae926a107bc2c4076b5b0e83a2a25c0e90
2 changes: 0 additions & 2 deletions Lib/test/test_asyncgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,6 @@ async def run():
fut.cancel()
self.loop.run_until_complete(asyncio.sleep(0.01))

@unittest.expectedFailure # TODO: RUSTPYTHON; gc_collect doesn't finalize async generators
def test_async_gen_asyncio_gc_aclose_09(self):
DONE = 0

Expand Down Expand Up @@ -1513,7 +1512,6 @@ async def main():
self.assertIn('an error occurred during closing of asynchronous generator',
message['message'])

@unittest.expectedFailure # TODO: RUSTPYTHON; gc_collect doesn't finalize async generators, different cleanup path
def test_async_gen_asyncio_shutdown_exception_02(self):
messages = []

Expand Down
8 changes: 0 additions & 8 deletions Lib/test/test_asyncio/test_free_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,6 @@ def tearDown(self):
def factory(self, loop, coro, **kwargs):
return asyncio.tasks._PyTask(coro, loop=loop, **kwargs)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC weak reference timing issue
def test_task_different_thread_finalized(self):
return super().test_task_different_thread_finalized()

@unittest.skip("TODO: RUSTPYTHON; hangs - Python _current_tasks dict not thread-safe")
def test_all_tasks_race(self):
return super().test_all_tasks_race()
Expand Down Expand Up @@ -228,10 +224,6 @@ class TestEagerPyFreeThreading(TestPyFreeThreading):
def factory(self, loop, coro, eager_start=True, **kwargs):
return asyncio.tasks._PyTask(coro, loop=loop, **kwargs, eager_start=eager_start)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC weak reference timing issue
def test_task_different_thread_finalized(self):
return super().test_task_different_thread_finalized()

@unittest.skip("TODO: RUSTPYTHON; hangs - Python _current_tasks dict not thread-safe")
def test_all_tasks_race(self):
return super().test_all_tasks_race()
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_asyncio/test_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,6 @@ def test_eof_feed_when_closing_writer(self):

self.assertEqual(messages, [])

@unittest.expectedFailure # TODO: RUSTPYTHON; GC finalization timing issue
def test_unclosed_resource_warnings(self):
async def inner(httpd):
rd, wr = await asyncio.open_connection(*httpd.address)
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_concurrent_futures/test_as_completed.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ def test_duplicate_futures(self):
]
self.assertEqual(len(completed), 1)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC weak reference not collected
def test_free_reference_yielded_future(self):
# Issue #14406: Generator should not keep references
# to finished futures.
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_concurrent_futures/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,9 @@ def _test(self, test_class):

self.assertEqual(_resource_tracker._exitcode, 0)

@unittest.expectedFailure # TODO: RUSTPYTHON; resource tracker exit code mismatch
def test_spawn(self):
self._test(ProcessPoolSpawnFailingInitializerTest)

@unittest.expectedFailure # TODO: RUSTPYTHON; resource tracker exit code mismatch
@support.skip_if_sanitizer("TSAN doesn't support threads after fork", thread=True)
def test_forkserver(self):
self._test(ProcessPoolForkserverFailingInitializerTest)
Expand Down
8 changes: 4 additions & 4 deletions Lib/test/test_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,15 +837,15 @@ class C(object):
v[x] = y
self.assertNotIn(x, u)

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect

def test_copy_weakkeydict(self):
self._check_copy_weakdict(weakref.WeakKeyDictionary)

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect

def test_copy_weakvaluedict(self):
self._check_copy_weakdict(weakref.WeakValueDictionary)

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect

def test_deepcopy_weakkeydict(self):
class C(object):
def __init__(self, i):
Expand All @@ -866,7 +866,7 @@ def __init__(self, i):
support.gc_collect() # For PyPy or other GCs.
self.assertEqual(len(v), 1)

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect

def test_deepcopy_weakvaluedict(self):
class C(object):
def __init__(self, i):
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,6 @@ def do_close(g):
g.close()
self._check_generator_cleanup_exc_state(do_close)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC generator cleanup timing
def test_generator_del_cleanup_exc_state(self):
def do_del(g):
g = None
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -3282,7 +3282,6 @@ def test_select_unbuffered(self):
finally:
p.wait()

@unittest.expectedFailure # TODO: RUSTPYTHON; GC Popen.__del__ timing
def test_zombie_fast_process_del(self):
# Issue #12650: on Unix, if Popen.__del__() was called before the
# process exited, it wouldn't be added to subprocess._active, and would
Expand All @@ -3307,7 +3306,6 @@ def test_zombie_fast_process_del(self):
# check that p is in the active processes list
self.assertIn(ident, [id(o) for o in subprocess._active])

@unittest.expectedFailure # TODO: RUSTPYTHON; GC Popen.__del__ timing
def test_leak_fast_process_del_killed(self):
# Issue #12650: on Unix, if Popen.__del__() was called before the
# process exited, and the process got killed by a signal, it would never
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_unittest/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,9 @@ def test_nothing(self):
self.assertEqual(suite._tests, [None])
self.assertIsNone(wref())

@unittest.expectedFailure # TODO: RUSTPYTHON; GC test not collected after run
def test_garbage_collect_test_after_run_BaseTestSuite(self):
self.assert_garbage_collect_test_after_run(unittest.BaseTestSuite)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC test not collected after run
def test_garbage_collect_test_after_run_TestSuite(self):
self.assert_garbage_collect_test_after_run(unittest.TestSuite)

Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_weakref.py
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,6 @@ def test_weak_keyed_len_race(self):
def test_weak_valued_len_race(self):
self.check_len_race(weakref.WeakValueDictionary, lambda k: (1, k))

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect
def test_weak_values(self):
#
# This exercises d.copy(), d.items(), d[], del d[], len(d).
Expand Down Expand Up @@ -1401,7 +1400,6 @@ def test_weak_values(self):
gc_collect() # For PyPy or other GCs.
self.assertRaises(KeyError, dict.__getitem__, 2)

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect
def test_weak_keys(self):
#
# This exercises d.copy(), d.items(), d[] = v, d[], del d[],
Expand Down Expand Up @@ -1767,7 +1765,6 @@ def test_weak_valued_dict_update(self):
self.assertEqual(list(d.keys()), [kw])
self.assertEqual(d[kw], o)

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect
def test_weak_valued_union_operators(self):
a = C()
b = C()
Expand Down Expand Up @@ -1820,7 +1817,6 @@ def test_weak_keyed_delitem(self):
self.assertEqual(len(d), 1)
self.assertEqual(list(d.keys()), [o2])

@unittest.expectedFailure # TODO: RUSTPYTHON; weakref callback not fired immediately by gc_collect
def test_weak_keyed_union_operators(self):
o1 = C()
o2 = C()
Expand Down
3 changes: 0 additions & 3 deletions Lib/test/test_weakset.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ def test_contains(self):
support.gc_collect() # For PyPy or other GCs.
self.assertNotIn(ustr('F'), self.fs)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC weak reference not collected
def test_union(self):
u = self.s.union(self.items2)
for c in self.letters:
Expand All @@ -92,7 +91,6 @@ def test_or(self):
self.assertEqual(self.s | set(self.items2), i)
self.assertEqual(self.s | frozenset(self.items2), i)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC weak reference not collected
def test_intersection(self):
s = WeakSet(self.letters)
i = s.intersection(self.items2)
Expand Down Expand Up @@ -130,7 +128,6 @@ def test_sub(self):
self.assertEqual(self.s - set(self.items2), i)
self.assertEqual(self.s - frozenset(self.items2), i)

@unittest.expectedFailure # TODO: RUSTPYTHON; GC weak reference not collected
def test_symmetric_difference(self):
i = self.s.symmetric_difference(self.items2)
for c in self.letters:
Expand Down
10 changes: 7 additions & 3 deletions crates/vm/src/builtins/asyncgenerator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,7 @@ impl PyAsyncGenAThrow {
) -> PyResult {
match self.state.load() {
AwaitableState::Closed => {
return Err(
vm.new_runtime_error("cannot reuse already awaited aclose()/athrow()")
);
return Err(vm.new_runtime_error("cannot reuse already awaited aclose()/athrow()"));
}
AwaitableState::Init => {
if self.ag.running_async.load() {
Expand Down Expand Up @@ -816,6 +814,12 @@ impl Destructor for PyAsyncGen {
}
}

impl Drop for PyAsyncGen {
fn drop(&mut self) {
self.inner.frame().clear_generator();
}
}

pub fn init(ctx: &Context) {
PyAsyncGen::extend_class(ctx, ctx.types.async_generator);
PyAsyncGenASend::extend_class(ctx, ctx.types.async_generator_asend);
Expand Down
6 changes: 6 additions & 0 deletions crates/vm/src/builtins/coroutine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ impl IterNext for PyCoroutineWrapper {
}
}

impl Drop for PyCoroutine {
fn drop(&mut self) {
self.inner.frame().clear_generator();
}
}

pub fn init(ctx: &Context) {
PyCoroutine::extend_class(ctx, ctx.types.coroutine_type);
PyCoroutineWrapper::extend_class(ctx, ctx.types.coroutine_wrapper_type);
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl Frame {
impl Py<Frame> {
#[pygetset]
fn f_generator(&self) -> Option<PyObjectRef> {
self.generator.lock().clone()
self.generator.to_owned()
}

#[pygetset]
Expand Down
6 changes: 3 additions & 3 deletions crates/vm/src/builtins/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,19 +531,19 @@ impl Py<PyFunction> {
(true, false) => {
let obj = PyGenerator::new(frame.clone(), self.__name__(), self.__qualname__())
.into_pyobject(vm);
frame.set_generator(obj.clone());
frame.set_generator(&obj);
Ok(obj)
}
(false, true) => {
let obj = PyCoroutine::new(frame.clone(), self.__name__(), self.__qualname__())
.into_pyobject(vm);
frame.set_generator(obj.clone());
frame.set_generator(&obj);
Ok(obj)
}
(true, true) => {
let obj = PyAsyncGen::new(frame.clone(), self.__name__(), self.__qualname__())
.into_pyobject(vm);
frame.set_generator(obj.clone());
frame.set_generator(&obj);
Ok(obj)
}
(false, false) => vm.run_frame(frame),
Expand Down
6 changes: 6 additions & 0 deletions crates/vm/src/builtins/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ impl IterNext for PyGenerator {
}
}

impl Drop for PyGenerator {
fn drop(&mut self) {
self.inner.frame().clear_generator();
}
}

pub fn init(ctx: &Context) {
PyGenerator::extend_class(ctx, ctx.types.generator_type);
}
22 changes: 16 additions & 6 deletions crates/vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
coroutine::Coro,
exceptions::ExceptionCtor,
function::{ArgMapping, Either, FuncArgs},
object::PyAtomicBorrow,
object::{Traverse, TraverseFn},
protocol::{PyIter, PyIterReturn},
scope::Scope,
Expand Down Expand Up @@ -85,8 +86,10 @@ pub struct Frame {
pub trace_lines: PyMutex<bool>,
pub trace_opcodes: PyMutex<bool>,
pub temporary_refs: PyMutex<Vec<PyObjectRef>>,
/// Back-reference to owning generator/coroutine/async generator
pub generator: PyMutex<Option<PyObjectRef>>,
/// Back-reference to owning generator/coroutine/async generator.
/// Borrowed reference (not ref-counted) to avoid Generator↔Frame cycle.
/// Cleared by the generator's Drop impl.
pub generator: PyAtomicBorrow,
}

impl PyPayload for Frame {
Expand Down Expand Up @@ -114,7 +117,7 @@ unsafe impl Traverse for Frame {
self.trace.traverse(tracer_fn);
self.state.traverse(tracer_fn);
self.temporary_refs.traverse(tracer_fn);
self.generator.traverse(tracer_fn);
// generator is a borrowed reference, not traversed
}
}

Expand Down Expand Up @@ -175,12 +178,19 @@ impl Frame {
trace_lines: PyMutex::new(true),
trace_opcodes: PyMutex::new(false),
temporary_refs: PyMutex::new(vec![]),
generator: PyMutex::new(None),
generator: PyAtomicBorrow::new(),
}
}

pub fn set_generator(&self, generator: PyObjectRef) {
*self.generator.lock() = Some(generator);
/// Store a borrowed back-reference to the owning generator/coroutine.
/// The caller must ensure the generator outlives the frame.
pub fn set_generator(&self, generator: &PyObject) {
self.generator.store(generator);
}

/// Clear the generator back-reference. Called when the generator is finalized.
pub fn clear_generator(&self) {
self.generator.clear();
}

pub fn current_location(&self) -> SourceLocation {
Expand Down
Loading