From 9ee227df9728eb57eddbdd0298d283d15739b0fc Mon Sep 17 00:00:00 2001 From: Rodrigo Neto Date: Tue, 21 Nov 2023 10:31:02 -0300 Subject: [PATCH] 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. --- CHANGELOG.rst | 22 ++++++++++++ src/oop_ext/interface/_interface.py | 2 +- .../interface/_tests/test_interface.py | 36 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d27262e..eeac99a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,28 @@ ------------------ * Added support for Python 3.10. +* #109: Non-generic implementers of generic interfaces are now correctly checked. + + 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. + + .. code-block:: python + + class IFoo(Interface, typing.Generic[T]): + def Bar(self) -> T: + ... + + + @ImplementsInterface(IFoo, no_check=True) + class Foo: + @Implements(IFoo.Bar) + def Bar(self) -> str: + return "baz" + + + AssertImplements(Foo, IFoo) # Ok now. + 2.1.0 (2021-03-19) ------------------ diff --git a/src/oop_ext/interface/_interface.py b/src/oop_ext/interface/_interface.py index d93994a..c3ac262 100644 --- a/src/oop_ext/interface/_interface.py +++ b/src/oop_ext/interface/_interface.py @@ -747,7 +747,7 @@ def AssertImplementsFullChecking( # set of methods that might be declared in interfaces but should be not be required by implementations -_INTERFACE_METHODS_TO_IGNORE = {"__init_subclass__"} +_INTERFACE_METHODS_TO_IGNORE = {"__init_subclass__", "__class_getitem__"} def _AssertImplementsFullChecking( diff --git a/src/oop_ext/interface/_tests/test_interface.py b/src/oop_ext/interface/_tests/test_interface.py index 6c0706d..270da63 100644 --- a/src/oop_ext/interface/_tests/test_interface.py +++ b/src/oop_ext/interface/_tests/test_interface.py @@ -1077,3 +1077,39 @@ def AfterCaption(*args): assert Foo.GetCaption() == "Foo" assert Foo().GetValues("m") == [0.1, 10.0] + + +def testGenericInterface() -> None: + """Generic interfaces need to support checking on both generic and non-generic implementers""" + from typing import FrozenSet, Generic, TypeVar + + T = TypeVar("T", covariant=True) + + class IFoo(Interface, Generic[T], TypeCheckingSupport): + # Class instance defined here as a workaround for this class to work in Python 3.6. + __abstractmethods__: FrozenSet = frozenset() + + def GetOutput(self) -> T: # type:ignore[empty-body] + ... + + @ImplementsInterface(IFoo, no_check=True) # Will check later. + class GenericFoo(Generic[T]): + def __init__(self, output: T) -> None: + self.output = output + + @Implements(IFoo.GetOutput) + def GetOutput(self) -> T: + return self.output + + @ImplementsInterface(IFoo, no_check=True) + class NonGenericFoo: + @Implements(IFoo.GetOutput) + def GetOutput(self) -> int: + return 1 + + # This works out of the box. + AssertImplements(GenericFoo, IFoo) + AssertImplements(GenericFoo[str](output="foo"), IFoo) + + # This only works if we skip the verification of `__class_getitem__` method. + AssertImplements(NonGenericFoo, IFoo)