Repository: Parquery/icontract

Issue:
*args and **kwargs not supported
Given the following code snippet:
```py
from icontract import require


def test_args():
    @require(lambda *args: args)
    def args(*args, **kwargs):
        pass
    args(0, x=0)


def test_kwargs():
    @require(lambda **kwargs: kwargs)
    def args(*args, **kwargs):
        pass
    args(0, x=0)


def test_both():
    @require(lambda *args, **kwargs: args and kwargs)
    def args(*args, **kwargs):
        pass
    args(0, x=0)
```
I get errors saying that precondition arguments could not be set (from `_checkers.py:114`).

Unless I've missed it, the README didn't explicitly state that * and ** aren't supported. My use case is a rather generic decorator applied to some endpoints. It shouldn't matter how these endpoints are called, but they must contain a request (for the decorator to do its magic), for which I want to create a contract.

Some observations:
- `@require(lambda args: ...)` without the asterisk only passes the first argument supplied to the function to the check (i.e. 0, not [0])
- `@require(lambda kwargs: ...)` without the asterisks raises the same error

I would gladly help with fixing/implementing this since I believe every language today should provide tools for DBC, and your library does a good job. Any thoughts on the feasibility of this? :)

Comments:

Patch:
diff --git a/README.rst b/README.rst
index 9fb69cb..db5af98 100644
--- a/README.rst
+++ b/README.rst
@@ -639,6 +639,78 @@ Here is an example of the error given as a callable:
     If you left ``enabled`` argument to its default ``__debug__``, the contract will *not* be verified in
     ``-O`` mode.)
 
+Variable Positional and Keyword Arguments
+-----------------------------------------
+Certain functions do not name their arguments explicitly, but operate on variable positional and/or
+keyword arguments supplied at the function call (*e.g.*, ``def some_func(*args, **kwargs): ...``).
+Contract conditions thus need a mechanism to refer to these variable arguments.
+To that end, we introduced two special condition arguments, ``_ARGS`` and ``_KWARGS``, that
+icontract will populate before evaluating the condition to capture the positional and keyword
+arguments, respectively, of the function call.
+
+To avoid intricacies of Python's argument resolution at runtime, icontract simply captures *all*
+positional and keyword arguments in these two variables, regardless of whether the function defines
+them or not. However, we would recommend you to explicitly name arguments in your conditions and
+use ``_ARGS`` and ``_KWARGS`` only for the variable arguments for readability.
+
+We present in the following a couple of valid contracts to demonstrate how to use these special
+arguments:
+
+.. code-block:: python
+
+    # The contract refers to the positional arguments of the *call*,
+    # though the decorated function does not handle
+    # variable positional arguments.
+    >>> @icontract.require(lambda _ARGS: _ARGS[0] > 0)
+    ... def function_a(x: int) -> int:
+    ...     return 123
+    >>> function_a(1)
+    123
+
+    # The contract refers to the keyword arguments of the *call*,
+    # though the decorated function does not handle variable keyword arguments.
+    >>> @icontract.require(lambda _KWARGS: _KWARGS["x"] > 0)
+    ... def function_b(x: int) -> int:
+    ...     return 123
+    >>> function_b(x=1)
+    123
+
+    # The contract refers both to the named argument and keyword arguments.
+    # The decorated function specifies an argument and handles
+    # variable keyword arguments at the same time.
+    >>> @icontract.require(lambda x, _KWARGS: x < _KWARGS["y"])
+    ... def function_c(x: int, **kwargs) -> int:
+    ...     return 123
+    >>> function_c(1, y=3)
+    123
+
+    # The decorated functions accepts only variable keyboard arguments.
+    >>> @icontract.require(lambda _KWARGS: _KWARGS["x"] > 0)
+    ... def function_d(**kwargs) -> int:
+    ...     return 123
+    >>> function_d(x=1)
+    123
+
+    # The decorated functions accepts only variable keyboard arguments.
+    # The keyword arguments are given an uncommon name (``parameters`` instead
+    # of ``kwargs``).
+    >>> @icontract.require(lambda _KWARGS: _KWARGS["x"] > 0)
+    ... def function_e(**parameters) -> int:
+    ...     return 123
+    >>> function_e(x=1)
+    123
+
+As a side note, we agree that the names picked for the placeholders are indeed a bit ugly.
+We decided against more aesthetic or ergonomic identifiers (such as ``_`` and ``__`` or
+``A`` and ``KW``) to avoid potential naming conflicts.
+
+The underscore in front of the placeholders is meant to motivate a bit deeper understanding
+of the condition.
+For example, the reader needs to be aware that the logic for resolving the keyword arguments
+passed to the function is *different* in condition and that ``_KWARGS`` *does not* refer to
+arbitrary keyword arguments *passed to the condition*. Though this might be obvious for some
+readers, we are almost certain that ``_ARGS`` and ``_KWARGS`` will cause some confusion.
+We hope that a small hint like an underscore will eventually help the reading.
 
 Implementation Details
 ----------------------
@@ -802,7 +874,7 @@ Here is a short code snippet to demonstrate where the current implementation fai
     >>> some_func(x=0)
     Traceback (most recent call last):
         ...
-    SyntaxError: Decorator corresponding to the line 1 could not be found in file <doctest README.rst[64]>: 'require_x_positive = icontract.require(lambda x: x > 0)\n'
+    SyntaxError: Decorator corresponding to the line 1 could not be found in file <doctest README.rst[74]>: 'require_x_positive = icontract.require(lambda x: x > 0)\n'
 
 However, we haven't faced a situation in the code base where we would do something like the above, so we are unsure
 whether this is a big issue. As long as decorators are directly applied to functions and classes, everything
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index 90d4a44..8b40d21 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -52,7 +52,15 @@ def _kwargs_from_call(param_names: List[str], kwdefaults: Dict[str, Any], args:
     :return: resolved arguments as they would be passed to the function
     """
     # pylint: disable=too-many-arguments
-    resolved_kwargs = dict()  # type: MutableMapping[str, Any]
+
+    # (Marko Ristin, 2020-12-01)
+    # Insert _ARGS and _KWARGS preemptively even if they are not needed by any contract.
+    # This makes the code logic much simpler since we do not explicitly check if a contract would
+    # need them, though it might incur a subtle computational overhead
+    # (*e.g.*, when the contracts do not need them or don't use any argument at all).
+    # We need to have a concrete issue where profiling helps us determine if this is a real
+    # bottleneck or not and not optimize for no real benefit.
+    resolved_kwargs = {'_ARGS': args, '_KWARGS': kwargs}
 
     # Set the default argument values as condition parameters.
     for param_name, param_value in kwdefaults.items():
@@ -224,7 +232,9 @@ def _assert_postcondition(contract: Contract, resolved_kwargs: Mapping[str, Any]
     both argument values captured in snapshots and actual argument values and the result of a function.
 
     :param contract: contract to be verified
-    :param resolved_kwargs: resolved keyword arguments (including the default values, ``result`` and ``OLD``)
+    :param resolved_kwargs:
+        resolved keyword arguments (including the default values, ``result``,``OLD``
+        ``_ARGS`` and ``_KWARGS``)
     :return:
     """
     assert 'result' in resolved_kwargs, \
@@ -324,6 +334,16 @@ def decorate_with_checker(func: CallableT) -> CallableT:
         "per function)."
 
     sign = inspect.signature(func)
+    if '_ARGS' in sign.parameters:
+        raise ValueError(
+            'The arguments of the function to be decorated with a contract checker include "_ARGS" which is '
+            'a reserved placeholder for positional arguments in the condition.')
+
+    if '_KWARGS' in sign.parameters:
+        raise ValueError(
+            'The arguments of the function to be decorated with a contract checker include "_KWARGS" which is '
+            'a reserved placeholder for keyword arguments in the condition.')
+
     param_names = list(sign.parameters.keys())
 
     # Determine the default argument values.
@@ -340,6 +360,14 @@ def decorate_with_checker(func: CallableT) -> CallableT:
         """Wrap func by checking the preconditions and postconditions."""
         # pylint: disable=too-many-branches
 
+        if '_ARGS' in kwargs:
+            raise TypeError('The arguments of the function call include "_ARGS" which is '
+                            'a placeholder for positional arguments in a condition.')
+
+        if '_KWARGS' in kwargs:
+            raise TypeError('The arguments of the function call include "_KWARGS" which is '
+                            'a placeholder for keyword arguments in a condition.')
+
         # Use try-finally instead of ExitStack for performance.
         try:
             # If the wrapper is already checking the contracts for the wrapped function, avoid a recursive loop


Test patch:
diff --git a/tests/test_args_and_kwargs_in_contract.py b/tests/test_args_and_kwargs_in_contract.py
new file mode 100644
index 0000000..e94343c
--- /dev/null
+++ b/tests/test_args_and_kwargs_in_contract.py
@@ -0,0 +1,239 @@
+# pylint: disable=missing-docstring
+# pylint: disable=no-self-use
+# pylint: disable=invalid-name
+# pylint: disable=unused-argument
+# pylint: disable=no-member
+# pylint: disable=unnecessary-lambda
+import copy
+import textwrap
+import unittest
+from typing import Optional, Any, Tuple, Dict
+
+import icontract
+import tests.error
+
+
+class TestArgs(unittest.TestCase):
+    def test_args_without_variable_positional_arguments(self) -> None:
+        recorded_args = None  # type: Optional[Tuple[Any, ...]]
+
+        def set_args(args: Tuple[Any, ...]) -> bool:
+            nonlocal recorded_args
+            recorded_args = copy.copy(args)
+            return True
+
+        @icontract.require(lambda _ARGS: set_args(_ARGS))
+        def some_func(x: int) -> None:
+            pass
+
+        some_func(3)
+
+        assert recorded_args is not None
+        self.assertTupleEqual((3, ), recorded_args)
+
+    def test_args_with_named_and_variable_positional_arguments(self) -> None:
+        recorded_args = None  # type: Optional[Tuple[Any, ...]]
+
+        def set_args(args: Tuple[Any, ...]) -> bool:
+            nonlocal recorded_args
+            recorded_args = copy.copy(args)
+            return True
+
+        @icontract.require(lambda _ARGS: set_args(_ARGS))
+        def some_func(x: int, *args: Any) -> None:
+            pass
+
+        some_func(3, 2)
+
+        assert recorded_args is not None
+        self.assertTupleEqual((3, 2), recorded_args)
+
+    def test_args_with_only_variable_positional_arguments(self) -> None:
+        recorded_args = None  # type: Optional[Tuple[Any, ...]]
+
+        def set_args(args: Tuple[Any, ...]) -> bool:
+            nonlocal recorded_args
+            recorded_args = copy.copy(args)
+            return True
+
+        @icontract.require(lambda _ARGS: set_args(_ARGS))
+        def some_func(*args: Any) -> None:
+            pass
+
+        some_func(3, 2, 1)
+
+        assert recorded_args is not None
+        self.assertTupleEqual((3, 2, 1), recorded_args)
+
+    def test_args_with_uncommon_variable_positional_arguments(self) -> None:
+        recorded_args = None  # type: Optional[Tuple[Any, ...]]
+
+        def set_args(args: Tuple[Any, ...]) -> bool:
+            nonlocal recorded_args
+            recorded_args = copy.copy(args)
+            return True
+
+        @icontract.require(lambda _ARGS: set_args(_ARGS))
+        def some_func(*arguments: Any) -> None:
+            pass
+
+        some_func(3, 2, 1, 0)
+
+        assert recorded_args is not None
+        self.assertTupleEqual((3, 2, 1, 0), recorded_args)
+
+    def test_fail(self) -> None:
+        @icontract.require(lambda _ARGS: len(_ARGS) > 2)
+        def some_func(*args: Any) -> None:
+            pass
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(3)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        assert violation_error is not None
+        self.assertEqual(
+            textwrap.dedent('''\
+                len(_ARGS) > 2:
+                _ARGS was (3,)
+                len(_ARGS) was 1'''), tests.error.wo_mandatory_location(str(violation_error)))
+
+
+class TestKwargs(unittest.TestCase):
+    def test_kwargs_without_variable_keyword_arguments(self) -> None:
+        recorded_kwargs = None  # type: Optional[Dict[str, Any]]
+
+        def set_kwargs(kwargs: Dict[str, Any]) -> bool:
+            nonlocal recorded_kwargs
+            recorded_kwargs = copy.copy(kwargs)
+            return True
+
+        @icontract.require(lambda _KWARGS: set_kwargs(_KWARGS))
+        def some_func(x: int) -> None:
+            pass
+
+        some_func(x=3)
+
+        assert recorded_kwargs is not None
+        self.assertDictEqual({"x": 3}, recorded_kwargs)
+
+    def test_kwargs_with_named_and_variable_keyword_arguments(self) -> None:
+        recorded_kwargs = None  # type: Optional[Dict[str, Any]]
+
+        def set_kwargs(kwargs: Dict[str, Any]) -> bool:
+            nonlocal recorded_kwargs
+            recorded_kwargs = copy.copy(kwargs)
+            return True
+
+        @icontract.require(lambda _KWARGS: set_kwargs(_KWARGS))
+        def some_func(x: int, **kwargs: Any) -> None:
+            pass
+
+        some_func(x=3, y=2)
+
+        assert recorded_kwargs is not None
+        self.assertDictEqual({"x": 3, "y": 2}, recorded_kwargs)
+
+    def test_kwargs_with_only_variable_keyword_arguments(self) -> None:
+        recorded_kwargs = None  # type: Optional[Dict[str, Any]]
+
+        def set_kwargs(kwargs: Dict[str, Any]) -> bool:
+            nonlocal recorded_kwargs
+            recorded_kwargs = copy.copy(kwargs)
+            return True
+
+        @icontract.require(lambda _KWARGS: set_kwargs(_KWARGS))
+        def some_func(**kwargs: Any) -> None:
+            pass
+
+        some_func(x=3, y=2, z=1)
+
+        assert recorded_kwargs is not None
+        self.assertDictEqual({"x": 3, "y": 2, "z": 1}, recorded_kwargs)
+
+    def test_kwargs_with_uncommon_argument_name_for_variable_keyword_arguments(self) -> None:
+        recorded_kwargs = None  # type: Optional[Dict[str, Any]]
+
+        def set_kwargs(kwargs: Dict[str, Any]) -> bool:
+            nonlocal recorded_kwargs
+            recorded_kwargs = copy.copy(kwargs)
+            return True
+
+        @icontract.require(lambda _KWARGS: set_kwargs(_KWARGS))
+        def some_func(**parameters: Any) -> None:
+            pass
+
+        some_func(x=3, y=2, z=1, a=0)
+
+        assert recorded_kwargs is not None
+        self.assertDictEqual({"x": 3, "y": 2, "z": 1, "a": 0}, recorded_kwargs)
+
+    def test_fail(self) -> None:
+        @icontract.require(lambda _KWARGS: 'x' in _KWARGS)
+        def some_func(**kwargs: Any) -> None:
+            pass
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            some_func(y=3)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        assert violation_error is not None
+        self.assertEqual(
+            textwrap.dedent("'x' in _KWARGS: _KWARGS was {'y': 3}"),
+            tests.error.wo_mandatory_location(str(violation_error)))
+
+
+class TestArgsAndKwargs(unittest.TestCase):
+    def test_that_args_and_kwargs_are_both_passed_as_placeholders(self) -> None:
+        recorded_args = None  # type: Optional[Tuple[Any, ...]]
+        recorded_kwargs = None  # type: Optional[Dict[str, Any]]
+
+        def set_args_and_kwargs(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> bool:
+            nonlocal recorded_args
+            nonlocal recorded_kwargs
+            recorded_args = copy.copy(args)
+            recorded_kwargs = copy.copy(kwargs)
+            return True
+
+        @icontract.require(lambda _ARGS, _KWARGS: set_args_and_kwargs(_ARGS, _KWARGS))
+        def some_func(*args: Any, **kwargs: Any) -> None:
+            pass
+
+        some_func(5, x=10, y=20, z=30)
+
+        assert recorded_args is not None
+        self.assertTupleEqual((5, ), recorded_args)
+
+        assert recorded_kwargs is not None
+        self.assertDictEqual({"x": 10, "y": 20, "z": 30}, recorded_kwargs)
+
+    def test_a_very_mixed_case(self) -> None:
+        recorded_args = None  # type: Optional[Tuple[Any, ...]]
+        recorded_kwargs = None  # type: Optional[Dict[str, Any]]
+
+        def set_args_and_kwargs(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> bool:
+            nonlocal recorded_args
+            nonlocal recorded_kwargs
+            recorded_args = copy.copy(args)
+            recorded_kwargs = copy.copy(kwargs)
+            return True
+
+        @icontract.require(lambda _ARGS, _KWARGS: set_args_and_kwargs(_ARGS, _KWARGS))
+        def some_func(x: int, y: int, *args: Any, **kwargs: Any) -> None:
+            pass
+
+        some_func(5, 10, 20, z=30)
+
+        assert recorded_args is not None
+        self.assertTupleEqual((5, 10, 20), recorded_args)
+
+        assert recorded_kwargs is not None
+        self.assertDictEqual({"z": 30}, recorded_kwargs)
+
+
+if __name__ == '__main__':
+    unittest.main()