From 6ac4e7d9cd6866413deb05ebcce72c16b5e33ad5 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 15 Jun 2023 19:10:21 +0100 Subject: [PATCH 1/3] Remove dead code in `typing.py` --- Lib/typing.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 1dd9398344639b..6a0f60672fa18d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1925,13 +1925,6 @@ def _proto_hook(other): 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 From dd75fb31ad7fde762453ec50293737effa19f043 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 16 Jun 2023 12:36:14 +0100 Subject: [PATCH 2/3] Fix --- Lib/test/test_typing.py | 48 +++++++++++++++++++++++++++++++++++++++++ Lib/typing.py | 7 ++++++ 2 files changed, 55 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3eb0fcad69e5e5..70786392d1caaa 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2759,6 +2759,54 @@ def x(self): ... with self.assertRaisesRegex(TypeError, only_classes_allowed): issubclass(1, BadPG) + def test_implicit_issubclass_between_two_protocols(self): + @runtime_checkable + class CallableMembersProto(Protocol): + def meth(self): ... + + # All the below protocols should be considered "subclasses" + # of CallableMembersProto at runtime, + # even though none of them explicitly subclass CallableMembersProto + + class IdenticalProto(Protocol): + def meth(self): ... + + class SupersetProto(Protocol): + def meth(self): ... + def meth2(self): ... + + class NonCallableMembersProto(Protocol): + meth: Callable[[], None] + + class NonCallableMembersSupersetProto(Protocol): + meth: Callable[[], None] + meth2: Callable[[str, int], bool] + + class MixedMembersProto1(Protocol): + meth: Callable[[], None] + def meth2(self): ... + + class MixedMembersProto2(Protocol): + def meth(self): ... + meth2: Callable[[str, int], bool] + + for proto in ( + IdenticalProto, SupersetProto, NonCallableMembersProto, + NonCallableMembersSupersetProto, MixedMembersProto1, MixedMembersProto2 + ): + with self.subTest(proto=proto.__name__): + self.assertIsSubclass(proto, CallableMembersProto) + + # These two shouldn't be considered subclasses of CallableMembersProto, however, + # since they don't have the `meth` protocol member + + class EmptyProtocol(Protocol): ... + class UnrelatedProtocol(Protocol): + def wut(self): ... + + self.assertNotIsSubclass(EmptyProtocol, CallableMembersProto) + self.assertNotIsSubclass(UnrelatedProtocol, CallableMembersProto) + def test_isinstance_checks_not_at_whim_of_gc(self): self.addCleanup(gc.enable) gc.disable() diff --git a/Lib/typing.py b/Lib/typing.py index 6a0f60672fa18d..1dd9398344639b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1925,6 +1925,13 @@ def _proto_hook(other): 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 From aad650270bffbee6796c9a1780390474d914c0e1 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 16 Jun 2023 16:20:12 +0100 Subject: [PATCH 3/3] Moar assertions --- Lib/test/test_typing.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 70786392d1caaa..ad67568770970f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2807,6 +2807,32 @@ def wut(self): ... self.assertNotIsSubclass(EmptyProtocol, CallableMembersProto) self.assertNotIsSubclass(UnrelatedProtocol, CallableMembersProto) + # These aren't protocols at all (despite having annotations), + # so they should only be considered subclasses of CallableMembersProto + # if they *actually have an attribute* matching the `meth` member + # (just having an annotation is insufficient) + + class AnnotatedButNotAProtocol: + meth: Callable[[], None] + + class NotAProtocolButAnImplicitSubclass: + def meth(self): pass + + class NotAProtocolButAnImplicitSubclass2: + meth: Callable[[], None] + def meth(self): pass + + class NotAProtocolButAnImplicitSubclass3: + meth: Callable[[], None] + meth2: Callable[[int, str], bool] + def meth(self): pass + def meth(self, x, y): return True + + self.assertNotIsSubclass(AnnotatedButNotAProtocol, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass2, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass3, CallableMembersProto) + def test_isinstance_checks_not_at_whim_of_gc(self): self.addCleanup(gc.enable) gc.disable()