Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/source/reference-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,16 @@ Asynchronous file objects

.. autofunction:: wrap_file(file)

.. autoclass:: AsyncIOWrapper

This is the concrete wrapper type returned by :func:`open_file`,
:func:`wrap_file`, and :meth:`Path.open`. It does not provide every file
operation on every instance: available operations depend on the wrapped
synchronous file object, and the type hints model that relationship. If an
API only needs a small subset of the asynchronous file interface, accepting
a structural protocol can be more flexible than requiring
``AsyncIOWrapper`` specifically.

.. interface:: Asynchronous file interface

Trio's asynchronous file objects have an interface that
Expand Down
3 changes: 3 additions & 0 deletions newsfragments/3390.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:class:`trio.AsyncIOWrapper` is now public, so values returned by
:func:`trio.open_file`, :func:`trio.wrap_file`, and :meth:`trio.Path.open` can
be annotated without importing from the private ``trio._file_io`` module.
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.

Instances are normally obtained from :func:`trio.open_file`,
:func:`trio.wrap_file`, or :meth:`trio.Path.open`. The generic parameter
records the type of the wrapped synchronous file object, and the available
file operations follow that wrapped type.
"""

def __init__(self, file: FileT_co) -> None:
Expand Down
13 changes: 12 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,17 @@ 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:
# AsyncIOWrapper intentionally has a different static class surface:
# these names model methods supplied dynamically by __getattr__ on
# instances. The file-I/O tests separately verify that the dynamic
# methods and the TYPE_CHECKING declarations stay in sync.
expected_extra = _file_io._FILE_SYNC_ATTRS | _file_io._FILE_ASYNC_METHODS
assert missing == {"__getattr__"}
assert extra == expected_extra
missing.clear()
extra.clear()

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

Expand Down
19 changes: 19 additions & 0 deletions src/trio/_tests/type_tests/file_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Check the public types for Trio's asynchronous file wrappers."""

import io

import trio
from typing_extensions import assert_type


async def open_file_results(path: str) -> None:
assert_type(await trio.open_file(path), trio.AsyncIOWrapper[io.TextIOWrapper])
assert_type(
await trio.open_file(path, "rb"),
trio.AsyncIOWrapper[io.BufferedReader],
)


def wrap_file_results(text: io.StringIO, binary: io.BytesIO) -> None:
assert_type(trio.wrap_file(text), trio.AsyncIOWrapper[io.StringIO])
assert_type(trio.wrap_file(binary), trio.AsyncIOWrapper[io.BytesIO])
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