Repository: Parquery/icontract

Issue:
require decorator removes type annotations for mypy
```
$ cat ic.py  
from icontract import require

@require(lambda x: x > 0)
def f1(x: int): return x

def f2(x: int): return 0

f1("this is wrong")
f2("this is too")

$ mypy ic.py              
ic.py:9: error: Argument 1 to "f2" has incompatible type "str"; expected "int"
```

Both function calls are invalid types but mypy only sees the call as f2 as wrong.  The decorator loses the type annotations?

Related https://github.com/python/mypy/issues/3157 



Discussion:

Patch:
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index 3e7e6d6..30ab186 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -4,6 +4,7 @@ import inspect
 from typing import Callable, Any, Iterable, Optional, Tuple, List, Mapping, MutableMapping, Dict
 
 import icontract._represent
+from icontract._globals import CallableT
 from icontract._types import Contract, Snapshot
 from icontract.errors import ViolationError
 
@@ -13,7 +14,7 @@ from icontract.errors import ViolationError
 # pylint: disable=unsubscriptable-object
 
 
-def _walk_decorator_stack(func: Callable[..., Any]) -> Iterable['Callable[..., Any]']:
+def _walk_decorator_stack(func: CallableT) -> Iterable['CallableT']:
     """
     Iterate through the stack of decorated functions until the original function.
 
@@ -27,9 +28,9 @@ def _walk_decorator_stack(func: Callable[..., Any]) -> Iterable['Callable[..., A
     yield func
 
 
-def find_checker(func: Callable[..., Any]) -> Optional[Callable[..., Any]]:
+def find_checker(func: CallableT) -> Optional[CallableT]:
     """Iterate through the decorator stack till we find the contract checker."""
-    contract_checker = None  # type: Optional[Callable[..., Any]]
+    contract_checker = None  # type: Optional[CallableT]
     for a_wrapper in _walk_decorator_stack(func):
         if hasattr(a_wrapper, "__preconditions__") or hasattr(a_wrapper, "__postconditions__"):
             contract_checker = a_wrapper
@@ -228,7 +229,7 @@ class _Old:
     def __init__(self, mapping: Mapping[str, Any]) -> None:
         self.__dict__.update(mapping)
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         raise AttributeError("The snapshot with the name {!r} is not available in the OLD of a postcondition. "
                              "Have you decorated the function with a corresponding snapshot decorator?".format(item))
 
@@ -236,7 +237,7 @@ class _Old:
         return "a bunch of OLD values"
 
 
-def decorate_with_checker(func: Callable[..., Any]) -> Callable[..., Any]:
+def decorate_with_checker(func: CallableT) -> CallableT:
     """Decorate the function with a checker that verifies the preconditions and postconditions."""
     assert not hasattr(func, "__preconditions__"), \
         "Expected func to have no list of preconditions (there should be only a single contract checker per function)."
@@ -311,7 +312,7 @@ def decorate_with_checker(func: Callable[..., Any]) -> Callable[..., Any]:
             for contract in postconditions:
                 _assert_postcondition(contract=contract, resolved_kwargs=resolved_kwargs)
 
-        return result
+        return result  # type: ignore
 
     # Copy __doc__ and other properties so that doctests can run
     functools.update_wrapper(wrapper=wrapper, wrapped=func)
@@ -330,7 +331,7 @@ def decorate_with_checker(func: Callable[..., Any]) -> Callable[..., Any]:
     setattr(wrapper, "__postcondition_snapshots__", [])
     setattr(wrapper, "__postconditions__", [])
 
-    return wrapper
+    return wrapper  # type: ignore
 
 
 def _find_self(param_names: List[str], args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Any:
@@ -344,7 +345,7 @@ def _find_self(param_names: List[str], args: Tuple[Any, ...], kwargs: Dict[str,
     return instance
 
 
-def _decorate_with_invariants(func: Callable[..., Any], is_init: bool) -> Callable[..., Any]:
+def _decorate_with_invariants(func: CallableT, is_init: bool) -> CallableT:
     """
     Decorate the function ``func`` of the class ``cls`` with invariant checks.
 
@@ -391,7 +392,7 @@ def _decorate_with_invariants(func: Callable[..., Any], is_init: bool) -> Callab
 
     setattr(wrapper, "__is_invariant_check__", True)
 
-    return wrapper
+    return wrapper  # type: ignore
 
 
 class _DummyClass:
@@ -403,7 +404,7 @@ class _DummyClass:
 _SLOT_WRAPPER_TYPE = type(_DummyClass.__init__)  # pylint: disable=invalid-name
 
 
-def _already_decorated_with_invariants(func: Callable[..., Any]) -> bool:
+def _already_decorated_with_invariants(func: CallableT) -> bool:
     """Check if the function has been already decorated with an invariant check by going through its decorator stack."""
     already_decorated = False
     for a_decorator in _walk_decorator_stack(func=func):
@@ -467,9 +468,9 @@ def add_invariant_checks(cls: type) -> None:
         setattr(cls, name, wrapper)
 
     for name, prop in names_properties:
-        fget = _decorate_with_invariants(func=prop.fget, is_init=False) if prop.fget else None
-        fset = _decorate_with_invariants(func=prop.fset, is_init=False) if prop.fset else None
-        fdel = _decorate_with_invariants(func=prop.fdel, is_init=False) if prop.fdel else None
-
-        new_prop = property(fget=fget, fset=fset, fdel=fdel, doc=prop.__doc__)
+        new_prop = property(  # type: ignore
+            fget=_decorate_with_invariants(func=prop.fget, is_init=False) if prop.fget else None,
+            fset=_decorate_with_invariants(func=prop.fset, is_init=False) if prop.fset else None,
+            fdel=_decorate_with_invariants(func=prop.fdel, is_init=False) if prop.fdel else None,
+            doc=prop.__doc__)
         setattr(cls, name, new_prop)
diff --git a/icontract/_decorators.py b/icontract/_decorators.py
index aaf62bb..65d3a08 100644
--- a/icontract/_decorators.py
+++ b/icontract/_decorators.py
@@ -2,6 +2,7 @@
 import reprlib
 from typing import Callable, Optional, Union, Any, List  # pylint: disable=unused-import
 
+from icontract._globals import CallableT
 from icontract._types import Contract, Snapshot
 
 import icontract._checkers
@@ -18,11 +19,11 @@ class require:  # pylint: disable=invalid-name
 
     # pylint: disable=too-many-instance-attributes
     def __init__(self,
-                 condition: Callable[..., bool],
+                 condition: Callable[..., Any],
                  description: Optional[str] = None,
                  a_repr: reprlib.Repr = icontract._globals.aRepr,
                  enabled: bool = __debug__,
-                 error: Union[Callable[..., Exception], type] = None) -> None:
+                 error: Optional[Union[Callable[..., Exception], type]] = None) -> None:
         """
         Initialize.
 
@@ -54,7 +55,7 @@ class require:  # pylint: disable=invalid-name
 
         self._contract = Contract(condition=condition, description=description, a_repr=a_repr, error=error)
 
-    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
+    def __call__(self, func: CallableT) -> CallableT:
         """
         Add the precondition to the list of preconditions of the function ``func``.
 
@@ -131,7 +132,7 @@ class snapshot:  # pylint: disable=invalid-name
         if enabled:
             self._snapshot = Snapshot(capture=capture, name=name)
 
-    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
+    def __call__(self, func: CallableT) -> CallableT:
         """
         Add the snapshot to the list of snapshots of the function ``func``.
 
@@ -179,11 +180,11 @@ class ensure:  # pylint: disable=invalid-name
 
     # pylint: disable=too-many-instance-attributes
     def __init__(self,
-                 condition: Callable[..., bool],
+                 condition: Callable[..., Any],
                  description: Optional[str] = None,
                  a_repr: reprlib.Repr = icontract._globals.aRepr,
                  enabled: bool = __debug__,
-                 error: Union[Callable[..., Exception], type] = None) -> None:
+                 error: Optional[Union[Callable[..., Exception], type]] = None) -> None:
         """
         Initialize.
 
@@ -215,7 +216,7 @@ class ensure:  # pylint: disable=invalid-name
 
         self._contract = Contract(condition=condition, description=description, a_repr=a_repr, error=error)
 
-    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
+    def __call__(self, func: CallableT) -> CallableT:
         """
         Add the postcondition to the list of postconditions of the function ``func``.
 
@@ -261,11 +262,11 @@ class invariant:  # pylint: disable=invalid-name
     """
 
     def __init__(self,
-                 condition: Callable[..., bool],
+                 condition: Callable[..., Any],
                  description: Optional[str] = None,
                  a_repr: reprlib.Repr = icontract._globals.aRepr,
                  enabled: bool = __debug__,
-                 error: Union[Callable[..., Exception], type] = None) -> None:
+                 error: Optional[Union[Callable[..., Exception], type]] = None) -> None:
         """
         Initialize a class decorator to establish the invariant on all the public methods.
 
diff --git a/icontract/_globals.py b/icontract/_globals.py
index e744d87..2ac0002 100644
--- a/icontract/_globals.py
+++ b/icontract/_globals.py
@@ -2,11 +2,13 @@
 
 import os
 import reprlib
+from typing import TypeVar, Callable
 
 # Default representation instance.
 #
 # The limits are set way higher than reprlib.aRepr since the default reprlib limits are not suitable for
 # the production systems.
+
 aRepr = reprlib.Repr()  # pylint: disable=invalid-name
 aRepr.maxdict = 50
 aRepr.maxlist = 50
@@ -25,3 +27,4 @@ aRepr.maxother = 256
 #
 # Contracts marked with SLOW are also disabled if the interpreter is run in optimized mode (``-O`` or ``-OO``).
 SLOW = __debug__ and os.environ.get("ICONTRACT_SLOW", "") != ""
+CallableT = TypeVar('CallableT', bound='Callable')
diff --git a/icontract/_types.py b/icontract/_types.py
index 13e7d63..b3eb87b 100644
--- a/icontract/_types.py
+++ b/icontract/_types.py
@@ -17,7 +17,7 @@ class Contract:
                  condition: Callable[..., bool],
                  description: Optional[str] = None,
                  a_repr: reprlib.Repr = icontract._globals.aRepr,
-                 error: Union[Callable[..., Exception], type] = None) -> None:
+                 error: Optional[Union[Callable[..., Exception], type]] = None) -> None:
         """
         Initialize.
 


Test patch:
diff --git a/tests/test_mypy_decorators.py b/tests/test_mypy_decorators.py
new file mode 100644
index 0000000..ab3019c
--- /dev/null
+++ b/tests/test_mypy_decorators.py
@@ -0,0 +1,50 @@
+# pylint: disable=missing-docstring
+
+import subprocess
+import tempfile
+import textwrap
+import unittest
+
+
+class TestMypyDecorators(unittest.TestCase):
+    def test_mypy_me(self):
+        with tempfile.NamedTemporaryFile(prefix="mypy_fail_case_", suffix=".py") as tmp:
+            tmp.file.write(
+                textwrap.dedent('''\
+                """Implement a fail case for mypy to test that the types are preserved with the decorators."""
+
+                import icontract
+
+                @icontract.require(lambda x: x > 0)
+                def f1(x: int) -> int:
+                    return x
+                f1("this is wrong")
+
+                @icontract.ensure(lambda result: result > 0)
+                def f2(x: int) -> int:
+                    return x
+                f2("this is wrong")
+
+                @icontract.snapshot(lambda x: x)
+                def f3(x: int) -> int:
+                    return x
+                f3("this is wrong")
+                ''').encode())
+
+            tmp.file.flush()
+
+            proc = subprocess.Popen(['mypy', '--strict', tmp.name], universal_newlines=True, stdout=subprocess.PIPE)
+            out, err = proc.communicate()
+
+            self.assertIsNone(err)
+            self.assertEqual(
+                textwrap.dedent('''\
+                    {path}:8: error: Argument 1 to "f1" has incompatible type "str"; expected "int"
+                    {path}:13: error: Argument 1 to "f2" has incompatible type "str"; expected "int"
+                    {path}:18: error: Argument 1 to "f3" has incompatible type "str"; expected "int"
+                    '''.format(path=tmp.name)),
+                out)
+
+
+if __name__ == '__main__':
+    unittest.main()