Skip to content

Commit

Permalink
gh-105237: Allow calling issubclass(X, typing.Protocol) again (#105239
Browse files Browse the repository at this point in the history
)
  • Loading branch information
AlexWaygood authored Jun 5, 2023
1 parent 69d1245 commit cdfb201
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 0 deletions.
59 changes: 59 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2758,6 +2758,65 @@ def x(self): ...
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, BadPG)

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, <subclass>)` 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)

def test_protocols_issubclass_non_callable(self):
class C:
x = 1
Expand Down
4 changes: 4 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,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')
Expand All @@ -1803,6 +1805,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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix longstanding bug where ``issubclass(object, typing.Protocol)`` would
evaluate to ``True`` in some edge cases. Patch by Alex Waygood.

0 comments on commit cdfb201

Please sign in to comment.