From 2c3c4281077a89884079c23e940c8eebd923bc46 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 08:51:10 -0700 Subject: [PATCH 1/5] gh-104935: Add back typing.Generic._is_protocol --- Lib/test/test_typing.py | 23 +++++++++++++++++++ ...-05-25-08-50-47.gh-issue-104935.-rm1BR.rst | 4 ++++ Objects/typevarobject.c | 5 ++++ 3 files changed, 32 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 46c8e7452004ed..90157389730f62 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2472,6 +2472,29 @@ def f(): self.assertNotIsSubclass(types.FunctionType, P) self.assertNotIsInstance(f, P) + def test_runtime_checkable_pep695(self): + with self.assertRaisesRegex( + TypeError, + "@runtime_checkable can be only applied to protocol classes", + ): + @runtime_checkable + class Foo[T]: ... + + @runtime_checkable + class HasX(Protocol): + x: int + class Bar[T]: + x: T + def __init__(self, x): + self.x = x + class Capybara[T]: + y: str + def __init__(self, y): + self.y = y + + self.assertIsInstance(Bar(1), HasX) + self.assertNotIsInstance(Capybara('a'), HasX) + def test_everything_implements_empty_protocol(self): @runtime_checkable class Empty(Protocol): diff --git a/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst b/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst new file mode 100644 index 00000000000000..9910a3b55f8244 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst @@ -0,0 +1,4 @@ +Fix bugs with the interaction between :func:`typing.runtime_checkable` and +:class:`typing.Generic` that were introduced by the :pep:`695` +implementation. All generic classes now again have an ``_is_protocol`` +attribute. Patch by Jelle Zijlstra. diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 6aa0d8a3bc53be..064a794b01cab4 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -1640,6 +1640,11 @@ int _Py_initialize_generic(PyInterpreterState *interp) MAKE_TYPE(paramspecargs); MAKE_TYPE(paramspeckwargs); #undef MAKE_TYPE + if (PyDict_SetItemString(interp->cached_objects.generic_type->tp_dict, + "_is_protocol", Py_False) < 0) { + return -1; + } + PyType_Modified(interp->cached_objects.generic_type); return 0; } From a61816b14990741acbb5b85c5684b3b126961766 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 09:03:43 -0700 Subject: [PATCH 2/5] Improve tests --- Lib/test/test_typing.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 90157389730f62..5eb6abdddb3c98 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2472,7 +2472,8 @@ def f(): self.assertNotIsSubclass(types.FunctionType, P) self.assertNotIsInstance(f, P) - def test_runtime_checkable_pep695(self): + def test_runtime_checkable_generic_non_protocol(self): + # Make sure this doesn't raise AttributeError with self.assertRaisesRegex( TypeError, "@runtime_checkable can be only applied to protocol classes", @@ -2480,6 +2481,22 @@ def test_runtime_checkable_pep695(self): @runtime_checkable class Foo[T]: ... + def test_runtime_checkable_generic(self): + @runtime_checkable + class Foo[T](Protocol): + def meth(self) -> T: ... + + class Impl: + def meth(self) -> int: ... + + self.assertIsSubclass(Impl, Foo) + + class NotImpl: + def method(self) -> int: ... + + self.assertNotIsSubclass(NotImpl, Foo) + + def test_pep695_generics_can_be_runtime_checkable(self): @runtime_checkable class HasX(Protocol): x: int From 19bbcb05fbee4049058e149ba7913407b14b89c7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 09:12:08 -0700 Subject: [PATCH 3/5] Update Lib/test/test_typing.py Co-authored-by: Alex Waygood --- Lib/test/test_typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5eb6abdddb3c98..c12694811ee27e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2500,10 +2500,12 @@ def test_pep695_generics_can_be_runtime_checkable(self): @runtime_checkable class HasX(Protocol): x: int + class Bar[T]: x: T def __init__(self, x): self.x = x + class Capybara[T]: y: str def __init__(self, y): From f299fbdfd107afd378de3336c5235d6b33d92c1d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 09:17:18 -0700 Subject: [PATCH 4/5] Better approach --- Lib/test/test_typing.py | 10 +++++----- Lib/typing.py | 6 +++--- Objects/typevarobject.c | 5 ----- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c12694811ee27e..d328b0ed735ae4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2485,17 +2485,17 @@ def test_runtime_checkable_generic(self): @runtime_checkable class Foo[T](Protocol): def meth(self) -> T: ... - + class Impl: def meth(self) -> int: ... - + self.assertIsSubclass(Impl, Foo) - + class NotImpl: def method(self) -> int: ... - + self.assertNotIsSubclass(NotImpl, Foo) - + def test_pep695_generics_can_be_runtime_checkable(self): @runtime_checkable class HasX(Protocol): diff --git a/Lib/typing.py b/Lib/typing.py index b32ff0c6ba4e25..85d129b8c887c4 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1894,7 +1894,7 @@ def _proto_hook(other): annotations = getattr(base, '__annotations__', {}) if (isinstance(annotations, collections.abc.Mapping) and attr in annotations and - issubclass(other, Generic) and other._is_protocol): + issubclass(other, Generic) and getattr(other, '_is_protocol', False)): break else: return NotImplemented @@ -1912,7 +1912,7 @@ def _proto_hook(other): 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 base._is_protocol): + issubclass(base, Generic) and getattr(base, '_is_protocol', False)): raise TypeError('Protocols can only inherit from other' ' protocols, got %r' % base) if cls.__init__ is Protocol.__init__: @@ -2059,7 +2059,7 @@ def close(self): ... Warning: this will check only the presence of the required methods, not their type signatures! """ - if not issubclass(cls, Generic) or not cls._is_protocol: + if not issubclass(cls, Generic) or not getattr(cls, '_is_protocol', False): raise TypeError('@runtime_checkable can be only applied to protocol classes,' ' got %r' % cls) cls._is_runtime_protocol = True diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 064a794b01cab4..6aa0d8a3bc53be 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -1640,11 +1640,6 @@ int _Py_initialize_generic(PyInterpreterState *interp) MAKE_TYPE(paramspecargs); MAKE_TYPE(paramspeckwargs); #undef MAKE_TYPE - if (PyDict_SetItemString(interp->cached_objects.generic_type->tp_dict, - "_is_protocol", Py_False) < 0) { - return -1; - } - PyType_Modified(interp->cached_objects.generic_type); return 0; } From 0859dcd9168f10b9083f732817d6495154094e8d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 09:18:34 -0700 Subject: [PATCH 5/5] Update news --- .../Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst b/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst index 9910a3b55f8244..7af52bce2c9185 100644 --- a/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst +++ b/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst @@ -1,4 +1,3 @@ Fix bugs with the interaction between :func:`typing.runtime_checkable` and :class:`typing.Generic` that were introduced by the :pep:`695` -implementation. All generic classes now again have an ``_is_protocol`` -attribute. Patch by Jelle Zijlstra. +implementation. Patch by Jelle Zijlstra.