From dad6e79cd3575211d6a039b38cf96e0fd31682bf Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 9 Apr 2025 12:52:04 +0200 Subject: [PATCH 1/9] Add Reader and Writer protocols Cf. python/cpython#127648 --- CHANGELOG.md | 8 ++++++++ src/test_typing_extensions.py | 26 ++++++++++++++++++++++++ src/typing_extensions.py | 38 +++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7f109c..7b55ab34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Unreleased +New features: + +- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by + Sebastian Rittau. + +Bugfixes: + - Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a `typing.TypeAliasType` on Python 3.12 and 3.13. Patch by [Joren Hammudoglu](https://github.com/jorenham). @@ -7,6 +14,7 @@ # Release 4.13.1 (April 3, 2025) Bugfixes: + - Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`. Patch by [Daraan](https://github.com/Daraan). - Fix `TypeError` when using `evaluate_forward_ref` on Python 3.10.1-2 and 3.9.8-10. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b8f5d4b7..9583a4cd 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4088,6 +4088,32 @@ def foo(self): pass self.assertIsSubclass(Bar, Functor) +class SpecificProtocolTests(BaseTestCase): + def test_reader_runtime_checkable(self): + class MyReader: + def read(self, n: int) -> bytes: + return b"" + + class WrongReader: + def readx(self, n: int) -> bytes: + return b"" + + assert isinstance(MyReader(), typing_extensions.Reader) + assert not isinstance(WrongReader(), typing_extensions.Reader) + + def test_writer_runtime_checkable(self): + class MyWriter: + def write(self, b: bytes) -> int: + return 0 + + class WrongWriter: + def writex(self, b: bytes) -> int: + return 0 + + assert isinstance(MyWriter(), typing_extensions.Writer) + assert not isinstance(WrongWriter(), typing_extensions.Writer) + + class Point2DGeneric(Generic[T], TypedDict): a: T b: T diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c6c3b88e..f26a3582 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -6,6 +6,7 @@ import enum import functools import inspect +import io import keyword import operator import sys @@ -56,6 +57,8 @@ 'SupportsIndex', 'SupportsInt', 'SupportsRound', + 'Reader', + 'Writer', # One-off things. 'Annotated', @@ -863,6 +866,41 @@ def __round__(self, ndigits: int = 0) -> T_co: pass +if hasattr(io, "Reader") and hasattr(io, "Writer"): + Reader = io.Reader + Writer = io.Writer +else: + @runtime_checkable + class Reader(Protocol[T_co]): + """Protocol for simple I/O reader instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def read(self, size: int = ..., /) -> T_co: + """Read data from the input stream and return it. + + If *size* is specified, at most *size* items (bytes/characters) will be + read. + """ + + @runtime_checkable + class Writer(Protocol[T_contra]): + """Protocol for simple I/O writer instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def write(self, data: T_contra, /) -> int: + """Write *data* to the output stream and return the number of items written.""" + + def _ensure_subclassable(mro_entries): def inner(func): if sys.implementation.name == "pypy" and sys.version_info < (3, 9): From 56bb7aa01f742861b6ca355e9234353ab4aba3eb Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 9 Apr 2025 12:54:56 +0200 Subject: [PATCH 2/9] Satisfy ruff --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f26a3582..cbe19abd 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -883,7 +883,7 @@ class Reader(Protocol[T_co]): def read(self, size: int = ..., /) -> T_co: """Read data from the input stream and return it. - If *size* is specified, at most *size* items (bytes/characters) will be + If size is specified, at most size items (bytes/characters) will be read. """ @@ -898,7 +898,7 @@ class Writer(Protocol[T_contra]): @abc.abstractmethod def write(self, data: T_contra, /) -> int: - """Write *data* to the output stream and return the number of items written.""" + """Write data to the output stream and return the number of items written.""" def _ensure_subclassable(mro_entries): From 14f72d5becf03af56bdd3cd36dd82da6f3abc4ed Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 28 Apr 2025 14:29:23 +0200 Subject: [PATCH 3/9] Fix CHANGELOG --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700afce5..8f9523f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,13 @@ # Unreleased -New features: - -- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by - Sebastian Rittau. +- Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). -Bugfixes: +New features: -- Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). - Add support for inline typed dictionaries ([PEP 764](https://peps.python.org/pep-0764/)). Patch by [Victorien Plot](https://github.com/Viicos). +- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by + Sebastian Rittau. # Release 4.13.2 (April 10, 2025) From 687d45312f68ff35955843ffa8d57b2d3bdc852d Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 28 Apr 2025 14:30:07 +0200 Subject: [PATCH 4/9] Remove merge artifact Co-authored-by: Alex Waygood --- src/typing_extensions.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c938f818..e2708b55 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -884,19 +884,6 @@ def write(self, data: T_contra, /) -> int: """Write data to the output stream and return the number of items written.""" -def _ensure_subclassable(mro_entries): - def inner(func): - if sys.implementation.name == "pypy" and sys.version_info < (3, 9): - cls_dict = { - "__call__": staticmethod(func), - "__mro_entries__": staticmethod(mro_entries) - } - t = type(func.__name__, (), cls_dict) - return functools.update_wrapper(t(), func) - else: - func.__mro_entries__ = mro_entries - return func - return inner _NEEDS_SINGLETONMETA = ( From 9e72db2003ee71707159b85896d313b9bb86f6b6 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 28 Apr 2025 14:45:32 +0200 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: Alex Waygood --- src/test_typing_extensions.py | 8 ++++---- src/typing_extensions.py | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 16c072ca..01e2b270 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4113,8 +4113,8 @@ class WrongReader: def readx(self, n: int) -> bytes: return b"" - assert isinstance(MyReader(), typing_extensions.Reader) - assert not isinstance(WrongReader(), typing_extensions.Reader) + self.assertIsInstance(MyReader(), typing_extensions.Reader) + self.assertNotIsInstance(WrongReader(), typing_extensions.Reader) def test_writer_runtime_checkable(self): class MyWriter: @@ -4125,8 +4125,8 @@ class WrongWriter: def writex(self, b: bytes) -> int: return 0 - assert isinstance(MyWriter(), typing_extensions.Writer) - assert not isinstance(WrongWriter(), typing_extensions.Writer) + self.assertIsInstance(MyWriter(), typing_extensions.Writer) + self.assertNotIsInstance(WrongWriter(), typing_extensions.Writer) class Point2DGeneric(Generic[T], TypedDict): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index e2708b55..2b82a110 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -884,8 +884,6 @@ def write(self, data: T_contra, /) -> int: """Write data to the output stream and return the number of items written.""" - - _NEEDS_SINGLETONMETA = ( not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") ) From 555ca2fb907c9718cf9dd882a79ec8c39b43b082 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 28 Apr 2025 16:06:21 +0200 Subject: [PATCH 6/9] Add doc entry --- doc/index.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/index.rst b/doc/index.rst index e652c9e4..325182eb 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -659,6 +659,18 @@ Protocols .. versionadded:: 4.6.0 +.. class:: Reader + + See :py:class:`io.Reader`. Added to the standard library in Python 3.14. + + .. versionadded:: 4.14.0 + +.. class:: Writer + + See :py:class:`io.Writer`. Added to the standard library in Python 3.14. + + .. versionadded:: 4.14.0 + Decorators ~~~~~~~~~~ From da0587b07ddcffa6f562d5155e011df17d6e83b9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 28 Apr 2025 16:53:17 +0200 Subject: [PATCH 7/9] Link to Python 3.14 docs --- doc/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index cbb15a70..db9b5185 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,7 +27,9 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -intersphinx_mapping = {'py': ('https://docs.python.org/3', None)} +# This should usually point to /3, unless there is a necessity to link to +# features in future versions of Python. +intersphinx_mapping = {'py': ('https://docs.python.org/3.14', None)} add_module_names = False From b59461b5cf108a4534dc60a61bcd07ab241f3c8c Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 29 Apr 2025 14:09:31 +0200 Subject: [PATCH 8/9] Use italics in docstring Co-authored-by: Alex Waygood --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 2b82a110..3c54f3c6 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -866,7 +866,7 @@ class Reader(Protocol[T_co]): def read(self, size: int = ..., /) -> T_co: """Read data from the input stream and return it. - If size is specified, at most size items (bytes/characters) will be + If *size* is specified, at most *size* items (bytes/characters) will be read. """ @@ -881,7 +881,7 @@ class Writer(Protocol[T_contra]): @abc.abstractmethod def write(self, data: T_contra, /) -> int: - """Write data to the output stream and return the number of items written.""" + """Write *data* to the output stream and return the number of items written.""" _NEEDS_SINGLETONMETA = ( From f6e2fb060a7bbb1b16259632cc558d05d33990d9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 29 Apr 2025 14:17:08 +0200 Subject: [PATCH 9/9] Silence flake8 --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 3c54f3c6..f2bee507 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -881,7 +881,7 @@ class Writer(Protocol[T_contra]): @abc.abstractmethod def write(self, data: T_contra, /) -> int: - """Write *data* to the output stream and return the number of items written.""" + """Write *data* to the output stream and return the number of items written.""" # noqa: E501 _NEEDS_SINGLETONMETA = (