mirror of
https://github.com/python/cpython.git
synced 2025-08-08 10:58:51 +00:00
[3.12] gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom __subclasshook__
methods (GH-105976) (#106032)
gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom `__subclasshook__` methods (GH-105976)
(cherry picked from commit 9499b0f138
)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
1ffcd49be2
commit
7d6ee298e9
3 changed files with 79 additions and 32 deletions
|
@ -3465,6 +3465,46 @@ class ProtocolTests(BaseTestCase):
|
||||||
self.assertIsSubclass(OKClass, C)
|
self.assertIsSubclass(OKClass, C)
|
||||||
self.assertNotIsSubclass(BadClass, 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):
|
def test_issubclass_fails_correctly(self):
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class P(Protocol):
|
class P(Protocol):
|
||||||
|
|
|
@ -1822,14 +1822,17 @@ class _ProtocolMeta(ABCMeta):
|
||||||
def __subclasscheck__(cls, other):
|
def __subclasscheck__(cls, other):
|
||||||
if cls is Protocol:
|
if cls is Protocol:
|
||||||
return type.__subclasscheck__(cls, other)
|
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 (
|
if (
|
||||||
getattr(cls, '_is_protocol', False)
|
getattr(cls, '_is_protocol', False)
|
||||||
and not _allow_reckless_class_checks()
|
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(
|
raise TypeError(
|
||||||
"Protocols with non-method members don't support issubclass()"
|
"Protocols with non-method members don't support issubclass()"
|
||||||
)
|
)
|
||||||
|
@ -1873,6 +1876,30 @@ class _ProtocolMeta(ABCMeta):
|
||||||
return False
|
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):
|
class Protocol(Generic, metaclass=_ProtocolMeta):
|
||||||
"""Base class for protocol classes.
|
"""Base class for protocol classes.
|
||||||
|
|
||||||
|
@ -1918,37 +1945,11 @@ class Protocol(Generic, metaclass=_ProtocolMeta):
|
||||||
cls._is_protocol = any(b is Protocol for b in cls.__bases__)
|
cls._is_protocol = any(b is Protocol for b in cls.__bases__)
|
||||||
|
|
||||||
# Set (or override) the protocol subclass hook.
|
# 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__:
|
if '__subclasshook__' not in cls.__dict__:
|
||||||
cls.__subclasshook__ = _proto_hook
|
cls.__subclasshook__ = _proto_hook
|
||||||
|
|
||||||
# We have nothing more to do for non-protocols...
|
# Prohibit instantiation for protocol classes
|
||||||
if not cls._is_protocol:
|
if cls._is_protocol and cls.__init__ is Protocol.__init__:
|
||||||
return
|
|
||||||
|
|
||||||
# ... otherwise prohibit instantiation.
|
|
||||||
if cls.__init__ is Protocol.__init__:
|
|
||||||
cls.__init__ = _no_init_or_replace_init
|
cls.__init__ = _no_init_or_replace_init
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Fix bug where a :class:`typing.Protocol` class that had one or more
|
||||||
|
non-callable members would raise :exc:`TypeError` when :func:`issubclass`
|
||||||
|
was called against it, even if it defined a custom ``__subclasshook__``
|
||||||
|
method. The behaviour in Python 3.11 and lower -- which has now been
|
||||||
|
restored -- was not to raise :exc:`TypeError` in these situations if a
|
||||||
|
custom ``__subclasshook__`` method was defined. Patch by Alex Waygood.
|
Loading…
Add table
Add a link
Reference in a new issue