Skip to content

Commit 5175026

Browse files
[3.12] gh-105237: Allow calling issubclass(X, typing.Protocol) again (GH-105239) (#105316)
gh-105237: Allow calling `issubclass(X, typing.Protocol)` again (GH-105239) (cherry picked from commit cdfb201) Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 6d03541 commit 5175026

File tree

3 files changed

+65
-0
lines changed

3 files changed

+65
-0
lines changed

Lib/test/test_typing.py

+59
Original file line numberDiff line numberDiff line change
@@ -2758,6 +2758,65 @@ def x(self): ...
27582758
with self.assertRaisesRegex(TypeError, only_classes_allowed):
27592759
issubclass(1, BadPG)
27602760

2761+
def test_issubclass_and_isinstance_on_Protocol_itself(self):
2762+
class C:
2763+
def x(self): pass
2764+
2765+
self.assertNotIsSubclass(object, Protocol)
2766+
self.assertNotIsInstance(object(), Protocol)
2767+
2768+
self.assertNotIsSubclass(str, Protocol)
2769+
self.assertNotIsInstance('foo', Protocol)
2770+
2771+
self.assertNotIsSubclass(C, Protocol)
2772+
self.assertNotIsInstance(C(), Protocol)
2773+
2774+
only_classes_allowed = r"issubclass\(\) arg 1 must be a class"
2775+
2776+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2777+
issubclass(1, Protocol)
2778+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2779+
issubclass('foo', Protocol)
2780+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2781+
issubclass(C(), Protocol)
2782+
2783+
T = TypeVar('T')
2784+
2785+
@runtime_checkable
2786+
class EmptyProtocol(Protocol): pass
2787+
2788+
@runtime_checkable
2789+
class SupportsStartsWith(Protocol):
2790+
def startswith(self, x: str) -> bool: ...
2791+
2792+
@runtime_checkable
2793+
class SupportsX(Protocol[T]):
2794+
def x(self): ...
2795+
2796+
for proto in EmptyProtocol, SupportsStartsWith, SupportsX:
2797+
with self.subTest(proto=proto.__name__):
2798+
self.assertIsSubclass(proto, Protocol)
2799+
2800+
# gh-105237 / PR #105239:
2801+
# check that the presence of Protocol subclasses
2802+
# where `issubclass(X, <subclass>)` evaluates to True
2803+
# doesn't influence the result of `issubclass(X, Protocol)`
2804+
2805+
self.assertIsSubclass(object, EmptyProtocol)
2806+
self.assertIsInstance(object(), EmptyProtocol)
2807+
self.assertNotIsSubclass(object, Protocol)
2808+
self.assertNotIsInstance(object(), Protocol)
2809+
2810+
self.assertIsSubclass(str, SupportsStartsWith)
2811+
self.assertIsInstance('foo', SupportsStartsWith)
2812+
self.assertNotIsSubclass(str, Protocol)
2813+
self.assertNotIsInstance('foo', Protocol)
2814+
2815+
self.assertIsSubclass(C, SupportsX)
2816+
self.assertIsInstance(C(), SupportsX)
2817+
self.assertNotIsSubclass(C, Protocol)
2818+
self.assertNotIsInstance(C(), Protocol)
2819+
27612820
def test_protocols_issubclass_non_callable(self):
27622821
class C:
27632822
x = 1

Lib/typing.py

+4
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,8 @@ def __init__(cls, *args, **kwargs):
17881788
)
17891789

17901790
def __subclasscheck__(cls, other):
1791+
if cls is Protocol:
1792+
return type.__subclasscheck__(cls, other)
17911793
if not isinstance(other, type):
17921794
# Same error message as for issubclass(1, int).
17931795
raise TypeError('issubclass() arg 1 must be a class')
@@ -1809,6 +1811,8 @@ def __subclasscheck__(cls, other):
18091811
def __instancecheck__(cls, instance):
18101812
# We need this method for situations where attributes are
18111813
# assigned in __init__.
1814+
if cls is Protocol:
1815+
return type.__instancecheck__(cls, instance)
18121816
if not getattr(cls, "_is_protocol", False):
18131817
# i.e., it's a concrete subclass of a protocol
18141818
return super().__instancecheck__(instance)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix longstanding bug where ``issubclass(object, typing.Protocol)`` would
2+
evaluate to ``True`` in some edge cases. Patch by Alex Waygood.

0 commit comments

Comments
 (0)