[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:
Miss Islington (bot) 2023-05-31 10:35:03 -07:00 committed by GitHub
parent 4f477c796c
commit 076f3cda14
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 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):

View file

@ -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...

View file

@ -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.