Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions docs/source/reference-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,8 @@ Asynchronous file objects

.. autofunction:: wrap_file(file)

.. autoclass:: AsyncIOWrapper()

.. interface:: Asynchronous file interface

Trio's asynchronous file objects have an interface that
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/3390.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The type returned by `trio.open_file`, `trio.wrap_file`, and `trio.Path.open`
is now publicly available as `trio.AsyncIOWrapper` for use in type annotations.
6 changes: 5 additions & 1 deletion src/trio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@
DTLSChannelStatistics as DTLSChannelStatistics,
DTLSEndpoint as DTLSEndpoint,
)
from ._file_io import open_file as open_file, wrap_file as wrap_file
from ._file_io import (
AsyncIOWrapper as AsyncIOWrapper,
open_file as open_file,
wrap_file as wrap_file,
)
from ._highlevel_generic import (
StapledStream as StapledStream,
aclose_forcefully as aclose_forcefully,
Expand Down
8 changes: 7 additions & 1 deletion src/trio/_file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import trio

from ._util import async_wraps
from ._util import async_wraps, final
from .abc import AsyncResource

if TYPE_CHECKING:
Expand Down Expand Up @@ -216,13 +216,19 @@ def close(self) -> None: ...

# FileT needs to be covariant for the protocol trick to work - the real IO types are effectively a
# subtype of the protocols.
@final
class AsyncIOWrapper(AsyncResource, Generic[FileT_co]):
"""A generic :class:`~io.IOBase` wrapper that implements the :term:`asynchronous
file object` interface. Wrapped methods that could block are executed in
:meth:`trio.to_thread.run_sync`.

All properties and methods defined in :mod:`~io` are exposed by this
wrapper, if they exist in the wrapped file object.

Obtain a wrapper using :func:`open_file`, :func:`wrap_file`, or
:meth:`Path.open`. For type annotations, parameterize this class with the
type of the wrapped synchronous file object, for example
``AsyncIOWrapper[io.TextIOWrapper]`` or ``AsyncIOWrapper[io.BytesIO]``.
"""

def __init__(self, file: FileT_co) -> None:
Expand Down
11 changes: 10 additions & 1 deletion src/trio/_tests/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import trio.testing
from trio._tests.pytest_plugin import RUN_SLOW, skip_if_optional_else_raise

from .. import _core, _util
from .. import _core, _file_io, _util
from .._core._tests.tutil import slow

if TYPE_CHECKING:
Expand Down Expand Up @@ -414,6 +414,15 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit
missing = runtime_names - static_names
extra = static_names - runtime_names

if class_ is trio.AsyncIOWrapper:
# The stubs describe methods and properties supplied by __getattr__
# on instances. Only detach is also defined on the runtime class.
missing.remove("__getattr__")
for name in (_file_io._FILE_SYNC_ATTRS | _file_io._FILE_ASYNC_METHODS) - {
"detach"
}:
extra.remove(name)

# using .remove() instead of .delete() to get an error in case they start not
# being missing

Expand Down
4 changes: 2 additions & 2 deletions src/trio/_tests/test_file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import pytest

import trio
from trio import _core, _file_io
from trio._file_io import _FILE_ASYNC_METHODS, _FILE_SYNC_ATTRS, AsyncIOWrapper
from trio import AsyncIOWrapper, _core, _file_io
from trio._file_io import _FILE_ASYNC_METHODS, _FILE_SYNC_ATTRS

if TYPE_CHECKING:
import pathlib
Expand Down
29 changes: 29 additions & 0 deletions src/trio/_tests/type_tests/file_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""File wrappers can be annotated using the public generic type."""

import io

import trio
from trio import AsyncIOWrapper
from typing_extensions import assert_type


async def open_results(path: str) -> None:
async with await trio.open_file(path) as text_file:
assert_type(text_file, AsyncIOWrapper[io.TextIOWrapper])
assert_type(await text_file.read(), str)
async with await trio.open_file(path, "rb") as binary_file:
assert_type(binary_file, AsyncIOWrapper[io.BufferedReader])
assert_type(await binary_file.read(), bytes)
async with await trio.open_file(path, "rb", buffering=0) as raw_file:
assert_type(raw_file, AsyncIOWrapper[io.FileIO])


async def wrapped_results(text: io.StringIO, binary: io.BytesIO) -> None:
async with trio.wrap_file(text) as text_file:
assert_type(text_file, AsyncIOWrapper[io.StringIO])
assert_type(text_file.wrapped, io.StringIO)
assert_type(await text_file.read(), str)
async with trio.wrap_file(binary) as binary_file:
assert_type(binary_file, AsyncIOWrapper[io.BytesIO])
assert_type(binary_file.wrapped, io.BytesIO)
assert_type(await binary_file.read(), bytes)
2 changes: 1 addition & 1 deletion src/trio/_tests/type_tests/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import IO, Any, BinaryIO

import trio
from trio._file_io import AsyncIOWrapper
from trio import AsyncIOWrapper
from typing_extensions import assert_type


Expand Down