Repository: ESSS/oop-ext

Issue:
Regression: callback.After changes signatures, breaking interface checks
Similar to #36, however it is more subtle because it requires someone to install a callback using `callback.After`

```python
    class IFoo(Interface):

        @classmethod
        def GetCaption(cls) -> str:
            ...

    @ImplementsInterface(IFoo)
    class AbstractFoo:
        @classmethod
        @Implements(IFoo.GetCaption)
        @Abstract
        def GetCaption(cls) -> str:
            ...

    class Foo(AbstractFoo):

        @classmethod
        @Override(AbstractFoo.GetCaption)
        def GetCaption(cls) -> str:
            return "Foo"

    from oop_ext.foundation import callback

    def AfterCaption(*args):
        pass

    callback.After(Foo.GetCaption, AfterCaption)

    AssertImplements(Foo, IFoo)  # Fails.
```

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 54b08e0..d6d4f7b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,10 @@
+1.1.2 (2021-02-23)
+------------------
+
+* #41: Fix regression introduced in ``1.1.0`` where installing a callback using
+  ``callback.After`` or ``callback.Before`` would make a method no longer compliant with
+  the signature required by its interface.
+
 1.1.1 (2021-02-23)
 ------------------
 
diff --git a/src/oop_ext/foundation/decorators.py b/src/oop_ext/foundation/decorators.py
index f1fa06a..d09f6fc 100644
--- a/src/oop_ext/foundation/decorators.py
+++ b/src/oop_ext/foundation/decorators.py
@@ -169,7 +169,7 @@ def Abstract(func: F) -> F:
 
     # 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:
+    def AbstractWrapper(self: object, *args: object, **kwargs: object) -> NoReturn:
         """
         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 520cf3d..fe55eb2 100644
--- a/src/oop_ext/interface/_interface.py
+++ b/src/oop_ext/interface/_interface.py
@@ -1067,7 +1067,7 @@ def _GetGenericImplementationSignatures() -> Set[inspect.Signature]:
     def func3(cls, *args, **kwargs):  # type:ignore[no-untyped-def]
         ...
 
-    def func4(*args: object, **kwargs: object) -> object:  # type:ignore[no-untyped-def]
+    def func4(*args: object, **kwargs: object) -> object:
         ...
 
     def func5(  # type:ignore[no-untyped-def]
@@ -1086,6 +1086,48 @@ def _GetGenericImplementationSignatures() -> Set[inspect.Signature]:
     def func8(cls: object, *args: object, **kwargs: object) -> object:
         ...
 
+    def func9(  # type:ignore[no-untyped-def]
+        *args: object, **kwargs: object
+    ) -> Any:
+        ...
+
+    def func10(  # type:ignore[no-untyped-def]
+        self, *args: object, **kwargs: object
+    ) -> Any:
+        ...
+
+    def func11(  # type:ignore[no-untyped-def]
+        cls, *args: object, **kwargs: object
+    ) -> Any:
+        ...
+
+    def func12(self: object, *args: object, **kwargs: object) -> Any:
+        ...
+
+    def func13(cls: object, *args: object, **kwargs: object) -> Any:
+        ...
+
+    def func14(  # type:ignore[no-untyped-def]
+        *args: object, **kwargs: object
+    ) -> NoReturn:
+        ...
+
+    def func15(  # type:ignore[no-untyped-def]
+        self, *args: object, **kwargs: object
+    ) -> NoReturn:
+        ...
+
+    def func16(  # type:ignore[no-untyped-def]
+        cls, *args: object, **kwargs: object
+    ) -> NoReturn:
+        ...
+
+    def func17(self: object, *args: object, **kwargs: object) -> NoReturn:
+        ...
+
+    def func18(cls: object, *args: object, **kwargs: object) -> NoReturn:
+        ...
+
     _GENERIC_IMPLEMENTATION_SIGNATURES = {
         inspect.Signature.from_callable(func1),
         inspect.Signature.from_callable(func2),
@@ -1095,5 +1137,15 @@ def _GetGenericImplementationSignatures() -> Set[inspect.Signature]:
         inspect.Signature.from_callable(func6),
         inspect.Signature.from_callable(func7),
         inspect.Signature.from_callable(func8),
+        inspect.Signature.from_callable(func9),
+        inspect.Signature.from_callable(func10),
+        inspect.Signature.from_callable(func11),
+        inspect.Signature.from_callable(func12),
+        inspect.Signature.from_callable(func13),
+        inspect.Signature.from_callable(func14),
+        inspect.Signature.from_callable(func15),
+        inspect.Signature.from_callable(func16),
+        inspect.Signature.from_callable(func17),
+        inspect.Signature.from_callable(func18),
     }
     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 9bd1c8a..8546b27 100644
--- a/src/oop_ext/interface/_tests/test_interface.py
+++ b/src/oop_ext/interface/_tests/test_interface.py
@@ -969,8 +969,9 @@ def testInterfaceTypeChecking(type_checker) -> None:
     result.assert_ok()
 
 
-def testDecorators():
-    """Interfaces and the foundation decorators need to play nice together."""
+@pytest.mark.parametrize("register_callback", [True, False])
+def testDecorators(register_callback: bool):
+    """Interfaces and the foundation decorators/callbacks need to play nice together."""
 
     class IFoo(Interface):
         def GetValues(self, unit: str) -> List[float]:
@@ -1003,5 +1004,20 @@ def testDecorators():
         def GetCaption(cls) -> str:
             return "Foo"
 
+    from oop_ext.foundation import callback
+
+    if register_callback:
+
+        def AfterCaption(*args):
+            pass
+
+        # Ensure methods overwritten by callbacks don't interfere with interface
+        # signature detection:  Callback.After overwrites the method object on the class,
+        # with potentially a different signature.
+        callback.After(Foo.GetCaption, AfterCaption)
+
+    AssertImplements(Foo, IFoo)
+    AssertImplements(Foo(), IFoo)
+
     assert Foo.GetCaption() == "Foo"
     assert Foo().GetValues("m") == [0.1, 10.0]