diff --git a/.travis.yml b/.travis.yml index 32624d8..7155fe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ python: - "3.5" - "3.6" - "3.7" - - "3.8" + - "3.8.5" install: - pip3 install -e .[dev] - pip3 install coveralls diff --git a/icontract/_recompute.py b/icontract/_recompute.py index cbe7709..4e2d980 100644 --- a/icontract/_recompute.py +++ b/icontract/_recompute.py @@ -5,7 +5,8 @@ import functools import platform import sys -from typing import Any, Mapping, Dict, List, Optional, Union, Tuple, Set, Callable # pylint: disable=unused-import +from typing import Any, Mapping, Dict, List, Optional, Union, Tuple, Set, Callable, \ + cast # pylint: disable=unused-import class Placeholder: @@ -37,8 +38,11 @@ def __init__(self, variable_lookup: List[Mapping[str, Any]]) -> None: :param variable_lookup: list of lookup tables to look-up the values of the variables, sorted by precedence """ - # Resolve precedence of variable lookup + # _name_to_value maps the variable names to variable values. + # This is important for Load contexts as well as Store contexts in, e.g., named expressions. self._name_to_value = dict() # type: Dict[str, Any] + + # Resolve precedence of variable lookups for lookup in variable_lookup: for name, value in lookup.items(): if name not in self._name_to_value: @@ -314,6 +318,24 @@ def visit_Attribute(self, node: ast.Attribute) -> Any: self.recomputed_values[node] = result return result + if sys.version_info >= (3, 8): + + def visit_NamedExpr(self, node: ast.NamedExpr) -> Any: + """Visit the node's ``value`` and assign it to both this node and the target.""" + value = self.visit(node=node.value) + self.recomputed_values[node] = value + + # This assignment is needed to make mypy happy. + target = cast(ast.Name, node.target) + + if not isinstance(target.ctx, ast.Store): + raise NotImplementedError( + "Expected Store context in the target of a named expression, but got: {}".format(target.ctx)) + + self._name_to_value[target.id] = value + + return value + def visit_Index(self, node: ast.Index) -> Any: """Visit the node's ``value``.""" result = self.visit(node=node.value) diff --git a/icontract/_represent.py b/icontract/_represent.py index 81a2082..e35ef35 100644 --- a/icontract/_represent.py +++ b/icontract/_represent.py @@ -88,6 +88,16 @@ def visit_Attribute(self, node: ast.Attribute) -> None: self.generic_visit(node=node) + def visit_NamedExpr(self, node: ast.NamedExpr) -> Any: + """Represent the target with the value of the node.""" + if node in self._recomputed_values: + value = self._recomputed_values[node] + + if _representable(value=value): + self.reprs[node.target.id] = value # type: ignore + + self.generic_visit(node=node) + def visit_Call(self, node: ast.Call) -> None: """Represent the call by dumping its source code.""" if node in self._recomputed_values: diff --git a/precommit.py b/precommit.py index 1592666..926809e 100755 --- a/precommit.py +++ b/precommit.py @@ -24,6 +24,10 @@ def main() -> int: print("YAPF'ing...") yapf_targets = ["tests", "icontract", "setup.py", "precommit.py", "benchmark.py", "benchmarks", "tests_with_others"] + + if sys.version_info >= (3, 8, 5): + yapf_targets.append('tests_3_8') + if overwrite: subprocess.check_call( ["yapf", "--in-place", "--style=style.yapf", "--recursive"] + yapf_targets, cwd=str(repo_root)) @@ -32,10 +36,18 @@ def main() -> int: ["yapf", "--diff", "--style=style.yapf", "--recursive"] + yapf_targets, cwd=str(repo_root)) print("Mypy'ing...") - subprocess.check_call(["mypy", "--strict", "icontract", "tests"], cwd=str(repo_root)) + mypy_targets = ["icontract", "tests"] + if sys.version_info >= (3, 8): + mypy_targets.append('tests_3_8') + + subprocess.check_call(["mypy", "--strict"] + mypy_targets, cwd=str(repo_root)) print("Pylint'ing...") - subprocess.check_call(["pylint", "--rcfile=pylint.rc", "tests", "icontract"], cwd=str(repo_root)) + pylint_targets = ['icontract', 'tests'] + + if sys.version_info >= (3, 8): + pylint_targets.append('tests_3_8') + subprocess.check_call(["pylint", "--rcfile=pylint.rc"] + pylint_targets, cwd=str(repo_root)) print("Pydocstyle'ing...") subprocess.check_call(["pydocstyle", "icontract"], cwd=str(repo_root)) @@ -45,10 +57,14 @@ def main() -> int: env['ICONTRACT_SLOW'] = 'true' # yapf: disable + unittest_targets = ['tests'] + if sys.version_info > (3, 8): + unittest_targets.append('tests_3_8') + subprocess.check_call( ["coverage", "run", "--source", "icontract", - "-m", "unittest", "discover", "tests"], + "-m", "unittest", "discover"] + unittest_targets, cwd=str(repo_root), env=env) # yapf: enable diff --git a/tests_3_8/__init__.py b/tests_3_8/__init__.py new file mode 100644 index 0000000..774c18e --- /dev/null +++ b/tests_3_8/__init__.py @@ -0,0 +1,7 @@ +""" +Test Python 3.8-specific features. + +For example, one such feature is walrus operator used in named expressions. +We have to exclude these tests running on prior versions of Python since the syntax would be considered +invalid. +""" diff --git a/tests_3_8/test_represent.py b/tests_3_8/test_represent.py new file mode 100644 index 0000000..2aa9d99 --- /dev/null +++ b/tests_3_8/test_represent.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-docstring,invalid-name,too-many-public-methods,no-self-use +# pylint: disable=unused-argument + +import textwrap +import unittest +from typing import Optional # pylint: disable=unused-import + +import icontract._represent +import tests.error +import tests.mock + + +class TestReprValues(unittest.TestCase): + def test_named_expression(self) -> None: + @icontract.require(lambda x: (t := x + 1) and t > 1) # pylint: disable=undefined-variable + def func(x: int) -> int: + return x + + violation_err = None # type: Optional[icontract.ViolationError] + try: + func(x=0) + except icontract.ViolationError as err: + violation_err = err + + self.assertIsNotNone(violation_err) + self.assertEqual( + textwrap.dedent('''\ + (t := x + 1) and t > 1: + t was 1 + x was 0'''), tests.error.wo_mandatory_location(str(violation_err))) + + +if __name__ == '__main__': + unittest.main()