mirror of
https://github.com/python/cpython.git
synced 2025-08-08 10:58:51 +00:00
[3.12] gh-105144: Runtime-checkable protocols: move all 'sanity checks' to _ProtocolMeta.__subclasscheck__
(GH-105152) (#105160)
(cherry picked from commit c05c31db8c
)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
4f477c796c
commit
076f3cda14
3 changed files with 111 additions and 37 deletions
|
@ -1,5 +1,6 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
import collections
|
import collections
|
||||||
|
import collections.abc
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import lru_cache, wraps
|
from functools import lru_cache, wraps
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -2722,19 +2723,41 @@ class ProtocolTests(BaseTestCase):
|
||||||
self.assertIsSubclass(C, PG)
|
self.assertIsSubclass(C, PG)
|
||||||
self.assertIsSubclass(BadP, PG)
|
self.assertIsSubclass(BadP, PG)
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
no_subscripted_generics = (
|
||||||
|
"Subscripted generics cannot be used with class and instance checks"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
|
||||||
issubclass(C, PG[T])
|
issubclass(C, PG[T])
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
|
||||||
issubclass(C, PG[C])
|
issubclass(C, PG[C])
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
|
only_runtime_checkable_protocols = (
|
||||||
|
"Instance and class checks can only be used with "
|
||||||
|
"@runtime_checkable protocols"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
|
||||||
issubclass(C, BadP)
|
issubclass(C, BadP)
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
|
||||||
issubclass(C, BadPG)
|
issubclass(C, BadPG)
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
|
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
|
||||||
issubclass(P, PG[T])
|
issubclass(P, PG[T])
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
|
||||||
issubclass(PG, PG[int])
|
issubclass(PG, PG[int])
|
||||||
|
|
||||||
|
only_classes_allowed = r"issubclass\(\) arg 1 must be a class"
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, only_classes_allowed):
|
||||||
|
issubclass(1, P)
|
||||||
|
with self.assertRaisesRegex(TypeError, only_classes_allowed):
|
||||||
|
issubclass(1, PG)
|
||||||
|
with self.assertRaisesRegex(TypeError, only_classes_allowed):
|
||||||
|
issubclass(1, BadP)
|
||||||
|
with self.assertRaisesRegex(TypeError, only_classes_allowed):
|
||||||
|
issubclass(1, BadPG)
|
||||||
|
|
||||||
def test_protocols_issubclass_non_callable(self):
|
def test_protocols_issubclass_non_callable(self):
|
||||||
class C:
|
class C:
|
||||||
x = 1
|
x = 1
|
||||||
|
@ -2743,12 +2766,19 @@ class ProtocolTests(BaseTestCase):
|
||||||
class PNonCall(Protocol):
|
class PNonCall(Protocol):
|
||||||
x = 1
|
x = 1
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
non_callable_members_illegal = (
|
||||||
|
"Protocols with non-method members don't support issubclass()"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
|
||||||
issubclass(C, PNonCall)
|
issubclass(C, PNonCall)
|
||||||
|
|
||||||
self.assertIsInstance(C(), PNonCall)
|
self.assertIsInstance(C(), PNonCall)
|
||||||
PNonCall.register(C)
|
PNonCall.register(C)
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
|
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
|
||||||
issubclass(C, PNonCall)
|
issubclass(C, PNonCall)
|
||||||
|
|
||||||
self.assertIsInstance(C(), PNonCall)
|
self.assertIsInstance(C(), PNonCall)
|
||||||
|
|
||||||
# check that non-protocol subclasses are not affected
|
# check that non-protocol subclasses are not affected
|
||||||
|
@ -2759,7 +2789,8 @@ class ProtocolTests(BaseTestCase):
|
||||||
D.register(C)
|
D.register(C)
|
||||||
self.assertIsSubclass(C, D)
|
self.assertIsSubclass(C, D)
|
||||||
self.assertIsInstance(C(), D)
|
self.assertIsInstance(C(), D)
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
|
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
|
||||||
issubclass(D, PNonCall)
|
issubclass(D, PNonCall)
|
||||||
|
|
||||||
def test_no_weird_caching_with_issubclass_after_isinstance(self):
|
def test_no_weird_caching_with_issubclass_after_isinstance(self):
|
||||||
|
@ -2778,7 +2809,10 @@ class ProtocolTests(BaseTestCase):
|
||||||
# as the cached result of the isinstance() check immediately above
|
# as the cached result of the isinstance() check immediately above
|
||||||
# would mean the issubclass() call would short-circuit
|
# would mean the issubclass() call would short-circuit
|
||||||
# before we got to the "raise TypeError" line
|
# before we got to the "raise TypeError" line
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(
|
||||||
|
TypeError,
|
||||||
|
"Protocols with non-method members don't support issubclass()"
|
||||||
|
):
|
||||||
issubclass(Eggs, Spam)
|
issubclass(Eggs, Spam)
|
||||||
|
|
||||||
def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
|
def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
|
||||||
|
@ -2795,7 +2829,10 @@ class ProtocolTests(BaseTestCase):
|
||||||
# as the cached result of the isinstance() check immediately above
|
# as the cached result of the isinstance() check immediately above
|
||||||
# would mean the issubclass() call would short-circuit
|
# would mean the issubclass() call would short-circuit
|
||||||
# before we got to the "raise TypeError" line
|
# before we got to the "raise TypeError" line
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(
|
||||||
|
TypeError,
|
||||||
|
"Protocols with non-method members don't support issubclass()"
|
||||||
|
):
|
||||||
issubclass(Eggs, Spam)
|
issubclass(Eggs, Spam)
|
||||||
|
|
||||||
def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
|
def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
|
||||||
|
@ -2816,7 +2853,10 @@ class ProtocolTests(BaseTestCase):
|
||||||
# as the cached result of the isinstance() check immediately above
|
# as the cached result of the isinstance() check immediately above
|
||||||
# would mean the issubclass() call would short-circuit
|
# would mean the issubclass() call would short-circuit
|
||||||
# before we got to the "raise TypeError" line
|
# before we got to the "raise TypeError" line
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(
|
||||||
|
TypeError,
|
||||||
|
"Protocols with non-method members don't support issubclass()"
|
||||||
|
):
|
||||||
issubclass(Eggs, Spam)
|
issubclass(Eggs, Spam)
|
||||||
|
|
||||||
def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self):
|
def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self):
|
||||||
|
@ -2835,7 +2875,10 @@ class ProtocolTests(BaseTestCase):
|
||||||
# as the cached result of the isinstance() check immediately above
|
# as the cached result of the isinstance() check immediately above
|
||||||
# would mean the issubclass() call would short-circuit
|
# would mean the issubclass() call would short-circuit
|
||||||
# before we got to the "raise TypeError" line
|
# before we got to the "raise TypeError" line
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(
|
||||||
|
TypeError,
|
||||||
|
"Protocols with non-method members don't support issubclass()"
|
||||||
|
):
|
||||||
issubclass(Eggs, Spam)
|
issubclass(Eggs, Spam)
|
||||||
|
|
||||||
def test_protocols_isinstance(self):
|
def test_protocols_isinstance(self):
|
||||||
|
@ -2883,13 +2926,21 @@ class ProtocolTests(BaseTestCase):
|
||||||
with self.subTest(klass=klass.__name__, proto=proto.__name__):
|
with self.subTest(klass=klass.__name__, proto=proto.__name__):
|
||||||
self.assertIsInstance(klass(), proto)
|
self.assertIsInstance(klass(), proto)
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
no_subscripted_generics = "Subscripted generics cannot be used with class and instance checks"
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
|
||||||
isinstance(C(), PG[T])
|
isinstance(C(), PG[T])
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
|
||||||
isinstance(C(), PG[C])
|
isinstance(C(), PG[C])
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
|
only_runtime_checkable_msg = (
|
||||||
|
"Instance and class checks can only be used "
|
||||||
|
"with @runtime_checkable protocols"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
|
||||||
isinstance(C(), BadP)
|
isinstance(C(), BadP)
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
|
||||||
isinstance(C(), BadPG)
|
isinstance(C(), BadPG)
|
||||||
|
|
||||||
def test_protocols_isinstance_properties_and_descriptors(self):
|
def test_protocols_isinstance_properties_and_descriptors(self):
|
||||||
|
@ -3274,7 +3325,7 @@ class ProtocolTests(BaseTestCase):
|
||||||
|
|
||||||
class C: pass
|
class C: pass
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
|
||||||
issubclass(C(), P)
|
issubclass(C(), P)
|
||||||
|
|
||||||
def test_defining_generic_protocols(self):
|
def test_defining_generic_protocols(self):
|
||||||
|
@ -3654,6 +3705,28 @@ class ProtocolTests(BaseTestCase):
|
||||||
|
|
||||||
Foo() # Previously triggered RecursionError
|
Foo() # Previously triggered RecursionError
|
||||||
|
|
||||||
|
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self):
|
||||||
|
# Ensure the cache is empty, or this test won't work correctly
|
||||||
|
collections.abc.Sized._abc_registry_clear()
|
||||||
|
|
||||||
|
class Foo(collections.abc.Sized, Protocol): pass
|
||||||
|
|
||||||
|
# gh-105144: this previously raised TypeError
|
||||||
|
# if a Protocol subclass of Sized had been created
|
||||||
|
# before any isinstance() checks against Sized
|
||||||
|
self.assertNotIsInstance(1, collections.abc.Sized)
|
||||||
|
|
||||||
|
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta_2(self):
|
||||||
|
# Ensure the cache is empty, or this test won't work correctly
|
||||||
|
collections.abc.Sized._abc_registry_clear()
|
||||||
|
|
||||||
|
class Foo(typing.Sized, Protocol): pass
|
||||||
|
|
||||||
|
# gh-105144: this previously raised TypeError
|
||||||
|
# if a Protocol subclass of Sized had been created
|
||||||
|
# before any isinstance() checks against Sized
|
||||||
|
self.assertNotIsInstance(1, typing.Sized)
|
||||||
|
|
||||||
|
|
||||||
class GenericTests(BaseTestCase):
|
class GenericTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
|
@ -1733,7 +1733,7 @@ def _caller(depth=1, default='__main__'):
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _allow_reckless_class_checks(depth=3):
|
def _allow_reckless_class_checks(depth=2):
|
||||||
"""Allow instance and class checks for special stdlib modules.
|
"""Allow instance and class checks for special stdlib modules.
|
||||||
|
|
||||||
The abc and functools modules indiscriminately call isinstance() and
|
The abc and functools modules indiscriminately call isinstance() and
|
||||||
|
@ -1788,14 +1788,22 @@ class _ProtocolMeta(ABCMeta):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __subclasscheck__(cls, other):
|
def __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')
|
||||||
if (
|
if (
|
||||||
getattr(cls, '_is_protocol', False)
|
getattr(cls, '_is_protocol', False)
|
||||||
and not cls.__callable_proto_members_only__
|
and not _allow_reckless_class_checks()
|
||||||
and not _allow_reckless_class_checks(depth=2)
|
|
||||||
):
|
):
|
||||||
raise TypeError(
|
if not cls.__callable_proto_members_only__:
|
||||||
"Protocols with non-method members don't support issubclass()"
|
raise TypeError(
|
||||||
)
|
"Protocols with non-method members don't support issubclass()"
|
||||||
|
)
|
||||||
|
if not getattr(cls, '_is_runtime_protocol', False):
|
||||||
|
raise TypeError(
|
||||||
|
"Instance and class checks can only be used with "
|
||||||
|
"@runtime_checkable protocols"
|
||||||
|
)
|
||||||
return super().__subclasscheck__(other)
|
return super().__subclasscheck__(other)
|
||||||
|
|
||||||
def __instancecheck__(cls, instance):
|
def __instancecheck__(cls, instance):
|
||||||
|
@ -1807,7 +1815,7 @@ class _ProtocolMeta(ABCMeta):
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not getattr(cls, '_is_runtime_protocol', False) and
|
not getattr(cls, '_is_runtime_protocol', False) and
|
||||||
not _allow_reckless_class_checks(depth=2)
|
not _allow_reckless_class_checks()
|
||||||
):
|
):
|
||||||
raise TypeError("Instance and class checks can only be used with"
|
raise TypeError("Instance and class checks can only be used with"
|
||||||
" @runtime_checkable protocols")
|
" @runtime_checkable protocols")
|
||||||
|
@ -1875,18 +1883,6 @@ class Protocol(Generic, metaclass=_ProtocolMeta):
|
||||||
if not cls.__dict__.get('_is_protocol', False):
|
if not cls.__dict__.get('_is_protocol', False):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
# First, perform various sanity checks.
|
|
||||||
if not getattr(cls, '_is_runtime_protocol', False):
|
|
||||||
if _allow_reckless_class_checks():
|
|
||||||
return NotImplemented
|
|
||||||
raise TypeError("Instance and class checks can only be used with"
|
|
||||||
" @runtime_checkable protocols")
|
|
||||||
|
|
||||||
if not isinstance(other, type):
|
|
||||||
# Same error message as for issubclass(1, int).
|
|
||||||
raise TypeError('issubclass() arg 1 must be a class')
|
|
||||||
|
|
||||||
# Second, perform the actual structural compatibility check.
|
|
||||||
for attr in cls.__protocol_attrs__:
|
for attr in cls.__protocol_attrs__:
|
||||||
for base in other.__mro__:
|
for base in other.__mro__:
|
||||||
# Check if the members appears in the class dictionary...
|
# Check if the members appears in the class dictionary...
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Fix a recent regression in the :mod:`typing` module. The regression meant
|
||||||
|
that doing ``class Foo(X, typing.Protocol)``, where ``X`` was a class that
|
||||||
|
had :class:`abc.ABCMeta` as its metaclass, would then cause subsequent
|
||||||
|
``isinstance(1, X)`` calls to erroneously raise :exc:`TypeError`. Patch by
|
||||||
|
Alex Waygood.
|
Loading…
Add table
Add a link
Reference in a new issue