Skip to content

Commit e411fac

Browse files
author
nightcityblade
committed
test: cover documented wrap_file lifecycle behavior
1 parent 3e94e40 commit e411fac

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

newsfragments/3379.doc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Clarified :func:`trio.wrap_file` ownership and lifecycle behavior, including that closing the async wrapper closes the underlying file, garbage collection does not close it automatically, and the original synchronous file object should not be used concurrently.

src/trio/_file_io.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,13 +498,13 @@ def wrap_file(file: FileT) -> AsyncIOWrapper[FileT]:
498498
:meth:`~trio.abc.AsyncResource.aclose` or ``async with``) will close the
499499
underlying file. However, if the wrapper is garbage collected without
500500
being explicitly closed, the underlying file is *not* closed
501-
automatically you should always close it explicitly.
501+
automatically, so you should always close it explicitly.
502502
503503
The original synchronous file object should not be used directly while
504504
the wrapper exists, as the wrapper may call file methods in a worker
505505
thread, and concurrent access from multiple threads is not safe for most
506506
file objects. If you need synchronous access, use the
507-
:attr:`~AsyncIOWrapper.wrapped` attribute.
507+
:attr:`~trio._file_io.AsyncIOWrapper.wrapped` attribute.
508508
509509
Example::
510510

src/trio/_tests/test_file_io.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import gc
34
import importlib
45
import io
56
import os
@@ -230,6 +231,35 @@ async def test_open_context_manager(path: pathlib.Path) -> None:
230231
assert f.closed
231232

232233

234+
async def test_wrap_file_aclose_closes_underlying_file() -> None:
235+
wrapped = io.StringIO("test")
236+
async_file = trio.wrap_file(wrapped)
237+
238+
await async_file.aclose()
239+
240+
assert wrapped.closed
241+
242+
243+
async def test_wrap_file_context_manager_closes_underlying_file() -> None:
244+
wrapped = io.StringIO("test")
245+
246+
async with trio.wrap_file(wrapped) as async_file:
247+
assert async_file.wrapped is wrapped
248+
assert not wrapped.closed
249+
250+
assert wrapped.closed
251+
252+
253+
def test_wrap_file_garbage_collection_does_not_close_underlying_file() -> None:
254+
wrapped = io.StringIO("test")
255+
trio.wrap_file(wrapped)
256+
257+
gc.collect()
258+
259+
assert not wrapped.closed
260+
wrapped.close()
261+
262+
233263
async def test_async_iter() -> None:
234264
async_file = trio.wrap_file(io.StringIO("test\nfoo\nbar"))
235265
expected = list(async_file.wrapped)

0 commit comments

Comments
 (0)