Repository: Parquery/icontract

Issue:
catch exceptions in boolyness of check
This is very much related to #97 

Consider this: 

```
$ cat i.py
from icontract import require, ensure
import numpy as np

@require(lambda x: np.array([True, False]))
def frobulate(x: int) -> int:
    return x

$ python -c "import i; i.frobulate(1)"                                                                                                                                                                                                 Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/james/.conda/envs/time_gym35/lib/python3.5/site-packages/icontract/_checkers.py", line 297, in wrapper
    _assert_precondition(contract=contract, resolved_kwargs=resolved_kwargs)
  File "/home/james/.conda/envs/time_gym35/lib/python3.5/site-packages/icontract/_checkers.py", line 91, in _assert_precondition
    if not check:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

```

If you are doing numeric programming with icontract it's very likely at some point you will mess up and return a bool vector rather than a bool.  At the moment, its impossible to know where you did this.  If you have many requirements you have no decent way of debugging them.  You might end up commenting them out one by one or moving them to plain old asserts in the function.

Its annoying that numpy arrays are not true or false but as such a large part of the ecosystem, perhaps we could catch the value error during contract evaluation...

i.e.

    try: 
        falsiness = not check 
     except ValueError:
        print('useful info on where the exception occurred')
        raise from None

    if falsisness:






Discussion:

Patch:
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index 1b27f5a..1dc79d3 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -66,6 +66,27 @@ def _kwargs_from_call(param_names: List[str], kwdefaults: Dict[str, Any], args:
     return mapping
 
 
+def _not_check(check: Any, contract: Contract) -> bool:
+    """
+    Negate the check value of a condition and capture missing boolyness (*e.g.*, when check is a numpy array).
+
+    :param check: value of the evaluated condition
+    :param contract: corresponding to the check
+    :return: negated check
+    :raise: ValueError if the check could not be negated
+    """
+    try:
+        return not check
+    except Exception as err:  # pylint: disable=broad-except
+        msg_parts = []  # type: List[str]
+        if contract.location is not None:
+            msg_parts.append("{}:\n".format(contract.location))
+
+        msg_parts.append('Failed to negate the evaluation of the condition.')
+
+        raise ValueError(''.join(msg_parts)) from err
+
+
 def _assert_precondition(contract: Contract, resolved_kwargs: Mapping[str, Any]) -> None:
     """
     Assert that the contract holds as a precondition.
@@ -88,7 +109,7 @@ def _assert_precondition(contract: Contract, resolved_kwargs: Mapping[str, Any])
 
     check = contract.condition(**condition_kwargs)
 
-    if not check:
+    if _not_check(check=check, contract=contract):
         if contract.error is not None and (inspect.ismethod(contract.error) or inspect.isfunction(contract.error)):
             assert contract.error_arg_set is not None, "Expected error_arg_set non-None if contract.error a function."
             assert contract.error_args is not None, "Expected error_args non-None if contract.error a function."
@@ -127,7 +148,7 @@ def _assert_invariant(contract: Contract, instance: Any) -> None:
     else:
         check = contract.condition()
 
-    if not check:
+    if _not_check(check=check, contract=contract):
         if contract.error is not None and (inspect.ismethod(contract.error) or inspect.isfunction(contract.error)):
             assert contract.error_arg_set is not None, "Expected error_arg_set non-None if contract.error a function."
             assert contract.error_args is not None, "Expected error_args non-None if contract.error a function."
@@ -199,7 +220,7 @@ def _assert_postcondition(contract: Contract, resolved_kwargs: Mapping[str, Any]
 
     check = contract.condition(**condition_kwargs)
 
-    if not check:
+    if _not_check(check=check, contract=contract):
         if contract.error is not None and (inspect.ismethod(contract.error) or inspect.isfunction(contract.error)):
             assert contract.error_arg_set is not None, "Expected error_arg_set non-None if contract.error a function."
             assert contract.error_args is not None, "Expected error_args non-None if contract.error a function."


Test patch:
diff --git a/tests/test_invariant.py b/tests/test_invariant.py
index 114649e..302bb5d 100644
--- a/tests/test_invariant.py
+++ b/tests/test_invariant.py
@@ -9,6 +9,7 @@ from typing import Optional  # pylint: disable=unused-import
 
 import icontract
 import tests.error
+import tests.mock
 
 
 class TestOK(unittest.TestCase):
@@ -587,3 +588,19 @@ class TestInvalid(unittest.TestCase):
         self.assertIsNotNone(val_err)
         self.assertEqual("Expected an invariant condition with at most an argument 'self', but got: ['self', 'z']",
                          str(val_err))
+
+    def test_no_boolyness(self):
+        @icontract.invariant(lambda self: tests.mock.NumpyArray([True, False]))
+        class A:
+            def __init__(self) -> None:
+                pass
+
+        value_error = None  # type: Optional[ValueError]
+        try:
+            _ = A()
+        except ValueError as err:
+            value_error = err
+
+        self.assertIsNotNone(value_error)
+        self.assertEqual('Failed to negate the evaluation of the condition.',
+                         tests.error.wo_mandatory_location(str(value_error)))
diff --git a/tests/test_postcondition.py b/tests/test_postcondition.py
index 3ea09f0..7e27bdc 100644
--- a/tests/test_postcondition.py
+++ b/tests/test_postcondition.py
@@ -11,6 +11,7 @@ from typing import Optional, List, Type  # pylint: disable=unused-import
 
 import icontract
 import tests.error
+import tests.mock
 
 
 class TestOK(unittest.TestCase):
@@ -438,3 +439,18 @@ class TestInvalid(unittest.TestCase):
         self.assertEqual("The argument(s) of the postcondition error have not been set: ['z']. "
                          "Does the original function define them? Did you supply them in the call?",
                          tests.error.wo_mandatory_location(str(type_error)))
+
+    def test_no_boolyness(self):
+        @icontract.ensure(lambda: tests.mock.NumpyArray([True, False]))
+        def some_func() -> None:
+            pass
+
+        value_error = None  # type: Optional[ValueError]
+        try:
+            some_func()
+        except ValueError as err:
+            value_error = err
+
+        self.assertIsNotNone(value_error)
+        self.assertEqual('Failed to negate the evaluation of the condition.',
+                         tests.error.wo_mandatory_location(str(value_error)))
diff --git a/tests/test_precondition.py b/tests/test_precondition.py
index 01b3e99..83ba7eb 100644
--- a/tests/test_precondition.py
+++ b/tests/test_precondition.py
@@ -12,6 +12,7 @@ from typing import Optional  # pylint: disable=unused-import
 
 import icontract
 import tests.error
+import tests.mock
 
 
 class TestOK(unittest.TestCase):
@@ -450,6 +451,21 @@ class TestInvalid(unittest.TestCase):
                          "Does the original function define them? Did you supply them in the call?",
                          tests.error.wo_mandatory_location(str(type_error)))
 
+    def test_no_boolyness(self):
+        @icontract.require(lambda: tests.mock.NumpyArray([True, False]))
+        def some_func() -> None:
+            pass
+
+        value_error = None  # type: Optional[ValueError]
+        try:
+            some_func()
+        except ValueError as err:
+            value_error = err
+
+        self.assertIsNotNone(value_error)
+        self.assertEqual('Failed to negate the evaluation of the condition.',
+                         tests.error.wo_mandatory_location(str(value_error)))
+
 
 if __name__ == '__main__':
     unittest.main()