gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom __subclasshook__
methods (#105976)
This commit is contained in:
parent
968435ddb1
commit
9499b0f138
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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…
x
Reference in New Issue
Block a user