Skip to content

Commit c6dd234

Browse files
authored
gh-127647: Add typing.Reader and Writer protocols (#127648)
1 parent 9c69150 commit c6dd234

File tree

9 files changed

+192
-9
lines changed

9 files changed

+192
-9
lines changed

Doc/library/io.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,55 @@ Text I/O
11471147
It inherits from :class:`codecs.IncrementalDecoder`.
11481148

11491149

1150+
Static Typing
1151+
-------------
1152+
1153+
The following protocols can be used for annotating function and method
1154+
arguments for simple stream reading or writing operations. They are decorated
1155+
with :deco:`typing.runtime_checkable`.
1156+
1157+
.. class:: Reader[T]
1158+
1159+
Generic protocol for reading from a file or other input stream. ``T`` will
1160+
usually be :class:`str` or :class:`bytes`, but can be any type that is
1161+
read from the stream.
1162+
1163+
.. versionadded:: next
1164+
1165+
.. method:: read()
1166+
read(size, /)
1167+
1168+
Read data from the input stream and return it. If *size* is
1169+
specified, it should be an integer, and at most *size* items
1170+
(bytes/characters) will be read.
1171+
1172+
For example::
1173+
1174+
def read_it(reader: Reader[str]):
1175+
data = reader.read(11)
1176+
assert isinstance(data, str)
1177+
1178+
.. class:: Writer[T]
1179+
1180+
Generic protocol for writing to a file or other output stream. ``T`` will
1181+
usually be :class:`str` or :class:`bytes`, but can be any type that can be
1182+
written to the stream.
1183+
1184+
.. versionadded:: next
1185+
1186+
.. method:: write(data, /)
1187+
1188+
Write *data* to the output stream and return the number of items
1189+
(bytes/characters) written.
1190+
1191+
For example::
1192+
1193+
def write_binary(writer: Writer[bytes]):
1194+
writer.write(b"Hello world!\n")
1195+
1196+
See :ref:`typing-io` for other I/O related protocols and classes that can be
1197+
used for static type checking.
1198+
11501199
Performance
11511200
-----------
11521201

Doc/library/typing.rst

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,17 +2834,35 @@ with :func:`@runtime_checkable <runtime_checkable>`.
28342834
An ABC with one abstract method ``__round__``
28352835
that is covariant in its return type.
28362836

2837-
ABCs for working with IO
2838-
------------------------
2837+
.. _typing-io:
2838+
2839+
ABCs and Protocols for working with I/O
2840+
---------------------------------------
28392841

2840-
.. class:: IO
2841-
TextIO
2842-
BinaryIO
2842+
.. class:: IO[AnyStr]
2843+
TextIO[AnyStr]
2844+
BinaryIO[AnyStr]
28432845

2844-
Generic type ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])``
2846+
Generic class ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])``
28452847
and ``BinaryIO(IO[bytes])``
28462848
represent the types of I/O streams such as returned by
2847-
:func:`open`.
2849+
:func:`open`. Please note that these classes are not protocols, and
2850+
their interface is fairly broad.
2851+
2852+
The protocols :class:`io.Reader` and :class:`io.Writer` offer a simpler
2853+
alternative for argument types, when only the ``read()`` or ``write()``
2854+
methods are accessed, respectively::
2855+
2856+
def read_and_write(reader: Reader[str], writer: Writer[bytes]):
2857+
data = reader.read()
2858+
writer.write(data.encode())
2859+
2860+
Also consider using :class:`collections.abc.Iterable` for iterating over
2861+
the lines of an input stream::
2862+
2863+
def read_config(stream: Iterable[str]):
2864+
for line in stream:
2865+
...
28482866

28492867
Functions and decorators
28502868
------------------------

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,11 @@ io
619619
:exc:`BlockingIOError` if the operation cannot immediately return bytes.
620620
(Contributed by Giovanni Siragusa in :gh:`109523`.)
621621

622+
* Add protocols :class:`io.Reader` and :class:`io.Writer` as a simpler
623+
alternatives to the pseudo-protocols :class:`typing.IO`,
624+
:class:`typing.TextIO`, and :class:`typing.BinaryIO`.
625+
(Contributed by Sebastian Rittau in :gh:`127648`.)
626+
622627

623628
json
624629
----

Lib/_pyio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
_setmode = None
1717

1818
import io
19-
from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END) # noqa: F401
19+
from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END, Reader, Writer) # noqa: F401
2020

2121
valid_seek_flags = {0, 1, 2} # Hardwired values
2222
if hasattr(os, 'SEEK_HOLE') :

Lib/io.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@
4646
"BufferedReader", "BufferedWriter", "BufferedRWPair",
4747
"BufferedRandom", "TextIOBase", "TextIOWrapper",
4848
"UnsupportedOperation", "SEEK_SET", "SEEK_CUR", "SEEK_END",
49-
"DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder"]
49+
"DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder",
50+
"Reader", "Writer"]
5051

5152

5253
import _io
5354
import abc
5455

56+
from _collections_abc import _check_methods
5557
from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation,
5658
open, open_code, FileIO, BytesIO, StringIO, BufferedReader,
5759
BufferedWriter, BufferedRWPair, BufferedRandom,
@@ -97,3 +99,55 @@ class TextIOBase(_io._TextIOBase, IOBase):
9799
pass
98100
else:
99101
RawIOBase.register(_WindowsConsoleIO)
102+
103+
#
104+
# Static Typing Support
105+
#
106+
107+
GenericAlias = type(list[int])
108+
109+
110+
class Reader(metaclass=abc.ABCMeta):
111+
"""Protocol for simple I/O reader instances.
112+
113+
This protocol only supports blocking I/O.
114+
"""
115+
116+
__slots__ = ()
117+
118+
@abc.abstractmethod
119+
def read(self, size=..., /):
120+
"""Read data from the input stream and return it.
121+
122+
If *size* is specified, at most *size* items (bytes/characters) will be
123+
read.
124+
"""
125+
126+
@classmethod
127+
def __subclasshook__(cls, C):
128+
if cls is Reader:
129+
return _check_methods(C, "read")
130+
return NotImplemented
131+
132+
__class_getitem__ = classmethod(GenericAlias)
133+
134+
135+
class Writer(metaclass=abc.ABCMeta):
136+
"""Protocol for simple I/O writer instances.
137+
138+
This protocol only supports blocking I/O.
139+
"""
140+
141+
__slots__ = ()
142+
143+
@abc.abstractmethod
144+
def write(self, data, /):
145+
"""Write *data* to the output stream and return the number of items written."""
146+
147+
@classmethod
148+
def __subclasshook__(cls, C):
149+
if cls is Writer:
150+
return _check_methods(C, "write")
151+
return NotImplemented
152+
153+
__class_getitem__ = classmethod(GenericAlias)

Lib/test/test_io.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4916,6 +4916,24 @@ class PySignalsTest(SignalsTest):
49164916
test_reentrant_write_text = None
49174917

49184918

4919+
class ProtocolsTest(unittest.TestCase):
4920+
class MyReader:
4921+
def read(self, sz=-1):
4922+
return b""
4923+
4924+
class MyWriter:
4925+
def write(self, b: bytes):
4926+
pass
4927+
4928+
def test_reader_subclass(self):
4929+
self.assertIsSubclass(MyReader, io.Reader[bytes])
4930+
self.assertNotIsSubclass(str, io.Reader[bytes])
4931+
4932+
def test_writer_subclass(self):
4933+
self.assertIsSubclass(MyWriter, io.Writer[bytes])
4934+
self.assertNotIsSubclass(str, io.Writer[bytes])
4935+
4936+
49194937
def load_tests(loader, tests, pattern):
49204938
tests = (CIOTest, PyIOTest, APIMismatchTest,
49214939
CBufferedReaderTest, PyBufferedReaderTest,

Lib/test/test_typing.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from functools import lru_cache, wraps, reduce
77
import gc
88
import inspect
9+
import io
910
import itertools
1011
import operator
1112
import os
@@ -4294,6 +4295,40 @@ def __release_buffer__(self, mv: memoryview) -> None:
42944295
self.assertNotIsSubclass(C, ReleasableBuffer)
42954296
self.assertNotIsInstance(C(), ReleasableBuffer)
42964297

4298+
def test_io_reader_protocol_allowed(self):
4299+
@runtime_checkable
4300+
class CustomReader(io.Reader[bytes], Protocol):
4301+
def close(self): ...
4302+
4303+
class A: pass
4304+
class B:
4305+
def read(self, sz=-1):
4306+
return b""
4307+
def close(self):
4308+
pass
4309+
4310+
self.assertIsSubclass(B, CustomReader)
4311+
self.assertIsInstance(B(), CustomReader)
4312+
self.assertNotIsSubclass(A, CustomReader)
4313+
self.assertNotIsInstance(A(), CustomReader)
4314+
4315+
def test_io_writer_protocol_allowed(self):
4316+
@runtime_checkable
4317+
class CustomWriter(io.Writer[bytes], Protocol):
4318+
def close(self): ...
4319+
4320+
class A: pass
4321+
class B:
4322+
def write(self, b):
4323+
pass
4324+
def close(self):
4325+
pass
4326+
4327+
self.assertIsSubclass(B, CustomWriter)
4328+
self.assertIsInstance(B(), CustomWriter)
4329+
self.assertNotIsSubclass(A, CustomWriter)
4330+
self.assertNotIsInstance(A(), CustomWriter)
4331+
42974332
def test_builtin_protocol_allowlist(self):
42984333
with self.assertRaises(TypeError):
42994334
class CustomProtocol(TestCase, Protocol):

Lib/typing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,7 @@ def _allow_reckless_class_checks(depth=2):
18761876
'Reversible', 'Buffer',
18771877
],
18781878
'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'],
1879+
'io': ['Reader', 'Writer'],
18791880
'os': ['PathLike'],
18801881
}
18811882

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add protocols :class:`io.Reader` and :class:`io.Writer` as
2+
alternatives to :class:`typing.IO`, :class:`typing.TextIO`, and
3+
:class:`typing.BinaryIO`.

0 commit comments

Comments
 (0)
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