8000 gh-105144: Runtime-checkable protocols: move all 'sanity checks' to `… · python/cpython@c05c31d · GitHub
[go: up one dir, main page]

Skip to content

Commit c05c31d

Browse files
authored
gh-105144: Runtime-checkable protocols: move all 'sanity checks' to _ProtocolMeta.__subclasscheck__ (#105152)
1 parent df396b5 commit c05c31d

File tree

3 files changed

+111
-37
lines changed

3 files changed

+111
-37
lines changed

Lib/test/test_typing.py

+91-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import collections
3+
import collections.abc
34
from collections import defaultdict
45
from functools import lru_cache, wraps
56
import inspect
@@ -2722,19 +2723,41 @@ def x(self): ...
27222723
self.assertIsSubclass(C, PG)
27232724
self.assertIsSubclass(BadP, PG)
27242725

2725-
with self.assertRaises(TypeError):
2726+
no_subscripted_generics = (
2727+
"Subscripted generics cannot be used with class and instance checks"
2728+
)
2729+
2730+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
27262731
issubclass(C, PG[T])
2727-
with self.assertRaises(TypeError):
2732+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
27282733
issubclass(C, PG[C])
2729-
with self.assertRaises(TypeError):
2734+
2735+
only_runtime_checkable_protocols = (
2736+
"Instance and class checks can only be used with "
2737+
"@runtime_checkable protocols"
2738+
)
2739+
2740+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
27302741
issubclass(C, BadP)
2731-
with self.assertRaises(TypeError):
2742+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
27322743
issubclass(C, BadPG)
2733-
with self.assertRaises(TypeError):
2744+
2745+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
27342746
issubclass(P, PG[T])
2735-
with self.assertRaises(TypeError):
2747+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
27362748
issubclass(PG, PG[int])
27372749

2750+
only_classes_allowed = r"issubclass\(\) arg 1 must be a class"
2751+
2752+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2753+
issubclass(1, P)
2754+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2755+
issubclass(1, PG)
2756+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2757+
issubclass(1, BadP)
2758+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
2759+
issubclass(1, BadPG)
2760+
27382761
def test_protocols_issubclass_non_callable(self):
27392762
class C:
27402763
x = 1
@@ -2743,12 +2766,19 @@ class C:
27432766
class PNonCall(Protocol):
27442767
x = 1
27452768

2746-
with self.assertRaises(TypeError):
2769+
non_callable_members_illegal = (
2770+
"Protocols with non-method members don't support issubclass()"
2771+
)
2772+
2773+
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
27472774
issubclass(C, PNonCall)
2775+
27482776
self.assertIsInstance(C(), PNonCall)
27492777
PNonCall.register(C)
2750-
with self.assertRaises(TypeError):
2778+
2779+
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
27512780
issubclass(C, PNonCall)
2781+
27522782
self.assertIsInstance(C(), PNonCall)
27532783

27542784
# check that non-protocol subclasses are not affected
@@ -2759,7 +2789,8 @@ class D(PNonCall): ...
27592789
D.register(C)
27602790
self.assertIsSubclass(C, D)
27612791
self.assertIsInstance(C(), D)
2762-
with self.assertRaises(TypeError):
2792+
2793+
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
27632794
issubclass(D, PNonCall)
27642795

27652796
def test_no_weird_caching_with_issubclass_after_isinstance(self):
@@ -2778,7 +2809,10 @@ def __init__(self) -> None:
27782809
# as the cached result of the isinstance() check immediately above
27792810
# would mean the issubclass() call would short-circuit
27802811
# before we got to the "raise TypeError" line
2781-
with self.assertRaises(TypeError):
2812+
with self.assertRaisesRegex(
2813+
TypeError,
2814+
"Protocols with non-method members don't support issubclass()"
2815+
):
27822816
issubclass(Eggs, Spam)
27832817

27842818
def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
@@ -2795,7 +2829,10 @@ class Eggs: ...
27952829
# as the cached result of the isinstance() check immediately above
27962830
# would mean the issubclass() call would short-circuit
27972831
# before we got to the "raise TypeError" line
2798-
with self.assertRaises(TypeError):
2832+
with self.assertRaisesRegex(
2833+
TypeError,
2834+
"Protocols with non-method members don't support issubclass()"
2835+
):
27992836
issubclass(Eggs, Spam)
28002837

28012838
def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
@@ -2816,7 +2853,10 @@ def __getattr__(self, attr):
28162853
# as the cached result of the isinstance() check immediately above
28172854
# would mean the issubclass() call would short-circuit
28182855
# before we got to the "raise TypeError" line
2819-
with self.assertRaises(TypeError):
2856+
with self.assertRaisesRegex(
2857+
TypeError,
2858+
"Protocols with non-method members don't support issubclass()"
2859+
):
28202860
issubclass(Eggs, Spam)
28212861

28222862
def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self):
@@ -2835,7 +2875,10 @@ def __init__(self, x: T) -> None:
28352875
# as the cached result of the isinstance() check immediately above
28362876
# would mean the issubclass() call would short-circuit
28372877
# before we got to the "raise TypeError" line
2838-
with self.assertRaises(TypeError):
2878+
with self.assertRaisesRegex(
2879+
TypeError,
2880+
"Protocols with non-method members don't support issubclass()"
2881+
):
28392882
issubclass(Eggs, Spam)
28402883

28412884
def test_protocols_isinstance(self):
@@ -2883,13 +2926,21 @@ def __init__(self):
28832926
with self.subTest(klass=klass.__name__, proto=proto.__name__):
28842927
self.assertIsInstance(klass(), proto)
28852928

2886-
with self.assertRaises(TypeError):
2929+
no_subscripted_generics = "Subscripted generics cannot be used with class and instance checks"
2930+
2931+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
28872932
isinstance(C(), PG[T])
2888-
with self.assertRaises(TypeError):
2933+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
28892934
isinstance(C(), PG[C])
2890-
with self.assertRaises(TypeError):
2935+
2936+
only_runtime_checkable_msg = (
2937+
"Instance and class checks can only be used "
2938+
"with @runtime_checkable protocols"
2939+
)
2940+
2941+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
28912942
isinstance(C(), BadP)
2892-
with self.assertRaises(TypeError):
2943+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
28932944
isinstance(C(), BadPG)
28942945

28952946
def test_protocols_isinstance_properties_and_descriptors(self):
@@ -3274,7 +3325,7 @@ class P(Protocol):
32743325

32753326
class C: pass
32763327

3277-
with self.assertRaises(TypeError):
3328+
with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
32783329
issubclass(C(), P)
32793330

32803331
def test_defining_generic_protocols(self):
@@ -3654,6 +3705,28 @@ def __init__(self):
36543705

36553706
Foo() # Previously triggered RecursionError
36563707

3708+
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self):
3709+
# Ensure the cache is empty, or this test won't work correctly
3710+
collections.abc.Sized._abc_registry_clear()
3711+
3712+
class Foo(collections.abc.Sized, Protocol): pass
3713+
3714+
# gh-105144: this previously raised TypeError
3715+
# if a Protocol subclass of Sized had been created
3716+
# before any isinstance() checks against Sized
3717+
self.assertNotIsInstance(1, collections.abc.Sized)
3718+
3719+
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta_2(self):
3720+
# Ensure the cache is empty, or this test won't work correctly
3721+
collections.abc.Sized._abc_registry_clear()
3722+
3723+
class Foo(typing.Sized, Protocol): pass
3724+
3725+
# gh-105144: this previously raised TypeError
3726+
# if a Protocol subclass of Sized had been created
3727+
# before any isinstance() checks against Sized
3728+
self.assertNotIsInstance(1, typing.Sized)
3729+
36573730

36583731
class GenericTests(BaseTestCase):
36593732

Lib/typing.py

+15-19
Original file line numberDiff line numberDiff line change
@@ -1727,7 +1727,7 @@ def _caller(depth=1, default='__main__'):
17271727
pass
17281728
return None
17291729

1730-
def _allow_reckless_class_checks(depth=3):
1730+
def _allow_reckless_class_checks(depth=2):
17311731
"""Allow instance and class checks for special stdlib modules.
17321732
17331733
The abc and functools modules indiscriminately call isinstance() and
@@ -1782,14 +1782,22 @@ def __init__(cls, *args, **kwargs):
17821782
)
17831783

17841784
def __subclasscheck__(cls, other):
1785+
if not isinstance(other, type):
1786+
# Same error message as for issubclass(1, int).
1787+
raise TypeError('issubclass() arg 1 must be a class')
17851788
if (
17861789
getattr(cls, '_is_protocol', False)
1787-
and not cls.__callable_proto_members_only__
1788-
and not _allow_reckless_class_checks(depth=2)
1790+
and not _allow_reckless_class_checks()
17891791
):
1790-
raise TypeError(
1791-
"Protocols with non-method members don't support issubclass()"
1792-
)
1792+
if not cls.__callable_proto_members_only__:
1793+
raise TypeError(
1794+
"Protocols with non-method members don't support issubclass()"
1795+
)
1796+
if not getattr(cls, '_is_runtime_protocol', False):
1797+
raise TypeError(
1798+
"Instance and class checks can only be used with "
1799+
"@runtime_checkable protocols"
1800+
)
17931801
return super().__subclasscheck__(other)
17941802

17951803
def __instancecheck__(cls, instance):
@@ -1801,7 +1809,7 @@ def __instancecheck__(cls, instance):
18011809

18021810
if (
18031811
not getattr(cls, '_is_runtime_protocol', False) and
1804-
not _allow_reckless_class_checks(depth=2)
1812+
not _allow_reckless_class_checks()
18051813
):
18061814
raise TypeError("Instance and class checks can only be used with"
18071815
" @runtime_checkable protocols")
@@ -1869,18 +1877,6 @@ def _proto_hook(other):
18691877
if not cls.__dict__.get('_is_protocol', False):
18701878
return NotImplemented
18711879

1872-
# First, perform various sanity checks.
1873-
if not getattr(cls, '_is_runtime_protocol', False):
1874-
if _allow_reckless_class_checks():
1875-
return NotImplemented
1876-
raise TypeError("Instance and class checks can only be used with"
1877-
" @runtime_checkable protocols")
1878-
1879-
if not isinstance(other, type):
1880-
# Same error message as for issubclass(1, int).
1881-
raise TypeError('issubclass() arg 1 must be a class')
1882-
1883-
# Second, perform the actual structural compatibility check.
18841880
for attr in cls.__protocol_attrs__:
18851881
for base in other.__mro__:
18861882
# Check if the members appears in the class dictionary...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a recent regression in the :mod:`typing` module. The regression meant
2+
that doing ``class Foo(X, typing.Protocol)``, where ``X`` was a class that
3+
had :class:`abc.ABCMeta` as its metaclass, would then cause subsequent
4+
``isinstance(1, X)`` calls to erroneously raise :exc:`TypeError`. Patch by
5+
Alex Waygood.

0 commit comments

Comments
 (0)
0