Skip to content

Commit 42b48ce

Browse files
AlexWaygoodcarljm
authored andcommitted
pythongh-104555: Runtime-checkable protocols: Don't let previous calls to isinstance() influence whether issubclass() raises an exception (python#104559)
Co-authored-by: Carl Meyer <[email protected]>
1 parent 2bfdbe8 commit 42b48ce

File tree

3 files changed

+96
-7
lines changed

3 files changed

+96
-7
lines changed

Lib/test/test_typing.py

+76
Original file line numberDiff line numberDiff line change
@@ -2695,6 +2695,82 @@ class D(PNonCall): ...
26952695
with self.assertRaises(TypeError):
26962696
issubclass(D, PNonCall)
26972697

2698+
def test_no_weird_caching_with_issubclass_after_isinstance(self):
2699+
@runtime_checkable
2700+
class Spam(Protocol):
2701+
x: int
2702+
2703+
class Eggs:
2704+
def __init__(self) -> None:
2705+
self.x = 42
2706+
2707+
self.assertIsInstance(Eggs(), Spam)
2708+
2709+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
2710+
# TypeError wouldn't be raised here,
2711+
# as the cached result of the isinstance() check immediately above
2712+
# would mean the issubclass() call would short-circuit
2713+
# before we got to the "raise TypeError" line
2714+
with self.assertRaises(TypeError):
2715+
issubclass(Eggs, Spam)
2716+
2717+
def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
2718+
@runtime_checkable
2719+
class Spam(Protocol):
2720+
x: int
2721+
2722+
class Eggs: ...
2723+
2724+
self.assertNotIsInstance(Eggs(), Spam)
2725+
2726+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
2727+
# TypeError wouldn't be raised here,
2728+
# as the cached result of the isinstance() check immediately above
2729+
# would mean the issubclass() call would short-circuit
2730+
# before we got to the "raise TypeError" line
2731+
with self.assertRaises(TypeError):
2732+
issubclass(Eggs, Spam)
2733+
2734+
def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
2735+
@runtime_checkable
2736+
class Spam(Protocol):
2737+
x: int
2738+
2739+
class Eggs:
2740+
def __getattr__(self, attr):
2741+
if attr == "x":
2742+
return 42
2743+
raise AttributeError(attr)
2744+
2745+
self.assertNotIsInstance(Eggs(), Spam)
2746+
2747+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
2748+
# TypeError wouldn't be raised here,
2749+
# as the cached result of the isinstance() check immediately above
2750+
# would mean the issubclass() call would short-circuit
2751+
# before we got to the "raise TypeError" line
2752+
with self.assertRaises(TypeError):
2753+
issubclass(Eggs, Spam)
2754+
2755+
def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self):
2756+
@runtime_checkable
2757+
class Spam[T](Protocol):
2758+
x: T
2759+
2760+
class Eggs[T]:
2761+
def __init__(self, x: T) -> None:
2762+
self.x = x
2763+
2764+
self.assertIsInstance(Eggs(42), Spam)
2765+
2766+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
2767+
# TypeError wouldn't be raised here,
2768+
# as the cached result of the isinstance() check immediately above
2769+
# would mean the issubclass() call would short-circuit
2770+
# before we got to the "raise TypeError" line
2771+
with self.assertRaises(TypeError):
2772+
issubclass(Eggs, Spam)
2773+
26982774
def test_protocols_isinstance(self):
26992775
T = TypeVar('T')
27002776

Lib/typing.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -1775,8 +1775,8 @@ def _pickle_pskwargs(pskwargs):
17751775

17761776

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

1789+
def __subclasscheck__(cls, other):
1790+
if (
1791+
getattr(cls, '_is_protocol', False)
1792+
and not cls.__callable_proto_members_only__
1793+
and not _allow_reckless_class_checks(depth=2)
1794+
):
1795+
raise TypeError(
1796+
"Protocols with non-method members don't support issubclass()"
1797+
)
1798+
return super().__subclasscheck__(other)
1799+
17891800
def __instancecheck__(cls, instance):
17901801
# We need this method for situations where attributes are
17911802
# assigned in __init__.
@@ -1869,11 +1880,6 @@ def _proto_hook(other):
18691880
raise TypeError("Instance and class checks can only be used with"
18701881
" @runtime_checkable protocols")
18711882

1872-
if not cls.__callable_proto_members_only__ :
1873-
if _allow_reckless_class_checks():
1874-
return NotImplemented
1875-
raise TypeError("Protocols with non-method members"
1876-
" don't support issubclass()")
18771883
if not isinstance(other, type):
18781884
# Same error message as for issubclass(1, int).
18791885
raise TypeError('issubclass() arg 1 must be a class')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix issue where an :func:`issubclass` check comparing a class ``X`` against a
2+
:func:`runtime-checkable protocol <typing.runtime_checkable>` ``Y`` with
3+
non-callable members would not cause :exc:`TypeError` to be raised if an
4+
:func:`isinstance` call had previously been made comparing an instance of ``X``
5+
to ``Y``. This issue was present in edge cases on Python 3.11, but became more
6+
prominent in 3.12 due to some unrelated changes that were made to
7+
runtime-checkable protocols. Patch by Alex Waygood.

0 commit comments

Comments
 (0)