Skip to content

Commit

Permalink
pythongh-104935: typing: Fix interactions between `@runtime_checkable…
Browse files Browse the repository at this point in the history
…` and `Generic` (python#104939)

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
  • Loading branch information
JelleZijlstra and AlexWaygood authored May 25, 2023
1 parent 77d7ec5 commit 2b7027d
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 3 deletions.
42 changes: 42 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 2b7027d

Please sign in to comment.