diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 46c8e7452004ed..d328b0ed735ae4 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 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/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.