Skip to content

Commit dad6e79

Browse files
committed
Add Reader and Writer protocols
Cf. python/cpython#127648
1 parent 281d7b0 commit dad6e79

File tree

3 files changed

+72
-0
lines changed

3 files changed

+72
-0
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
# Unreleased
22

3+
New features:
4+
5+
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
6+
Sebastian Rittau.
7+
8+
Bugfixes:
9+
310
- Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a
411
`typing.TypeAliasType` on Python 3.12 and 3.13.
512
Patch by [Joren Hammudoglu](https://github.com/jorenham).
613

714
# Release 4.13.1 (April 3, 2025)
815

916
Bugfixes:
17+
1018
- Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`.
1119
Patch by [Daraan](https://github.com/Daraan).
1220
- Fix `TypeError` when using `evaluate_forward_ref` on Python 3.10.1-2 and 3.9.8-10.

src/test_typing_extensions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4088,6 +4088,32 @@ def foo(self): pass
40884088
self.assertIsSubclass(Bar, Functor)
40894089

40904090

4091+
class SpecificProtocolTests(BaseTestCase):
4092+
def test_reader_runtime_checkable(self):
4093+
class MyReader:
4094+
def read(self, n: int) -> bytes:
4095+
return b""
4096+
4097+
class WrongReader:
4098+
def readx(self, n: int) -> bytes:
4099+
return b""
4100+
4101+
assert isinstance(MyReader(), typing_extensions.Reader)
4102+
assert not isinstance(WrongReader(), typing_extensions.Reader)
4103+
4104+
def test_writer_runtime_checkable(self):
4105+
class MyWriter:
4106+
def write(self, b: bytes) -> int:
4107+
return 0
4108+
4109+
class WrongWriter:
4110+
def writex(self, b: bytes) -> int:
4111+
return 0
4112+
4113+
assert isinstance(MyWriter(), typing_extensions.Writer)
4114+
assert not isinstance(WrongWriter(), typing_extensions.Writer)
4115+
4116+
40914117
class Point2DGeneric(Generic[T], TypedDict):
40924118
a: T
40934119
b: T

src/typing_extensions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import enum
77
import functools
88
import inspect
9+
import io
910
import keyword
1011
import operator
1112
import sys
@@ -56,6 +57,8 @@
5657
'SupportsIndex',
5758
'SupportsInt',
5859
'SupportsRound',
60+
'Reader',
61+
'Writer',
5962

6063
# One-off things.
6164
'Annotated',
@@ -863,6 +866,41 @@ def __round__(self, ndigits: int = 0) -> T_co:
863866
pass
864867

865868

869+
if hasattr(io, "Reader") and hasattr(io, "Writer"):
870+
Reader = io.Reader
871+
Writer = io.Writer
872+
else:
873+
@runtime_checkable
874+
class Reader(Protocol[T_co]):
875+
"""Protocol for simple I/O reader instances.
876+
877+
This protocol only supports blocking I/O.
878+
"""
879+
880+
__slots__ = ()
881+
882+
@abc.abstractmethod
883+
def read(self, size: int = ..., /) -> T_co:
884+
"""Read data from the input stream and return it.
885+
886+
If *size* is specified, at most *size* items (bytes/characters) will be
887+
read.
888+
"""
889+
890+
@runtime_checkable
891+
class Writer(Protocol[T_contra]):
892+
"""Protocol for simple I/O writer instances.
893+
894+
This protocol only supports blocking I/O.
895+
"""
896+
897+
__slots__ = ()
898+
899+
@abc.abstractmethod
900+
def write(self, data: T_contra, /) -> int:
901+
"""Write *data* to the output stream and return the number of items written."""
902+
903+
866904
def _ensure_subclassable(mro_entries):
867905
def inner(func):
868906
if sys.implementation.name == "pypy" and sys.version_info < (3, 9):

0 commit comments

Comments
 (0)