summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorAlex Waygood <Alex.Waygood@Gmail.com>2023-06-23 14:59:25 (GMT)
committerGitHub <noreply@github.com>2023-06-23 14:59:25 (GMT)
commit9499b0f138cc53b9a2590350d0b545d2f69ee126 (patch)
tree5645c328afb0e9205ae4c8264b48460ca86ec681 /Lib
parent968435ddb1c1af9333befb26f7970cded8a5c710 (diff)
downloadcpython-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.py40
-rw-r--r--Lib/typing.py65
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