From c17b8edd713ce90e16cbc896373b118009e50ce1 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 4 Jun 2023 12:17:23 +0100 Subject: [PATCH 1/4] Ensure `isinstance([], collections.abc.Mapping)` always evaluates to `False` --- Lib/test/test_typing.py | 14 ++++++++++ Lib/typing.py | 28 +++++++++++++------ ...-06-04-12-16-47.gh-issue-105280.srRbCe.rst | 4 +++ 3 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-06-04-12-16-47.gh-issue-105280.srRbCe.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f7114eb1fdbdd9..c4d7c0be92f815 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3,6 +3,7 @@ import collections.abc from collections import defaultdict from functools import lru_cache, wraps +import gc import inspect import itertools import pickle @@ -2758,6 +2759,19 @@ def x(self): ... with self.assertRaisesRegex(TypeError, only_classes_allowed): issubclass(1, BadPG) + def test_isinstance_checks_not_at_whim_of_gc(self): + gc.disable() + self.addCleanup(gc.enable) + + with self.assertRaisesRegex( + TypeError, + "Protocols can only inherit from other protocols" + ): + class Foo(collections.abc.Mapping, Protocol): + pass + + self.assertNotIsInstance([], collections.abc.Mapping) + def test_protocols_issubclass_non_callable(self): class C: x = 1 diff --git a/Lib/typing.py b/Lib/typing.py index f589be7295c755..884c2230e93b76 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1771,6 +1771,25 @@ def _pickle_pskwargs(pskwargs): class _ProtocolMeta(ABCMeta): # This metaclass is somewhat unfortunate, # but is necessary for several reasons... + def __new__(mcls, name, bases, namespace, /, **kwargs): + if name == "Protocol" and bases == (Generic,): + pass + elif Protocol in bases: + for base in bases: + if not ( + base in {object, Generic} + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, {}) + or ( + isinstance(base, _ProtocolMeta) + and getattr(base, "_is_protocol", False) + ) + ): + raise TypeError( + f"Protocols can only inherit from other protocols, " + f"got {base!r}" + ) + return super().__new__(mcls, name, bases, namespace, **kwargs) + def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) if getattr(cls, "_is_protocol", False): @@ -1902,14 +1921,7 @@ def _proto_hook(other): if not cls._is_protocol: return - # ... otherwise check consistency of bases, and prohibit instantiation. - for base in cls.__bases__: - if not (base in (object, Generic) or - base.__module__ in _PROTO_ALLOWLIST and - base.__name__ in _PROTO_ALLOWLIST[base.__module__] or - issubclass(base, Generic) and getattr(base, '_is_protocol', False)): - raise TypeError('Protocols can only inherit from other' - ' protocols, got %r' % base) + # ... otherwise prohibit instantiation. if cls.__init__ is Protocol.__init__: cls.__init__ = _no_init_or_replace_init diff --git a/Misc/NEWS.d/next/Library/2023-06-04-12-16-47.gh-issue-105280.srRbCe.rst b/Misc/NEWS.d/next/Library/2023-06-04-12-16-47.gh-issue-105280.srRbCe.rst new file mode 100644 index 00000000000000..8e469646604316 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-04-12-16-47.gh-issue-105280.srRbCe.rst @@ -0,0 +1,4 @@ +Fix bug where ``isinstance([], collections.abc.Mapping)`` could evaluate to +``True`` if garbage collection happened at the wrong time. The bug was +caused by changes to the implementation of :class:`typing.Protocol` in +Python 3.12. From 612c551870906e0f2a391950e4e7eb33fa0f4601 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 5 Jun 2023 14:17:35 +0100 Subject: [PATCH 2/4] Add the cleanup before changing the state --- Lib/test/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c4d7c0be92f815..4f5db4c0d85d9f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2760,8 +2760,8 @@ def x(self): ... issubclass(1, BadPG) def test_isinstance_checks_not_at_whim_of_gc(self): - gc.disable() self.addCleanup(gc.enable) + gc.disable() with self.assertRaisesRegex( TypeError, From 99ec54cc60b62e0f16eb2751bde1e40021eab43b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 5 Jun 2023 14:24:24 +0100 Subject: [PATCH 3/4] don't needlessly break compatibility with typing_extensions --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 884c2230e93b76..29359e9d9a7499 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1780,7 +1780,7 @@ def __new__(mcls, name, bases, namespace, /, **kwargs): base in {object, Generic} or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, {}) or ( - isinstance(base, _ProtocolMeta) + issubclass(base, Generic) and getattr(base, "_is_protocol", False) ) ): From 55368cd8124af622a02f37a51c9baeabc54cde3e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 5 Jun 2023 14:25:37 +0100 Subject: [PATCH 4/4] nit --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 29359e9d9a7499..49d0698b8b5e5e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1778,7 +1778,7 @@ def __new__(mcls, name, bases, namespace, /, **kwargs): for base in bases: if not ( base in {object, Generic} - or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, {}) + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, []) or ( issubclass(base, Generic) and getattr(base, "_is_protocol", False)