diff --git a/Doc/library/io.rst b/Doc/library/io.rst index 0d8cc5171d5476..cb2182334e5063 100644 --- a/Doc/library/io.rst +++ b/Doc/library/io.rst @@ -1147,6 +1147,55 @@ Text I/O It inherits from :class:`codecs.IncrementalDecoder`. +Static Typing +------------- + +The following protocols can be used for annotating function and method +arguments for simple stream reading or writing operations. They are decorated +with :deco:`typing.runtime_checkable`. + +.. class:: Reader[T] + + Generic protocol for reading from a file or other input stream. ``T`` will + usually be :class:`str` or :class:`bytes`, but can be any type that is + read from the stream. + + .. versionadded:: next + + .. method:: read() + read(size, /) + + Read data from the input stream and return it. If *size* is + specified, it should be an integer, and at most *size* items + (bytes/characters) will be read. + + For example:: + + def read_it(reader: Reader[str]): + data = reader.read(11) + assert isinstance(data, str) + +.. class:: Writer[T] + + Generic protocol for writing to a file or other output stream. ``T`` will + usually be :class:`str` or :class:`bytes`, but can be any type that can be + written to the stream. + + .. versionadded:: next + + .. method:: write(data, /) + + Write *data* to the output stream and return the number of items + (bytes/characters) written. + + For example:: + + def write_binary(writer: Writer[bytes]): + writer.write(b"Hello world!\n") + +See :ref:`typing-io` for other I/O related protocols and classes that can be +used for static type checking. + Performance ----------- diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index aa613ee9f52f0a..3bbc8c0e818975 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -2834,17 +2834,35 @@ with :func:`@runtime_checkable `. An ABC with one abstract method ``__round__`` that is covariant in its return type. -ABCs for working with IO ------------------------- +.. _typing-io: + +ABCs and Protocols for working with I/O +--------------------------------------- -.. class:: IO - TextIO - BinaryIO +.. class:: IO[AnyStr] + TextIO[AnyStr] + BinaryIO[AnyStr] - Generic type ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])`` + Generic class ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])`` and ``BinaryIO(IO[bytes])`` represent the types of I/O streams such as returned by - :func:`open`. + :func:`open`. Please note that these classes are not protocols, and + their interface is fairly broad. + +The protocols :class:`io.Reader` and :class:`io.Writer` offer a simpler +alternative for argument types, when only the ``read()`` or ``write()`` +methods are accessed, respectively:: + + def read_and_write(reader: Reader[str], writer: Writer[bytes]): + data = reader.read() + writer.write(data.encode()) + +Also consider using :class:`collections.abc.Iterable` for iterating over +the lines of an input stream:: + + def read_config(stream: Iterable[str]): + for line in stream: + ... Functions and decorators ------------------------ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index a178ba51c89c48..7fd23fc31dc830 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -619,6 +619,11 @@ io :exc:`BlockingIOError` if the operation cannot immediately return bytes. (Contributed by Giovanni Siragusa in :gh:`109523`.) +* Add protocols :class:`io.Reader` and :class:`io.Writer` as a simpler + alternatives to the pseudo-protocols :class:`typing.IO`, + :class:`typing.TextIO`, and :class:`typing.BinaryIO`. + (Contributed by Sebastian Rittau in :gh:`127648`.) + json ---- diff --git a/Lib/_pyio.py b/Lib/_pyio.py index f7370dff19efc8..e915e5b138a623 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -16,7 +16,7 @@ _setmode = None import io -from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END) # noqa: F401 +from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END, Reader, Writer) # noqa: F401 valid_seek_flags = {0, 1, 2} # Hardwired values if hasattr(os, 'SEEK_HOLE') : diff --git a/Lib/io.py b/Lib/io.py index f0e2fa15d5abcf..e9fe619392e3d9 100644 --- a/Lib/io.py +++ b/Lib/io.py @@ -46,12 +46,14 @@ "BufferedReader", "BufferedWriter", "BufferedRWPair", "BufferedRandom", "TextIOBase", "TextIOWrapper", "UnsupportedOperation", "SEEK_SET", "SEEK_CUR", "SEEK_END", - "DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder"] + "DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder", + "Reader", "Writer"] import _io import abc +from _collections_abc import _check_methods from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation, open, open_code, FileIO, BytesIO, StringIO, BufferedReader, BufferedWriter, BufferedRWPair, BufferedRandom, @@ -97,3 +99,55 @@ class TextIOBase(_io._TextIOBase, IOBase): pass else: RawIOBase.register(_WindowsConsoleIO) + +# +# Static Typing Support +# + +GenericAlias = type(list[int]) + + +class Reader(metaclass=abc.ABCMeta): + """Protocol for simple I/O reader instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def read(self, size=..., /): + """Read data from the input stream and return it. + + If *size* is specified, at most *size* items (bytes/characters) will be + read. + """ + + @classmethod + def __subclasshook__(cls, C): + if cls is Reader: + return _check_methods(C, "read") + return NotImplemented + + __class_getitem__ = classmethod(GenericAlias) + + +class Writer(metaclass=abc.ABCMeta): + """Protocol for simple I/O writer instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def write(self, data, /): + """Write *data* to the output stream and return the number of items written.""" + + @classmethod + def __subclasshook__(cls, C): + if cls is Writer: + return _check_methods(C, "write") + return NotImplemented + + __class_getitem__ = classmethod(GenericAlias) diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index e59d3977df4134..3b8ff1d20030b3 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -4916,6 +4916,24 @@ class PySignalsTest(SignalsTest): test_reentrant_write_text = None +class ProtocolsTest(unittest.TestCase): + class MyReader: + def read(self, sz=-1): + return b"" + + class MyWriter: + def write(self, b: bytes): + pass + + def test_reader_subclass(self): + self.assertIsSubclass(MyReader, io.Reader[bytes]) + self.assertNotIsSubclass(str, io.Reader[bytes]) + + def test_writer_subclass(self): + self.assertIsSubclass(MyWriter, io.Writer[bytes]) + self.assertNotIsSubclass(str, io.Writer[bytes]) + + def load_tests(loader, tests, pattern): tests = (CIOTest, PyIOTest, APIMismatchTest, CBufferedReaderTest, PyBufferedReaderTest, diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a7901dfa6a4ef0..402353404cb0fb 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6,6 +6,7 @@ from functools import lru_cache, wraps, reduce import gc import inspect +import io import itertools import operator import os @@ -4294,6 +4295,40 @@ def __release_buffer__(self, mv: memoryview) -> None: self.assertNotIsSubclass(C, ReleasableBuffer) self.assertNotIsInstance(C(), ReleasableBuffer) + def test_io_reader_protocol_allowed(self): + @runtime_checkable + class CustomReader(io.Reader[bytes], Protocol): + def close(self): ... + + class A: pass + class B: + def read(self, sz=-1): + return b"" + def close(self): + pass + + self.assertIsSubclass(B, CustomReader) + self.assertIsInstance(B(), CustomReader) + self.assertNotIsSubclass(A, CustomReader) + self.assertNotIsInstance(A(), CustomReader) + + def test_io_writer_protocol_allowed(self): + @runtime_checkable + class CustomWriter(io.Writer[bytes], Protocol): + def close(self): ... + + class A: pass + class B: + def write(self, b): + pass + def close(self): + pass + + self.assertIsSubclass(B, CustomWriter) + self.assertIsInstance(B(), CustomWriter) + self.assertNotIsSubclass(A, CustomWriter) + self.assertNotIsInstance(A(), CustomWriter) + def test_builtin_protocol_allowlist(self): with self.assertRaises(TypeError): class CustomProtocol(TestCase, Protocol): diff --git a/Lib/typing.py b/Lib/typing.py index 1dd115473fb927..96211553a21e39 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1876,6 +1876,7 @@ def _allow_reckless_class_checks(depth=2): 'Reversible', 'Buffer', ], 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], + 'io': ['Reader', 'Writer'], 'os': ['PathLike'], } diff --git a/Misc/NEWS.d/next/Library/2024-12-05-19-54-16.gh-issue-127647.Xd78Vs.rst b/Misc/NEWS.d/next/Library/2024-12-05-19-54-16.gh-issue-127647.Xd78Vs.rst new file mode 100644 index 00000000000000..8f0b812dcab639 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-05-19-54-16.gh-issue-127647.Xd78Vs.rst @@ -0,0 +1,3 @@ +Add protocols :class:`io.Reader` and :class:`io.Writer` as +alternatives to :class:`typing.IO`, :class:`typing.TextIO`, and +:class:`typing.BinaryIO`. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy