[Python-checkins] [3.12] gh-105144: Runtime-checkable protocols: move all 'sanity checks' to `_ProtocolMeta.__subclasscheck__` (GH-105152) (#105160)

JelleZijlstra webhook-mailer at python.org
Wed May 31 13:35:11 EDT 2023


https://github.com/python/cpython/commit/076f3cda140a45b08c2c9518bc19624aae50d3a3
commit: 076f3cda140a45b08c2c9518bc19624aae50d3a3
branch: 3.12
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: JelleZijlstra <jelle.zijlstra at gmail.com>
date: 2023年05月31日T17:35:03Z
summary:
[3.12] gh-105144: Runtime-checkable protocols: move all 'sanity checks' to `_ProtocolMeta.__subclasscheck__` (GH-105152) (#105160)
(cherry picked from commit c05c31db8c9dfd708b9857bb57f8e5f3ce40266d)
Co-authored-by: Alex Waygood <Alex.Waygood at Gmail.com>
files:
A Misc/NEWS.d/next/Library/2023-05-31-16-58-42.gh-issue-105144.Oqfn0V.rst
M Lib/test/test_typing.py
M Lib/typing.py
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index ae9878f872fd7..5480a981ad564 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -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 @@ def x(self): ...
 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 C:
 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 D(PNonCall): ...
 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 @@ def __init__(self) -> None:
 # 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 Eggs: ...
 # 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 @@ def __getattr__(self, attr):
 # 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 @@ def __init__(self, x: T) -> None:
 # 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 @@ def __init__(self):
 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 P(Protocol):
 
 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 @@ def __init__(self):
 
 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):
 
diff --git a/Lib/typing.py b/Lib/typing.py
index a7b2566b25342..2383d807ec58d 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1733,7 +1733,7 @@ def _caller(depth=1, default='__main__'):
 pass
 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.
 
 The abc and functools modules indiscriminately call isinstance() and
@@ -1788,14 +1788,22 @@ def __init__(cls, *args, **kwargs):
 )
 
 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 (
 getattr(cls, '_is_protocol', False)
- and not cls.__callable_proto_members_only__
- and not _allow_reckless_class_checks(depth=2)
+ and not _allow_reckless_class_checks()
 ):
- raise TypeError(
- "Protocols with non-method members don't support issubclass()"
- )
+ if not cls.__callable_proto_members_only__:
+ 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)
 
 def __instancecheck__(cls, instance):
@@ -1807,7 +1815,7 @@ def __instancecheck__(cls, instance):
 
 if (
 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"
 " @runtime_checkable protocols")
@@ -1875,18 +1883,6 @@ def _proto_hook(other):
 if not cls.__dict__.get('_is_protocol', False):
 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 base in other.__mro__:
 # Check if the members appears in the class dictionary...
diff --git a/Misc/NEWS.d/next/Library/2023-05-31-16-58-42.gh-issue-105144.Oqfn0V.rst b/Misc/NEWS.d/next/Library/2023-05-31-16-58-42.gh-issue-105144.Oqfn0V.rst
new file mode 100644
index 0000000000000..7e4d6fbc4911b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-05-31-16-58-42.gh-issue-105144.Oqfn0V.rst
@@ -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.


More information about the Python-checkins mailing list

AltStyle によって変換されたページ (->オリジナル) /