diff options
author | Alex Waygood <Alex.Waygood@Gmail.com> | 2023-06-23 14:59:25 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-23 14:59:25 (GMT) |
commit | 9499b0f138cc53b9a2590350d0b545d2f69ee126 (patch) | |
tree | 5645c328afb0e9205ae4c8264b48460ca86ec681 /Lib | |
parent | 968435ddb1c1af9333befb26f7970cded8a5c710 (diff) | |
download | cpython-9499b0f138cc53b9a2590350d0b545d2f69ee126.zip cpython-9499b0f138cc53b9a2590350d0b545d2f69ee126.tar.gz cpython-9499b0f138cc53b9a2590350d0b545d2f69ee126.tar.bz2 |
gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom `__subclasshook__` methods (#105976)
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/test/test_typing.py | 40 | ||||
-rw-r--r-- | Lib/typing.py | 65 |
2 files changed, 73 insertions, 32 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ad67568..1df2192 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3477,6 +3477,46 @@ class ProtocolTests(BaseTestCase): self.assertIsSubclass(OKClass, C) self.assertNotIsSubclass(BadClass, C) + def test_custom_subclasshook_2(self): + @runtime_checkable + class HasX(Protocol): + # The presence of a non-callable member + # would mean issubclass() checks would fail with TypeError + # if it weren't for the custom `__subclasshook__` method + x = 1 + + @classmethod + def __subclasshook__(cls, other): + return hasattr(other, 'x') + + class Empty: pass + + class ImplementsHasX: + x = 1 + + self.assertIsInstance(ImplementsHasX(), HasX) + self.assertNotIsInstance(Empty(), HasX) + self.assertIsSubclass(ImplementsHasX, HasX) + self.assertNotIsSubclass(Empty, HasX) + + # isinstance() and issubclass() checks against this still raise TypeError, + # despite the presence of the custom __subclasshook__ method, + # as it's not decorated with @runtime_checkable + class NotRuntimeCheckable(Protocol): + @classmethod + def __subclasshook__(cls, other): + return hasattr(other, 'x') + + must_be_runtime_checkable = ( + "Instance and class checks can only be used " + "with @runtime_checkable protocols" + ) + + with self.assertRaisesRegex(TypeError, must_be_runtime_checkable): + issubclass(object, NotRuntimeCheckable) + with self.assertRaisesRegex(TypeError, must_be_runtime_checkable): + isinstance(object(), NotRuntimeCheckable) + def test_issubclass_fails_correctly(self): @runtime_checkable class P(Protocol): diff --git a/Lib/typing.py b/Lib/typing.py index 1dd9398..9187b74 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1818,14 +1818,17 @@ class _ProtocolMeta(ABCMeta): def __subclasscheck__(cls, other): if cls is Protocol: return type.__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 _allow_reckless_class_checks() ): - if not cls.__callable_proto_members_only__: + if not isinstance(other, type): + # Same error message as for issubclass(1, int). + raise TypeError('issubclass() arg 1 must be a class') + if ( + not cls.__callable_proto_members_only__ + and cls.__dict__.get("__subclasshook__") is _proto_hook + ): raise TypeError( "Protocols with non-method members don't support issubclass()" ) @@ -1869,6 +1872,30 @@ class _ProtocolMeta(ABCMeta): return False +@classmethod +def _proto_hook(cls, other): + if not cls.__dict__.get('_is_protocol', False): + return NotImplemented + + for attr in cls.__protocol_attrs__: + for base in other.__mro__: + # Check if the members appears in the class dictionary... + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + + # ...or in annotations, if it is a sub-protocol. + annotations = getattr(base, '__annotations__', {}) + if (isinstance(annotations, collections.abc.Mapping) and + attr in annotations and + issubclass(other, Generic) and getattr(other, '_is_protocol', False)): + break + else: + return NotImplemented + return True + + class Protocol(Generic, metaclass=_ProtocolMeta): """Base class for protocol classes. @@ -1914,37 +1941,11 @@ class Protocol(Generic, metaclass=_ProtocolMeta): cls._is_protocol = any(b is Protocol for b in cls.__bases__) # Set (or override) the protocol subclass hook. - def _proto_hook(other): - if not cls.__dict__.get('_is_protocol', False): - return NotImplemented - - for attr in cls.__protocol_attrs__: - for base in other.__mro__: - # Check if the members appears in the class dictionary... - if attr in base.__dict__: - if base.__dict__[attr] is None: - return NotImplemented - break - - # ...or in annotations, if it is a sub-protocol. - annotations = getattr(base, '__annotations__', {}) - if (isinstance(annotations, collections.abc.Mapping) and - attr in annotations and - issubclass(other, Generic) and getattr(other, '_is_protocol', False)): - break - else: - return NotImplemented - return True - if '__subclasshook__' not in cls.__dict__: cls.__subclasshook__ = _proto_hook - # We have nothing more to do for non-protocols... - if not cls._is_protocol: - return - - # ... otherwise prohibit instantiation. - if cls.__init__ is Protocol.__init__: + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: cls.__init__ = _no_init_or_replace_init |