Handle reentrant detach in TextIOWrapper.close() (#7277)

When a custom `closed` property on the underlying buffer calls
`detach()` during the `file_closed()` check in `close()`, the
wrapper's internal buffer becomes None. Subsequent flush/close
operations then fail with AttributeError on NoneType.

Add a guard after the `file_closed()` check to detect if the buffer
was detached reentrantly, and return early in that case (detach has
already flushed the stream).

This mirrors the fix applied in CPython
(https://github.com/python/cpython/issues/142594\).
This commit is contained in:
Jiseok CHOI authored and GitHub committed 2026-03-01 10:44:13 +09:00
1 parent bef46f17d5
commit be39a67f27
2 files changed
+7 -1

No files matched your search

-1
View File
@@ -4211,7 +4211,6 @@ class CTextIOWrapperTest(TextIOWrapperTest):
self.assertEqual([b"abcdef", b"middle", b"g"*chunk_size],
buf._write_stack)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'NoneType' object has no attribute 'closed'
def test_issue142594(self):
wrapper = None
detached = False
+7
View File
@@ -3793,6 +3793,13 @@ mod _io {
if file_closed(&buffer, vm)? {
return Ok(());
}
// https://github.com/python/cpython/issues/142594
// The file_closed() check above may have triggered a reentrant
// call to detach() via a custom `closed` property.
// If so, the buffer is now detached and we should return early.
if vm.is_none(&zelf.lock(vm)?.buffer) {
return Ok(());
}
if zelf.finalizing.load(Ordering::Relaxed) {
// _dealloc_warn: delegate to buffer._dealloc_warn(source)
let _ = vm.call_method(&buffer, "_dealloc_warn", (zelf.as_object().to_owned(),));