Repository: Parquery/icontract

Issue:
Crashes when functions are given wrong number of (positional) arguments
The following case, where `f` is given one too many arguments, should give the _original error message_ `TypeError: f() takes 0 positional arguments but 1 was given`:

```python
import icontract


@icontract.require(lambda: True)
def f():
    pass


f(0)
```

Instead, icontract crashes with the following traceback:
```
Traceback (most recent call last):
  File "f.py", line 9, in <module>
    f(0)
  File "/data/envs/e37/lib/python3.7/site-packages/icontract/_checkers.py", line 281, in wrapper
    resolved_kwargs = _kwargs_from_call(param_names=param_names, kwdefaults=kwdefaults, args=args, kwargs=kwargs)
  File "/data/envs/e37/lib/python3.7/site-packages/icontract/_checkers.py", line 61, in _kwargs_from_call
    mapping[param_names[i]] = func_arg
IndexError: list index out of range
```

This makes it difficult to locate the actual position of the bug.

Discussion:

Patch:
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index e5909e2..19b1dcf 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -58,7 +58,13 @@ def _kwargs_from_call(param_names: List[str], kwdefaults: Dict[str, Any], args:
 
     # Override the defaults with the values actually suplied to the function.
     for i, func_arg in enumerate(args):
-        mapping[param_names[i]] = func_arg
+        if i < len(param_names):
+            mapping[param_names[i]] = func_arg
+        else:
+            # Silently ignore call arguments that were not specified in the function.
+            # This way we let the underlying decorated function raise the exception
+            # instead of frankensteining the exception here.
+            pass
 
     for key, val in kwargs.items():
         mapping[key] = val
@@ -336,7 +342,9 @@ def decorate_with_checker(func: CallableT) -> CallableT:
 
             resolved_kwargs['OLD'] = _Old(mapping=old_as_mapping)
 
-        # Execute the wrapped function
+        # Ideally, we would catch any exception here and strip the checkers from the traceback.
+        # Unfortunately, this can not be done in Python 3, see
+        # https://stackoverflow.com/questions/44813333/how-can-i-elide-a-function-wrapper-from-the-traceback-in-python-3
         result = func(*args, **kwargs)
 
         if postconditions:


Test patch:
diff --git a/tests/test_precondition.py b/tests/test_precondition.py
index 8709edb..0d4a8dc 100644
--- a/tests/test_precondition.py
+++ b/tests/test_precondition.py
@@ -509,6 +509,34 @@ class TestInvalid(unittest.TestCase):
         self.assertEqual('Failed to negate the evaluation of the condition.',
                          tests.error.wo_mandatory_location(str(value_error)))
 
+    def test_unexpected_positional_argument(self):
+        @icontract.require(lambda: True)
+        def some_func() -> None:
+            pass
+
+        type_error = None  # type: Optional[TypeError]
+        try:
+            some_func(0)  # pylint: disable=too-many-function-args
+        except TypeError as err:
+            type_error = err
+
+        self.assertIsNotNone(type_error)
+        self.assertEqual('some_func() takes 0 positional arguments but 1 was given', str(type_error))
+
+    def test_unexpected_keyword_argument(self):
+        @icontract.require(lambda: True)
+        def some_func() -> None:
+            pass
+
+        type_error = None  # type: Optional[TypeError]
+        try:
+            some_func(x=0)  # pylint: disable=unexpected-keyword-arg
+        except TypeError as err:
+            type_error = err
+
+        self.assertIsNotNone(type_error)
+        self.assertEqual("some_func() got an unexpected keyword argument 'x'", str(type_error))
+
 
 if __name__ == '__main__':
     unittest.main()