Repository: ESSS/oop-ext

Issue:
Regression: Abstract/Override/Implement decorators in interface implementations
While updating to `1.1.0`, found a regression involving `Abstract`, `Override`, and `Implements` decorators being used in interface implementations.

In `1.1.0` the decorator implementations were type decorated, however this doesn't play nice with the _runtime_ mechanism used by interfaces to validate implementations against their interface declarations.

Example:

```python
from typing import List

from oop_ext.foundation.decorators import Implements, Abstract
from oop_ext.interface import ImplementsInterface, Interface


class IFoo(Interface):
    def GetValues(self, unit: str) -> List[float]:
        ...

    @classmethod
    def GetCaption(cls) -> str:
        ...


@ImplementsInterface(IFoo)
class AbstractFoo:
    @Implements(IFoo.GetValues)
    @Abstract
    def GetValues(self, unit: str) -> List[float]:
        ...

    @classmethod
    @Implements(IFoo.GetCaption)
    @Abstract
    def GetCaption(cls) -> str:
        ...

```

Fails with:

```
Traceback (most recent call last):
  File "src\foo.py", line 16, in <module>
    @ImplementsInterface(IFoo)
  File "d:\projects\oop-ext\src\oop_ext\interface\_interface.py", line 922, in __call__
    AssertImplements(type_, I)
  File "d:\projects\oop-ext\src\oop_ext\interface\_interface.py", line 417, in AssertImplements
    assert is_implementation, reason
AssertionError:
Method AbstractFoo.GetCaption signature:
  (*args:object, **kwargs:object) -> NoReturn
differs from defined in interface IFoo
  () -> str
```

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f2ece4a..cd58cce 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,9 @@
+1.1.1 (UNRELEASED)
+------------------
+
+* #36: Fix regression introduced in ``1.1.0`` where ``Abstract`` and ``Implements`` decorators
+  could no longer be used in interfaces implementations.
+
 1.1.0 (2021-02-19)
 ------------------
 
diff --git a/src/oop_ext/foundation/decorators.py b/src/oop_ext/foundation/decorators.py
index 289f01f..f1fa06a 100644
--- a/src/oop_ext/foundation/decorators.py
+++ b/src/oop_ext/foundation/decorators.py
@@ -167,7 +167,9 @@ def Abstract(func: F) -> F:
 
     '''
 
-    def AbstractWrapper(self: object, *args: object, **kwargs: object) -> NoReturn:
+    # Make sure to use one of the valid general signatures accepted by AssertImplements
+    # so this decorator can be used in interface implementations.
+    def AbstractWrapper(self: object, *args: object, **kwargs: object) -> object:
         """
         This wrapper method replaces the implementation of the (abstract) method, providing a
         friendly message to the user.
diff --git a/src/oop_ext/interface/_interface.py b/src/oop_ext/interface/_interface.py
index dfa256d..520cf3d 100644
--- a/src/oop_ext/interface/_interface.py
+++ b/src/oop_ext/interface/_interface.py
@@ -384,6 +384,11 @@ def AssertImplements(
     If given a class, will try to match the class against a given interface. If given an object
     (instance), will try to match the class of the given object.
 
+    Each method will need to match the interface exactly, including type annotations. The exception
+    is that implementations might declare generic signatures (like ``*args, **kwargs`` or
+    ``*args: object, **kwargs: object``), which will then bypass this checking. This is done
+    so wrappers/decorators can be used together in interfaces.
+
     :type class_or_instance: type or classobj or object
 
     :type interface: Interface
@@ -790,12 +795,7 @@ def _AssertImplementsFullChecking(
         else:
             return inspect.signature(method)
 
-    # methods which use the following signatures always match against interface method checks
-    acceptable_impl_signatures = {
-        inspect.Signature.from_callable(lambda *args, **kwargs: None),
-        inspect.Signature.from_callable(lambda self, *args, **kwargs: None),
-        inspect.Signature.from_callable(lambda cls, *args, **kwargs: None),
-    }
+    acceptable_impl_signatures = _GetGenericImplementationSignatures()
 
     class_ = _GetClassForInterfaceChecking(class_or_instance)
 
@@ -1042,3 +1042,58 @@ def AssertDeclaresInterface(
     class_or_instance: object, interface: Type[Interface]
 ) -> None:
     AssertImplements(class_or_instance, interface)
+
+
+# Cache for _GetGenericImplementationSignatures result.
+_GENERIC_IMPLEMENTATION_SIGNATURES: Optional[Set[inspect.Signature]] = None
+
+
+def _GetGenericImplementationSignatures() -> Set[inspect.Signature]:
+    """
+    Return a set of signatures that should always be considered a match against interface
+    methods: they represent generic signatures that are commonly used by wrappers.
+    """
+    global _GENERIC_IMPLEMENTATION_SIGNATURES
+
+    if _GENERIC_IMPLEMENTATION_SIGNATURES is not None:
+        return _GENERIC_IMPLEMENTATION_SIGNATURES
+
+    def func1(*args, **kwargs):  # type:ignore[no-untyped-def]
+        ...
+
+    def func2(self, *args, **kwargs):  # type:ignore[no-untyped-def]
+        ...
+
+    def func3(cls, *args, **kwargs):  # type:ignore[no-untyped-def]
+        ...
+
+    def func4(*args: object, **kwargs: object) -> object:  # type:ignore[no-untyped-def]
+        ...
+
+    def func5(  # type:ignore[no-untyped-def]
+        self, *args: object, **kwargs: object
+    ) -> object:
+        ...
+
+    def func6(  # type:ignore[no-untyped-def]
+        cls, *args: object, **kwargs: object
+    ) -> object:
+        ...
+
+    def func7(self: object, *args: object, **kwargs: object) -> object:
+        ...
+
+    def func8(cls: object, *args: object, **kwargs: object) -> object:
+        ...
+
+    _GENERIC_IMPLEMENTATION_SIGNATURES = {
+        inspect.Signature.from_callable(func1),
+        inspect.Signature.from_callable(func2),
+        inspect.Signature.from_callable(func3),
+        inspect.Signature.from_callable(func4),
+        inspect.Signature.from_callable(func5),
+        inspect.Signature.from_callable(func6),
+        inspect.Signature.from_callable(func7),
+        inspect.Signature.from_callable(func8),
+    }
+    return _GENERIC_IMPLEMENTATION_SIGNATURES


Test patch:
diff --git a/src/oop_ext/interface/_tests/test_interface.py b/src/oop_ext/interface/_tests/test_interface.py
index d31a287..9bd1c8a 100644
--- a/src/oop_ext/interface/_tests/test_interface.py
+++ b/src/oop_ext/interface/_tests/test_interface.py
@@ -1,8 +1,10 @@
 import re
 import textwrap
+from typing import List
 
 import pytest
 
+from oop_ext.foundation.decorators import Implements, Abstract, Override
 from oop_ext.foundation.types_ import Method, Null
 from oop_ext.interface import (
     AssertImplements,
@@ -965,3 +967,41 @@ def testInterfaceTypeChecking(type_checker) -> None:
     )
     result = type_checker.run()
     result.assert_ok()
+
+
+def testDecorators():
+    """Interfaces and the foundation decorators need to play nice together."""
+
+    class IFoo(Interface):
+        def GetValues(self, unit: str) -> List[float]:
+            ...
+
+        @classmethod
+        def GetCaption(cls) -> str:
+            ...
+
+    @ImplementsInterface(IFoo)
+    class AbstractFoo:
+        @Implements(IFoo.GetValues)
+        @Abstract
+        def GetValues(self, unit: str) -> List[float]:
+            ...
+
+        @classmethod
+        @Implements(IFoo.GetCaption)
+        @Abstract
+        def GetCaption(cls) -> str:
+            ...
+
+    class Foo(AbstractFoo):
+        @Override(AbstractFoo.GetValues)
+        def GetValues(self, unit: str) -> List[float]:
+            return [0.1, 10.0]
+
+        @classmethod
+        @Override(AbstractFoo.GetCaption)
+        def GetCaption(cls) -> str:
+            return "Foo"
+
+    assert Foo.GetCaption() == "Foo"
+    assert Foo().GetValues("m") == [0.1, 10.0]