Skip to content

Commit f9d21b1

Browse files
authored
Backport CPython PR 105152 (#208)
1 parent cef8f0e commit f9d21b1

File tree

3 files changed

+120
-39
lines changed

3 files changed

+120
-39
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Unreleased
22

3+
- Fix a regression introduced in v4.6.0 in the implementation of
4+
runtime-checkable protocols. The regression meant
5+
that doing `class Foo(X, typing_extensions.Protocol)`, where `X` was a class that
6+
had `abc.ABCMeta` as its metaclass, would then cause subsequent
7+
`isinstance(1, X)` calls to erroneously raise `TypeError`. Patch by
8+
Alex Waygood (backporting the CPython PR
9+
https://github.com/python/cpython/pull/105152).
310
- Sync the repository's LICENSE file with that of CPython.
411
`typing_extensions` is distributed under the same license as
512
CPython itself.

src/test_typing_extensions.py

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,7 +1698,7 @@ class NT(NamedTuple):
16981698

16991699
skip_if_py312b1 = skipIf(
17001700
sys.version_info == (3, 12, 0, 'beta', 1),
1701-
"CPython had a bug in 3.12.0b1"
1701+
"CPython had bugs in 3.12.0b1"
17021702
)
17031703

17041704

@@ -1902,40 +1902,75 @@ def x(self): ...
19021902
self.assertIsSubclass(C, P)
19031903
self.assertIsSubclass(C, PG)
19041904
self.assertIsSubclass(BadP, PG)
1905-
with self.assertRaises(TypeError):
1905+
1906+
no_subscripted_generics = (
1907+
"Subscripted generics cannot be used with class and instance checks"
1908+
)
1909+
1910+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
19061911
issubclass(C, PG[T])
1907-
with self.assertRaises(TypeError):
1912+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
19081913
issubclass(C, PG[C])
1909-
with self.assertRaises(TypeError):
1914+
1915+
only_runtime_checkable_protocols = (
1916+
"Instance and class checks can only be used with "
1917+
"@runtime_checkable protocols"
1918+
)
1919+
1920+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
19101921
issubclass(C, BadP)
1911-
with self.assertRaises(TypeError):
1922+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
19121923
issubclass(C, BadPG)
1913-
with self.assertRaises(TypeError):
1924+
1925+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
19141926
issubclass(P, PG[T])
1915-
with self.assertRaises(TypeError):
1927+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
19161928
issubclass(PG, PG[int])
19171929

1930+
only_classes_allowed = r"issubclass\(\) arg 1 must be a class"
1931+
1932+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
1933+
issubclass(1, P)
1934+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
1935+
issubclass(1, PG)
1936+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
1937+
issubclass(1, BadP)
1938+
with self.assertRaisesRegex(TypeError, only_classes_allowed):
1939+
issubclass(1, BadPG)
1940+
19181941
def test_protocols_issubclass_non_callable(self):
19191942
class C:
19201943
x = 1
1944+
19211945
@runtime_checkable
19221946
class PNonCall(Protocol):
19231947
x = 1
1924-
with self.assertRaises(TypeError):
1948+
1949+
non_callable_members_illegal = (
1950+
"Protocols with non-method members don't support issubclass()"
1951+
)
1952+
1953+
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
19251954
issubclass(C, PNonCall)
1955+
19261956
self.assertIsInstance(C(), PNonCall)
19271957
PNonCall.register(C)
1928-
with self.assertRaises(TypeError):
1958+
1959+
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
19291960
issubclass(C, PNonCall)
1961+
19301962
self.assertIsInstance(C(), PNonCall)
1963+
19311964
# check that non-protocol subclasses are not affected
19321965
class D(PNonCall): ...
1966+
19331967
self.assertNotIsSubclass(C, D)
19341968
self.assertNotIsInstance(C(), D)
19351969
D.register(C)
19361970
self.assertIsSubclass(C, D)
19371971
self.assertIsInstance(C(), D)
1938-
with self.assertRaises(TypeError):
1972+
1973+
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
19391974
issubclass(D, PNonCall)
19401975

19411976
def test_no_weird_caching_with_issubclass_after_isinstance(self):
@@ -1954,7 +1989,10 @@ def __init__(self) -> None:
19541989
# as the cached result of the isinstance() check immediately above
19551990
# would mean the issubclass() call would short-circuit
19561991
# before we got to the "raise TypeError" line
1957-
with self.assertRaises(TypeError):
1992+
with self.assertRaisesRegex(
1993+
TypeError,
1994+
"Protocols with non-method members don't support issubclass()"
1995+
):
19581996
issubclass(Eggs, Spam)
19591997

19601998
def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
@@ -1971,7 +2009,10 @@ class Eggs: ...
19712009
# as the cached result of the isinstance() check immediately above
19722010
# would mean the issubclass() call would short-circuit
19732011
# before we got to the "raise TypeError" line
1974-
with self.assertRaises(TypeError):
2012+
with self.assertRaisesRegex(
2013+
TypeError,
2014+
"Protocols with non-method members don't support issubclass()"
2015+
):
19752016
issubclass(Eggs, Spam)
19762017

19772018
def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
@@ -1992,7 +2033,10 @@ def __getattr__(self, attr):
19922033
# as the cached result of the isinstance() check immediately above
19932034
# would mean the issubclass() call would short-circuit
19942035
# before we got to the "raise TypeError" line
1995-
with self.assertRaises(TypeError):
2036+
with self.assertRaisesRegex(
2037+
TypeError,
2038+
"Protocols with non-method members don't support issubclass()"
2039+
):
19962040
issubclass(Eggs, Spam)
19972041

19982042
def test_protocols_isinstance(self):
@@ -2028,13 +2072,24 @@ def __init__(self):
20282072
for proto in P, PG, WeirdProto, WeirdProto2, WeirderProto:
20292073
with self.subTest(klass=klass.__name__, proto=proto.__name__):
20302074
self.assertIsInstance(klass(), proto)
2031-
with self.assertRaises(TypeError):
2075+
2076+
no_subscripted_generics = (
2077+
"Subscripted generics cannot be used with class and instance checks"
2078+
)
2079+
2080+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
20322081
isinstance(C(), PG[T])
2033-
with self.assertRaises(TypeError):
2082+
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
20342083
isinstance(C(), PG[C])
2035-
with self.assertRaises(TypeError):
2084+
2085+
only_runtime_checkable_msg = (
2086+
"Instance and class checks can only be used "
2087+
"with @runtime_checkable protocols"
2088+
)
2089+
2090+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
20362091
isinstance(C(), BadP)
2037-
with self.assertRaises(TypeError):
2092+
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
20382093
isinstance(C(), BadPG)
20392094

20402095
def test_protocols_isinstance_properties_and_descriptors(self):
@@ -2435,12 +2490,13 @@ def __subclasshook__(cls, other):
24352490
self.assertIsSubclass(OKClass, C)
24362491
self.assertNotIsSubclass(BadClass, C)
24372492

2493+
@skip_if_py312b1
24382494
def test_issubclass_fails_correctly(self):
24392495
@runtime_checkable
24402496
class P(Protocol):
24412497
x = 1
24422498
class C: pass
2443-
with self.assertRaises(TypeError):
2499+
with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
24442500
issubclass(C(), P)
24452501

24462502
def test_defining_generic_protocols(self):
@@ -2768,6 +2824,30 @@ def __call__(self, *args: Unpack[Ts]) -> T: ...
27682824
self.assertEqual(Y.__parameters__, ())
27692825
self.assertEqual(Y.__args__, (int, bytes, memoryview))
27702826

2827+
@skip_if_py312b1
2828+
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self):
2829+
# Ensure the cache is empty, or this test won't work correctly
2830+
collections.abc.Sized._abc_registry_clear()
2831+
2832+
class Foo(collections.abc.Sized, Protocol): pass
2833+
2834+
# CPython gh-105144: this previously raised TypeError
2835+
# if a Protocol subclass of Sized had been created
2836+
# before any isinstance() checks against Sized
2837+
self.assertNotIsInstance(1, collections.abc.Sized)
2838+
2839+
@skip_if_py312b1
2840+
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta_2(self):
2841+
# Ensure the cache is empty, or this test won't work correctly
2842+
collections.abc.Sized._abc_registry_clear()
2843+
2844+
class Foo(typing.Sized, Protocol): pass
2845+
2846+
# CPython gh-105144: this previously raised TypeError
2847+
# if a Protocol subclass of Sized had been created
2848+
# before any isinstance() checks against Sized
2849+
self.assertNotIsInstance(1, typing.Sized)
2850+
27712851

27722852
class Point2DGeneric(Generic[T], TypedDict):
27732853
a: T

src/typing_extensions.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ def _caller(depth=2):
547547
Protocol = typing.Protocol
548548
runtime_checkable = typing.runtime_checkable
549549
else:
550-
def _allow_reckless_class_checks(depth=4):
550+
def _allow_reckless_class_checks(depth=3):
551551
"""Allow instance and class checks for special stdlib modules.
552552
The abc and functools modules indiscriminately call isinstance() and
553553
issubclass() on the whole MRO of a user class, which may contain protocols.
@@ -572,14 +572,22 @@ def __init__(cls, *args, **kwargs):
572572
)
573573

574574
def __subclasscheck__(cls, other):
575+
if not isinstance(other, type):
576+
# Same error message as for issubclass(1, int).
577+
raise TypeError('issubclass() arg 1 must be a class')
575578
if (
576579
getattr(cls, '_is_protocol', False)
577-
and not cls.__callable_proto_members_only__
578-
and not _allow_reckless_class_checks(depth=3)
580+
and not _allow_reckless_class_checks()
579581
):
580-
raise TypeError(
581-
"Protocols with non-method members don't support issubclass()"
582-
)
582+
if not cls.__callable_proto_members_only__:
583+
raise TypeError(
584+
"Protocols with non-method members don't support issubclass()"
585+
)
586+
if not getattr(cls, '_is_runtime_protocol', False):
587+
raise TypeError(
588+
"Instance and class checks can only be used with "
589+
"@runtime_checkable protocols"
590+
)
583591
return super().__subclasscheck__(other)
584592

585593
def __instancecheck__(cls, instance):
@@ -591,7 +599,7 @@ def __instancecheck__(cls, instance):
591599

592600
if (
593601
not getattr(cls, '_is_runtime_protocol', False) and
594-
not _allow_reckless_class_checks(depth=2)
602+
not _allow_reckless_class_checks()
595603
):
596604
raise TypeError("Instance and class checks can only be used with"
597605
" @runtime_checkable protocols")
@@ -632,18 +640,6 @@ def _proto_hook(cls, other):
632640
if not cls.__dict__.get('_is_protocol', False):
633641
return NotImplemented
634642

635-
# First, perform various sanity checks.
636-
if not getattr(cls, '_is_runtime_protocol', False):
637-
if _allow_reckless_class_checks():
638-
return NotImplemented
639-
raise TypeError("Instance and class checks can only be used with"
640-
" @runtime_checkable protocols")
641-
642-
if not isinstance(other, type):
643-
# Same error message as for issubclass(1, int).
644-
raise TypeError('issubclass() arg 1 must be a class')
645-
646-
# Second, perform the actual structural compatibility check.
647643
for attr in cls.__protocol_attrs__:
648644
for base in other.__mro__:
649645
# Check if the members appears in the class dictionary...
@@ -658,8 +654,6 @@ def _proto_hook(cls, other):
658654
isinstance(annotations, collections.abc.Mapping)
659655
and attr in annotations
660656
and issubclass(other, (typing.Generic, _ProtocolMeta))
661-
# All subclasses of Generic have an _is_proto attribute on 3.8+
662-
# But not on 3.7
663657
and getattr(other, "_is_protocol", False)
664658
):
665659
break

0 commit comments

Comments
 (0)