From 51af4bf4f5a9aec7802d9ed33b4028bdccdb05be Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 18:51:21 +0100 Subject: [PATCH 1/7] Make BufferedReader generic over a protocol --- stdlib/_io.pyi | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 54efd3199760..e534f7f0e3c2 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -88,9 +88,31 @@ class BytesIO(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] def readlines(self, size: int | None = None, /) -> list[bytes]: ... def seek(self, pos: int, whence: int = 0, /) -> int: ... -class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes - raw: RawIOBase - def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ... +class _BufferedReaderStream(Protocol): + def read(self, n: int = ..., /) -> bytes: ... + # Optional: def readall(self) -> bytes: ... + def readinto(self, b: memoryview, /) -> int: ... + def seek(self, pos: int, whence: int, /) -> int: ... + def tell(self) -> int: ... + def truncate(self, size: int, /) -> int: ... + def flush(self) -> object: ... + def close(self) -> object: ... + def readable(self) -> bool: ... + def seekable(self) -> bool: ... + @property + def closed(self) -> bool: ... + @property + def name(self) -> str: ... + @property + def mode(self) -> str: ... + def fileno(self) -> int: ... + def isatty(self) -> bool: ... + +_BufferedReaderStreamT = TypeVar("_BufferedReaderStreamT", bound=_BufferedReaderStream, default=_BufferedReaderStream) + +class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_BufferedReaderStreamT]): # type: ignore[misc] # incompatible definitions of methods in the base classes + raw: _BufferedReaderStreamT + def __init__(self, raw: _BufferedReaderStreamT, buffer_size: int = 8192) -> None: ... def peek(self, size: int = 0, /) -> bytes: ... def seek(self, target: int, whence: int = 0, /) -> int: ... def truncate(self, pos: int | None = None, /) -> int: ... From 74037827042ad9d8e5d5e4eb5a8b9c43c55b9f1c Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 18:52:43 +0100 Subject: [PATCH 2/7] Make BufferedRWPair partly generic --- stdlib/_io.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index e534f7f0e3c2..352d78f75edf 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -133,8 +133,8 @@ class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore def peek(self, size: int = 0, /) -> bytes: ... def truncate(self, pos: int | None = None, /) -> int: ... -class BufferedRWPair(BufferedIOBase, _BufferedIOBase): - def __init__(self, reader: RawIOBase, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ... +class BufferedRWPair(BufferedIOBase, _BufferedIOBase, Generic[_BufferedReaderStreamT]): + def __init__(self, reader: _BufferedReaderStreamT, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ... def peek(self, size: int = 0, /) -> bytes: ... class _TextIOBase(_IOBase): From ee3f7e2eeb9453909750f68a179f0e9b9091ef01 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 19:07:04 +0100 Subject: [PATCH 3/7] Use `Any` for `name` --- stdlib/_io.pyi | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 352d78f75edf..dbac97f86515 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -102,7 +102,7 @@ class _BufferedReaderStream(Protocol): @property def closed(self) -> bool: ... @property - def name(self) -> str: ... + def name(self) -> Any: ... # Type is inconsistent between the various I/O types. @property def mode(self) -> str: ... def fileno(self) -> int: ... @@ -153,8 +153,7 @@ class _TextIOBase(_IOBase): @type_check_only class _WrappedBuffer(Protocol): # "name" is wrapped by TextIOWrapper. Its type is inconsistent between - # the various I/O types, see the comments on TextIOWrapper.name and - # TextIO.name. + # the various I/O types. @property def name(self) -> Any: ... @property From 8d2e0ca4cce64e338affe1e30f971fa61e0e7513 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 19:27:58 +0100 Subject: [PATCH 4/7] Mark `name` and `mode` as optional --- stdlib/@tests/test_cases/check_io.py | 43 +++++++++++++++++++++++++++- stdlib/_io.pyi | 9 +++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/stdlib/@tests/test_cases/check_io.py b/stdlib/@tests/test_cases/check_io.py index d0713a26ae86..c644bf201668 100644 --- a/stdlib/@tests/test_cases/check_io.py +++ b/stdlib/@tests/test_cases/check_io.py @@ -1,7 +1,48 @@ +from _io import BufferedReader from gzip import GzipFile -from io import FileIO, TextIOWrapper +from io import FileIO, RawIOBase, TextIOWrapper from typing_extensions import assert_type +BufferedReader(RawIOBase()) + assert_type(TextIOWrapper(FileIO("")).buffer, FileIO) assert_type(TextIOWrapper(FileIO(13)).detach(), FileIO) assert_type(TextIOWrapper(GzipFile("")).buffer, GzipFile) + + +# def read(self, n: int = ..., /) -> bytes: ... +# def readinto(self, b: memoryview, /) -> int: ... +## def seek(self, pos: int, whence: int, /) -> int: ... +## def tell(self) -> int: ... +## def truncate(self, size: int, /) -> int: ... +## def flush(self) -> object: ... +## def close(self) -> object: ... +## def readable(self) -> bool: ... +## def seekable(self) -> bool: ... +## @property +## def closed(self) -> bool: ... +# @property +# def name(self) -> Any: ... # Type is inconsistent between the various I/O types. +# @property +# def mode(self) -> str: ... +## def fileno(self) -> int: ... +## def isatty(self) -> bool: ... + + +# class _RawIOBase(_IOBase): +# # def close(self) -> None: ... +# # def fileno(self) -> int: ... +# # def flush(self) -> None: ... +# # def isatty(self) -> bool: ... +# # def readable(self) -> bool: ... +# read: Callable[..., Any] +# # def seek(self, offset: int, whence: int = 0, /) -> int: ... +# # def seekable(self) -> bool: ... +# # def tell(self) -> int: ... +# # def truncate(self, size: int | None = None, /) -> int: ... +# # @property +# # def closed(self) -> bool: ... +# # The following methods can return None if the file is in non-blocking mode +# # and no data is available. +# def readinto(self, buffer: WriteableBuffer, /) -> int | MaybeNone: ... +# def read(self, size: int = -1, /) -> bytes | MaybeNone: ... diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index dbac97f86515..97defc02c0b5 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -101,10 +101,11 @@ class _BufferedReaderStream(Protocol): def seekable(self) -> bool: ... @property def closed(self) -> bool: ... - @property - def name(self) -> Any: ... # Type is inconsistent between the various I/O types. - @property - def mode(self) -> str: ... + # Optional: + # @property + # def name(self) -> Any: ... # Type is inconsistent between the various I/O types. + # @property + # def mode(self) -> str: ... def fileno(self) -> int: ... def isatty(self) -> bool: ... From e4f965dcd8b97fc2b709adecd1a0b7e93d8659f9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 19:33:44 +0100 Subject: [PATCH 5/7] Mark more attributes as optional --- stdlib/_io.pyi | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 97defc02c0b5..3e2b292ed43c 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -98,16 +98,20 @@ class _BufferedReaderStream(Protocol): def flush(self) -> object: ... def close(self) -> object: ... def readable(self) -> bool: ... - def seekable(self) -> bool: ... - @property - def closed(self) -> bool: ... - # Optional: + + # The following methods just pass through to the underlying stream. Since + # not all streams support them, they are marked as optional here, and will + # raise an AttributeError if called on a stream that does not support them. + + # def seekable(self) -> bool: ... + # @property + # def closed(self) -> bool: ... # @property # def name(self) -> Any: ... # Type is inconsistent between the various I/O types. # @property # def mode(self) -> str: ... - def fileno(self) -> int: ... - def isatty(self) -> bool: ... + # def fileno(self) -> int: ... + # def isatty(self) -> bool: ... _BufferedReaderStreamT = TypeVar("_BufferedReaderStreamT", bound=_BufferedReaderStream, default=_BufferedReaderStream) From 1042ddebc5b307aaa2ea14d6def73968b3a02d87 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 19:34:45 +0100 Subject: [PATCH 6/7] Mark readable and seekable as mandatory again --- stdlib/_io.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 3e2b292ed43c..1c1dae025e06 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -97,15 +97,15 @@ class _BufferedReaderStream(Protocol): def truncate(self, size: int, /) -> int: ... def flush(self) -> object: ... def close(self) -> object: ... + @property + def closed(self) -> bool: ... def readable(self) -> bool: ... + def seekable(self) -> bool: ... # The following methods just pass through to the underlying stream. Since # not all streams support them, they are marked as optional here, and will # raise an AttributeError if called on a stream that does not support them. - # def seekable(self) -> bool: ... - # @property - # def closed(self) -> bool: ... # @property # def name(self) -> Any: ... # Type is inconsistent between the various I/O types. # @property From da23e86ecc7e70c90060b320df4a9738293d3a34 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 24 Feb 2025 19:51:14 +0100 Subject: [PATCH 7/7] BufferedReader actually handles the case where `readinto()` returns `None` --- stdlib/@tests/test_cases/check_io.py | 38 ---------------------------- stdlib/_io.pyi | 2 +- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/stdlib/@tests/test_cases/check_io.py b/stdlib/@tests/test_cases/check_io.py index c644bf201668..ce8c34aedbad 100644 --- a/stdlib/@tests/test_cases/check_io.py +++ b/stdlib/@tests/test_cases/check_io.py @@ -8,41 +8,3 @@ assert_type(TextIOWrapper(FileIO("")).buffer, FileIO) assert_type(TextIOWrapper(FileIO(13)).detach(), FileIO) assert_type(TextIOWrapper(GzipFile("")).buffer, GzipFile) - - -# def read(self, n: int = ..., /) -> bytes: ... -# def readinto(self, b: memoryview, /) -> int: ... -## def seek(self, pos: int, whence: int, /) -> int: ... -## def tell(self) -> int: ... -## def truncate(self, size: int, /) -> int: ... -## def flush(self) -> object: ... -## def close(self) -> object: ... -## def readable(self) -> bool: ... -## def seekable(self) -> bool: ... -## @property -## def closed(self) -> bool: ... -# @property -# def name(self) -> Any: ... # Type is inconsistent between the various I/O types. -# @property -# def mode(self) -> str: ... -## def fileno(self) -> int: ... -## def isatty(self) -> bool: ... - - -# class _RawIOBase(_IOBase): -# # def close(self) -> None: ... -# # def fileno(self) -> int: ... -# # def flush(self) -> None: ... -# # def isatty(self) -> bool: ... -# # def readable(self) -> bool: ... -# read: Callable[..., Any] -# # def seek(self, offset: int, whence: int = 0, /) -> int: ... -# # def seekable(self) -> bool: ... -# # def tell(self) -> int: ... -# # def truncate(self, size: int | None = None, /) -> int: ... -# # @property -# # def closed(self) -> bool: ... -# # The following methods can return None if the file is in non-blocking mode -# # and no data is available. -# def readinto(self, buffer: WriteableBuffer, /) -> int | MaybeNone: ... -# def read(self, size: int = -1, /) -> bytes | MaybeNone: ... diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 1c1dae025e06..c77d75287c25 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -91,7 +91,7 @@ class BytesIO(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] class _BufferedReaderStream(Protocol): def read(self, n: int = ..., /) -> bytes: ... # Optional: def readall(self) -> bytes: ... - def readinto(self, b: memoryview, /) -> int: ... + def readinto(self, b: memoryview, /) -> int | None: ... def seek(self, pos: int, whence: int, /) -> int: ... def tell(self) -> int: ... def truncate(self, size: int, /) -> int: ...