diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8022123..c92eef6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,6 +32,10 @@ jobs: if: matrix.python-version == '3.13' && runner.os == 'Linux' run: "uv run --group tox tox -e lint" + - name: "Typecheck" + if: matrix.python-version == '3.13' && runner.os == 'Linux' + run: "uv run --group tox tox -e typecheck" + - name: "Upload coverage data" uses: "actions/upload-artifact@v4" with: diff --git a/CHANGELOG.md b/CHANGELOG.md index b327ffb..72349c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 25.1.0 (UNRELEASED) +- Added type stubs from [typeshed](https://github.com/python/typeshed) - Switch to [uv](https://docs.astral.sh/uv/) + add Python v3.14 support. ([#219](https://github.com/Tinche/aiofiles/pull/219)) - Add `ruff` formatter and linter. diff --git a/Justfile b/Justfile index ccae1b8..e2d49b4 100644 --- a/Justfile +++ b/Justfile @@ -15,5 +15,8 @@ format: lint: format {{ run_prefix }}ruff check --fix {{ code_dirs }} +typecheck: + {{ run_prefix }}mypy src + test: - {{ run_prefix }}pytest -x --ff {{ tests_dir }} \ No newline at end of file + {{ run_prefix }}pytest -x --ff {{ tests_dir }} diff --git a/README.md b/README.md index 3aa8657..7af5114 100644 --- a/README.md +++ b/README.md @@ -181,3 +181,8 @@ async def test_stuff(): Contributions are very welcome. Tests can be run with `tox`, please ensure the coverage at least stays the same before you submit a pull request. + +If your change touches the public API or any function/class signatures, please +also update the type stub files (`*.pyi`) to match the runtime code. Please +also run a type check locally (e.g., `tox -e typecheck`) before you submit a +pull request diff --git a/pyproject.toml b/pyproject.toml index 3efac5f..294fd84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ fixable = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] -"src/**/*.py" = [ +"src/**/*.{py,pyi}" = [ "TID252", # https://docs.astral.sh/ruff/rules/relative-imports/ ] "tests/**/*.py" = [ diff --git a/src/aiofiles/__init__.pyi b/src/aiofiles/__init__.pyi new file mode 100644 index 0000000..5591efa --- /dev/null +++ b/src/aiofiles/__init__.pyi @@ -0,0 +1,33 @@ +from . import tempfile as tempfile +from .threadpool import ( + open as open, +) +from .threadpool import ( + stderr as stderr, +) +from .threadpool import ( + stderr_bytes as stderr_bytes, +) +from .threadpool import ( + stdin as stdin, +) +from .threadpool import ( + stdin_bytes as stdin_bytes, +) +from .threadpool import ( + stdout as stdout, +) +from .threadpool import ( + stdout_bytes as stdout_bytes, +) + +__all__ = [ + "open", + "tempfile", + "stdin", + "stdout", + "stderr", + "stdin_bytes", + "stdout_bytes", + "stderr_bytes", +] diff --git a/src/aiofiles/base.pyi b/src/aiofiles/base.pyi new file mode 100644 index 0000000..cff61d6 --- /dev/null +++ b/src/aiofiles/base.pyi @@ -0,0 +1,41 @@ +from asyncio.events import AbstractEventLoop +from collections.abc import Awaitable, Callable, Generator +from concurrent.futures import Executor +from contextlib import AbstractAsyncContextManager +from types import TracebackType +from typing import Any, BinaryIO, Generic, TextIO, TypeVar + +from typing_extensions import Self + +_T = TypeVar("_T") +_V_co = TypeVar("_V_co", covariant=True) + +class AsyncBase(Generic[_T]): + def __init__( + self, + file: TextIO | BinaryIO | None, + loop: AbstractEventLoop | None, + executor: Executor | None, + ) -> None: ... + def __aiter__(self) -> Self: ... + async def __anext__(self) -> _T: ... + +class AsyncIndirectBase(AsyncBase[_T]): + def __init__( + self, + name: str, + loop: AbstractEventLoop | None, + executor: Executor | None, + indirect: Callable[[], TextIO | BinaryIO], + ) -> None: ... + +class AiofilesContextManager(Awaitable[_V_co], AbstractAsyncContextManager[_V_co]): + def __init__(self, coro: Awaitable[_V_co]) -> None: ... + def __await__(self) -> Generator[Any, Any, _V_co]: ... + async def __aenter__(self) -> _V_co: ... + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: ... diff --git a/src/aiofiles/os.pyi b/src/aiofiles/os.pyi new file mode 100644 index 0000000..78b7847 --- /dev/null +++ b/src/aiofiles/os.pyi @@ -0,0 +1,219 @@ +import sys +from asyncio.events import AbstractEventLoop +from collections.abc import Sequence +from concurrent.futures import Executor +from os import _ScandirIterator, stat_result +from typing import AnyStr, overload + +from _typeshed import ( + BytesPath, + FileDescriptorOrPath, + GenericPath, + ReadableBuffer, + StrOrBytesPath, + StrPath, +) + +from aiofiles import ospath +from aiofiles.ospath import wrap as wrap + +__all__ = [ + "path", + "stat", + "rename", + "renames", + "replace", + "remove", + "unlink", + "mkdir", + "makedirs", + "rmdir", + "removedirs", + "link", + "symlink", + "readlink", + "listdir", + "scandir", + "access", + "wrap", + "getcwd", +] + +path = ospath + +async def stat( + path: FileDescriptorOrPath, + *, + dir_fd: int | None = None, + follow_symlinks: bool = True, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> stat_result: ... +async def rename( + src: StrOrBytesPath, + dst: StrOrBytesPath, + *, + src_dir_fd: int | None = None, + dst_dir_fd: int | None = None, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def renames( + old: StrOrBytesPath, + new: StrOrBytesPath, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def replace( + src: StrOrBytesPath, + dst: StrOrBytesPath, + *, + src_dir_fd: int | None = None, + dst_dir_fd: int | None = None, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def remove( + path: StrOrBytesPath, + *, + dir_fd: int | None = None, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def unlink( + path: StrOrBytesPath, + *, + dir_fd: int | None = ..., + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def mkdir( + path: StrOrBytesPath, + mode: int = 511, + *, + dir_fd: int | None = None, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def makedirs( + name: StrOrBytesPath, + mode: int = 511, + exist_ok: bool = False, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def link( + src: StrOrBytesPath, + dst: StrOrBytesPath, + *, + src_dir_fd: int | None = ..., + dst_dir_fd: int | None = ..., + follow_symlinks: bool = ..., + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def symlink( + src: StrOrBytesPath, + dst: StrOrBytesPath, + target_is_directory: bool = ..., + *, + dir_fd: int | None = ..., + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def readlink( + path: AnyStr, + *, + dir_fd: int | None = ..., + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> AnyStr: ... +async def rmdir( + path: StrOrBytesPath, + *, + dir_fd: int | None = None, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +async def removedirs( + name: StrOrBytesPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> None: ... +@overload +async def scandir( + path: None = None, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> _ScandirIterator[str]: ... +@overload +async def scandir( + path: int, *, loop: AbstractEventLoop | None = ..., executor: Executor | None = ... +) -> _ScandirIterator[str]: ... +@overload +async def scandir( + path: GenericPath[AnyStr], + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> _ScandirIterator[AnyStr]: ... +@overload +async def listdir( + path: StrPath | None, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> list[str]: ... +@overload +async def listdir( + path: BytesPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> list[bytes]: ... +@overload +async def listdir( + path: int, *, loop: AbstractEventLoop | None = ..., executor: Executor | None = ... +) -> list[str]: ... +async def access( + path: FileDescriptorOrPath, + mode: int, + *, + dir_fd: int | None = None, + effective_ids: bool = False, + follow_symlinks: bool = True, +) -> bool: ... +async def getcwd() -> str: ... + +if sys.platform != "win32": + from os import statvfs_result + + @overload + async def sendfile( + out_fd: int, + in_fd: int, + offset: int | None, + count: int, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., + ) -> int: ... + @overload + async def sendfile( + out_fd: int, + in_fd: int, + offset: int, + count: int, + headers: Sequence[ReadableBuffer] = ..., + trailers: Sequence[ReadableBuffer] = ..., + flags: int = ..., + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., + ) -> int: ... # FreeBSD and Mac OS X only + async def statvfs(path: FileDescriptorOrPath) -> statvfs_result: ... # Unix only + + __all__ += ["statvfs", "sendfile"] diff --git a/src/aiofiles/ospath.pyi b/src/aiofiles/ospath.pyi new file mode 100644 index 0000000..38ee68e --- /dev/null +++ b/src/aiofiles/ospath.pyi @@ -0,0 +1,70 @@ +from asyncio.events import AbstractEventLoop +from collections.abc import Awaitable, Callable +from concurrent.futures import Executor +from os import PathLike +from typing import AnyStr, TypeVar + +from _typeshed import FileDescriptorOrPath + +_R = TypeVar("_R") + +def wrap(func: Callable[..., _R]) -> Callable[..., Awaitable[_R]]: ... +async def exists( + path: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> bool: ... +async def isfile( + path: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> bool: ... +async def isdir( + s: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> bool: ... +async def islink(path: FileDescriptorOrPath) -> bool: ... +async def ismount(path: FileDescriptorOrPath) -> bool: ... +async def getsize( + filename: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> int: ... +async def getmtime( + filename: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> float: ... +async def getatime( + filename: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> float: ... +async def getctime( + filename: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> float: ... +async def samefile( + f1: FileDescriptorOrPath, + f2: FileDescriptorOrPath, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> bool: ... +async def sameopenfile( + fp1: int, + fp2: int, + *, + loop: AbstractEventLoop | None = ..., + executor: Executor | None = ..., +) -> bool: ... +async def abspath(path: PathLike[AnyStr] | AnyStr) -> AnyStr: ... diff --git a/src/aiofiles/py.typed b/src/aiofiles/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/aiofiles/tempfile/__init__.pyi b/src/aiofiles/tempfile/__init__.pyi new file mode 100644 index 0000000..b7442ad --- /dev/null +++ b/src/aiofiles/tempfile/__init__.pyi @@ -0,0 +1,330 @@ +import sys +from asyncio import AbstractEventLoop +from concurrent.futures import Executor +from typing import AnyStr, Literal, overload + +from _typeshed import ( + BytesPath, + OpenBinaryMode, + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, + OpenTextMode, + StrOrBytesPath, + StrPath, +) + +from ..base import AiofilesContextManager +from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO +from ..threadpool.text import AsyncTextIOWrapper + +# Text mode: always returns AsyncTextIOWrapper +@overload +def TemporaryFile( + mode: OpenTextMode, + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncTextIOWrapper]: ... + +# Unbuffered binary: returns a FileIO +@overload +def TemporaryFile( + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncFileIO]: ... + +# Buffered binary reading/updating: AsyncBufferedReader +@overload +def TemporaryFile( + mode: OpenBinaryModeReading | OpenBinaryModeUpdating = "w+b", + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedReader]: ... + +# Buffered binary writing: AsyncBufferedIOBase +@overload +def TemporaryFile( + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedIOBase]: ... + +# 3.12 added `delete_on_close` +if sys.version_info >= (3, 12): + # Text mode: always returns AsyncTextIOWrapper + @overload + def NamedTemporaryFile( + mode: OpenTextMode, + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + delete_on_close: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncTextIOWrapper]: ... + + # Unbuffered binary: returns a FileIO + @overload + def NamedTemporaryFile( + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + delete_on_close: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncFileIO]: ... + + # Buffered binary reading/updating: AsyncBufferedReader + @overload + def NamedTemporaryFile( + mode: OpenBinaryModeReading | OpenBinaryModeUpdating = "w+b", + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + delete_on_close: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncBufferedReader]: ... + + # Buffered binary writing: AsyncBufferedIOBase + @overload + def NamedTemporaryFile( + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + delete_on_close: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncBufferedIOBase]: ... + +else: + # Text mode: always returns AsyncTextIOWrapper + @overload + def NamedTemporaryFile( + mode: OpenTextMode, + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncTextIOWrapper]: ... + + # Unbuffered binary: returns a FileIO + @overload + def NamedTemporaryFile( + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncFileIO]: ... + + # Buffered binary reading/updating: AsyncBufferedReader + @overload + def NamedTemporaryFile( + mode: OpenBinaryModeReading | OpenBinaryModeUpdating = "w+b", + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncBufferedReader]: ... + + # Buffered binary writing: AsyncBufferedIOBase + @overload + def NamedTemporaryFile( + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + delete: bool = True, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, + ) -> AiofilesContextManager[AsyncBufferedIOBase]: ... + +# Text mode: always returns AsyncTextIOWrapper +@overload +def SpooledTemporaryFile( + max_size: int = 0, + *, + mode: OpenTextMode, + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncTextIOWrapper]: ... +@overload +def SpooledTemporaryFile( + max_size: int, + mode: OpenTextMode, + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncTextIOWrapper]: ... + +# Unbuffered binary: returns a FileIO +@overload +def SpooledTemporaryFile( + max_size: int = 0, + mode: OpenBinaryMode = "w+b", + *, + buffering: Literal[0], + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncFileIO]: ... +@overload +def SpooledTemporaryFile( + max_size: int, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncFileIO]: ... + +# Buffered binary reading/updating: AsyncBufferedReader +@overload +def SpooledTemporaryFile( + max_size: int = 0, + mode: OpenBinaryModeReading | OpenBinaryModeUpdating = "w+b", + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedReader]: ... + +# Buffered binary writing: AsyncBufferedIOBase +@overload +def SpooledTemporaryFile( + max_size: int = 0, + *, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedIOBase]: ... +@overload +def SpooledTemporaryFile( + max_size: int, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + newline: None = None, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: StrOrBytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedIOBase]: ... +@overload +def TemporaryDirectory( + suffix: str | None = None, + prefix: str | None = None, + dir: StrPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManagerTempDir: ... +@overload +def TemporaryDirectory( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: BytesPath | None = None, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManagerTempDir: ... + +class AiofilesContextManagerTempDir(AiofilesContextManager[str]): + async def __aenter__(self) -> str: ... + +__all__ = [ + "NamedTemporaryFile", + "TemporaryFile", + "SpooledTemporaryFile", + "TemporaryDirectory", +] diff --git a/src/aiofiles/tempfile/temptypes.pyi b/src/aiofiles/tempfile/temptypes.pyi new file mode 100644 index 0000000..2f0477e --- /dev/null +++ b/src/aiofiles/tempfile/temptypes.pyi @@ -0,0 +1,59 @@ +from asyncio import AbstractEventLoop +from collections.abc import Generator, Iterable +from concurrent.futures import Executor +from tempfile import TemporaryDirectory +from typing import TypeVar + +from _typeshed import Incomplete, OpenBinaryMode, ReadableBuffer + +from aiofiles.base import AsyncBase as AsyncBase +from aiofiles.threadpool.utils import ( + cond_delegate_to_executor as cond_delegate_to_executor, +) +from aiofiles.threadpool.utils import ( + delegate_to_executor as delegate_to_executor, +) +from aiofiles.threadpool.utils import ( + proxy_property_directly as proxy_property_directly, +) + +_T = TypeVar("_T") + +class AsyncSpooledTemporaryFile(AsyncBase[_T]): + def fileno(self) -> Generator[Incomplete]: ... + def rollover(self) -> Generator[Incomplete]: ... + async def close(self) -> None: ... + async def flush(self) -> None: ... + async def isatty(self) -> bool: ... + async def read(self, n: int = ..., /) -> str | bytes: ... + async def readline(self, limit: int | None = ..., /) -> str | bytes: ... + async def readlines(self, hint: int = ..., /) -> list[str | bytes]: ... + async def seek(self, offset: int, whence: int = ...) -> int: ... + async def tell(self) -> int: ... + async def truncate(self, size: int | None = ...) -> None: ... + @property + def closed(self) -> bool: ... + @property + def encoding(self) -> str: ... + @property + def mode(self) -> OpenBinaryMode: ... + @property + def name(self) -> str | bytes: ... + @property + def newlines(self) -> str: ... + async def write(self, s: str | bytes | ReadableBuffer) -> int: ... + async def writelines( + self, iterable: Iterable[str | bytes | ReadableBuffer] + ) -> None: ... + +class AsyncTemporaryDirectory: + async def cleanup(self) -> None: ... + @property + def name(self) -> str | bytes: ... + def __init__( + self, + file: TemporaryDirectory[Incomplete], + loop: AbstractEventLoop | None, + executor: Executor | None, + ) -> None: ... + async def close(self) -> None: ... diff --git a/src/aiofiles/threadpool/__init__.pyi b/src/aiofiles/threadpool/__init__.pyi new file mode 100644 index 0000000..4fe2f5c --- /dev/null +++ b/src/aiofiles/threadpool/__init__.pyi @@ -0,0 +1,123 @@ +from asyncio import AbstractEventLoop +from collections.abc import Callable +from concurrent.futures import Executor +from typing import Literal, overload + +from _typeshed import ( + FileDescriptorOrPath, + OpenBinaryMode, + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, + OpenTextMode, +) +from typing_extensions import TypeAlias + +from ..base import AiofilesContextManager +from .binary import ( + AsyncBufferedIOBase, + AsyncBufferedReader, + AsyncFileIO, + AsyncIndirectBufferedIOBase, + _UnknownAsyncBinaryIO, +) +from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper + +_Opener: TypeAlias = Callable[[str, int], int] + +# Text mode: always returns AsyncTextIOWrapper +@overload +def open( + file: FileDescriptorOrPath, + mode: OpenTextMode = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + closefd: bool = True, + opener: _Opener | None = None, + *, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncTextIOWrapper]: ... + +# Unbuffered binary: returns a FileIO +@overload +def open( + file: FileDescriptorOrPath, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = True, + opener: _Opener | None = None, + *, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncFileIO]: ... + +# Buffered binary reading/updating: AsyncBufferedReader +@overload +def open( + file: FileDescriptorOrPath, + mode: OpenBinaryModeReading | OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = True, + opener: _Opener | None = None, + *, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedReader]: ... + +# Buffered binary writing: AsyncBufferedIOBase +@overload +def open( + file: FileDescriptorOrPath, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = True, + opener: _Opener | None = None, + *, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[AsyncBufferedIOBase]: ... + +# Buffering cannot be determined: fall back to _UnknownAsyncBinaryIO +@overload +def open( + file: FileDescriptorOrPath, + mode: OpenBinaryMode, + buffering: int = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = True, + opener: _Opener | None = None, + *, + loop: AbstractEventLoop | None = None, + executor: Executor | None = None, +) -> AiofilesContextManager[_UnknownAsyncBinaryIO]: ... + +stdin: AsyncTextIndirectIOWrapper +stdout: AsyncTextIndirectIOWrapper +stderr: AsyncTextIndirectIOWrapper +stdin_bytes: AsyncIndirectBufferedIOBase +stdout_bytes: AsyncIndirectBufferedIOBase +stderr_bytes: AsyncIndirectBufferedIOBase + +__all__ = ( + "open", + "stdin", + "stdout", + "stderr", + "stdin_bytes", + "stdout_bytes", + "stderr_bytes", +) diff --git a/src/aiofiles/threadpool/binary.pyi b/src/aiofiles/threadpool/binary.pyi new file mode 100644 index 0000000..8dc5a0e --- /dev/null +++ b/src/aiofiles/threadpool/binary.pyi @@ -0,0 +1,58 @@ +from collections.abc import Iterable +from io import FileIO +from typing import type_check_only + +from _typeshed import FileDescriptorOrPath, ReadableBuffer, WriteableBuffer + +from ..base import AsyncBase, AsyncIndirectBase + +# This class does not exist at runtime and instead these methods are +# all dynamically patched in. +@type_check_only +class _UnknownAsyncBinaryIO(AsyncBase[bytes]): + async def close(self) -> None: ... + async def flush(self) -> None: ... + async def isatty(self) -> bool: ... + async def read(self, size: int = ..., /) -> bytes: ... + async def readinto(self, buffer: WriteableBuffer, /) -> int | None: ... + async def readline(self, size: int | None = ..., /) -> bytes: ... + async def readlines(self, hint: int = ..., /) -> list[bytes]: ... + async def seek(self, offset: int, whence: int = ..., /) -> int: ... + async def seekable(self) -> bool: ... + async def tell(self) -> int: ... + async def truncate(self, size: int | None = ..., /) -> int: ... + async def writable(self) -> bool: ... + async def write(self, b: ReadableBuffer, /) -> int: ... + async def writelines(self, lines: Iterable[ReadableBuffer], /) -> None: ... + def fileno(self) -> int: ... + def readable(self) -> bool: ... + @property + def closed(self) -> bool: ... + @property + def mode(self) -> str: ... + @property + def name(self) -> FileDescriptorOrPath: ... + +class AsyncBufferedIOBase(_UnknownAsyncBinaryIO): + async def read1(self, size: int = ..., /) -> bytes: ... + def detach(self) -> FileIO: ... + @property + def raw(self) -> FileIO: ... + +class AsyncIndirectBufferedIOBase(AsyncIndirectBase[bytes], _UnknownAsyncBinaryIO): + async def read1(self, size: int = ..., /) -> bytes: ... + def detach(self) -> FileIO: ... + @property + def raw(self) -> FileIO: ... + +class AsyncBufferedReader(AsyncBufferedIOBase): + async def peek(self, size: int = ..., /) -> bytes: ... + +class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase): + async def peek(self, size: int = ..., /) -> bytes: ... + +class AsyncFileIO(_UnknownAsyncBinaryIO): + async def readall(self) -> bytes: ... + +class AsyncIndirectFileIO(AsyncIndirectBase[bytes], _UnknownAsyncBinaryIO): + async def readall(self) -> bytes: ... diff --git a/src/aiofiles/threadpool/text.pyi b/src/aiofiles/threadpool/text.pyi new file mode 100644 index 0000000..edf11f4 --- /dev/null +++ b/src/aiofiles/threadpool/text.pyi @@ -0,0 +1,44 @@ +from collections.abc import Iterable +from typing import BinaryIO, type_check_only + +from _typeshed import FileDescriptorOrPath + +from ..base import AsyncBase, AsyncIndirectBase + +@type_check_only +class _UnknownAsyncTextIO(AsyncBase[str]): + async def close(self) -> None: ... + async def flush(self) -> None: ... + async def isatty(self) -> bool: ... + async def read(self, size: int | None = ..., /) -> str: ... + async def readline(self, size: int = ..., /) -> str: ... + async def readlines(self, hint: int = ..., /) -> list[str]: ... + async def seek(self, offset: int, whence: int = ..., /) -> int: ... + async def seekable(self) -> bool: ... + async def tell(self) -> int: ... + async def truncate(self, size: int | None = ..., /) -> int: ... + async def writable(self) -> bool: ... + async def write(self, b: str, /) -> int: ... + async def writelines(self, lines: Iterable[str], /) -> None: ... + def detach(self) -> BinaryIO: ... + def fileno(self) -> int: ... + def readable(self) -> bool: ... + @property + def buffer(self) -> BinaryIO: ... + @property + def closed(self) -> bool: ... + @property + def encoding(self) -> str: ... + @property + def errors(self) -> str | None: ... + @property + def line_buffering(self) -> bool: ... + @property + def newlines(self) -> str | tuple[str, ...] | None: ... + @property + def name(self) -> FileDescriptorOrPath: ... + @property + def mode(self) -> str: ... + +class AsyncTextIOWrapper(_UnknownAsyncTextIO): ... +class AsyncTextIndirectIOWrapper(AsyncIndirectBase[str], _UnknownAsyncTextIO): ... diff --git a/src/aiofiles/threadpool/utils.pyi b/src/aiofiles/threadpool/utils.pyi new file mode 100644 index 0000000..438a685 --- /dev/null +++ b/src/aiofiles/threadpool/utils.pyi @@ -0,0 +1,10 @@ +from collections.abc import Callable +from typing import TypeVar + +_T = TypeVar("_T", bound=type) + +# All these function actually mutate the given type: +def delegate_to_executor(*attrs: str) -> Callable[[_T], _T]: ... +def proxy_method_directly(*attrs: str) -> Callable[[_T], _T]: ... +def proxy_property_directly(*attrs: str) -> Callable[[_T], _T]: ... +def cond_delegate_to_executor(*attrs: str) -> Callable[[_T], _T]: ... diff --git a/tox.ini b/tox.ini index 9257f0f..c0f3f0b 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,15 @@ commands = dependency_groups = lint +[testenv:typecheck] +skip_install = false +basepython = python3.13 +allowlist_externals = just +commands = + just typecheck +dependency_groups = + lint + [testenv] runner = uv-venv-lock-runner allowlist_externals = just