Skip to content

Commit e65b036

Browse files
authored
Backport CPython PR 105976 (#252)
1 parent e703629 commit e65b036

File tree

3 files changed

+65
-16
lines changed

3 files changed

+65
-16
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# Unreleased
2+
3+
- Fix bug where a `typing_extensions.Protocol` class that had one or more
4+
non-callable members would raise `TypeError` when `issubclass()`
5+
was called against it, even if it defined a custom `__subclasshook__`
6+
method. The correct behaviour -- which has now been restored -- is not to
7+
raise `TypeError` in these situations if a custom `__subclasshook__` method
8+
is defined. Patch by Alex Waygood (backporting
9+
https://github.com/python/cpython/pull/105976).
10+
111
# Release 4.7.0rc1 (June 21, 2023)
212

313
- Add `typing_extensions.get_protocol_members` and

src/test_typing_extensions.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2713,6 +2713,50 @@ def __subclasshook__(cls, other):
27132713
self.assertIsSubclass(OKClass, C)
27142714
self.assertNotIsSubclass(BadClass, C)
27152715

2716+
@skipIf(
2717+
sys.version_info[:4] == (3, 12, 0, 'beta') and sys.version_info[4] < 4,
2718+
"Early betas of Python 3.12 had a bug"
2719+
)
2720+
def test_custom_subclasshook_2(self):
2721+
@runtime_checkable
2722+
class HasX(Protocol):
2723+
# The presence of a non-callable member
2724+
# would mean issubclass() checks would fail with TypeError
2725+
# if it weren't for the custom `__subclasshook__` method
2726+
x = 1
2727+
2728+
@classmethod
2729+
def __subclasshook__(cls, other):
2730+
return hasattr(other, 'x')
2731+
2732+
class Empty: pass
2733+
2734+
class ImplementsHasX:
2735+
x = 1
2736+
2737+
self.assertIsInstance(ImplementsHasX(), HasX)
2738+
self.assertNotIsInstance(Empty(), HasX)
2739+
self.assertIsSubclass(ImplementsHasX, HasX)
2740+
self.assertNotIsSubclass(Empty, HasX)
2741+
2742+
# isinstance() and issubclass() checks against this still raise TypeError,
2743+
# despite the presence of the custom __subclasshook__ method,
2744+
# as it's not decorated with @runtime_checkable
2745+
class NotRuntimeCheckable(Protocol):
2746+
@classmethod
2747+
def __subclasshook__(cls, other):
2748+
return hasattr(other, 'x')
2749+
2750+
must_be_runtime_checkable = (
2751+
"Instance and class checks can only be used "
2752+
"with @runtime_checkable protocols"
2753+
)
2754+
2755+
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
2756+
issubclass(object, NotRuntimeCheckable)
2757+
with self.assertRaisesRegex(TypeError, must_be_runtime_checkable):
2758+
isinstance(object(), NotRuntimeCheckable)
2759+
27162760
@skip_if_py312b1
27172761
def test_issubclass_fails_correctly(self):
27182762
@runtime_checkable

src/typing_extensions.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -644,14 +644,17 @@ def __init__(cls, *args, **kwargs):
644644
def __subclasscheck__(cls, other):
645645
if cls is Protocol:
646646
return type.__subclasscheck__(cls, other)
647-
if not isinstance(other, type):
648-
# Same error message as for issubclass(1, int).
649-
raise TypeError('issubclass() arg 1 must be a class')
650647
if (
651648
getattr(cls, '_is_protocol', False)
652649
and not _allow_reckless_class_checks()
653650
):
654-
if not cls.__callable_proto_members_only__:
651+
if not isinstance(other, type):
652+
# Same error message as for issubclass(1, int).
653+
raise TypeError('issubclass() arg 1 must be a class')
654+
if (
655+
not cls.__callable_proto_members_only__
656+
and cls.__dict__.get("__subclasshook__") is _proto_hook
657+
):
655658
raise TypeError(
656659
"Protocols with non-method members don't support issubclass()"
657660
)
@@ -752,12 +755,8 @@ def __init_subclass__(cls, *args, **kwargs):
752755
if '__subclasshook__' not in cls.__dict__:
753756
cls.__subclasshook__ = _proto_hook
754757

755-
# We have nothing more to do for non-protocols...
756-
if not cls._is_protocol:
757-
return
758-
759-
# ... otherwise prohibit instantiation.
760-
if cls.__init__ is Protocol.__init__:
758+
# Prohibit instantiation for protocol classes
759+
if cls._is_protocol and cls.__init__ is Protocol.__init__:
761760
cls.__init__ = _no_init
762761

763762
else:
@@ -847,12 +846,8 @@ def __init_subclass__(cls, *args, **kwargs):
847846
if '__subclasshook__' not in cls.__dict__:
848847
cls.__subclasshook__ = _proto_hook
849848

850-
# We have nothing more to do for non-protocols.
851-
if not cls._is_protocol:
852-
return
853-
854-
# Prohibit instantiation
855-
if cls.__init__ is Protocol.__init__:
849+
# Prohibit instantiation for protocol classes
850+
if cls._is_protocol and cls.__init__ is Protocol.__init__:
856851
cls.__init__ = _no_init
857852

858853

0 commit comments

Comments
 (0)