Skip to content
Merged
72 changes: 72 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2654,6 +2654,78 @@ class D(PNonCall): ...
with self.assertRaises(TypeError):
issubclass(D, PNonCall)

def test_no_weird_caching_with_issubclass_after_isinstance(self):
@runtime_checkable
class Spam(Protocol):
x: int

class Eggs:
def __init__(self) -> None:
self.x = 42

# gh-104555: ABCMeta might cache the result of this isinstance check
# if we called super().__instancecheck__ in the wrong place
# in _ProtocolMeta.__instancecheck__...
self.assertIsInstance(Eggs(), Spam)

# ...and if it did, then TypeError wouldn't be raised here!
with self.assertRaises(TypeError):
issubclass(Eggs, Spam)

def test_no_weird_caching_with_issubclass_after_isinstance2(self):
@runtime_checkable
class Spam(Protocol):
x: int

class Eggs: ...

# gh-104555: ABCMeta might cache the result of this isinstance check
# if we called super().__instancecheck__ in the wrong place
# in _ProtocolMeta.__instancecheck__...
self.assertNotIsInstance(Eggs(), Spam)

# ...and if it did, then TypeError wouldn't be raised here!
with self.assertRaises(TypeError):
issubclass(Eggs, Spam)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails on 3.11. We could consider backporting a version of this PR to 3.11, but I'm not sure if that would be a good idea or not. It's a somewhat invasive bugfix.


def test_no_weird_caching_with_issubclass_after_isinstance3(self):
@runtime_checkable
class Spam(Protocol):
x: int

class Eggs:
def __getattr__(self, attr):
if attr == "x":
return 42
raise AttributeError(attr)

# gh-104555: ABCMeta might cache the result of this isinstance check
# if we called super().__instancecheck__ in the wrong place
# in _ProtocolMeta.__instancecheck__...
self.assertNotIsInstance(Eggs(), Spam)

# ...and if it did, then TypeError wouldn't be raised here!
with self.assertRaises(TypeError):
issubclass(Eggs, Spam)

def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self):
@runtime_checkable
class Spam[T](Protocol):
x: T

class Eggs[T]:
def __init__(self, x: T) -> None:
self.x = x

# gh-104555: ABCMeta might cache the result of this isinstance check
# if we called super().__instancecheck__ in the wrong place
# in _ProtocolMeta.__instancecheck__...
self.assertIsInstance(Eggs(42), Spam)

# ...and if it did, then TypeError wouldn't be raised here!
with self.assertRaises(TypeError):
issubclass(Eggs, Spam)

def test_protocols_isinstance(self):
T = TypeVar('T')

Expand Down
20 changes: 13 additions & 7 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1775,8 +1775,8 @@ def _pickle_pskwargs(pskwargs):


class _ProtocolMeta(ABCMeta):
# This metaclass is really unfortunate and exists only because of
# the lack of __instancehook__.
# This metaclass is somewhat unfortunate,
# but is necessary for several reasons...
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
Expand All @@ -1786,6 +1786,17 @@ def __init__(cls, *args, **kwargs):
callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
)

def __subclasscheck__(cls, other):
if (
getattr(cls, '_is_protocol', False)
and not cls.__callable_proto_members_only__
and not _allow_reckless_class_checks(depth=2)
):
raise TypeError(
"Protocols with non-method members don't support issubclass()"
)
return super().__subclasscheck__(other)

def __instancecheck__(cls, instance):
# We need this method for situations where attributes are
# assigned in __init__.
Expand Down Expand Up @@ -1869,11 +1880,6 @@ def _proto_hook(other):
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")

if not cls.__callable_proto_members_only__ :
if _allow_reckless_class_checks():
return NotImplemented
raise TypeError("Protocols with non-method members"
" don't support issubclass()")
if not isinstance(other, type):
# Same error message as for issubclass(1, int).
raise TypeError('issubclass() arg 1 must be a class')
Expand Down