From 70cc4e5cb31ec2dae1fb11969e0dfd827b84c05b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 09:43:40 -0700 Subject: [PATCH] gh-104935: typing: Fix interactions between `@runtime_checkable` and `Generic` (GH-104939) --------- (cherry picked from commit 2b7027d0b2ee2e102a24a0da27d01b8221f9351c) Co-authored-by: Jelle Zijlstra Co-authored-by: Alex Waygood --- Lib/test/test_typing.py | 42 +++++++++++++++++++ Lib/typing.py | 6 +-- ...-05-25-08-50-47.gh-issue-104935.-rm1BR.rst | 3 ++ 3 files changed, 48 insertions(+), 3 deletions(-) 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 7fe137f8d7c526..c8bf94307c8011 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2472,6 +2472,48 @@ def f(): self.assertNotIsSubclass(types.FunctionType, P) self.assertNotIsInstance(f, P) + 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", + ): + @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 + + 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/Lib/typing.py b/Lib/typing.py index 88837db4b744ab..dd172b17639b33 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1899,7 +1899,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 @@ -1917,7 +1917,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__: @@ -2064,7 +2064,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/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..7af52bce2c9185 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-25-08-50-47.gh-issue-104935.-rm1BR.rst @@ -0,0 +1,3 @@ +Fix bugs with the interaction between :func:`typing.runtime_checkable` and +:class:`typing.Generic` that were introduced by the :pep:`695` +implementation. Patch by Jelle Zijlstra.