gh-105974: Revert unintentional behaviour change for protocols with non-callable members and custom __subclasshook__ methods (#105976)

This commit is contained in:
Alex Waygood 2023-06-23 15:59:25 +01:00 committed by GitHub
parent 968435ddb1
commit 9499b0f138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 32 deletions

View File

@ -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):

View File

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

View File

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