From a0de3a8af685d5d0d0401035685cc7a3da28bc93 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 4 Jun 2023 12:32:42 +0100 Subject: [PATCH 1/4] Backport recent fixes to `Protocol` from 3.12 --- .github/workflows/third_party.yml | 4 +- CHANGELOG.md | 4 ++ src/test_typing_extensions.py | 75 +++++++++++++++++++++++++++++++ src/typing_extensions.py | 38 ++++++++++------ 4 files changed, 105 insertions(+), 16 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 4cc74225..cde11c14 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -265,9 +265,7 @@ jobs: strategy: fail-fast: false matrix: - # TODO: add 3.7 back to this matrix when tests pass on 3.7 again - # (issue #213) - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 39bf2b53..ec4c968b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Fix tests on Python 3.13, which removes support for creating `TypedDict` classes through the keyword-argument syntax. Patch by Jelle Zijlstra. +- Fix a regression introduced in v4.6.3 that meant that + ``issubclass(object, typing.Protocol)`` would erroneously raise + ``TypeError``. Patch by Alex Waygood (backporting the CPython PR + https://github.com/python/cpython/pull/105239). # Release 4.6.3 (June 1, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 62c70be6..c7b73dfc 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,6 +1,7 @@ import sys import os import abc +import gc import io import contextlib import collections @@ -1938,6 +1939,80 @@ def x(self): ... with self.assertRaisesRegex(TypeError, only_classes_allowed): issubclass(1, BadPG) + @skip_if_py312b1 + def test_issubclass_and_isinstance_on_Protocol_itself(self): + class C: + def x(self): pass + + self.assertNotIsSubclass(object, Protocol) + self.assertNotIsInstance(object(), Protocol) + + self.assertNotIsSubclass(str, Protocol) + self.assertNotIsInstance('foo', Protocol) + + self.assertNotIsSubclass(C, Protocol) + self.assertNotIsInstance(C(), Protocol) + + only_classes_allowed = r"issubclass\(\) arg 1 must be a class" + + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, Protocol) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass('foo', Protocol) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(C(), Protocol) + + T = TypeVar('T') + + @runtime_checkable + class EmptyProtocol(Protocol): pass + + @runtime_checkable + class SupportsStartsWith(Protocol): + def startswith(self, x: str) -> bool: ... + + @runtime_checkable + class SupportsX(Protocol[T]): + def x(self): ... + + for proto in EmptyProtocol, SupportsStartsWith, SupportsX: + with self.subTest(proto=proto.__name__): + self.assertIsSubclass(proto, Protocol) + + # gh-105237 / PR #105239: + # check that the presence of Protocol subclasses + # where `issubclass(X, )` evaluates to True + # doesn't influence the result of `issubclass(X, Protocol)` + + self.assertIsSubclass(object, EmptyProtocol) + self.assertIsInstance(object(), EmptyProtocol) + self.assertNotIsSubclass(object, Protocol) + self.assertNotIsInstance(object(), Protocol) + + self.assertIsSubclass(str, SupportsStartsWith) + self.assertIsInstance('foo', SupportsStartsWith) + self.assertNotIsSubclass(str, Protocol) + self.assertNotIsInstance('foo', Protocol) + + self.assertIsSubclass(C, SupportsX) + self.assertIsInstance(C(), SupportsX) + self.assertNotIsSubclass(C, Protocol) + self.assertNotIsInstance(C(), Protocol) + + @skip_if_py312b1 + def test_isinstance_checks_not_at_whim_of_gc(self): + gc.disable() + self.addCleanup(gc.enable) + + with self.assertRaisesRegex( + TypeError, + "Protocols can only inherit from other protocols" + ): + class Foo(collections.abc.Mapping, Protocol): + pass + + self.assertNotIsInstance([], collections.abc.Mapping) + def test_protocols_issubclass_non_callable(self): class C: x = 1 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1b92c396..3f6a9a22 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -561,6 +561,25 @@ def _no_init(self, *args, **kwargs): class _ProtocolMeta(abc.ABCMeta): # This metaclass is somewhat unfortunate, # but is necessary for several reasons... + def __new__(mcls, name, bases, namespace, **kwargs): + if name == "Protocol" and len(bases) < 2: + pass + elif Protocol in bases: + for base in bases: + if not ( + base in {object, typing.Generic} + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, {}) + or ( + isinstance(base, _ProtocolMeta) + and getattr(base, "_is_protocol", False) + ) + ): + raise TypeError( + f"Protocols can only inherit from other protocols, " + f"got {base!r}" + ) + return super().__new__(mcls, name, bases, namespace, **kwargs) + def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) if getattr(cls, "_is_protocol", False): @@ -572,6 +591,8 @@ def __init__(cls, *args, **kwargs): ) def __subclasscheck__(cls, other): + if cls is Protocol: + return type.__subclasscheck__(cls, other) if not isinstance(other, type): # Same error message as for issubclass(1, int). raise TypeError('issubclass() arg 1 must be a class') @@ -593,6 +614,8 @@ def __subclasscheck__(cls, other): def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. + if cls is Protocol: + return type.__instancecheck__(cls, instance) if not getattr(cls, "_is_protocol", False): # i.e., it's a concrete subclass of a protocol return super().__instancecheck__(instance) @@ -661,15 +684,6 @@ def _proto_hook(cls, other): return NotImplemented return True - def _check_proto_bases(cls): - for base in cls.__bases__: - if not (base in (object, typing.Generic) or - base.__module__ in _PROTO_ALLOWLIST and - base.__name__ in _PROTO_ALLOWLIST[base.__module__] or - isinstance(base, _ProtocolMeta) and base._is_protocol): - raise TypeError('Protocols can only inherit from other' - f' protocols, got {repr(base)}') - if sys.version_info >= (3, 8): class Protocol(typing.Generic, metaclass=_ProtocolMeta): __doc__ = typing.Protocol.__doc__ @@ -692,8 +706,7 @@ def __init_subclass__(cls, *args, **kwargs): if not cls._is_protocol: return - # ... otherwise check consistency of bases, and prohibit instantiation. - _check_proto_bases(cls) + # ... otherwise prohibit instantiation. if cls.__init__ is Protocol.__init__: cls.__init__ = _no_init @@ -788,8 +801,7 @@ def __init_subclass__(cls, *args, **kwargs): if not cls._is_protocol: return - # Check consistency of bases. - _check_proto_bases(cls) + # Prohibit instantiation if cls.__init__ is Protocol.__init__: cls.__init__ = _no_init From 8a5191b4abde40155f234a96cd5226aa3f1cc338 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 4 Jun 2023 13:45:57 +0100 Subject: [PATCH 2/4] This isn't cpython --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4c968b..6eb91117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ `TypedDict` classes through the keyword-argument syntax. Patch by Jelle Zijlstra. - Fix a regression introduced in v4.6.3 that meant that - ``issubclass(object, typing.Protocol)`` would erroneously raise + ``issubclass(object, typing_extensions.Protocol)`` would erroneously raise ``TypeError``. Patch by Alex Waygood (backporting the CPython PR https://github.com/python/cpython/pull/105239). From 8bd8afa1d2d11eb3e9620510d5660164222899bf Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 5 Jun 2023 14:18:31 +0100 Subject: [PATCH 3/4] Add cleanup before changing the state --- src/test_typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index c7b73dfc..9344dda6 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2001,8 +2001,8 @@ def x(self): ... @skip_if_py312b1 def test_isinstance_checks_not_at_whim_of_gc(self): - gc.disable() self.addCleanup(gc.enable) + gc.disable() with self.assertRaisesRegex( TypeError, From ada3f099ecd29535eab3ef793ce09e07f124c9b6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 5 Jun 2023 15:00:34 +0100 Subject: [PATCH 4/4] sync with CPython PR --- 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 3f6a9a22..e2629947 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -568,7 +568,7 @@ def __new__(mcls, name, bases, namespace, **kwargs): for base in bases: if not ( base in {object, typing.Generic} - or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, {}) + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, []) or ( isinstance(base, _ProtocolMeta) and getattr(base, "_is_protocol", False)