Skip to content

Commit 9ee227d

Browse files
committed
Fix implementation checks on generic interfaces
Previously checking if a non-generic class implements a generic interface always used to fail, as the latter has a special method (`__class_getitem__`) that the former does't have, which led to false negatives.
1 parent e03adf1 commit 9ee227d

File tree

3 files changed

+59
-1
lines changed

3 files changed

+59
-1
lines changed

CHANGELOG.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22
------------------
33

44
* Added support for Python 3.10.
5+
* #109: Non-generic implementers of generic interfaces are now correctly checked.
6+
7+
Previously checking if a non-generic class implements a generic interface always used to fail, as
8+
the latter has a special method (``__class_getitem__``) that the former does't have, which led to
9+
false negatives.
10+
11+
.. code-block:: python
12+
13+
class IFoo(Interface, typing.Generic[T]):
14+
def Bar(self) -> T:
15+
...
16+
17+
18+
@ImplementsInterface(IFoo, no_check=True)
19+
class Foo:
20+
@Implements(IFoo.Bar)
21+
def Bar(self) -> str:
22+
return "baz"
23+
24+
25+
AssertImplements(Foo, IFoo) # Ok now.
26+
527
628
2.1.0 (2021-03-19)
729
------------------

src/oop_ext/interface/_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ def AssertImplementsFullChecking(
747747

748748

749749
# set of methods that might be declared in interfaces but should be not be required by implementations
750-
_INTERFACE_METHODS_TO_IGNORE = {"__init_subclass__"}
750+
_INTERFACE_METHODS_TO_IGNORE = {"__init_subclass__", "__class_getitem__"}
751751

752752

753753
def _AssertImplementsFullChecking(

src/oop_ext/interface/_tests/test_interface.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,3 +1077,39 @@ def AfterCaption(*args):
10771077

10781078
assert Foo.GetCaption() == "Foo"
10791079
assert Foo().GetValues("m") == [0.1, 10.0]
1080+
1081+
1082+
def testGenericInterface() -> None:
1083+
"""Generic interfaces need to support checking on both generic and non-generic implementers"""
1084+
from typing import FrozenSet, Generic, TypeVar
1085+
1086+
T = TypeVar("T", covariant=True)
1087+
1088+
class IFoo(Interface, Generic[T], TypeCheckingSupport):
1089+
# Class instance defined here as a workaround for this class to work in Python 3.6.
1090+
__abstractmethods__: FrozenSet = frozenset()
1091+
1092+
def GetOutput(self) -> T: # type:ignore[empty-body]
1093+
...
1094+
1095+
@ImplementsInterface(IFoo, no_check=True) # Will check later.
1096+
class GenericFoo(Generic[T]):
1097+
def __init__(self, output: T) -> None:
1098+
self.output = output
1099+
1100+
@Implements(IFoo.GetOutput)
1101+
def GetOutput(self) -> T:
1102+
return self.output
1103+
1104+
@ImplementsInterface(IFoo, no_check=True)
1105+
class NonGenericFoo:
1106+
@Implements(IFoo.GetOutput)
1107+
def GetOutput(self) -> int:
1108+
return 1
1109+
1110+
# This works out of the box.
1111+
AssertImplements(GenericFoo, IFoo)
1112+
AssertImplements(GenericFoo[str](output="foo"), IFoo)
1113+
1114+
# This only works if we skip the verification of `__class_getitem__` method.
1115+
AssertImplements(NonGenericFoo, IFoo)

0 commit comments

Comments
 (0)