Repository: Parquery/icontract

Issue:
checking of contract arguments is too strict
consider:

```
$ cat i.py                                                                                                                                                                                                          (time_gym) 
from icontract import require, ensure

def gt(x, y=0):
    return x > y

@require(gt)
def frobulate(x: int) -> int:
    return x

frobulate(-1)

$ python i.py                                                                                                                                                                                                       Traceback (most recent call last):
  File "i.py", line 10, in <module>
    frobulate(-1)
  File "/home/james/Src/icontract/icontract/_checkers.py", line 309, in wrapper
    _assert_precondition(contract=contract, resolved_kwargs=resolved_kwargs)
  File "/home/james/Src/icontract/icontract/_checkers.py", line 93, in _assert_precondition
    "Does the original function define them? Did you supply them in the call?").format(missing_args))
TypeError: The argument(s) of the precondition have not been set: ['y']. Does the original function define them? Did you supply them in the call?
```

Should I have to set y here?  I don't think so.

Discussion:

Patch:
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index 1dc79d3..e5909e2 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -92,11 +92,12 @@ def _assert_precondition(contract: Contract, resolved_kwargs: Mapping[str, Any])
     Assert that the contract holds as a precondition.
 
     :param contract: contract to be verified
-    :param resolved_kwargs: resolved keyword arguments (including the default values)
+    :param resolved_kwargs:
+        resolved keyword arguments of the call (including the default argument values of the decorated function)
     :return:
     """
     # Check that all arguments to the condition function have been set.
-    missing_args = [arg_name for arg_name in contract.condition_args if arg_name not in resolved_kwargs]
+    missing_args = [arg_name for arg_name in contract.mandatory_args if arg_name not in resolved_kwargs]
     if missing_args:
         raise TypeError(
             ("The argument(s) of the precondition have not been set: {}. "
@@ -207,7 +208,7 @@ def _assert_postcondition(contract: Contract, resolved_kwargs: Mapping[str, Any]
         "Expected 'result' to be set in the resolved keyword arguments of a postcondition."
 
     # Check that all arguments to the condition function have been set.
-    missing_args = [arg_name for arg_name in contract.condition_args if arg_name not in resolved_kwargs]
+    missing_args = [arg_name for arg_name in contract.mandatory_args if arg_name not in resolved_kwargs]
     if missing_args:
         raise TypeError(
             ("The argument(s) of the postcondition have not been set: {}. "
diff --git a/icontract/_decorators.py b/icontract/_decorators.py
index f4cb5dc..e48b090 100644
--- a/icontract/_decorators.py
+++ b/icontract/_decorators.py
@@ -326,7 +326,7 @@ class invariant:  # pylint: disable=invalid-name
         self._contract = Contract(
             condition=condition, description=description, a_repr=a_repr, error=error, location=location)
 
-        if self._contract.condition_args and self._contract.condition_args != ['self']:
+        if self._contract.mandatory_args and self._contract.mandatory_args != ['self']:
             raise ValueError("Expected an invariant condition with at most an argument 'self', but got: {}".format(
                 self._contract.condition_args))
 
diff --git a/icontract/_types.py b/icontract/_types.py
index c6e80d0..a7e01da 100644
--- a/icontract/_types.py
+++ b/icontract/_types.py
@@ -37,9 +37,17 @@ class Contract:
         # pylint: disable=too-many-arguments
         self.condition = condition
 
-        self.condition_args = list(inspect.signature(condition).parameters.keys())  # type: List[str]
+        signature = inspect.signature(condition)
+
+        # All argument names of the condition
+        self.condition_args = list(signature.parameters.keys())  # type: List[str]
         self.condition_arg_set = set(self.condition_args)  # type: Set[str]
 
+        # Names of the mandatory arguments of the condition
+        self.mandatory_args = [
+            name for name, param in signature.parameters.items() if param.default == inspect.Parameter.empty
+        ]
+
         self.description = description
         self._a_repr = a_repr
 


Test patch:
diff --git a/tests/test_invariant.py b/tests/test_invariant.py
index a971cd7..c18eba0 100644
--- a/tests/test_invariant.py
+++ b/tests/test_invariant.py
@@ -386,6 +386,62 @@ class TestViolation(unittest.TestCase):
         self.assertIsNotNone(violation_error)
         self.assertEqual("z != 42: z was 42", tests.error.wo_mandatory_location(str(violation_error)))
 
+    def test_condition_as_function(self):
+        def some_condition(self: 'A') -> bool:  # type: ignore
+            return self.x > 0
+
+        @icontract.invariant(some_condition)
+        class A:
+            def __init__(self) -> None:
+                self.x = 100
+
+            def some_method(self) -> None:
+                self.x = -1
+
+            def __repr__(self) -> str:
+                return "A(x={})".format(self.x)
+
+        # Valid call
+        a = A()
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.some_method()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual('some_condition: self was A(x=-1)', tests.error.wo_mandatory_location(str(violation_error)))
+
+    def test_condition_as_function_with_default_argument_value(self):
+        def some_condition(self: 'A', y: int = 0) -> bool:  # type: ignore
+            return self.x > y
+
+        @icontract.invariant(some_condition)
+        class A:
+            def __init__(self) -> None:
+                self.x = 100
+
+            def some_method(self) -> None:
+                self.x = -1
+
+            def __repr__(self) -> str:
+                return "A(x={})".format(self.x)
+
+        # Valid call
+        a = A()
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.some_method()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual('some_condition: self was A(x=-1)', tests.error.wo_mandatory_location(str(violation_error)))
+
 
 class TestProperty(unittest.TestCase):
     def test_property_getter(self):
diff --git a/tests/test_postcondition.py b/tests/test_postcondition.py
index 3cbe98a..4804dde 100644
--- a/tests/test_postcondition.py
+++ b/tests/test_postcondition.py
@@ -41,6 +41,71 @@ class TestViolation(unittest.TestCase):
                          "result was -4\n"
                          "x was 1", tests.error.wo_mandatory_location(str(violation_error)))
 
+    def test_condition_as_function(self):
+        def some_condition(result: int) -> bool:
+            return result > 3
+
+        @icontract.ensure(some_condition)
+        def some_func(x: int) -> int:
+            return x
+
+        # Valid call
+        some_func(x=4)
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(x=1)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual('some_condition: result was 1', tests.error.wo_mandatory_location(str(violation_error)))
+
+    def test_condition_as_function_with_default_argument_value(self):
+        def some_condition(result: int, y: int = 0) -> bool:
+            return result > y
+
+        @icontract.ensure(some_condition)
+        def some_func(x: int) -> int:
+            return x
+
+        # Valid call
+        some_func(x=1)
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(x=-1)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual('some_condition: result was -1', tests.error.wo_mandatory_location(str(violation_error)))
+
+    def test_condition_as_function_with_default_argument_value_set(self):
+        def some_condition(result: int, y: int = 0) -> bool:
+            return result > y
+
+        @icontract.ensure(some_condition)
+        def some_func(x: int, y: int) -> int:
+            return x
+
+        # Valid call
+        some_func(x=-1, y=-2)
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(x=1, y=3)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual('some_condition:\n'
+                         'result was 1\n'
+                         'y was 3', tests.error.wo_mandatory_location(str(violation_error)))
+
     def test_with_description(self):
         @icontract.ensure(lambda result, x: result > x, "expected summation")
         def some_func(x: int, y: int = 5) -> int:
diff --git a/tests/test_precondition.py b/tests/test_precondition.py
index 9b08408..8709edb 100644
--- a/tests/test_precondition.py
+++ b/tests/test_precondition.py
@@ -55,7 +55,7 @@ class TestViolation(unittest.TestCase):
         self.assertEqual("x must not be small: x > 3: x was 1", tests.error.wo_mandatory_location(str(violation_error)))
 
     def test_condition_as_function(self):
-        def some_condition(x: int):
+        def some_condition(x: int) -> bool:
             return x > 3
 
         @icontract.require(some_condition)
@@ -71,6 +71,50 @@ class TestViolation(unittest.TestCase):
         self.assertIsNotNone(violation_error)
         self.assertEqual("some_condition: x was 1", tests.error.wo_mandatory_location(str(violation_error)))
 
+    def test_condition_as_function_with_default_argument_value(self):
+        def some_condition(x: int, y: int = 0) -> bool:
+            return x > y
+
+        @icontract.require(some_condition)
+        def some_func(x: int) -> None:
+            pass
+
+        # Valid call
+        some_func(x=1)
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(x=-1)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual("some_condition: x was -1", tests.error.wo_mandatory_location(str(violation_error)))
+
+    def test_condition_as_function_with_default_argument_value_set(self):
+        def some_condition(x: int, y: int = 0) -> bool:
+            return x > y
+
+        @icontract.require(some_condition)
+        def some_func(x: int, y: int) -> None:
+            pass
+
+        # Valid call
+        some_func(x=3, y=1)
+
+        # Invalid call
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(x=-1, y=1)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual('some_condition:\n'
+                         'x was -1\n'
+                         'y was 1', tests.error.wo_mandatory_location(str(violation_error)))
+
     def test_with_pathlib(self):
         @icontract.require(lambda path: path.exists())
         def some_func(path: pathlib.Path) -> None: