mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	gh-127647: Add typing.Reader and Writer protocols (#127648)
This commit is contained in:
		
							parent
							
								
									9c691500f9
								
							
						
					
					
						commit
						c6dd2348ca
					
				
					 9 changed files with 192 additions and 9 deletions
				
			
		|  | @ -1147,6 +1147,55 @@ Text I/O | ||||||
|    It inherits from :class:`codecs.IncrementalDecoder`. |    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 | Performance | ||||||
| ----------- | ----------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2834,17 +2834,35 @@ with :func:`@runtime_checkable <runtime_checkable>`. | ||||||
|     An ABC with one abstract method ``__round__`` |     An ABC with one abstract method ``__round__`` | ||||||
|     that is covariant in its return type. |     that is covariant in its return type. | ||||||
| 
 | 
 | ||||||
| ABCs for working with IO | .. _typing-io: | ||||||
| ------------------------ |  | ||||||
| 
 | 
 | ||||||
| .. class:: IO | ABCs and Protocols for working with I/O | ||||||
|            TextIO | --------------------------------------- | ||||||
|            BinaryIO |  | ||||||
| 
 | 
 | ||||||
|    Generic type ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])`` | .. class:: IO[AnyStr] | ||||||
|  |            TextIO[AnyStr] | ||||||
|  |            BinaryIO[AnyStr] | ||||||
|  | 
 | ||||||
|  |    Generic class ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])`` | ||||||
|    and ``BinaryIO(IO[bytes])`` |    and ``BinaryIO(IO[bytes])`` | ||||||
|    represent the types of I/O streams such as returned by |    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 | Functions and decorators | ||||||
| ------------------------ | ------------------------ | ||||||
|  |  | ||||||
|  | @ -619,6 +619,11 @@ io | ||||||
|   :exc:`BlockingIOError` if the operation cannot immediately return bytes. |   :exc:`BlockingIOError` if the operation cannot immediately return bytes. | ||||||
|   (Contributed by Giovanni Siragusa in :gh:`109523`.) |   (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 | json | ||||||
| ---- | ---- | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
|     _setmode = None |     _setmode = None | ||||||
| 
 | 
 | ||||||
| import io | 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 | valid_seek_flags = {0, 1, 2}  # Hardwired values | ||||||
| if hasattr(os, 'SEEK_HOLE') : | if hasattr(os, 'SEEK_HOLE') : | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								Lib/io.py
									
										
									
									
									
								
							
							
						
						
									
										56
									
								
								Lib/io.py
									
										
									
									
									
								
							|  | @ -46,12 +46,14 @@ | ||||||
|            "BufferedReader", "BufferedWriter", "BufferedRWPair", |            "BufferedReader", "BufferedWriter", "BufferedRWPair", | ||||||
|            "BufferedRandom", "TextIOBase", "TextIOWrapper", |            "BufferedRandom", "TextIOBase", "TextIOWrapper", | ||||||
|            "UnsupportedOperation", "SEEK_SET", "SEEK_CUR", "SEEK_END", |            "UnsupportedOperation", "SEEK_SET", "SEEK_CUR", "SEEK_END", | ||||||
|            "DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder"] |            "DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder", | ||||||
|  |            "Reader", "Writer"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| import _io | import _io | ||||||
| import abc | import abc | ||||||
| 
 | 
 | ||||||
|  | from _collections_abc import _check_methods | ||||||
| from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation, | from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation, | ||||||
|                  open, open_code, FileIO, BytesIO, StringIO, BufferedReader, |                  open, open_code, FileIO, BytesIO, StringIO, BufferedReader, | ||||||
|                  BufferedWriter, BufferedRWPair, BufferedRandom, |                  BufferedWriter, BufferedRWPair, BufferedRandom, | ||||||
|  | @ -97,3 +99,55 @@ class TextIOBase(_io._TextIOBase, IOBase): | ||||||
|     pass |     pass | ||||||
| else: | else: | ||||||
|     RawIOBase.register(_WindowsConsoleIO) |     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) | ||||||
|  |  | ||||||
|  | @ -4916,6 +4916,24 @@ class PySignalsTest(SignalsTest): | ||||||
|     test_reentrant_write_text = None |     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): | def load_tests(loader, tests, pattern): | ||||||
|     tests = (CIOTest, PyIOTest, APIMismatchTest, |     tests = (CIOTest, PyIOTest, APIMismatchTest, | ||||||
|              CBufferedReaderTest, PyBufferedReaderTest, |              CBufferedReaderTest, PyBufferedReaderTest, | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
| from functools import lru_cache, wraps, reduce | from functools import lru_cache, wraps, reduce | ||||||
| import gc | import gc | ||||||
| import inspect | import inspect | ||||||
|  | import io | ||||||
| import itertools | import itertools | ||||||
| import operator | import operator | ||||||
| import os | import os | ||||||
|  | @ -4294,6 +4295,40 @@ def __release_buffer__(self, mv: memoryview) -> None: | ||||||
|         self.assertNotIsSubclass(C, ReleasableBuffer) |         self.assertNotIsSubclass(C, ReleasableBuffer) | ||||||
|         self.assertNotIsInstance(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): |     def test_builtin_protocol_allowlist(self): | ||||||
|         with self.assertRaises(TypeError): |         with self.assertRaises(TypeError): | ||||||
|             class CustomProtocol(TestCase, Protocol): |             class CustomProtocol(TestCase, Protocol): | ||||||
|  |  | ||||||
|  | @ -1876,6 +1876,7 @@ def _allow_reckless_class_checks(depth=2): | ||||||
|         'Reversible', 'Buffer', |         'Reversible', 'Buffer', | ||||||
|     ], |     ], | ||||||
|     'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], |     'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], | ||||||
|  |     'io': ['Reader', 'Writer'], | ||||||
|     'os': ['PathLike'], |     'os': ['PathLike'], | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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`. | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Sebastian Rittau
						Sebastian Rittau