From 99fb43ee09cf3e4174a9eb3acf125e954e8b3421 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sun, 11 May 2025 18:21:21 +0200 Subject: [PATCH 1/5] Make BufferedWriter generic over a protocol --- stdlib/@tests/test_cases/check_io.py | 3 ++- stdlib/_io.pyi | 38 ++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/stdlib/@tests/test_cases/check_io.py b/stdlib/@tests/test_cases/check_io.py index ce8c34aedbad..6e9f35e5f90f 100644 --- a/stdlib/@tests/test_cases/check_io.py +++ b/stdlib/@tests/test_cases/check_io.py @@ -1,9 +1,10 @@ -from _io import BufferedReader +from _io import BufferedReader, BufferedWriter from gzip import GzipFile from io import FileIO, RawIOBase, TextIOWrapper from typing_extensions import assert_type BufferedReader(RawIOBase()) +BufferedWriter(RawIOBase()) assert_type(TextIOWrapper(FileIO("")).buffer, FileIO) assert_type(TextIOWrapper(FileIO(13)).detach(), FileIO) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index c77d75287c25..5cc86f78aa7f 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -88,6 +88,7 @@ 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: ... +@type_check_only class _BufferedReaderStream(Protocol): def read(self, n: int = ..., /) -> bytes: ... # Optional: def readall(self) -> bytes: ... @@ -122,9 +123,36 @@ class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_Buffere def seek(self, target: int, whence: int = 0, /) -> int: ... def truncate(self, pos: int | None = None, /) -> int: ... -class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of writelines in the base classes - raw: RawIOBase - def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ... +@type_check_only +class _BufferedWriterStream(Protocol): + def write(self, b: WriteableBuffer, /) -> 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: ... + @property + def closed(self) -> bool: ... + def writable(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. + + # @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: ... + +_BufferedWriterStreamT = TypeVar("_BufferedWriterStreamT", bound=_BufferedWriterStream, default=_BufferedWriterStream) + + +class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_BufferedWriterStreamT]): # type: ignore[misc] # incompatible definitions of writelines in the base classes + raw: _BufferedWriterStreamT + def __init__(self, raw: _BufferedWriterStreamT, buffer_size: int = 8192) -> None: ... def write(self, buffer: ReadableBuffer, /) -> int: ... def seek(self, target: int, whence: int = 0, /) -> int: ... def truncate(self, pos: int | None = None, /) -> int: ... @@ -138,8 +166,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, Generic[_BufferedReaderStreamT]): - def __init__(self, reader: _BufferedReaderStreamT, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ... +class BufferedRWPair(BufferedIOBase, _BufferedIOBase, Generic[_BufferedReaderStreamT, _BufferedWriterStreamT]): + def __init__(self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 8192, /) -> None: ... def peek(self, size: int = 0, /) -> bytes: ... class _TextIOBase(_IOBase): From f11ce854e7710b0bd236f5a3c57adf4c5f989b11 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 16:23:09 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks --- stdlib/_io.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 5cc86f78aa7f..2b9d112e2d56 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -149,7 +149,6 @@ class _BufferedWriterStream(Protocol): _BufferedWriterStreamT = TypeVar("_BufferedWriterStreamT", bound=_BufferedWriterStream, default=_BufferedWriterStream) - class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_BufferedWriterStreamT]): # type: ignore[misc] # incompatible definitions of writelines in the base classes raw: _BufferedWriterStreamT def __init__(self, raw: _BufferedWriterStreamT, buffer_size: int = 8192) -> None: ... From e4ccafe27cb689b9336e637637d4b5e3d251d545 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sun, 11 May 2025 18:23:38 +0200 Subject: [PATCH 3/5] Add a test --- stdlib/@tests/test_cases/check_io.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stdlib/@tests/test_cases/check_io.py b/stdlib/@tests/test_cases/check_io.py index 6e9f35e5f90f..7299d11a6187 100644 --- a/stdlib/@tests/test_cases/check_io.py +++ b/stdlib/@tests/test_cases/check_io.py @@ -1,4 +1,4 @@ -from _io import BufferedReader, BufferedWriter +from _io import BufferedReader, BufferedRWPair, BufferedWriter from gzip import GzipFile from io import FileIO, RawIOBase, TextIOWrapper from typing_extensions import assert_type @@ -6,6 +6,8 @@ BufferedReader(RawIOBase()) BufferedWriter(RawIOBase()) +BufferedRWPair(open("", "rb"), open("", "wb")) + assert_type(TextIOWrapper(FileIO("")).buffer, FileIO) assert_type(TextIOWrapper(FileIO(13)).detach(), FileIO) assert_type(TextIOWrapper(GzipFile("")).buffer, GzipFile) From 0668ed6fa139bfa6dd7d8a8a44d443af8cab8b5f Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sun, 11 May 2025 19:02:38 +0200 Subject: [PATCH 4/5] `BufferedWriter` can handle non-blocking I/O --- stdlib/@tests/test_cases/check_io.py | 5 +++++ stdlib/_io.pyi | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/stdlib/@tests/test_cases/check_io.py b/stdlib/@tests/test_cases/check_io.py index 7299d11a6187..c3ee5aa4c6c0 100644 --- a/stdlib/@tests/test_cases/check_io.py +++ b/stdlib/@tests/test_cases/check_io.py @@ -1,10 +1,15 @@ from _io import BufferedReader, BufferedRWPair, BufferedWriter from gzip import GzipFile from io import FileIO, RawIOBase, TextIOWrapper +from socket import SocketIO +from typing import Any from typing_extensions import assert_type +socket: Any = None + BufferedReader(RawIOBase()) BufferedWriter(RawIOBase()) +BufferedWriter(SocketIO(socket, "r")) BufferedRWPair(open("", "rb"), open("", "wb")) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index 2b9d112e2d56..a95b83d97c76 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -125,7 +125,7 @@ class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_Buffere @type_check_only class _BufferedWriterStream(Protocol): - def write(self, b: WriteableBuffer, /) -> int: ... + def write(self, b: WriteableBuffer, /) -> int | None: ... def seek(self, pos: int, whence: int, /) -> int: ... def tell(self) -> int: ... def truncate(self, size: int, /) -> int: ... From 821fcf348a1600f68f999ee2bf1c8ef00a008d74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:54:55 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks --- stdlib/_io.pyi | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/stdlib/_io.pyi b/stdlib/_io.pyi index c06923660162..7a8b49fa6203 100644 --- a/stdlib/_io.pyi +++ b/stdlib/_io.pyi @@ -196,6 +196,7 @@ class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_Buffere def __init__(self, raw: _BufferedWriterStreamT, buffer_size: int = 131072) -> None: ... else: def __init__(self, raw: _BufferedWriterStreamT, buffer_size: int = 8192) -> None: ... + def write(self, buffer: ReadableBuffer, /) -> int: ... def seek(self, target: int, whence: int = 0, /) -> int: ... def truncate(self, pos: int | None = None, /) -> int: ... @@ -217,9 +218,14 @@ class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore @disjoint_base class BufferedRWPair(BufferedIOBase, _BufferedIOBase, Generic[_BufferedReaderStreamT, _BufferedWriterStreamT]): if sys.version_info >= (3, 14): - def __init__(self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 131072, /) -> None: ... + def __init__( + self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 131072, / + ) -> None: ... else: - def __init__(self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 8192, /) -> None: ... + def __init__( + self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 8192, / + ) -> None: ... + def peek(self, size: int = 0, /) -> bytes: ... class _TextIOBase(_IOBase):