gh-105144: Runtime-checkable protocols: move all 'sanity checks' to _ProtocolMeta.__subclasscheck__ (#105152)

This commit is contained in:
Alex Waygood 2023-05-31 18:02:25 +01:00 committed by GitHub
parent df396b59af
commit c05c31db8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 37 deletions

View file

@ -1,5 +1,6 @@
import contextlib
import collections
import collections.abc
from collections import defaultdict
from functools import lru_cache, wraps
import inspect
@ -2722,19 +2723,41 @@ class ProtocolTests(BaseTestCase):
self.assertIsSubclass(C, 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])
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
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)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
issubclass(C, BadPG)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
issubclass(P, PG[T])
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
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):
class C:
x = 1
@ -2743,12 +2766,19 @@ class ProtocolTests(BaseTestCase):
class PNonCall(Protocol):
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)
self.assertIsInstance(C(), PNonCall)
PNonCall.register(C)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
issubclass(C, PNonCall)
self.assertIsInstance(C(), PNonCall)
# check that non-protocol subclasses are not affected
@ -2759,7 +2789,8 @@ class ProtocolTests(BaseTestCase):
D.register(C)
self.assertIsSubclass(C, D)
self.assertIsInstance(C(), D)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
issubclass(D, PNonCall)
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
# would mean the issubclass() call would short-circuit
# 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)
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
# would mean the issubclass() call would short-circuit
# 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)
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
# would mean the issubclass() call would short-circuit
# 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)
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
# would mean the issubclass() call would short-circuit
# 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)
def test_protocols_isinstance(self):
@ -2883,13 +2926,21 @@ class ProtocolTests(BaseTestCase):
with self.subTest(klass=klass.__name__, proto=proto.__name__):
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])
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
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)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
isinstance(C(), BadPG)
def test_protocols_isinstance_properties_and_descriptors(self):
@ -3274,7 +3325,7 @@ class ProtocolTests(BaseTestCase):
class C: pass
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
issubclass(C(), P)
def test_defining_generic_protocols(self):
@ -3654,6 +3705,28 @@ class ProtocolTests(BaseTestCase):
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):