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 13a9c3fc..a424b723 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_extensions.Protocol)`` would erroneously raise + ``TypeError``. Patch by Alex Waygood (backporting the CPython PR + https://github.com/python/cpython/pull/105239). - Allow `Protocol` classes to inherit from `typing_extensions.Buffer` or `collections.abc.Buffer`. Patch by Alex Waygood (backporting https://github.com/python/cpython/pull/104827, by Jelle Zijlstra). diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 5fa9c0c3..37d4c5d7 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 @@ -1931,6 +1932,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): + self.addCleanup(gc.enable) + gc.disable() + + 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 5ac6dcf2..135eefd9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -562,6 +562,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): @@ -573,6 +592,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') @@ -594,6 +615,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) @@ -662,15 +685,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__ @@ -693,8 +707,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 @@ -789,8 +802,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