Repository: Parquery/icontract (python)

Issue:
Missing arg for ViolationError in python 3.10
Minimal working example in `/tmp/spam.py`:

```python
from icontract import require, ViolationError


@require(lambda _ARGS: bool([s for s in _ARGS]))
def spam(*args):
    pass


if __name__ == '__main__':
    try:
        spam(*[])
    except ViolationError:
        print(bool([s for s in []]))
```

Expected behavior (python `3.9`): 

```shell
$ python --version
Python 3.9.7
$ python -m pip freeze | grep icontract
icontract==2.5.5
$ python /tmp/spam.py
False
```

False behavior (python `3.10`):

```shell
$ python --version
Python 3.10.0
$ python -m pip freeze | grep icontract
icontract==2.5.5
$ python /tmp/spam.py
Traceback (most recent call last):
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_checkers.py", line 181, in _create_violation_error
    msg = icontract._represent.generate_message(contract=contract, resolved_kwargs=resolved_kwargs)
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_represent.py", line 542, in generate_message
    repr_vals = repr_values(
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_represent.py", line 463, in repr_values
    recompute_visitor.visit(node=lambda_inspection.node.body)
  File "/usr/lib/python3.10/ast.py", line 410, in visit
    return visitor(node)
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_recompute.py", line 577, in visit_Call
    args.append(self.visit(node=arg_node))
  File "/usr/lib/python3.10/ast.py", line 410, in visit
    return visitor(node)
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_recompute.py", line 882, in visit_ListComp
    result = self._execute_comprehension(node=node)
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_recompute.py", line 807, in _execute_comprehension
    code = compile(source=module_node, filename='<ast>', mode='exec')
TypeError: required field "posonlyargs" missing from arguments

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tmp/spam.py", line 11, in <module>
    spam(*[])
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_checkers.py", line 628, in wrapper
    violation_error = _assert_preconditions(
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_checkers.py", line 290, in _assert_preconditions
    exception = _create_violation_error(contract=contract, resolved_kwargs=resolved_kwargs)
  File "/tmp/venv/lib/python3.10/site-packages/icontract/_checkers.py", line 192, in _create_violation_error
    raise RuntimeError(''.join(parts)) from err
RuntimeError: Failed to recompute the values of the contract condition:
File /tmp/spam.py, line 4 in <module>:
lambda _ARGS: bool([s for s in _ARGS])
```

Pull request:
Added support for Python 3.9 and 3.10
This patch introduces Python versions 3.9 and 3.10 in continuous
integration as well as minor fixes caused by the deprecations in
these new versions.

Also fixes #235.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ad9431f..269d4c5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8]
+        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
 
     steps:
       - uses: actions/checkout@master
diff --git a/.gitignore b/.gitignore
index 81ed89a..8aab198 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
-venv3/
-venv/
+venv*/
 .mypy_cache/
 .idea/
 *.pyc
diff --git a/icontract/_metaclass.py b/icontract/_metaclass.py
index 2fb9932..234bf0a 100644
--- a/icontract/_metaclass.py
+++ b/icontract/_metaclass.py
@@ -1,7 +1,7 @@
 """Define the metaclass necessary to inherit the contracts from the base classes."""
 import abc
 import inspect
-import platform
+import sys
 import weakref
 from typing import List, MutableMapping, Any, Callable, Optional, cast, Set, Type, \
     TypeVar  # pylint: disable=unused-import
@@ -316,12 +316,12 @@ class DBCMeta(abc.ABCMeta):
     # instead of ``mcs``.
     # pylint: disable=bad-mcs-classmethod-argument
 
-    if platform.python_version_tuple() < ('3', ):
-        raise NotImplementedError("Python versions below not supported, got: {}".format(platform.python_version()))
+    if sys.version_info < (3, ):
+        raise NotImplementedError("Python versions below not supported, got: {}".format(sys.version_info))
 
-    if platform.python_version_tuple() <= ('3', '5'):
+    if sys.version_info < (3, 6):
         # pylint: disable=arguments-differ
-        def __new__(mlcs, name, bases, namespace):  # type: ignore
+        def __new__(mlcs, name, bases, namespace):
             """Create a class with inherited preconditions, postconditions and invariants."""
             _dbc_decorate_namespace(bases, namespace)
 
@@ -335,7 +335,7 @@ class DBCMeta(abc.ABCMeta):
             # This usually works since icontract-hypothesis does not use DBCMeta,
             # but blows up since icontract creates DBC with DBCMeta meta-class at the import time.
             if cls.__module__ != __name__:
-                _register_for_hypothesis(cls)  # type: ignore
+                _register_for_hypothesis(cls)
 
             return cls
     else:
diff --git a/icontract/_recompute.py b/icontract/_recompute.py
index 3dbf895..9d69582 100644
--- a/icontract/_recompute.py
+++ b/icontract/_recompute.py
@@ -5,7 +5,6 @@ import builtins
 import copy
 import functools
 import inspect
-import platform
 import sys
 import uuid
 from typing import (Any, Mapping, Dict, List, Optional, Union, Tuple, Set, Callable, cast, Iterable, TypeVar)  # pylint: disable=unused-import
@@ -163,11 +162,11 @@ def _translate_all_expression_to_a_module(generator_exp: ast.GeneratorExp, gener
 
     args = [ast.arg(arg=name, annotation=None) for name in sorted(name_to_value.keys())]
 
-    if platform.python_version_tuple() < ('3', '5'):
-        raise NotImplementedError("Python versions below 3.5 not supported, got: {}".format(platform.python_version()))
+    if sys.version_info < (3, 5):
+        raise NotImplementedError("Python versions below 3.5 not supported, got: {}".format(sys.version_info))
 
     if not is_async:
-        if platform.python_version_tuple() < ('3', '8'):
+        if sys.version_info < (3, 8):
             func_def_node = ast.FunctionDef(
                 name=generated_function_name,
                 args=ast.arguments(args=args, kwonlyargs=[], kw_defaults=[], defaults=[], vararg=None, kwarg=None),
@@ -185,23 +184,23 @@ def _translate_all_expression_to_a_module(generator_exp: ast.GeneratorExp, gener
 
             module_node = ast.Module(body=[func_def_node], type_ignores=[])
     else:
-        if platform.python_version_tuple() < ('3', '8'):
-            func_def_node = ast.AsyncFunctionDef(
+        if sys.version_info < (3, 8):
+            async_func_def_node = ast.AsyncFunctionDef(
                 name=generated_function_name,
                 args=ast.arguments(args=args, kwonlyargs=[], kw_defaults=[], defaults=[], vararg=None, kwarg=None),
                 decorator_list=[],
                 body=block)
 
-            module_node = ast.Module(body=[func_def_node])
+            module_node = ast.Module(body=[async_func_def_node])
         else:
-            func_def_node = ast.AsyncFunctionDef(
+            async_func_def_node = ast.AsyncFunctionDef(
                 name=generated_function_name,
                 args=ast.arguments(
                     args=args, posonlyargs=[], kwonlyargs=[], kw_defaults=[], defaults=[], vararg=None, kwarg=None),
                 decorator_list=[],
                 body=block)
 
-            module_node = ast.Module(body=[func_def_node], type_ignores=[])
+            module_node = ast.Module(body=[async_func_def_node], type_ignores=[])
 
     ast.fix_missing_locations(module_node)
 
@@ -664,16 +663,18 @@ class Visitor(ast.NodeVisitor):
 
             return value
 
-    def visit_Index(self, node: ast.Index) -> Any:
-        """Visit the node's ``value``."""
-        result = self.visit(node=node.value)
+    if sys.version_info < (3, 9):
 
-        # Please see "NOTE ABOUT PLACEHOLDERS AND RE-COMPUTATION"
-        if result is PLACEHOLDER:
-            return PLACEHOLDER
+        def visit_Index(self, node: ast.Index) -> Any:
+            """Visit the node's ``value``."""
+            result = self.visit(node=node.value)
 
-        self.recomputed_values[node] = result
-        return result
+            # Please see "NOTE ABOUT PLACEHOLDERS AND RE-COMPUTATION"
+            if result is PLACEHOLDER:
+                return PLACEHOLDER
+
+            self.recomputed_values[node] = result
+            return result
 
     def visit_Slice(self, node: ast.Slice) -> Union[slice, Placeholder]:
         """Visit ``lower``, ``upper`` and ``step`` and recompute the node as a ``slice``."""
@@ -698,15 +699,17 @@ class Visitor(ast.NodeVisitor):
         self.recomputed_values[node] = result
         return result
 
-    def visit_ExtSlice(self, node: ast.ExtSlice) -> Union[Tuple[Any, ...], Placeholder]:
-        """Visit each dimension of the advanced slicing and assemble the dimensions in a tuple."""
-        result = tuple(self.visit(node=dim) for dim in node.dims)
+    if sys.version_info < (3, 9):
 
-        if any(value is PLACEHOLDER for value in result):
-            return PLACEHOLDER
+        def visit_ExtSlice(self, node: ast.ExtSlice) -> Union[Tuple[Any, ...], Placeholder]:
+            """Visit each dimension of the advanced slicing and assemble the dimensions in a tuple."""
+            result = tuple(self.visit(node=dim) for dim in node.dims)
 
-        self.recomputed_values[node] = result
-        return result
+            if any(value is PLACEHOLDER for value in result):
+                return PLACEHOLDER
+
+            self.recomputed_values[node] = result
+            return result
 
     def visit_Subscript(self, node: ast.Subscript) -> Any:
         """Visit the ``slice`` and a ``value`` and get the element."""
@@ -780,11 +783,10 @@ class Visitor(ast.NodeVisitor):
 
         args = [ast.arg(arg=name, annotation=None) for name in sorted(self._name_to_value.keys())]
 
-        if platform.python_version_tuple() < ('3', ):
-            raise NotImplementedError("Python versions below 3 not supported, got: {}".format(
-                platform.python_version()))
+        if sys.version_info < (3, ):
+            raise NotImplementedError("Python versions below 3 not supported, got: {}".format(sys.version_info))
 
-        if platform.python_version_tuple() < ('3', '8'):
+        if sys.version_info < (3, 8):
             func_def_node = ast.FunctionDef(
                 name="generator_expr",
                 args=ast.arguments(args=args, kwonlyargs=[], kw_defaults=[], defaults=[]),
diff --git a/setup.py b/setup.py
index 0700560..6e77036 100644
--- a/setup.py
+++ b/setup.py
@@ -41,7 +41,9 @@ setup(
         'License :: OSI Approved :: MIT License',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8'
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10'
         # yapf: enable
     ],
     license='License :: OSI Approved :: MIT License',
@@ -50,7 +52,7 @@ setup(
     install_requires=install_requires,
     extras_require={
         'dev': [
-            'mypy==0.910', 'pylint==2.9.6', 'yapf==0.20.2', 'tox>=3.0.0', 'pydocstyle>=2.1.1,<3', 'coverage>=4.5.1,<5',
+            'mypy==0.910', 'pylint==2.9.6', 'yapf==0.20.2', 'tox>=3.0.0', 'pydocstyle>=6.1.1,<7', 'coverage>=4.5.1,<5',
             'docutils>=0.14,<1', 'pygments>=2.2.0,<3', 'dpcontracts==0.6.0', 'tabulate>=0.8.7,<1',
             'py-cpuinfo>=5.0.0,<6', 'typeguard>=2,<3', 'astor==0.8.1', 'numpy>=1,<2'
         ] + (['deal==4.1.0'] if sys.version_info >= (3, 8) else []) + (['asyncstdlib==3.9.1']


Test patch:
diff --git a/tests/test_checkers.py b/tests/test_checkers.py
index f7bb841..8a771c6 100644
--- a/tests/test_checkers.py
+++ b/tests/test_checkers.py
@@ -66,7 +66,8 @@ class TestResolveKwargs(unittest.TestCase):
             type_error = error
 
         assert type_error is not None
-        self.assertEqual("some_func() takes 2 positional arguments but 3 were given", str(type_error))
+        self.assertRegex(
+            str(type_error), r"^([a-zA-Z_0-9<>.]+\.)?some_func\(\) takes 2 positional arguments but 3 were given$")
 
     def test_that_result_in_kwargs_raises_an_error(self) -> None:
         @icontract.ensure(lambda result: result > 0)
diff --git a/tests/test_inheritance_postcondition.py b/tests/test_inheritance_postcondition.py
index fd63ea9..82c87d7 100644
--- a/tests/test_inheritance_postcondition.py
+++ b/tests/test_inheritance_postcondition.py
@@ -3,6 +3,7 @@
 # pylint: disable=no-self-use
 
 import abc
+import sys
 import textwrap
 import unittest
 from typing import Optional  # pylint: disable=unused-import
@@ -478,7 +479,10 @@ class TestInvalid(unittest.TestCase):
             type_err = err
 
         self.assertIsNotNone(type_err)
-        self.assertEqual("Can't instantiate abstract class B with abstract methods func", str(type_err))
+        if sys.version_info < (3, 9):
+            self.assertEqual("Can't instantiate abstract class B with abstract methods func", str(type_err))
+        else:
+            self.assertEqual("Can't instantiate abstract class B with abstract method func", str(type_err))
 
 
 if __name__ == '__main__':
diff --git a/tests/test_inheritance_precondition.py b/tests/test_inheritance_precondition.py
index 0c92801..52d7d44 100644
--- a/tests/test_inheritance_precondition.py
+++ b/tests/test_inheritance_precondition.py
@@ -3,6 +3,7 @@
 # pylint: disable=no-self-use
 
 import abc
+import sys
 import textwrap
 import unittest
 from typing import Optional, Sequence, cast  # pylint: disable=unused-import
@@ -526,7 +527,10 @@ class TestInvalid(unittest.TestCase):
             type_err = err
 
         self.assertIsNotNone(type_err)
-        self.assertEqual("Can't instantiate abstract class B with abstract methods func", str(type_err))
+        if sys.version_info < (3, 9):
+            self.assertEqual("Can't instantiate abstract class B with abstract methods func", str(type_err))
+        else:
+            self.assertEqual("Can't instantiate abstract class B with abstract method func", str(type_err))
 
     def test_cant_weaken_base_function_without_preconditions(self) -> None:
         class A(icontract.DBC):
diff --git a/tests/test_precondition.py b/tests/test_precondition.py
index fd7eced..c500f35 100644
--- a/tests/test_precondition.py
+++ b/tests/test_precondition.py
@@ -9,7 +9,7 @@ import pathlib
 import textwrap
 import time
 import unittest
-from typing import Optional, Callable, Any  # pylint: disable=unused-import
+from typing import Optional  # pylint: disable=unused-import
 
 import icontract
 from icontract._globals import CallableT
@@ -611,7 +611,8 @@ class TestInvalid(unittest.TestCase):
             type_error = err
 
         self.assertIsNotNone(type_error)
-        self.assertEqual('some_func() takes 0 positional arguments but 1 was given', str(type_error))
+        self.assertRegex(
+            str(type_error), r'^([a-zA-Z_0-9<>.]+\.)?some_func\(\) takes 0 positional arguments but 1 was given$')
 
     def test_unexpected_keyword_argument(self) -> None:
         @icontract.require(lambda: True)
@@ -626,7 +627,8 @@ class TestInvalid(unittest.TestCase):
             type_error = err
 
         self.assertIsNotNone(type_error)
-        self.assertEqual("some_func() got an unexpected keyword argument 'x'", str(type_error))
+        self.assertRegex(
+            str(type_error), r"^([a-zA-Z_0-9<>.]+\.)?some_func\(\) got an unexpected keyword argument 'x'$")
 
 
 if __name__ == '__main__':