Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e65b036

Browse filesBrowse files
authored
Backport CPython PR 105976 (#252)
1 parent e703629 commit e65b036
Copy full SHA for e65b036

File tree

Expand file treeCollapse file tree

3 files changed

+65
-16
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+65
-16
lines changed

‎CHANGELOG.md

Copy file name to clipboardExpand all lines: CHANGELOG.md
+10Lines 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

Copy file name to clipboardExpand all lines: src/test_typing_extensions.py
+44Lines 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

Copy file name to clipboardExpand all lines: src/typing_extensions.py
+11-16Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.