├── .github └── workflows │ ├── ci.yml │ └── publish-to-pypi.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.rst ├── LICENSE.txt ├── README.rst ├── benchmark.py ├── benchmarks ├── against_others │ ├── compare_invariant.py │ ├── compare_postcondition.py │ └── compare_precondition.py ├── benchmark_2.0.7.rst ├── import_cost │ ├── classes_100_with_10_disabled_invariants.py │ ├── classes_100_with_10_invariants.py │ ├── classes_100_with_1_disabled_invariant.py │ ├── classes_100_with_1_invariant.py │ ├── classes_100_with_5_disabled_invariants.py │ ├── classes_100_with_5_invariants.py │ ├── classes_100_with_no_invariant.py │ ├── functions_100_with_10_contracts.py │ ├── functions_100_with_10_disabled_contracts.py │ ├── functions_100_with_1_contract.py │ ├── functions_100_with_1_disabled_contract.py │ ├── functions_100_with_5_contracts.py │ ├── functions_100_with_5_disabled_contracts.py │ ├── functions_100_with_no_contract.py │ ├── generate.py │ ├── measure.py │ └── runme.py └── runtime_cost │ ├── my_sqrt.py │ ├── my_sqrt_assert_as_contract.py │ ├── my_sqrt_function_as_contract.py │ ├── my_sqrt_with_icontract.py │ └── my_sqrt_with_icontract_disabled.py ├── docs ├── .gitignore ├── Makefile ├── how-to-build-docs.md └── source │ ├── api.rst │ ├── async.rst │ ├── benchmarks.rst │ ├── changelog.rst │ ├── checking_types_at_runtime.rst │ ├── conf.py │ ├── development.rst │ ├── implementation_details.rst │ ├── index.rst │ ├── introduction.rst │ ├── known_issues.rst │ ├── recipes.rst │ └── usage.rst ├── icontract ├── __init__.py ├── _checkers.py ├── _decorators.py ├── _globals.py ├── _metaclass.py ├── _recompute.py ├── _represent.py ├── _types.py ├── errors.py └── py.typed ├── mypy.ini ├── precommit.py ├── pylint.rc ├── requirements-doc.txt ├── setup.py ├── style.yapf ├── tests ├── __init__.py ├── error.py ├── mock.py ├── test_args_and_kwargs_in_contract.py ├── test_checkers.py ├── test_error.py ├── test_for_integrators.py ├── test_globals.py ├── test_inheritance_invariant.py ├── test_inheritance_postcondition.py ├── test_inheritance_precondition.py ├── test_inheritance_snapshot.py ├── test_invariant.py ├── test_mypy_decorators.py ├── test_postcondition.py ├── test_precondition.py ├── test_recompute.py ├── test_recursion.py ├── test_represent.py ├── test_snapshot.py ├── test_threading.py └── test_typeguard.py ├── tests_3_6 ├── __init__.py └── test_represent.py ├── tests_3_7 ├── __init__.py └── test_invariant.py ├── tests_3_8 ├── __init__.py ├── async │ ├── __init__.py │ ├── separately_test_concurrent.py │ ├── test_args_and_kwargs_in_contract.py │ ├── test_coroutine_example.py │ ├── test_exceptions.py │ ├── test_invariant.py │ ├── test_postcondition.py │ ├── test_precondition.py │ └── test_recursion.py ├── test_error.py ├── test_invariant.py └── test_represent.py ├── tests_with_others ├── test_deal.py └── test_dpcontracts.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | tags: [ "**" ] 7 | pull_request: 8 | branches: [ "**" ] 9 | 10 | jobs: 11 | Execute: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 16 | 17 | steps: 18 | - uses: actions/checkout@master 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: | 27 | python3 -m pip install --upgrade pip 28 | pip3 install --upgrade coveralls 29 | pip3 install -e .[dev] 30 | 31 | - name: Run checks 32 | run: | 33 | python3 precommit.py 34 | 35 | - name: Upload Coverage 36 | run: coveralls --service=github 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | COVERALLS_FLAG_NAME: ${{ matrix.python-version }} 40 | COVERALLS_PARALLEL: true 41 | 42 | Finish-Coveralls: 43 | name: Finish Coveralls 44 | needs: Execute 45 | runs-on: ubuntu-latest 46 | container: python:3-slim 47 | steps: 48 | - name: Finish Coveralls 49 | run: | 50 | pip3 install --upgrade coveralls 51 | coveralls --finish --service=github 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml 2 | name: Publish to Pypi 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | push: 9 | branches: 10 | - '*/fixed-publishing-to-pypi' 11 | 12 | jobs: 13 | deploy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install setuptools wheel twine 29 | 30 | - name: Build and publish 31 | env: 32 | TWINE_USERNAME: "__token__" 33 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 34 | run: | 35 | python setup.py sdist bdist_wheel 36 | twine upload dist/* 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv*/ 2 | .mypy_cache/ 3 | .idea/ 4 | *.pyc 5 | .precommit_hashes 6 | *.egg-info 7 | .tox/ 8 | dist/ 9 | .coverage 10 | htmlcov 11 | README.html 12 | build/ 13 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF 13 | formats: 14 | - pdf 15 | 16 | build: 17 | os: ubuntu-22.04 18 | tools: 19 | python: "3.8" 20 | 21 | python: 22 | install: 23 | - method: pip 24 | path: . 25 | - requirements: requirements-doc.txt 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Parquery AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | icontract 2 | ========= 3 | .. image:: https://github.com/Parquery/icontract/workflows/CI/badge.svg 4 | :target: https://github.com/Parquery/icontract/actions?query=workflow%3ACI 5 | :alt: Continuous integration 6 | 7 | .. image:: https://coveralls.io/repos/github/Parquery/icontract/badge.svg?branch=master 8 | :target: https://coveralls.io/github/Parquery/icontract 9 | 10 | .. image:: https://badge.fury.io/py/icontract.svg 11 | :target: https://badge.fury.io/py/icontract 12 | :alt: PyPI - version 13 | 14 | .. image:: https://img.shields.io/pypi/pyversions/icontract.svg 15 | :alt: PyPI - Python Version 16 | 17 | .. image:: https://readthedocs.org/projects/icontract/badge/?version=latest 18 | :target: https://icontract.readthedocs.io/en/latest/ 19 | :alt: Documentation 20 | 21 | .. image:: https://badges.gitter.im/gitterHQ/gitter.svg 22 | :target: https://gitter.im/Parquery-icontract/community 23 | :alt: Gitter chat 24 | 25 | icontract provides `design-by-contract `_ to Python3 with informative 26 | violation messages and inheritance. 27 | 28 | It also gives a base for a flourishing of a wider ecosystem: 29 | 30 | * A linter `pyicontract-lint`_, 31 | * A sphinx plug-in `sphinx-icontract`_, 32 | * A tool `icontract-hypothesis`_ for automated testing and ghostwriting test files which infers 33 | `Hypothesis`_ strategies based on the contracts, 34 | 35 | * together with IDE integrations such as 36 | `icontract-hypothesis-vim`_, 37 | `icontract-hypothesis-pycharm`_, and 38 | `icontract-hypothesis-vscode`_, 39 | * Directly integrated into `CrossHair`_, a tool for automatic verification of Python programs, 40 | 41 | * together with IDE integrations such as 42 | `crosshair-pycharm`_ and `crosshair-vscode`_, and 43 | * An integration with `FastAPI`_ through `fastapi-icontract`_ to enforce contracts on your HTTP API and display them 44 | in OpenAPI 3 schema and Swagger UI, and 45 | * An extensive corpus, `Python-by-contract corpus`_, of Python programs annotated with contracts for educational, testing and research purposes. 46 | 47 | .. _pyicontract-lint: https://pypi.org/project/pyicontract-lint 48 | .. _sphinx-icontract: https://pypi.org/project/sphinx-icontract 49 | .. _icontract-hypothesis: https://github.com/mristin/icontract-hypothesis 50 | .. _Hypothesis: https://hypothesis.readthedocs.io/en/latest/ 51 | .. _icontract-hypothesis-vim: https://github.com/mristin/icontract-hypothesis-vim 52 | .. _icontract-hypothesis-pycharm: https://github.com/mristin/icontract-hypothesis-pycharm 53 | .. _icontract-hypothesis-vscode: https://github.com/mristin/icontract-hypothesis-vscode 54 | .. _CrossHair: https://github.com/pschanely/CrossHair 55 | .. _crosshair-pycharm: https://github.com/mristin/crosshair-pycharm/ 56 | .. _crosshair-vscode: https://github.com/mristin/crosshair-vscode/ 57 | .. _FastAPI: https://github.com/tiangolo/fastapi/issues/1996 58 | .. _fastapi-icontract: https://pypi.org/project/fastapi-icontract/ 59 | .. _Python-by-contract corpus: https://github.com/mristin/python-by-contract-corpus 60 | 61 | Related Projects 62 | ---------------- 63 | There exist a couple of contract libraries. However, at the time of this writing (September 2018), they all required the 64 | programmer either to learn a new syntax (`PyContracts `_) or to write 65 | redundant condition descriptions ( 66 | *e.g.*, 67 | `contracts `_, 68 | `covenant `_, 69 | `deal `_, 70 | `dpcontracts `_, 71 | `pyadbc `_ and 72 | `pcd `_). 73 | 74 | This library was strongly inspired by them, but we go two steps further. 75 | 76 | First, our violation message on contract breach are much more informative. The message includes the source code of the 77 | contract condition as well as variable values at the time of the breach. This promotes don't-repeat-yourself principle 78 | (`DRY `_) and spare the programmer the tedious task of repeating 79 | the message that was already written in code. 80 | 81 | Second, icontract allows inheritance of the contracts and supports weakining of the preconditions 82 | as well as strengthening of the postconditions and invariants. Notably, weakining and strengthening of the contracts 83 | is a feature indispensable for modeling many non-trivial class hierarchies. Please see Section 84 | `Inheritance `_. 85 | To the best of our knowledge, there is currently no other Python library that supports inheritance of the contracts in a 86 | correct way. 87 | 88 | In the long run, we hope that design-by-contract will be adopted and integrated in the language. Consider this library 89 | a work-around till that happens. You might be also interested in the archived discussion on how to bring 90 | design-by-contract into Python language on 91 | `python-ideas mailing list `_. 92 | 93 | Teasers 94 | ======= 95 | We give a couple of teasers here to motivate the library. 96 | Please see the documentation available on `icontract.readthedocs.io 97 | `_ for a full scope of its 98 | capabilities. 99 | 100 | The script is also available as a `repl.it post`_. 101 | 102 | .. _repl.it post: https://repl.it/talk/share/icontract-example-script/121190 103 | 104 | .. code-block:: python 105 | 106 | >>> import icontract 107 | 108 | >>> @icontract.require(lambda x: x > 3) 109 | ... def some_func(x: int, y: int = 5) -> None: 110 | ... pass 111 | ... 112 | 113 | >>> some_func(x=5) 114 | 115 | # Pre-condition violation 116 | >>> some_func(x=1) 117 | Traceback (most recent call last): 118 | ... 119 | icontract.errors.ViolationError: File , line 1 in : 120 | x > 3: 121 | x was 1 122 | y was 5 123 | 124 | # Pre-condition violation with a description 125 | >>> @icontract.require(lambda x: x > 3, "x must not be small") 126 | ... def some_func(x: int, y: int = 5) -> None: 127 | ... pass 128 | ... 129 | >>> some_func(x=1) 130 | Traceback (most recent call last): 131 | ... 132 | icontract.errors.ViolationError: File , line 1 in : 133 | x must not be small: x > 3: 134 | x was 1 135 | y was 5 136 | 137 | # Pre-condition violation with more complex values 138 | >>> class B: 139 | ... def __init__(self) -> None: 140 | ... self.x = 7 141 | ... 142 | ... def y(self) -> int: 143 | ... return 2 144 | ... 145 | ... def __repr__(self) -> str: 146 | ... return "instance of B" 147 | ... 148 | >>> class A: 149 | ... def __init__(self) -> None: 150 | ... self.b = B() 151 | ... 152 | ... def __repr__(self) -> str: 153 | ... return "instance of A" 154 | ... 155 | >>> SOME_GLOBAL_VAR = 13 156 | >>> @icontract.require(lambda a: a.b.x + a.b.y() > SOME_GLOBAL_VAR) 157 | ... def some_func(a: A) -> None: 158 | ... pass 159 | ... 160 | >>> an_a = A() 161 | >>> some_func(an_a) 162 | Traceback (most recent call last): 163 | ... 164 | icontract.errors.ViolationError: File , line 1 in : 165 | a.b.x + a.b.y() > SOME_GLOBAL_VAR: 166 | SOME_GLOBAL_VAR was 13 167 | a was instance of A 168 | a.b was instance of B 169 | a.b.x was 7 170 | a.b.y() was 2 171 | 172 | # Post-condition 173 | >>> @icontract.ensure(lambda result, x: result > x) 174 | ... def some_func(x: int, y: int = 5) -> int: 175 | ... return x - y 176 | ... 177 | >>> some_func(x=10) 178 | Traceback (most recent call last): 179 | ... 180 | icontract.errors.ViolationError: File , line 1 in : 181 | result > x: 182 | result was 5 183 | x was 10 184 | y was 5 185 | 186 | 187 | # Pre-conditions fail before post-conditions. 188 | >>> @icontract.ensure(lambda result, x: result > x) 189 | ... @icontract.require(lambda x: x > 3, "x must not be small") 190 | ... def some_func(x: int, y: int = 5) -> int: 191 | ... return x - y 192 | ... 193 | >>> some_func(x=3) 194 | Traceback (most recent call last): 195 | ... 196 | icontract.errors.ViolationError: File , line 2 in : 197 | x must not be small: x > 3: 198 | x was 3 199 | y was 5 200 | 201 | # Invariant 202 | >>> @icontract.invariant(lambda self: self.x > 0) 203 | ... class SomeClass: 204 | ... def __init__(self) -> None: 205 | ... self.x = -1 206 | ... 207 | ... def __repr__(self) -> str: 208 | ... return "an instance of SomeClass" 209 | ... 210 | >>> some_instance = SomeClass() 211 | Traceback (most recent call last): 212 | ... 213 | icontract.errors.ViolationError: File , line 1 in : 214 | self.x > 0: 215 | self was an instance of SomeClass 216 | self.x was -1 217 | 218 | 219 | Installation 220 | ============ 221 | 222 | * Install icontract with pip: 223 | 224 | .. code-block:: bash 225 | 226 | pip3 install icontract 227 | 228 | Versioning 229 | ========== 230 | We follow `Semantic Versioning `_. The version X.Y.Z indicates: 231 | 232 | * X is the major version (backward-incompatible), 233 | * Y is the minor version (backward-compatible), and 234 | * Z is the patch version (backward-compatible bug fix). 235 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pathlib 3 | import platform 4 | import subprocess 5 | import sys 6 | from typing import List 7 | 8 | import cpuinfo 9 | 10 | import icontract 11 | 12 | """Run benchmarks and, if specified, overwrite README.""" 13 | 14 | 15 | def benchmark_against_others(repo_root: pathlib.Path, overwrite: bool) -> None: 16 | """Run benchmars against other libraries and include them in the Readme.""" 17 | script_rel_paths = [ 18 | "benchmarks/against_others/compare_invariant.py", 19 | "benchmarks/against_others/compare_precondition.py", 20 | "benchmarks/against_others/compare_postcondition.py", 21 | ] 22 | 23 | if not overwrite: 24 | for i, script_rel_path in enumerate(script_rel_paths): 25 | if i > 0: 26 | print() 27 | subprocess.check_call([sys.executable, str(repo_root / script_rel_path)]) 28 | else: 29 | out = ["The following scripts were run:\n\n"] 30 | for script_rel_path in script_rel_paths: 31 | out.append( 32 | "* `{0} `_\n".format( 33 | script_rel_path 34 | ) 35 | ) 36 | out.append("\n") 37 | 38 | out.append( 39 | ( 40 | "The benchmarks were executed on {}.\nWe used Python {}, " 41 | "icontract {}, deal 4.23.3 and dpcontracts 0.6.0.\n\n" 42 | ).format( 43 | cpuinfo.get_cpu_info()["brand"], 44 | platform.python_version(), 45 | icontract.__version__, 46 | ) 47 | ) 48 | 49 | out.append("The following tables summarize the results.\n\n") 50 | stdouts = [] # type: List[str] 51 | 52 | for script_rel_path in script_rel_paths: 53 | stdout = subprocess.check_output( 54 | [sys.executable, str(repo_root / script_rel_path)] 55 | ).decode() 56 | stdouts.append(stdout) 57 | 58 | out.append(stdout) 59 | out.append("\n") 60 | 61 | readme_path = repo_root / "docs" / "source" / "benchmarks.rst" 62 | readme = readme_path.read_text(encoding="utf-8") 63 | marker_start = ".. Becnhmark report from benchmark.py starts." 64 | marker_end = ".. Benchmark report from benchmark.py ends." 65 | lines = readme.splitlines() 66 | 67 | try: 68 | index_start = lines.index(marker_start) 69 | except ValueError as exc: 70 | raise ValueError( 71 | "Could not find the marker for the benchmarks in the {}: {}".format( 72 | readme_path, marker_start 73 | ) 74 | ) from exc 75 | 76 | try: 77 | index_end = lines.index(marker_end) 78 | except ValueError as exc: 79 | raise ValueError( 80 | "Could not find the start marker for the benchmarks in the {}: {}".format( 81 | readme_path, marker_end 82 | ) 83 | ) from exc 84 | 85 | assert ( 86 | index_start < index_end 87 | ), "Unexpected end marker before start marker for the benchmarks." 88 | 89 | lines = ( 90 | lines[: index_start + 1] 91 | + ["\n"] 92 | + ("".join(out)).splitlines() 93 | + ["\n"] 94 | + lines[index_end:] 95 | ) 96 | readme_path.write_text("\n".join(lines) + "\n", encoding="utf-8") 97 | 98 | # This is necessary so that the benchmarks do not complain on a Windows machine if the console encoding has not 99 | # been properly set. 100 | sys.stdout.buffer.write(("\n\n".join(stdouts) + "\n").encode("utf-8")) 101 | 102 | 103 | def main() -> int: 104 | """ "Execute main routine.""" 105 | parser = argparse.ArgumentParser(description=__doc__) 106 | parser.add_argument( 107 | "--overwrite", 108 | help="Overwrites the corresponding section in the docs.", 109 | action="store_true", 110 | ) 111 | 112 | args = parser.parse_args() 113 | 114 | overwrite = bool(args.overwrite) 115 | 116 | print("Benchmarking against other libraries...") 117 | repo_root = pathlib.Path(__file__).parent 118 | benchmark_against_others(repo_root=repo_root, overwrite=overwrite) 119 | 120 | return 0 121 | 122 | 123 | if __name__ == "__main__": 124 | sys.exit(main()) 125 | -------------------------------------------------------------------------------- /benchmarks/against_others/compare_invariant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark icontract against dpcontracts and no contracts. 4 | 5 | The benchmark was supplied by: https://github.com/Parquery/icontract/issues/142 6 | """ 7 | import os 8 | import sys 9 | import timeit 10 | from typing import List 11 | 12 | import deal 13 | import dpcontracts 14 | import tabulate 15 | 16 | import icontract 17 | 18 | 19 | @icontract.invariant(lambda self: len(self.parts) > 0) 20 | class ClassWithIcontract: 21 | def __init__(self, identifier: str) -> None: 22 | self.parts = identifier.split(".") 23 | 24 | def some_func(self) -> str: 25 | return ".".join(self.parts) 26 | 27 | 28 | @dpcontracts.invariant("some dummy invariant", lambda self: len(self.parts) > 0) 29 | class ClassWithDpcontracts: 30 | def __init__(self, identifier: str) -> None: 31 | self.parts = identifier.split(".") 32 | 33 | def some_func(self) -> str: 34 | return ".".join(self.parts) 35 | 36 | 37 | @deal.inv(validator=lambda self: len(self.parts) > 0, message="some dummy invariant") 38 | class ClassWithDeal: 39 | def __init__(self, identifier: str) -> None: 40 | self.parts = identifier.split(".") 41 | 42 | def some_func(self) -> str: 43 | return ".".join(self.parts) 44 | 45 | 46 | class ClassWithInlineContract: 47 | def __init__(self, identifier: str) -> None: 48 | self.parts = identifier.split(".") 49 | assert len(self.parts) > 0 50 | 51 | def some_func(self) -> str: 52 | assert len(self.parts) > 0 53 | result = ".".join(self.parts) 54 | assert len(self.parts) > 0 55 | return result 56 | 57 | 58 | # dpcontracts change __name__ attribute of the class, so we can not use 59 | # ClassWithDpcontractsInvariant.__name__ for a more maintainable list. 60 | clses = [ 61 | "ClassWithIcontract", 62 | "ClassWithDpcontracts", 63 | "ClassWithDeal", 64 | "ClassWithInlineContract", 65 | ] 66 | 67 | 68 | def writeln_utf8(text: str) -> None: 69 | """ 70 | write the text to STDOUT using UTF-8 encoding followed by a new-line character. 71 | 72 | We can not use ``print()`` as we can not rely on the correct encoding in Windows. 73 | See: https://stackoverflow.com/questions/31469707/changing-the-locale-preferred-encoding-in-python-3-in-windows 74 | """ 75 | sys.stdout.buffer.write(text.encode("utf-8")) 76 | sys.stdout.buffer.write(os.linesep.encode("utf-8")) 77 | 78 | 79 | def measure_invariant_at_init() -> None: 80 | durations = [0.0] * len(clses) 81 | 82 | number = 1 * 1000 * 1000 83 | 84 | for i, cls in enumerate(clses): 85 | duration = timeit.timeit( 86 | "{}('X.Y')".format(cls), 87 | setup="from __main__ import {}".format(cls), 88 | number=number, 89 | ) 90 | durations[i] = duration 91 | 92 | writeln_utf8("Benchmarking invariant at __init__:\n") 93 | table = [] # type: List[List[str]] 94 | 95 | for cls, duration in zip(clses, durations): 96 | # fmt: off 97 | table.append([ 98 | '`{}`'.format(cls), 99 | '{:.2f} s'.format(duration), 100 | '{:.2f} μs'.format(duration * 1000 * 1000 / number), 101 | '{:.0f}%'.format(duration * 100 / durations[0]) 102 | ]) 103 | # fmt: on 104 | 105 | # fmt: off 106 | table_str = tabulate.tabulate( 107 | table, 108 | headers=['Case', 'Total time', 'Time per run', 'Relative time per run'], 109 | colalign=('left', 'right', 'right', 'right'), 110 | tablefmt='rst') 111 | # fmt: on 112 | 113 | writeln_utf8(table_str) 114 | 115 | 116 | def measure_invariant_at_function() -> None: 117 | durations = [0.0] * len(clses) 118 | 119 | number = 1 * 1000 * 1000 120 | 121 | for i, cls in enumerate(clses): 122 | duration = timeit.timeit( 123 | "a.some_func()", 124 | setup="from __main__ import {0}; a = {0}('X.Y')".format(cls), 125 | number=number, 126 | ) 127 | durations[i] = duration 128 | 129 | writeln_utf8("Benchmarking invariant at a function:\n") 130 | table = [] # type: List[List[str]] 131 | 132 | for cls, duration in zip(clses, durations): 133 | # fmt: off 134 | table.append([ 135 | '`{}`'.format(cls), 136 | '{:.2f} s'.format(duration), 137 | '{:.2f} μs'.format(duration * 1000 * 1000 / number), 138 | '{:.0f}%'.format(duration * 100 / durations[0]) 139 | ]) 140 | # fmt: on 141 | 142 | # fmt: off 143 | table_str = tabulate.tabulate( 144 | table, 145 | headers=['Case', 'Total time', 'Time per run', 'Relative time per run'], 146 | colalign=('left', 'right', 'right', 'right'), 147 | tablefmt='rst') 148 | # fmt: on 149 | 150 | writeln_utf8(table_str) 151 | 152 | 153 | if __name__ == "__main__": 154 | measure_invariant_at_init() 155 | writeln_utf8("") 156 | measure_invariant_at_function() 157 | -------------------------------------------------------------------------------- /benchmarks/against_others/compare_postcondition.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark icontract against dpcontracts and no contracts. 4 | 5 | The benchmark was supplied by: https://github.com/Parquery/icontract/issues/142 6 | """ 7 | import math 8 | import os 9 | import sys 10 | import timeit 11 | from typing import List 12 | 13 | import deal 14 | import dpcontracts 15 | import icontract 16 | import tabulate 17 | 18 | 19 | @icontract.ensure(lambda result: result > 0) 20 | def function_with_icontract(some_arg: int) -> float: 21 | return math.sqrt(some_arg) 22 | 23 | 24 | @dpcontracts.ensure("some dummy contract", lambda args, result: result > 0) 25 | def function_with_dpcontracts(some_arg: int) -> float: 26 | return math.sqrt(some_arg) 27 | 28 | 29 | @deal.post(lambda result: result > 0, message="some dummy contract") 30 | def function_with_deal_post(some_arg: int) -> float: 31 | return math.sqrt(some_arg) 32 | 33 | 34 | @deal.ensure(lambda some_arg, result: result > 0, message="some dummy contract") 35 | def function_with_deal_ensure(some_arg: int) -> float: 36 | return math.sqrt(some_arg) 37 | 38 | 39 | def function_with_inline_contract(some_arg: int) -> float: 40 | result = math.sqrt(some_arg) 41 | assert result > 0 42 | return result 43 | 44 | 45 | def writeln_utf8(text: str) -> None: 46 | """ 47 | write the text to STDOUT using UTF-8 encoding followed by a new-line character. 48 | 49 | We can not use ``print()`` as we can not rely on the correct encoding in Windows. 50 | See: https://stackoverflow.com/questions/31469707/changing-the-locale-preferred-encoding-in-python-3-in-windows 51 | """ 52 | sys.stdout.buffer.write(text.encode("utf-8")) 53 | sys.stdout.buffer.write(os.linesep.encode("utf-8")) 54 | 55 | 56 | def measure_functions() -> None: 57 | funcs = [ 58 | "function_with_icontract", 59 | "function_with_dpcontracts", 60 | "function_with_deal_post", 61 | "function_with_deal_ensure", 62 | "function_with_inline_contract", 63 | ] 64 | 65 | durations = [0.0] * len(funcs) 66 | 67 | number = 10 * 1000 68 | 69 | for i, func in enumerate(funcs): 70 | duration = timeit.timeit( 71 | "{}(198.4)".format(func), 72 | setup="from __main__ import {}".format(func), 73 | number=number, 74 | ) 75 | durations[i] = duration 76 | 77 | table = [] # type: List[List[str]] 78 | 79 | for func, duration in zip(funcs, durations): 80 | # fmt: off 81 | table.append([ 82 | '`{}`'.format(func), 83 | '{:.2f} s'.format(duration), 84 | '{:.2f} μs'.format(duration * 1000 * 1000 / number), 85 | '{:.0f}%'.format(duration * 100 / durations[0]) 86 | ]) 87 | # fmt: on 88 | 89 | # fmt: off 90 | table_str = tabulate.tabulate( 91 | table, 92 | headers=['Case', 'Total time', 'Time per run', 'Relative time per run'], 93 | colalign=('left', 'right', 'right', 'right'), 94 | tablefmt='rst') 95 | # fmt: on 96 | 97 | writeln_utf8(table_str) 98 | 99 | 100 | if __name__ == "__main__": 101 | writeln_utf8("Benchmarking postcondition:") 102 | writeln_utf8("") 103 | measure_functions() 104 | -------------------------------------------------------------------------------- /benchmarks/against_others/compare_precondition.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark icontract against dpcontracts and no contracts. 4 | 5 | The benchmark was supplied by: https://github.com/Parquery/icontract/issues/142 6 | """ 7 | 8 | import math 9 | import os 10 | import sys 11 | import timeit 12 | from typing import List 13 | 14 | import deal 15 | import dpcontracts 16 | import icontract 17 | import tabulate 18 | 19 | 20 | @icontract.require(lambda some_arg: some_arg > 0) 21 | def function_with_icontract(some_arg: int) -> float: 22 | return math.sqrt(some_arg) 23 | 24 | 25 | @dpcontracts.require("some dummy contract", lambda args: args.some_arg > 0) 26 | def function_with_dpcontracts(some_arg: int) -> float: 27 | return math.sqrt(some_arg) 28 | 29 | 30 | @deal.pre(lambda _: _.some_arg > 0) 31 | def function_with_deal(some_arg: int) -> float: 32 | return math.sqrt(some_arg) 33 | 34 | 35 | def function_with_inline_contract(some_arg: int) -> float: 36 | assert some_arg > 0 37 | return math.sqrt(some_arg) 38 | 39 | 40 | def function_without_contracts(some_arg: int) -> float: 41 | return math.sqrt(some_arg) 42 | 43 | 44 | def writeln_utf8(text: str) -> None: 45 | """ 46 | write the text to STDOUT using UTF-8 encoding followed by a new-line character. 47 | 48 | We can not use ``print()`` as we can not rely on the correct encoding in Windows. 49 | See: https://stackoverflow.com/questions/31469707/changing-the-locale-preferred-encoding-in-python-3-in-windows 50 | """ 51 | sys.stdout.buffer.write(text.encode("utf-8")) 52 | sys.stdout.buffer.write(os.linesep.encode("utf-8")) 53 | 54 | 55 | def measure_functions() -> None: 56 | funcs = [ 57 | "function_with_icontract", 58 | "function_with_dpcontracts", 59 | "function_with_deal", 60 | "function_with_inline_contract", 61 | ] 62 | 63 | durations = [0.0] * len(funcs) 64 | 65 | number = 10 * 1000 66 | 67 | for i, func in enumerate(funcs): 68 | duration = timeit.timeit( 69 | "{}(198.4)".format(func), 70 | setup="from __main__ import {}".format(func), 71 | number=number, 72 | ) 73 | durations[i] = duration 74 | 75 | table = [] # type: List[List[str]] 76 | 77 | for func, duration in zip(funcs, durations): 78 | # fmt: off 79 | table.append([ 80 | '`{}`'.format(func), 81 | '{:.2f} s'.format(duration), 82 | '{:.2f} μs'.format(duration * 1000 * 1000 / number), 83 | '{:.0f}%'.format(duration * 100 / durations[0]) 84 | ]) 85 | # fmt: on 86 | 87 | # fmt: off 88 | table_str = tabulate.tabulate( 89 | table, 90 | headers=['Case', 'Total time', 'Time per run', 'Relative time per run'], 91 | colalign=('left', 'right', 'right', 'right'), 92 | tablefmt='rst') 93 | # fmt: on 94 | 95 | writeln_utf8(table_str) 96 | 97 | 98 | if __name__ == "__main__": 99 | writeln_utf8("Benchmarking precondition:") 100 | writeln_utf8("") 101 | measure_functions() 102 | -------------------------------------------------------------------------------- /benchmarks/import_cost/functions_100_with_no_contract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import icontract 3 | 4 | 5 | def some_func0(x: int) -> None: 6 | pass 7 | 8 | 9 | def some_func1(x: int) -> None: 10 | pass 11 | 12 | 13 | def some_func2(x: int) -> None: 14 | pass 15 | 16 | 17 | def some_func3(x: int) -> None: 18 | pass 19 | 20 | 21 | def some_func4(x: int) -> None: 22 | pass 23 | 24 | 25 | def some_func5(x: int) -> None: 26 | pass 27 | 28 | 29 | def some_func6(x: int) -> None: 30 | pass 31 | 32 | 33 | def some_func7(x: int) -> None: 34 | pass 35 | 36 | 37 | def some_func8(x: int) -> None: 38 | pass 39 | 40 | 41 | def some_func9(x: int) -> None: 42 | pass 43 | 44 | 45 | def some_func10(x: int) -> None: 46 | pass 47 | 48 | 49 | def some_func11(x: int) -> None: 50 | pass 51 | 52 | 53 | def some_func12(x: int) -> None: 54 | pass 55 | 56 | 57 | def some_func13(x: int) -> None: 58 | pass 59 | 60 | 61 | def some_func14(x: int) -> None: 62 | pass 63 | 64 | 65 | def some_func15(x: int) -> None: 66 | pass 67 | 68 | 69 | def some_func16(x: int) -> None: 70 | pass 71 | 72 | 73 | def some_func17(x: int) -> None: 74 | pass 75 | 76 | 77 | def some_func18(x: int) -> None: 78 | pass 79 | 80 | 81 | def some_func19(x: int) -> None: 82 | pass 83 | 84 | 85 | def some_func20(x: int) -> None: 86 | pass 87 | 88 | 89 | def some_func21(x: int) -> None: 90 | pass 91 | 92 | 93 | def some_func22(x: int) -> None: 94 | pass 95 | 96 | 97 | def some_func23(x: int) -> None: 98 | pass 99 | 100 | 101 | def some_func24(x: int) -> None: 102 | pass 103 | 104 | 105 | def some_func25(x: int) -> None: 106 | pass 107 | 108 | 109 | def some_func26(x: int) -> None: 110 | pass 111 | 112 | 113 | def some_func27(x: int) -> None: 114 | pass 115 | 116 | 117 | def some_func28(x: int) -> None: 118 | pass 119 | 120 | 121 | def some_func29(x: int) -> None: 122 | pass 123 | 124 | 125 | def some_func30(x: int) -> None: 126 | pass 127 | 128 | 129 | def some_func31(x: int) -> None: 130 | pass 131 | 132 | 133 | def some_func32(x: int) -> None: 134 | pass 135 | 136 | 137 | def some_func33(x: int) -> None: 138 | pass 139 | 140 | 141 | def some_func34(x: int) -> None: 142 | pass 143 | 144 | 145 | def some_func35(x: int) -> None: 146 | pass 147 | 148 | 149 | def some_func36(x: int) -> None: 150 | pass 151 | 152 | 153 | def some_func37(x: int) -> None: 154 | pass 155 | 156 | 157 | def some_func38(x: int) -> None: 158 | pass 159 | 160 | 161 | def some_func39(x: int) -> None: 162 | pass 163 | 164 | 165 | def some_func40(x: int) -> None: 166 | pass 167 | 168 | 169 | def some_func41(x: int) -> None: 170 | pass 171 | 172 | 173 | def some_func42(x: int) -> None: 174 | pass 175 | 176 | 177 | def some_func43(x: int) -> None: 178 | pass 179 | 180 | 181 | def some_func44(x: int) -> None: 182 | pass 183 | 184 | 185 | def some_func45(x: int) -> None: 186 | pass 187 | 188 | 189 | def some_func46(x: int) -> None: 190 | pass 191 | 192 | 193 | def some_func47(x: int) -> None: 194 | pass 195 | 196 | 197 | def some_func48(x: int) -> None: 198 | pass 199 | 200 | 201 | def some_func49(x: int) -> None: 202 | pass 203 | 204 | 205 | def some_func50(x: int) -> None: 206 | pass 207 | 208 | 209 | def some_func51(x: int) -> None: 210 | pass 211 | 212 | 213 | def some_func52(x: int) -> None: 214 | pass 215 | 216 | 217 | def some_func53(x: int) -> None: 218 | pass 219 | 220 | 221 | def some_func54(x: int) -> None: 222 | pass 223 | 224 | 225 | def some_func55(x: int) -> None: 226 | pass 227 | 228 | 229 | def some_func56(x: int) -> None: 230 | pass 231 | 232 | 233 | def some_func57(x: int) -> None: 234 | pass 235 | 236 | 237 | def some_func58(x: int) -> None: 238 | pass 239 | 240 | 241 | def some_func59(x: int) -> None: 242 | pass 243 | 244 | 245 | def some_func60(x: int) -> None: 246 | pass 247 | 248 | 249 | def some_func61(x: int) -> None: 250 | pass 251 | 252 | 253 | def some_func62(x: int) -> None: 254 | pass 255 | 256 | 257 | def some_func63(x: int) -> None: 258 | pass 259 | 260 | 261 | def some_func64(x: int) -> None: 262 | pass 263 | 264 | 265 | def some_func65(x: int) -> None: 266 | pass 267 | 268 | 269 | def some_func66(x: int) -> None: 270 | pass 271 | 272 | 273 | def some_func67(x: int) -> None: 274 | pass 275 | 276 | 277 | def some_func68(x: int) -> None: 278 | pass 279 | 280 | 281 | def some_func69(x: int) -> None: 282 | pass 283 | 284 | 285 | def some_func70(x: int) -> None: 286 | pass 287 | 288 | 289 | def some_func71(x: int) -> None: 290 | pass 291 | 292 | 293 | def some_func72(x: int) -> None: 294 | pass 295 | 296 | 297 | def some_func73(x: int) -> None: 298 | pass 299 | 300 | 301 | def some_func74(x: int) -> None: 302 | pass 303 | 304 | 305 | def some_func75(x: int) -> None: 306 | pass 307 | 308 | 309 | def some_func76(x: int) -> None: 310 | pass 311 | 312 | 313 | def some_func77(x: int) -> None: 314 | pass 315 | 316 | 317 | def some_func78(x: int) -> None: 318 | pass 319 | 320 | 321 | def some_func79(x: int) -> None: 322 | pass 323 | 324 | 325 | def some_func80(x: int) -> None: 326 | pass 327 | 328 | 329 | def some_func81(x: int) -> None: 330 | pass 331 | 332 | 333 | def some_func82(x: int) -> None: 334 | pass 335 | 336 | 337 | def some_func83(x: int) -> None: 338 | pass 339 | 340 | 341 | def some_func84(x: int) -> None: 342 | pass 343 | 344 | 345 | def some_func85(x: int) -> None: 346 | pass 347 | 348 | 349 | def some_func86(x: int) -> None: 350 | pass 351 | 352 | 353 | def some_func87(x: int) -> None: 354 | pass 355 | 356 | 357 | def some_func88(x: int) -> None: 358 | pass 359 | 360 | 361 | def some_func89(x: int) -> None: 362 | pass 363 | 364 | 365 | def some_func90(x: int) -> None: 366 | pass 367 | 368 | 369 | def some_func91(x: int) -> None: 370 | pass 371 | 372 | 373 | def some_func92(x: int) -> None: 374 | pass 375 | 376 | 377 | def some_func93(x: int) -> None: 378 | pass 379 | 380 | 381 | def some_func94(x: int) -> None: 382 | pass 383 | 384 | 385 | def some_func95(x: int) -> None: 386 | pass 387 | 388 | 389 | def some_func96(x: int) -> None: 390 | pass 391 | 392 | 393 | def some_func97(x: int) -> None: 394 | pass 395 | 396 | 397 | def some_func98(x: int) -> None: 398 | pass 399 | 400 | 401 | def some_func99(x: int) -> None: 402 | pass 403 | -------------------------------------------------------------------------------- /benchmarks/import_cost/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Generate a module source code containing functions to benchmark the start-up time of the library with contracts.""" 3 | 4 | import argparse 5 | import io 6 | import os 7 | import pathlib 8 | import sys 9 | import textwrap 10 | 11 | 12 | def generate_functions(functions: int, contracts: int, disabled: bool) -> str: 13 | out = io.StringIO() 14 | out.write("#!/usr/bin/env python3\n") 15 | out.write("import icontract\n\n") 16 | 17 | for i in range(0, functions): 18 | if i > 0: 19 | out.write("\n") 20 | 21 | for j in range(0, contracts): 22 | if not disabled: 23 | out.write("@icontract.require(lambda x: x > {})\n".format(j)) 24 | else: 25 | out.write( 26 | "@icontract.require(lambda x: x > {}, enabled=False)\n".format(j) 27 | ) 28 | 29 | out.write("def some_func{}(x: int) -> None:\n pass\n".format(i)) 30 | 31 | return out.getvalue() 32 | 33 | 34 | def generate_classes(classes: int, invariants: int, disabled: bool) -> str: 35 | out = io.StringIO() 36 | out.write("#!/usr/bin/env python3\n") 37 | out.write("import icontract\n\n") 38 | 39 | for i in range(0, classes): 40 | if i > 0: 41 | out.write("\n") 42 | 43 | for j in range(0, invariants): 44 | if not disabled: 45 | out.write("@icontract.invariant(lambda self: self.x > {})\n".format(j)) 46 | else: 47 | out.write( 48 | "@icontract.invariant(lambda self: self.x > {}, enabled=False)\n".format( 49 | j 50 | ) 51 | ) 52 | 53 | out.write( 54 | textwrap.dedent( 55 | """\ 56 | class SomeClass{}: 57 | def __init__(self) -> None: 58 | self.x = 100 59 | 60 | def some_func(self) -> None: 61 | pass 62 | """.format( 63 | i 64 | ) 65 | ) 66 | ) 67 | 68 | return out.getvalue() 69 | 70 | 71 | def main() -> None: 72 | """ "Execute the main routine.""" 73 | parser = argparse.ArgumentParser(description=__doc__) 74 | parser.add_argument( 75 | "--outdir", help="output directory", default=os.path.dirname(__file__) 76 | ) 77 | args = parser.parse_args() 78 | 79 | outdir = pathlib.Path(args.outdir) 80 | if not outdir.exists(): 81 | raise FileNotFoundError("Output directory is missing: {}".format(outdir)) 82 | 83 | for contracts in [0, 1, 5, 10]: 84 | if contracts == 0: 85 | pth = outdir / "functions_100_with_no_contract.py" 86 | elif contracts == 1: 87 | pth = outdir / "functions_100_with_1_contract.py" 88 | else: 89 | pth = outdir / "functions_100_with_{}_contracts.py".format(contracts) 90 | 91 | text = generate_functions(functions=100, contracts=contracts, disabled=False) 92 | pth.write_text(text) 93 | 94 | for contracts in [1, 5, 10]: 95 | if contracts == 1: 96 | pth = outdir / "functions_100_with_1_disabled_contract.py" 97 | else: 98 | pth = outdir / "functions_100_with_{}_disabled_contracts.py".format( 99 | contracts 100 | ) 101 | 102 | text = generate_functions(functions=100, contracts=contracts, disabled=True) 103 | pth.write_text(text) 104 | 105 | for invariants in [0, 1, 5, 10]: 106 | if invariants == 0: 107 | pth = outdir / "classes_100_with_no_invariant.py" 108 | elif invariants == 1: 109 | pth = outdir / "classes_100_with_1_invariant.py" 110 | else: 111 | pth = outdir / "classes_100_with_{}_invariants.py".format(invariants) 112 | 113 | text = generate_classes(classes=100, invariants=invariants, disabled=False) 114 | pth.write_text(text) 115 | 116 | for invariants in [1, 5, 10]: 117 | if invariants == 1: 118 | pth = outdir / "classes_100_with_1_disabled_invariant.py" 119 | else: 120 | pth = outdir / "classes_100_with_{}_disabled_invariants.py".format( 121 | invariants 122 | ) 123 | 124 | text = generate_classes(classes=100, invariants=invariants, disabled=True) 125 | pth.write_text(text) 126 | 127 | 128 | if __name__ == "__main__": 129 | main() 130 | -------------------------------------------------------------------------------- /benchmarks/import_cost/measure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Measure the import time of a module with contracts. 4 | 5 | Execute this script multiple times to get a better estimate. 6 | """ 7 | 8 | import argparse 9 | import time 10 | 11 | 12 | def main() -> None: 13 | """ "Execute the main routine.""" 14 | parser = argparse.ArgumentParser(description=__doc__) 15 | parser.add_argument( 16 | "--module", 17 | help="name of the module to import", 18 | choices=[ 19 | "functions_100_with_no_contract", 20 | "functions_100_with_1_contract", 21 | "functions_100_with_5_contracts", 22 | "functions_100_with_10_contracts", 23 | "functions_100_with_1_disabled_contract", 24 | "functions_100_with_5_disabled_contracts", 25 | "functions_100_with_10_disabled_contracts", 26 | "classes_100_with_no_invariant", 27 | "classes_100_with_1_invariant", 28 | "classes_100_with_5_invariants", 29 | "classes_100_with_10_invariants", 30 | "classes_100_with_1_disabled_invariant", 31 | "classes_100_with_5_disabled_invariants", 32 | "classes_100_with_10_disabled_invariants", 33 | ], 34 | required=True, 35 | ) 36 | args = parser.parse_args() 37 | 38 | a_module = str(args.module) 39 | if a_module == "functions_100_with_no_contract": 40 | start = time.time() 41 | import functions_100_with_no_contract 42 | 43 | print(time.time() - start) 44 | 45 | elif a_module == "functions_100_with_1_contract": 46 | start = time.time() 47 | import functions_100_with_1_contract 48 | 49 | print(time.time() - start) 50 | 51 | elif a_module == "functions_100_with_5_contracts": 52 | start = time.time() 53 | import functions_100_with_5_contracts 54 | 55 | print(time.time() - start) 56 | 57 | elif a_module == "functions_100_with_10_contracts": 58 | start = time.time() 59 | import functions_100_with_10_contracts 60 | 61 | print(time.time() - start) 62 | 63 | elif a_module == "functions_100_with_1_disabled_contract": 64 | start = time.time() 65 | import functions_100_with_1_disabled_contract 66 | 67 | print(time.time() - start) 68 | 69 | elif a_module == "functions_100_with_5_disabled_contracts": 70 | start = time.time() 71 | import functions_100_with_5_disabled_contracts 72 | 73 | print(time.time() - start) 74 | 75 | elif a_module == "functions_100_with_10_disabled_contracts": 76 | start = time.time() 77 | import functions_100_with_10_disabled_contracts 78 | 79 | print(time.time() - start) 80 | 81 | elif a_module == "classes_100_with_no_invariant": 82 | start = time.time() 83 | import classes_100_with_no_invariant 84 | 85 | print(time.time() - start) 86 | 87 | elif a_module == "classes_100_with_1_invariant": 88 | start = time.time() 89 | import classes_100_with_1_invariant 90 | 91 | print(time.time() - start) 92 | 93 | elif a_module == "classes_100_with_5_invariants": 94 | start = time.time() 95 | import classes_100_with_5_invariants 96 | 97 | print(time.time() - start) 98 | 99 | elif a_module == "classes_100_with_10_invariants": 100 | start = time.time() 101 | import classes_100_with_10_invariants 102 | 103 | print(time.time() - start) 104 | 105 | elif a_module == "classes_100_with_1_disabled_invariant": 106 | start = time.time() 107 | import classes_100_with_1_disabled_invariant 108 | 109 | print(time.time() - start) 110 | 111 | elif a_module == "classes_100_with_5_disabled_invariants": 112 | start = time.time() 113 | import classes_100_with_5_disabled_invariants 114 | 115 | print(time.time() - start) 116 | 117 | elif a_module == "classes_100_with_10_disabled_invariants": 118 | start = time.time() 119 | import classes_100_with_10_disabled_invariants 120 | 121 | print(time.time() - start) 122 | 123 | else: 124 | raise NotImplementedError("Unhandled module: {}".format(a_module)) 125 | 126 | 127 | if __name__ == "__main__": 128 | main() 129 | -------------------------------------------------------------------------------- /benchmarks/import_cost/runme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Measure the start-up time of the modules with differing number of contracts.""" 3 | import os 4 | import statistics 5 | import subprocess 6 | from typing import List 7 | 8 | 9 | def main() -> None: 10 | """ "Execute the main routine.""" 11 | modules = [ 12 | "functions_100_with_no_contract", 13 | "functions_100_with_1_contract", 14 | "functions_100_with_5_contracts", 15 | "functions_100_with_10_contracts", 16 | "functions_100_with_1_disabled_contract", 17 | "functions_100_with_5_disabled_contracts", 18 | "functions_100_with_10_disabled_contracts", 19 | "classes_100_with_no_invariant", 20 | "classes_100_with_1_invariant", 21 | "classes_100_with_5_invariants", 22 | "classes_100_with_10_invariants", 23 | "classes_100_with_1_disabled_invariant", 24 | "classes_100_with_5_disabled_invariants", 25 | "classes_100_with_10_disabled_invariants", 26 | ] 27 | 28 | for a_module in modules: 29 | durations = [] # type: List[float] 30 | for i in range(0, 10): 31 | duration = float( 32 | subprocess.check_output( 33 | ["./measure.py", "--module", a_module], 34 | cwd=os.path.dirname(__file__), 35 | ).strip() 36 | ) 37 | durations.append(duration) 38 | 39 | print( 40 | "Duration to import the module {} (in milliseconds): {:.2f} ± {:.2f}".format( 41 | a_module, 42 | statistics.mean(durations) * 10e3, 43 | statistics.stdev(durations) * 10e3, 44 | ) 45 | ) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /benchmarks/runtime_cost/my_sqrt.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def my_sqrt(x: float) -> float: 5 | return math.sqrt(x) 6 | -------------------------------------------------------------------------------- /benchmarks/runtime_cost/my_sqrt_assert_as_contract.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def my_sqrt(x: float) -> float: 5 | assert x >= 0 6 | return math.sqrt(x) 7 | -------------------------------------------------------------------------------- /benchmarks/runtime_cost/my_sqrt_function_as_contract.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def check_non_negative(x: float) -> None: 5 | assert x >= 0 6 | 7 | 8 | def my_sqrt(x: float) -> float: 9 | check_non_negative(x=x) 10 | return math.sqrt(x) 11 | -------------------------------------------------------------------------------- /benchmarks/runtime_cost/my_sqrt_with_icontract.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import icontract 4 | 5 | 6 | @icontract.require(lambda x: x >= 0) 7 | def my_sqrt(x: float) -> float: 8 | return math.sqrt(x) 9 | -------------------------------------------------------------------------------- /benchmarks/runtime_cost/my_sqrt_with_icontract_disabled.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import icontract 4 | 5 | 6 | @icontract.require(lambda x: x >= 0, enabled=False) 7 | def my_sqrt(x: float) -> float: 8 | return math.sqrt(x) 9 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/how-to-build-docs.md: -------------------------------------------------------------------------------- 1 | Activate your virtual environment 2 | 3 | Install icontract requirements: 4 | 5 | ``` 6 | pip3 install -r requirements.txt 7 | ``` 8 | 9 | Install the documentation requirements: 10 | 11 | ``` 12 | pip3 install -r requirements-doc.txt 13 | ``` 14 | 15 | Build with Sphinx: 16 | 17 | ``` 18 | cd docs 19 | sphinx-build source build 20 | ``` 21 | 22 | 23 | The documentation is in the `docs/build` directory. 24 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | .. py:currentmodule:: icontract 4 | 5 | require 6 | ------- 7 | .. autoclass:: require() 8 | :members: 9 | 10 | .. automethod:: __init__ 11 | .. automethod:: __call__ 12 | 13 | snapshot 14 | -------- 15 | .. autoclass:: snapshot() 16 | :members: 17 | 18 | .. automethod:: __init__ 19 | .. automethod:: __call__ 20 | 21 | ensure 22 | ------ 23 | .. autoclass:: ensure() 24 | :members: 25 | 26 | .. automethod:: __init__ 27 | .. automethod:: __call__ 28 | 29 | invariant 30 | --------- 31 | .. autoclass:: invariant() 32 | :members: 33 | 34 | .. automethod:: __init__ 35 | .. automethod:: __call__ 36 | 37 | DBCMeta 38 | ------- 39 | .. autoclass:: DBCMeta 40 | 41 | DBC 42 | --- 43 | .. autoclass:: DBC 44 | 45 | ViolationError 46 | -------------- 47 | .. autoclass:: ViolationError 48 | 49 | InvariantCheckEvent 50 | ------------------- 51 | .. autoclass:: InvariantCheckEvent -------------------------------------------------------------------------------- /docs/source/async.rst: -------------------------------------------------------------------------------- 1 | Async 2 | ===== 3 | 4 | Icontract supports both adding sync contracts to `coroutine functions `_ as well as enforcing 5 | *async conditions* (and capturing *async snapshots*). 6 | 7 | .. _coroutine function: https://docs.python.org/3/glossary.html#term-coroutine-function 8 | 9 | You simply define your conditions as decorators of a `coroutine function`_: 10 | 11 | .. code-block:: python 12 | 13 | import icontract 14 | 15 | @icontract.require(lambda x: x > 0) 16 | @icontract.ensure(lambda x, result: x < result) 17 | async def do_something(x: int) -> int: 18 | ... 19 | 20 | 21 | Special Considerations 22 | ---------------------- 23 | **Async conditions**. 24 | If you want to enforce async conditions, the function also needs to be defined as async: 25 | 26 | .. code-block:: python 27 | 28 | import icontract 29 | 30 | async def has_author(author_id: str) -> bool: 31 | ... 32 | 33 | @icontract.ensure(has_author) 34 | async def upsert_author(name: str) -> str: 35 | ... 36 | 37 | It is not possible to add an async condition to a sync function. 38 | Doing so will raise a `ValueError`_ at runtime. 39 | The reason behind this limitation is that the wrapper around the function would need to be made async, which would 40 | break the code calling the original function and expecting it to be synchronous. 41 | 42 | **Invariants**. 43 | As invariants need to wrap dunder methods, including ``__init__``, their conditions *can not* be 44 | async, as most dunder methods need to be synchronous methods, and wrapping them with async code would 45 | break that constraint. 46 | You can, of course, use synchronous invariants on *async* method functions without problems. 47 | 48 | .. _no_async_lambda_limitation: 49 | 50 | **No async lambda**. 51 | Another practical limitation is that Python does not support async lambda (see `this Python issue`_), 52 | so defining async conditions (and snapshots) is indeed tedious (see the 53 | :ref:`next section `). 54 | Please consider asking for async lambdas on `python-ideas mailing list`_ to give the issue some visibility. 55 | 56 | .. _this Python issue: https://bugs.python.org/issue33447 57 | .. _python-ideas mailing list: https://mail.python.org/mailman3/lists/python-ideas.python.org/ 58 | 59 | .. _coroutine as condition result: 60 | 61 | **Coroutine as condition result**. 62 | If the condition returns a `coroutine`_, the `coroutine`_ will be awaited before it is evaluated for truthiness. 63 | 64 | This means in practice that you can work around :ref:`no-async lambda limitation ` applying coroutine functions 65 | on your condition arguments (which in turn makes the condition result in a `coroutine`_). 66 | 67 | .. _coroutine: https://docs.python.org/3/glossary.html#term-coroutine 68 | 69 | For example: 70 | 71 | .. code-block:: python 72 | 73 | async def some_condition(a: float, b: float) -> bool: 74 | ... 75 | 76 | @icontract.require(lambda x: some_condition(a=x, b=x**2)) 77 | async def some_func(x: float) -> None: 78 | ... 79 | 80 | A big fraction of contracts on sequences require an `all`_ operation to check that all the item of a sequence are 81 | ``True``. 82 | Unfortunately, `all`_ does not automatically operate on a sequence of `Awaitables `_, 83 | but the library `asyncstdlib`_ comes in very handy: 84 | 85 | .. _all: https://docs.python.org/3/library/functions.html#all 86 | .. _awaitable: https://docs.python.org/3/library/asyncio-task.html#awaitables 87 | .. _asyncstdlib: https://pypi.org/project/asyncstdlib/ 88 | 89 | .. code-block:: python 90 | 91 | import asyncstdlib as a 92 | 93 | Here is a practical example that uses `asyncstdlib.map`_, `asyncstdlib.all`_ and `asyncstdlib.await_each`_: 94 | 95 | .. _asyncstdlib.map: https://asyncstdlib.readthedocs.io/en/latest/source/api/builtins.html#asyncstdlib.builtins.map 96 | .. _asyncstdlib.all: https://asyncstdlib.readthedocs.io/en/latest/source/api/builtins.html#asyncstdlib.builtins.all 97 | .. _asyncstdlib.await_each: https://asyncstdlib.readthedocs.io/en/latest/source/api/asynctools.html#asyncstdlib.asynctools.await_each 98 | 99 | .. code-block:: python 100 | 101 | import asyncstdlib as a 102 | 103 | async def has_author(identifier: str) -> bool: 104 | ... 105 | 106 | async def has_category(category: str) -> bool: 107 | ... 108 | 109 | @dataclasses.dataclass 110 | class Book: 111 | identifier: str 112 | author: str 113 | 114 | @icontract.require(lambda categories: a.map(has_category, categories)) 115 | @icontract.ensure( 116 | lambda result: a.all(a.await_each(has_author(book.author) for book in result))) 117 | async def list_books(categories: List[str]) -> List[Book]: 118 | ... 119 | 120 | **Coroutines have side effects.** 121 | If the condition of a contract returns a `coroutine`_, the condition can not be 122 | re-computed upon the violation to produce an informative violation message. 123 | This means that you need to :ref:`specify an explicit error ` which should be raised 124 | on contract violation. 125 | 126 | For example: 127 | 128 | .. code-block:: python 129 | 130 | async def some_condition() -> bool: 131 | ... 132 | 133 | @icontract.require( 134 | lambda: some_condition(), 135 | error=lambda: icontract.ViolationError("Something went wrong.")) 136 | 137 | If you do not specify the error, and the condition returns a `coroutine`_, the decorator will raise a 138 | `ValueError`_ at re-computation time. 139 | 140 | .. _ValueError: https://docs.python.org/3/library/exceptions.html#ValueError 141 | -------------------------------------------------------------------------------- /docs/source/benchmarks.rst: -------------------------------------------------------------------------------- 1 | Benchmarks 2 | ========== 3 | We run benchmarks against `deal` and `dpcontracts` libraries as part of our continuous integration. 4 | 5 | The bodies of the constructors and functions were intentionally left simple so that you can 6 | better estimate **overhead** of the contracts in absolute terms rather than relative. 7 | This means that the code without contracts will run extremely fast (nanoseconds) in the benchmarks 8 | which might make the contracts seem sluggish. However, the methods in the real world usually run 9 | in the order of microseconds and milliseconds, not nanoseconds. As long as the overhead 10 | of the contract is in the order of microseconds, it is often practically acceptable. 11 | 12 | .. Becnhmark report from benchmark.py starts. 13 | 14 | 15 | The following scripts were run: 16 | 17 | * `benchmarks/against_others/compare_invariant.py `_ 18 | * `benchmarks/against_others/compare_precondition.py `_ 19 | * `benchmarks/against_others/compare_postcondition.py `_ 20 | 21 | The benchmarks were executed on Intel(R) Xeon(R) E-2276M CPU @ 2.80GHz. 22 | We used Python 3.9.9, icontract 2.6.1, deal 4.23.3 and dpcontracts 0.6.0. 23 | 24 | The following tables summarize the results. 25 | 26 | Benchmarking invariant at __init__: 27 | 28 | ========================= ============ ============== ======================= 29 | Case Total time Time per run Relative time per run 30 | ========================= ============ ============== ======================= 31 | `ClassWithIcontract` 1.45 s 1.45 μs 100% 32 | `ClassWithDpcontracts` 0.48 s 0.48 μs 33% 33 | `ClassWithDeal` 1.73 s 1.73 μs 119% 34 | `ClassWithInlineContract` 0.28 s 0.28 μs 19% 35 | ========================= ============ ============== ======================= 36 | 37 | Benchmarking invariant at a function: 38 | 39 | ========================= ============ ============== ======================= 40 | Case Total time Time per run Relative time per run 41 | ========================= ============ ============== ======================= 42 | `ClassWithIcontract` 2.04 s 2.04 μs 100% 43 | `ClassWithDpcontracts` 0.49 s 0.49 μs 24% 44 | `ClassWithDeal` 4.67 s 4.67 μs 230% 45 | `ClassWithInlineContract` 0.23 s 0.23 μs 11% 46 | ========================= ============ ============== ======================= 47 | 48 | Benchmarking precondition: 49 | 50 | =============================== ============ ============== ======================= 51 | Case Total time Time per run Relative time per run 52 | =============================== ============ ============== ======================= 53 | `function_with_icontract` 0.04 s 3.91 μs 100% 54 | `function_with_dpcontracts` 0.54 s 53.92 μs 1377% 55 | `function_with_deal` 0.04 s 4.16 μs 106% 56 | `function_with_inline_contract` 0.00 s 0.15 μs 4% 57 | =============================== ============ ============== ======================= 58 | 59 | Benchmarking postcondition: 60 | 61 | =============================== ============ ============== ======================= 62 | Case Total time Time per run Relative time per run 63 | =============================== ============ ============== ======================= 64 | `function_with_icontract` 0.04 s 4.39 μs 100% 65 | `function_with_dpcontracts` 0.53 s 52.51 μs 1197% 66 | `function_with_deal_post` 0.01 s 1.16 μs 26% 67 | `function_with_deal_ensure` 0.01 s 1.04 μs 24% 68 | `function_with_inline_contract` 0.00 s 0.15 μs 3% 69 | =============================== ============ ============== ======================= 70 | 71 | 72 | 73 | .. Benchmark report from benchmark.py ends. 74 | 75 | Note that neither the `dpcontracts` nor the `deal` library support recursion and inheritance of the contracts. 76 | This allows them to use faster enforcement mechanisms and thus gain a speed-up. 77 | 78 | We also ran a much more extensive battery of benchmarks on icontract 2.0.7. Unfortunately, 79 | it would cost us too much effort to integrate the results in the continuous integration. 80 | The report is available at: 81 | `benchmarks/benchmark_2.0.7.rst `_. 82 | 83 | The scripts are available at: 84 | `benchmarks/import_cost/ `_ 85 | and 86 | `benchmarks/runtime_cost/ `_. 87 | Please re-run the scripts manually to obtain the results with the latest icontract version. 88 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | CHANGELOG 3 | ********* 4 | 5 | .. include:: ../../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/source/checking_types_at_runtime.rst: -------------------------------------------------------------------------------- 1 | Checking Types at Runtime 2 | ========================= 3 | Icontract focuses on logical contracts in the code. Theoretically, you could use icontract to check the types 4 | at runtime and condition the contracts using 5 | `material implication `_: 6 | 7 | .. code-block:: python 8 | 9 | @icontract.require(lambda x: not isinstance(x, int) or x > 0) 10 | @icontract.require(lambda x: not isinstance(x, str) or x.startswith('x-')) 11 | def some_func(x: Any) -> None 12 | ... 13 | 14 | This is a good solution if your code lacks type annotations or if you do not know the type in advance. 15 | 16 | However, if you already annotated the code with the type annotations, re-stating the types in the contracts 17 | breaks the `DRY principle `_ and makes the code 18 | unnecessarily hard to maintain and read: 19 | 20 | .. code-block:: python 21 | 22 | @icontract.require(lambda x: isinstance(x, int)) 23 | def some_func(x: int) -> None 24 | ... 25 | 26 | Elegant runtime type checks are out of icontract's scope. We would recommend you to use one of the available 27 | libraries specialized only on such checks such as `typeguard `_. 28 | 29 | The icontract's test suite explicitly includes tests to make sure that icontract and typeguard work well together and 30 | to enforce their interplay in the future. 31 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath('../..')) 19 | 20 | import icontract 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = "icontract" 25 | copyright = icontract.__copyright__ 26 | author = icontract.__author__ 27 | description = "Provide design-by-contract to Python3 with informative violation messages and inheritance." 28 | 29 | # The short X.Y version 30 | version = '' 31 | # The full version, including alpha/beta/rc tags 32 | release = icontract.__version__ 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.doctest', 46 | 'sphinx.ext.autosectionlabel', 47 | 'sphinx.ext.viewcode', 48 | 'autodocsumm' 49 | ] 50 | 51 | autodoc_typehints = 'signature' 52 | 53 | autodoc_default_options = { 54 | 'members': True, 55 | 'undoc-members': True, 56 | 'member-order': 'bysource', 57 | 'autosummary': True, 58 | } 59 | 60 | # Add any paths that contain templates here, relative to this directory. 61 | templates_path = ['_templates'] 62 | 63 | # The suffix(es) of source filenames. 64 | # You can specify multiple suffix as a list of string: 65 | # 66 | # source_suffix = ['.rst', '.md'] 67 | source_suffix = '.rst' 68 | 69 | # The master toctree document. 70 | master_doc = 'index' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This pattern also affects html_static_path and html_extra_path. 82 | exclude_patterns = [] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # -- Options for HTML output ------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | html_theme = 'sphinx_rtd_theme' 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | # html_static_path = ['_static'] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # The default sidebars (for documents that don't match any pattern) are 109 | # defined by theme itself. Builtin themes are using these templates by 110 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 111 | # 'searchbox.html']``. 112 | # 113 | # html_sidebars = {} 114 | 115 | 116 | # -- Options for HTMLHelp output --------------------------------------------- 117 | 118 | # Output file base name for HTML help builder. 119 | htmlhelp_basename = 'icontractdoc' 120 | 121 | # -- Options for LaTeX output ------------------------------------------------ 122 | 123 | latex_elements = { 124 | # The paper size ('letterpaper' or 'a4paper'). 125 | # 126 | # 'papersize': 'letterpaper', 127 | 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | 132 | # Additional stuff for the LaTeX preamble. 133 | # 134 | # 'preamble': '', 135 | 136 | # Latex figure (float) alignment 137 | # 138 | # 'figure_align': 'htbp', 139 | } 140 | 141 | # Grouping the document tree into LaTeX files. List of tuples 142 | # (source start file, target name, title, 143 | # author, documentclass [howto, manual, or own class]). 144 | latex_documents = [ 145 | (master_doc, 146 | '{}.tex'.format(project), 147 | '{} Documentation'.format(project), 148 | author, 149 | 'manual'), 150 | ] 151 | 152 | # -- Options for manual page output ------------------------------------------ 153 | 154 | # One entry per manual page. List of tuples 155 | # (source start file, name, description, authors, manual section). 156 | man_pages = [ 157 | (master_doc, 158 | project, 159 | '{} Documentation'.format(project), 160 | [author], 161 | 1) 162 | ] 163 | 164 | # -- Options for Texinfo output ---------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | (master_doc, project, '{} Documentation'.format(project), 171 | author, project, description, 172 | 'Miscellaneous'), 173 | ] 174 | 175 | # -- Options for Epub output ------------------------------------------------- 176 | 177 | # Bibliographic Dublin Core info. 178 | epub_title = project 179 | 180 | # The unique identifier of the text. This can be a ISBN number 181 | # or the project homepage. 182 | # 183 | # epub_identifier = '' 184 | 185 | # A unique identification for the text. 186 | # 187 | # epub_uid = '' 188 | 189 | # A list of files that should not be packed into the epub file. 190 | epub_exclude_files = ['search.html'] 191 | 192 | # -- Extension configuration ------------------------------------------------- 193 | -------------------------------------------------------------------------------- /docs/source/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | * Check out the repository. 5 | 6 | * In the repository root, create the virtual environment: 7 | 8 | .. code-block:: bash 9 | 10 | python3 -m venv venv3 11 | 12 | * Activate the virtual environment: 13 | 14 | .. code-block:: bash 15 | 16 | source venv3/bin/activate 17 | 18 | * Install the development dependencies: 19 | 20 | .. code-block:: bash 21 | 22 | pip3 install -e .[dev] 23 | 24 | * We use tox for testing and packaging the distribution. Run: 25 | 26 | .. code-block:: bash 27 | 28 | tox 29 | 30 | * We also provide a set of pre-commit checks that lint and check code for formatting. Run them locally from an activated 31 | virtual environment with development dependencies: 32 | 33 | .. code-block:: bash 34 | 35 | ./precommit.py 36 | 37 | * The pre-commit script can also automatically format the code: 38 | 39 | .. code-block:: bash 40 | 41 | ./precommit.py --overwrite 42 | 43 | 44 | Commit Message Style 45 | -------------------- 46 | 47 | Use the following guidelines for commit message. 48 | 49 | * Past tense in the subject & body 50 | * Max. 50 characters subject 51 | * Max. 72 characters line length in the body (multiple lines are ok) 52 | * Past tense in the body 53 | * Have separate commits for the releases where the important changes are highlighted 54 | 55 | See examples from past commits at https://github.com/Parquery/icontract/commits/master/ 56 | -------------------------------------------------------------------------------- /docs/source/implementation_details.rst: -------------------------------------------------------------------------------- 1 | Implementation Details 2 | ====================== 3 | 4 | Decorator Stack 5 | --------------- 6 | The precondition and postcondition decorators have to be stacked together to allow for inheritance. 7 | Hence, when multiple precondition and postcondition decorators are given, the function is actually decorated only once 8 | with a precondition/postcondition checker while the contracts are stacked to the checker's ``__preconditions__`` and 9 | ``__postconditions__`` attribute, respectively. The checker functions iterates through these two attributes to verify 10 | the contracts at run-time. 11 | 12 | All the decorators in the function's decorator stack are expected to call `functools.update_wrapper`_. 13 | Notably, we use ``__wrapped__`` attribute to iterate through the decorator stack and find the checker function which is 14 | set with `functools.update_wrapper`_. 15 | Mind that this implies that preconditions and postconditions are verified at the inner-most decorator and *not* when 16 | outer preconditions and postconditions are defined. 17 | 18 | Consider the following example: 19 | 20 | .. code-block:: python 21 | 22 | @some_custom_decorator 23 | @icontract.require(lambda x: x > 0) 24 | @another_custom_decorator 25 | @icontract.require(lambda x, y: y < x) 26 | def some_func(x: int, y: int) -> None: 27 | # ... 28 | 29 | The checker function will verify the two preconditions after both ``some_custom_decorator`` and 30 | ``another_custom_decorator`` have been applied, whily you would expect that the outer precondition (``x > 0``) 31 | is verified immediately after ``some_custom_decorator`` is applied. 32 | 33 | To prevent bugs due to unexpected behavior, we recommend to always group preconditions and postconditions together. 34 | 35 | Decoration with Invariants 36 | -------------------------- 37 | Since invariants are handled by a class decorator (in contrast to function decorators that handle 38 | preconditions and postconditions), they do not need to be stacked. The first invariant decorator wraps each public 39 | method of a class with a checker function. The invariants are added to the class attribute ``__invariants__``. 40 | At run-time, the checker function iterates through the ``__invariants__`` attribute when it needs to actually verify the 41 | invariants. 42 | 43 | Mind that we still expect each class decorator that decorates the class functions to use `functools.update_wrapper`_ 44 | in order to be able to iterate through decorator stacks of the individual functions. 45 | 46 | Recursion in Contracts 47 | ---------------------- 48 | In certain cases functions depend on each other through contracts. Consider the following snippet: 49 | 50 | .. code-block:: python 51 | 52 | @icontract.require(lambda: another_func()) 53 | def some_func() -> bool: 54 | ... 55 | 56 | @icontract.require(lambda: some_func()) 57 | def another_func() -> bool: 58 | ... 59 | 60 | some_func() 61 | 62 | Naively evaluating such preconditions and postconditions would result in endless recursions. Therefore, icontract 63 | suspends any further contract checking for a function when re-entering it for the second time while checking its 64 | contracts. 65 | 66 | Invariants depending on the instance methods would analogously result in endless recursions. The following snippet 67 | gives an example of such an invariant: 68 | 69 | .. code-block:: python 70 | 71 | @icontract.invariant(lambda self: self.some_func()) 72 | class SomeClass(icontract.DBC): 73 | def __init__(self) -> None: 74 | ... 75 | 76 | def some_func(self) -> bool: 77 | ... 78 | 79 | To avoid endless recursion icontract suspends further invariant checks while checking an invariant. The dunder 80 | ``__dbc_invariant_check_is_in_progress__`` is set on the instance for a diode effect as soon as invariant check is 81 | in progress and removed once the invariants checking finished. As long as the dunder 82 | ``__dbc_invariant_check_is_in_progress__`` is present, the wrappers that check invariants simply return the result of 83 | the function. 84 | 85 | Invariant checks also need to be disabled during the construction since calling member functions would trigger invariant 86 | checks which, on their hand, might check on yet-to-be-defined instance attributes. See the following snippet: 87 | 88 | .. code-block:: python 89 | 90 | @icontract.invariant(lambda self: self.some_attribute > 0) 91 | class SomeClass(icontract.DBC): 92 | def __init__(self) -> None: 93 | self.some_attribute = self.some_func() 94 | 95 | def some_func(self) -> int: 96 | return 1984 97 | 98 | .. _functools.update_wrapper: https://docs.python.org/3/library/functools.html#functools.update_wrapper -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to icontract's documentation! 2 | ================================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | introduction 9 | usage 10 | checking_types_at_runtime 11 | async 12 | recipes 13 | implementation_details 14 | known_issues 15 | benchmarks 16 | api 17 | development 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | 22 | changelog 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | Icontract provides `design-by-contract `_ to Python3 with informative 4 | violation messages and inheritance. 5 | 6 | It also gives a base for a flourishing of a wider ecosystem: 7 | 8 | * A linter `pyicontract-lint`_, 9 | * A sphinx plug-in `sphinx-icontract`_, 10 | * A tool `icontract-hypothesis`_ for automated testing and ghostwriting test files which infers 11 | `Hypothesis`_ strategies based on the contracts, 12 | 13 | * together with IDE integrations such as 14 | `icontract-hypothesis-vim`_, 15 | `icontract-hypothesis-pycharm`_, and 16 | `icontract-hypothesis-vscode`_, 17 | * Directly integrated into `CrossHair`_, a tool for automatic verification of Python programs, 18 | 19 | * together with IDE integrations such as 20 | `crosshair-pycharm`_ and `crosshair-vscode`_, and 21 | * An integration with `FastAPI`_ through `fastapi-icontract`_ to enforce contracts on your HTTP API and display them 22 | in OpenAPI 3 schema and Swagger UI, and 23 | * An extensive corpus, `Python-by-contract corpus`_, of Python programs annotated with contracts for educational, testing and research purposes. 24 | 25 | .. _pyicontract-lint: https://pypi.org/project/pyicontract-lint 26 | .. _sphinx-icontract: https://pypi.org/project/sphinx-icontract 27 | .. _icontract-hypothesis: https://github.com/mristin/icontract-hypothesis 28 | .. _Hypothesis: https://hypothesis.readthedocs.io/en/latest/ 29 | .. _icontract-hypothesis-vim: https://github.com/mristin/icontract-hypothesis-vim 30 | .. _icontract-hypothesis-pycharm: https://github.com/mristin/icontract-hypothesis-pycharm 31 | .. _icontract-hypothesis-vscode: https://github.com/mristin/icontract-hypothesis-vscode 32 | .. _CrossHair: https://github.com/pschanely/CrossHair 33 | .. _crosshair-pycharm: https://github.com/mristin/crosshair-pycharm/ 34 | .. _crosshair-vscode: https://github.com/mristin/crosshair-vscode/ 35 | .. _FastAPI: https://github.com/tiangolo/fastapi/issues/1996 36 | .. _fastapi-icontract: https://pypi.org/project/fastapi-icontract/ 37 | .. _Python-by-contract corpus: https://github.com/mristin/python-by-contract-corpus 38 | 39 | Related Projects 40 | ---------------- 41 | 42 | There exist a couple of contract libraries. However, at the time of this writing (September 2018), they all required the 43 | programmer either to learn a new syntax (`PyContracts `_) or to write 44 | redundant condition descriptions ( 45 | *e.g.*, 46 | `contracts `_, 47 | `covenant `_, 48 | `deal `_, 49 | `dpcontracts `_, 50 | `pyadbc `_ and 51 | `pcd `_). 52 | 53 | This library was strongly inspired by them, but we go two steps further. 54 | 55 | First, our violation message on contract breach are much more informative. The message includes the source code of the 56 | contract condition as well as variable values at the time of the breach. This promotes don't-repeat-yourself principle 57 | (`DRY `_) and spare the programmer the tedious task of repeating 58 | the message that was already written in code. 59 | 60 | Second, icontract allows inheritance of the contracts and supports weakening of the preconditions 61 | as well as strengthening of the postconditions and invariants. Notably, weakening and strengthening of the contracts 62 | is a feature indispensable for modeling many non-trivial class hierarchies. Please see Section :ref:`Inheritance`. 63 | To the best of our knowledge, there is currently no other Python library that supports inheritance of the contracts in a 64 | correct way. 65 | 66 | In the long run, we hope that design-by-contract will be adopted and integrated in the language. Consider this library 67 | a work-around till that happens. You might be also interested in the archived discussion on how to bring 68 | design-by-contract into Python language on 69 | `python-ideas mailing list `_. 70 | -------------------------------------------------------------------------------- /docs/source/known_issues.rst: -------------------------------------------------------------------------------- 1 | Known Issues 2 | ============ 3 | Integration with ``help()`` 4 | --------------------------- 5 | 6 | We wanted to include the contracts in the output of ``help()``. Unfortunately, 7 | ``help()`` renders the ``__doc__`` of the class and not of the instance. For functions, this is the class 8 | "function" which you can not inherit from. See this 9 | `discussion on python-ideas `_ for more details. 10 | 11 | Defining contracts outside of decorators 12 | ---------------------------------------- 13 | We need to inspect the source code of the condition and error lambdas to 14 | generate the violation message and infer the error type in the documentation, respectively. ``inspect.getsource(.)`` 15 | is broken on lambdas defined in decorators in Python 3.5.2+ (see 16 | `this bug report `_). We circumvented this bug by using ``inspect.findsource(.)``, 17 | ``inspect.getsourcefile(.)`` and examining the local source code of the lambda by searching for other decorators 18 | above and other decorators and a function or class definition below. The decorator code is parsed and then we match 19 | the condition and error arguments in the AST of the decorator. This is brittle as it prevents us from having 20 | partial definitions of contract functions or from sharing the contracts among functions. 21 | 22 | Here is a short code snippet to demonstrate where the current implementation fails: 23 | 24 | .. code-block:: python 25 | 26 | >>> import icontract 27 | 28 | >>> require_x_positive = icontract.require(lambda x: x > 0) 29 | 30 | >>> @require_x_positive 31 | ... def some_func(x: int) -> None: 32 | ... pass 33 | 34 | >>> some_func(x=0) 35 | Traceback (most recent call last): 36 | ... 37 | SyntaxError: Decorator corresponding to the line 1 could not be found in file : 'require_x_positive = icontract.require(lambda x: x > 0)\n' 38 | 39 | However, we haven't faced a situation in the code base where we would do something like the above, so we are unsure 40 | whether this is a big issue. As long as decorators are directly applied to functions and classes, everything 41 | worked fine on our code base. 42 | 43 | ``*args`` and ``**kwargs`` 44 | -------------------------- 45 | Since handling variable number of positional and/or keyword arguments requires complex 46 | logic and entails many edge cases (in particular in relation to how the arguments from the actual call are resolved and 47 | passed to the contract), we did not implement it. These special cases also impose changes that need to propagate to 48 | rendering the violation messages and related tools such as pyicontract-lint and sphinx-icontract. This is a substantial 49 | effort and needs to be prioritized accordingly. 50 | 51 | Before we spend a large amount of time on this feature, please give us a signal through 52 | `the issue 147 `_ and describe your concrete use case and its 53 | relevance. If there is enough feedback from the users, we will of course consider implementing it. 54 | 55 | ``dataclasses`` 56 | --------------- 57 | When you define contracts for `dataclasses `_, make sure you define the contracts *after* decorating the class with ``@dataclass`` decorator: 58 | 59 | .. code-block:: python 60 | 61 | >>> import icontract 62 | >>> import dataclasses 63 | 64 | >>> @icontract.invariant(lambda self: self.x > 0) 65 | ... @dataclasses.dataclass 66 | ... class Foo: 67 | ... x: int = dataclasses.field(default=42) 68 | 69 | 70 | This is necessary as we can not re-decorate the methods that ``dataclass`` decorator inserts. 71 | -------------------------------------------------------------------------------- /icontract/__init__.py: -------------------------------------------------------------------------------- 1 | """Decorate functions with contracts.""" 2 | 3 | # Please keep the meta information in sync with setup.py. 4 | # 5 | # (mristin, 2020-10-09) We had to denormalize icontract_meta module (which 6 | # used to be referenced from setup.py and this file) since readthedocs had 7 | # problems with installing icontract through pip on their servers with 8 | # imports in setup.py. 9 | 10 | # Don't forget to update the version in __init__.py and CHANGELOG.rst! 11 | __version__ = "2.7.1" 12 | __author__ = "Marko Ristin" 13 | __copyright__ = "Copyright 2019 Parquery AG" 14 | __license__ = "MIT" 15 | __status__ = "Production" 16 | 17 | # pylint: disable=invalid-name 18 | # pylint: disable=wrong-import-position 19 | 20 | # We need to explicitly assign the aliases instead of using 21 | # ``from ... import ... as ...`` statements since mypy complains 22 | # that the module icontract lacks these imports. 23 | # See also: 24 | # https://stackoverflow.com/questions/44344327/cant-make-mypy-work-with-init-py-aliases 25 | 26 | import icontract._decorators 27 | 28 | require = icontract._decorators.require 29 | snapshot = icontract._decorators.snapshot 30 | ensure = icontract._decorators.ensure 31 | invariant = icontract._decorators.invariant 32 | 33 | import icontract._globals 34 | 35 | aRepr = icontract._globals.aRepr 36 | SLOW = icontract._globals.SLOW 37 | 38 | import icontract._metaclass 39 | 40 | DBCMeta = icontract._metaclass.DBCMeta 41 | DBC = icontract._metaclass.DBC 42 | 43 | import icontract._types 44 | 45 | _Contract = icontract._types.Contract 46 | _Snapshot = icontract._types.Snapshot 47 | 48 | import icontract.errors 49 | 50 | ViolationError = icontract.errors.ViolationError 51 | 52 | InvariantCheckEvent = icontract._types.InvariantCheckEvent 53 | -------------------------------------------------------------------------------- /icontract/_globals.py: -------------------------------------------------------------------------------- 1 | """Define global variables used among the modules.""" 2 | 3 | import os 4 | import reprlib 5 | from typing import TypeVar, Callable, Any # pylint: disable=unused-import 6 | 7 | # Default representation instance. 8 | # 9 | # The limits are set way higher than reprlib.aRepr since the default reprlib limits are not suitable for 10 | # the production systems. 11 | 12 | aRepr = reprlib.Repr() # pylint: disable=invalid-name 13 | aRepr.maxdict = 50 14 | aRepr.maxlist = 50 15 | aRepr.maxtuple = 50 16 | aRepr.maxset = 50 17 | aRepr.maxfrozenset = 50 18 | aRepr.maxdeque = 50 19 | aRepr.maxarray = 50 20 | aRepr.maxstring = 256 21 | aRepr.maxother = 256 22 | 23 | # SLOW provides a unified environment variable (ICONTRACT_SLOW) to enable the contracts which are slow to execute. 24 | # 25 | # Use SLOW to mark any contracts that are even too slow to make it to the normal (__debug__) execution of 26 | # the interpreted program. 27 | # 28 | # Contracts marked with SLOW are also disabled if the interpreter is run in optimized mode (``-O`` or ``-OO``). 29 | SLOW = __debug__ and os.environ.get("ICONTRACT_SLOW", "") != "" 30 | CallableT = TypeVar("CallableT", bound=Callable[..., Any]) 31 | ClassT = TypeVar("ClassT", bound=type) 32 | ExceptionT = TypeVar("ExceptionT", bound=BaseException) 33 | -------------------------------------------------------------------------------- /icontract/_types.py: -------------------------------------------------------------------------------- 1 | """Define data structures shared among the modules.""" 2 | import enum 3 | import inspect 4 | import reprlib 5 | from typing import ( 6 | Callable, 7 | Optional, 8 | Union, 9 | Set, 10 | List, 11 | Any, 12 | Type, 13 | cast, 14 | ) # pylint: disable=unused-import 15 | 16 | import icontract._globals 17 | 18 | from icontract._globals import ExceptionT 19 | 20 | 21 | class Contract: 22 | """Represent a contract to be enforced as a precondition, postcondition or as an invariant.""" 23 | 24 | def __init__( 25 | self, 26 | condition: Callable[..., Any], 27 | description: Optional[str] = None, 28 | a_repr: reprlib.Repr = icontract._globals.aRepr, 29 | error: Optional[ 30 | Union[Callable[..., ExceptionT], Type[ExceptionT], BaseException] 31 | ] = None, 32 | location: Optional[str] = None, 33 | ) -> None: 34 | """ 35 | Initialize. 36 | 37 | :param condition: condition predicate 38 | :param description: textual description of the contract 39 | :param a_repr: representation instance that defines how the values are represented 40 | :param error: 41 | if given as a callable, ``error`` is expected to accept a subset of function arguments 42 | (*e.g.*, also including ``result`` for preconditions, only ``self`` for invariants *etc.*) and return 43 | an exception. The ``error`` is called on contract violation and the resulting exception is raised. 44 | 45 | Otherwise, it is expected to denote an Exception class which is instantiated with the violation message 46 | and raised on contract violation. 47 | :param location: indicate where the contract was defined (*e.g.*, path and line number) 48 | """ 49 | self.condition = condition 50 | 51 | signature = inspect.signature(condition) 52 | 53 | # All argument names of the condition 54 | self.condition_args = list(signature.parameters.keys()) # type: List[str] 55 | self.condition_arg_set = set(self.condition_args) # type: Set[str] 56 | 57 | # Names of the mandatory arguments of the condition 58 | self.mandatory_args = [ 59 | name 60 | for name, param in signature.parameters.items() 61 | if param.default == inspect.Parameter.empty 62 | ] 63 | 64 | self.description = description 65 | self._a_repr = a_repr 66 | 67 | self.error = error 68 | self.error_args = None # type: Optional[List[str]] 69 | self.error_arg_set = None # type: Optional[Set[str]] 70 | if error is not None and (inspect.isfunction(error) or inspect.ismethod(error)): 71 | error_as_callable = cast(Callable[..., ExceptionT], error) 72 | self.error_args = list( 73 | inspect.signature(error_as_callable).parameters.keys() 74 | ) 75 | self.error_arg_set = set(self.error_args) 76 | 77 | self.location = location 78 | 79 | 80 | class Snapshot: 81 | """Define a snapshot of an argument *prior* to the function invocation that is later supplied to a postcondition.""" 82 | 83 | def __init__( 84 | self, 85 | capture: Callable[..., Any], 86 | name: Optional[str] = None, 87 | location: Optional[str] = None, 88 | ) -> None: 89 | """ 90 | Initialize. 91 | 92 | :param capture: 93 | function to capture the snapshot accepting a single argument (from a set of arguments 94 | of the original function) 95 | 96 | :param name: name of the captured variable in OLD that is passed to postconditions 97 | :param location: indicate where the snapshot was defined (*e.g.*, path and line number) 98 | 99 | """ 100 | self.capture = capture 101 | 102 | args = list(inspect.signature(capture).parameters.keys()) # type: List[str] 103 | 104 | if name is None: 105 | if len(args) == 0: 106 | raise ValueError( 107 | "You must name a snapshot if no argument was given in the capture function." 108 | ) 109 | elif len(args) > 1: 110 | raise ValueError( 111 | "You must name a snapshot if multiple arguments were given in the capture function." 112 | ) 113 | else: 114 | assert len(args) == 1 115 | name = args[0] 116 | 117 | assert ( 118 | name is not None 119 | ), "Expected ``name`` to be set in the preceding execution paths." 120 | self.name = name 121 | 122 | self.args = args 123 | self.arg_set = set(args) 124 | 125 | self.location = location 126 | 127 | 128 | class InvariantCheckEvent(enum.Flag): 129 | """Define when an invariant should be checked.""" 130 | 131 | #: Evaluate the invariant before and after all calls to a method. 132 | CALL = enum.auto() 133 | 134 | #: Evaluate the invariant before and after all the calls to ``__setattr__``. 135 | SETATTR = enum.auto() 136 | 137 | #: Always evaluate the invariant, *i.e., both on calls and on attributes set. 138 | ALL = CALL | SETATTR 139 | 140 | 141 | class Invariant(Contract): 142 | """Represent a contract which is checked on all or some of the class operations.""" 143 | 144 | # NOTE (mristin): 145 | # The class ``Invariant`` inherits from ``Contract`` so that we can maintain 146 | # the backwards compatibility with the integrators after introducing 147 | # the ``check_on`` feature. 148 | 149 | def __init__( 150 | self, 151 | check_on: InvariantCheckEvent, 152 | condition: Callable[..., Any], 153 | description: Optional[str] = None, 154 | a_repr: reprlib.Repr = icontract._globals.aRepr, 155 | error: Optional[ 156 | Union[Callable[..., ExceptionT], Type[ExceptionT], BaseException] 157 | ] = None, 158 | location: Optional[str] = None, 159 | ) -> None: 160 | """Initialize with the given values.""" 161 | assert not hasattr(self, "check_on") 162 | self.check_on = check_on 163 | 164 | super().__init__( 165 | condition=condition, 166 | description=description, 167 | a_repr=a_repr, 168 | error=error, 169 | location=location, 170 | ) 171 | -------------------------------------------------------------------------------- /icontract/errors.py: -------------------------------------------------------------------------------- 1 | """Define public errors and exceptions.""" 2 | 3 | 4 | class ViolationError(AssertionError): 5 | """Indicate a violation of a contract.""" 6 | -------------------------------------------------------------------------------- /icontract/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The mypy package uses inline types. 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | [mypy-asttokens] 4 | ignore_missing_imports = True 5 | 6 | [mypy-dpcontracts] 7 | ignore_missing_imports = True 8 | 9 | [mypy-icontract_hypothesis] 10 | ignore_missing_imports = True 11 | 12 | [mypy-hypothesis.strategies._internal.types] 13 | ignore_missing_imports = True 14 | 15 | [mypy-hypothesis.strategies._internal] 16 | ignore_missing_imports = True 17 | 18 | [mypy-hypothesis.strategies] 19 | ignore_missing_imports = True 20 | 21 | [mypy-hypothesis] 22 | ignore_missing_imports = True 23 | 24 | [mypy-asyncstdlib] 25 | ignore_missing_imports = True 26 | 27 | [mypy-astor] 28 | ignore_missing_imports = True 29 | 30 | [mypy-numpy] 31 | ignore_missing_imports = True 32 | -------------------------------------------------------------------------------- /precommit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Runs precommit checks on the repository.""" 3 | import argparse 4 | import os 5 | import pathlib 6 | import subprocess 7 | import sys 8 | 9 | 10 | def main() -> int: 11 | """ "Execute main routine.""" 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument( 14 | "--overwrite", 15 | help="Overwrites the unformatted source files with the well-formatted code in place. " 16 | "If not set, an exception is raised if any of the files do not conform to the style guide.", 17 | action="store_true", 18 | ) 19 | 20 | args = parser.parse_args() 21 | 22 | overwrite = bool(args.overwrite) 23 | 24 | repo_root = pathlib.Path(__file__).parent 25 | 26 | if sys.version_info < (3, 8): 27 | print( 28 | "Our formatter, black, supports only Python versions from 3.8 on. " 29 | "However, you are running Python {}. Hence, the reformatting step " 30 | "will be skipped.".format(sys.version_info) 31 | ) 32 | else: 33 | print("Reformatting...") 34 | 35 | reformat_targets = [ 36 | "tests", 37 | "icontract", 38 | "setup.py", 39 | "precommit.py", 40 | "benchmark.py", 41 | "benchmarks", 42 | "tests_with_others", 43 | ] 44 | 45 | if sys.version_info >= (3, 6): 46 | reformat_targets.append("tests_3_6") 47 | 48 | if sys.version_info >= (3, 7): 49 | reformat_targets.append("tests_3_7") 50 | 51 | if sys.version_info >= (3, 8, 5): 52 | reformat_targets.append("tests_3_8") 53 | 54 | if overwrite: 55 | subprocess.check_call( 56 | [sys.executable, "-m", "black"] + reformat_targets, cwd=str(repo_root) 57 | ) 58 | else: 59 | subprocess.check_call( 60 | [sys.executable, "-m", "black"] + reformat_targets, cwd=str(repo_root) 61 | ) 62 | 63 | if sys.version_info < (3, 8): 64 | print( 65 | "Mypy since 1.5 dropped support for Python 3.7 and " 66 | "you are running Python {}, so skipping.".format(sys.version_info) 67 | ) 68 | else: 69 | print("Mypy'ing...") 70 | mypy_targets = ["icontract", "tests"] 71 | if sys.version_info >= (3, 6): 72 | mypy_targets.append("tests_3_6") 73 | 74 | if sys.version_info >= (3, 7): 75 | mypy_targets.append("tests_3_7") 76 | 77 | if sys.version_info >= (3, 8): 78 | mypy_targets.append("tests_3_8") 79 | mypy_targets.append("tests_with_others") 80 | 81 | subprocess.check_call(["mypy", "--strict"] + mypy_targets, cwd=str(repo_root)) 82 | 83 | if sys.version_info < (3, 7): 84 | print( 85 | "Pylint dropped support for Python 3.6 and " 86 | "you are running Python {}, so skipping.".format(sys.version_info) 87 | ) 88 | else: 89 | print("Pylint'ing...") 90 | pylint_targets = ["icontract", "tests"] 91 | 92 | if sys.version_info >= (3, 6): 93 | pylint_targets.append("tests_3_6") 94 | 95 | if sys.version_info >= (3, 7): 96 | pylint_targets.append("tests_3_7") 97 | 98 | if sys.version_info >= (3, 8): 99 | pylint_targets.append("tests_3_8") 100 | pylint_targets.append("tests_with_others") 101 | 102 | subprocess.check_call( 103 | ["pylint", "--rcfile=pylint.rc"] + pylint_targets, cwd=str(repo_root) 104 | ) 105 | 106 | print("Pydocstyle'ing...") 107 | subprocess.check_call(["pydocstyle", "icontract"], cwd=str(repo_root)) 108 | 109 | print("Testing...") 110 | env = os.environ.copy() 111 | env["ICONTRACT_SLOW"] = "true" 112 | 113 | # fmt: off 114 | subprocess.check_call( 115 | ["coverage", "run", 116 | "--source", "icontract", 117 | "-m", "unittest", "discover"], 118 | cwd=str(repo_root), 119 | env=env) 120 | # fmt: on 121 | 122 | if sys.version_info >= (3, 8): 123 | # fmt: off 124 | subprocess.check_call( 125 | ["coverage", "run", 126 | "--source", "icontract", 127 | "-a", "-m", "tests_3_8.async.separately_test_concurrent"], 128 | cwd=str(repo_root), 129 | env=env) 130 | # fmt: on 131 | 132 | subprocess.check_call(["coverage", "report"]) 133 | 134 | if (3, 8) <= sys.version_info < (3, 9): 135 | print("Doctesting...") 136 | doc_files = ["README.rst"] 137 | for pth in (repo_root / "docs" / "source").glob("**/*.rst"): 138 | doc_files.append(str(pth.relative_to(repo_root))) 139 | subprocess.check_call([sys.executable, "-m", "doctest"] + doc_files) 140 | 141 | for pth in (repo_root / "icontract").glob("**/*.py"): 142 | subprocess.check_call([sys.executable, "-m", "doctest", str(pth)]) 143 | else: 144 | print( 145 | "We pin the doctests at Python 3.8 as the output of the exception " 146 | "traceback changes between the Python versions. You are running Python " 147 | "{}, so we will not run the doctests.".format(sys.version_info) 148 | ) 149 | 150 | print("Checking the restructured text of the readme...") 151 | subprocess.check_call( 152 | [sys.executable, "setup.py", "check", "--restructuredtext", "--strict"] 153 | ) 154 | 155 | return 0 156 | 157 | 158 | if __name__ == "__main__": 159 | sys.exit(main()) 160 | -------------------------------------------------------------------------------- /pylint.rc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-modules = numpy 3 | ignored-classes = numpy,PurePath 4 | generated-members=bottle\.request\.forms\.decode,bottle\.request\.query\.decode 5 | 6 | [FORMAT] 7 | max-line-length=120 8 | 9 | [MESSAGES CONTROL] 10 | disable=too-few-public-methods,len-as-condition,duplicate-code,no-else-raise,too-many-locals,too-many-branches,too-many-lines,too-many-arguments,too-many-statements,too-many-nested-blocks,too-many-function-args,too-many-instance-attributes,too-many-public-methods,protected-access,consider-using-in,no-member,consider-using-f-string,use-dict-literal,redundant-keyword-arg,no-else-return 11 | 12 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | asttokens>=2,<3 2 | typing_extensions 3 | sphinx>=5,<6 4 | sphinx-autodoc-typehints>=1.11.1 5 | sphinx-rtd-theme>=1,<2 6 | autodocsumm>=0.2.2,<1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/en/latest/distributing.html 5 | https://github.com/pypa/sampleproject 6 | """ 7 | import os 8 | import sys 9 | 10 | from setuptools import setup, find_packages 11 | 12 | # pylint: disable=redefined-builtin 13 | 14 | here = os.path.abspath(os.path.dirname(__file__)) # pylint: disable=invalid-name 15 | 16 | with open(os.path.join(here, "README.rst"), encoding="utf-8") as fid: 17 | long_description = fid.read() # pylint: disable=invalid-name 18 | 19 | # Please keep the meta information in sync with icontract/__init__.py. 20 | # 21 | # (mristin, 2020-10-09) We had to denormalize icontract_meta module (which 22 | # used to be referenced from setup.py and this file) since readthedocs had 23 | # problems with installing icontract through pip on their servers with 24 | # imports in setup.py. 25 | setup( 26 | name="icontract", 27 | # Don't forget to update the version in __init__.py and CHANGELOG.rst! 28 | version="2.7.1", 29 | description="Provide design-by-contract with informative violation messages.", 30 | long_description=long_description, 31 | url="https://github.com/Parquery/icontract", 32 | author="Marko Ristin", 33 | author_email="marko@ristin.ch", 34 | classifiers=[ 35 | # fmt: off 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: MIT License', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | 'Programming Language :: Python :: 3.8', 42 | 'Programming Language :: Python :: 3.9', 43 | 'Programming Language :: Python :: 3.10', 44 | 'Programming Language :: Python :: 3.11', 45 | # fmt: on 46 | ], 47 | license="License :: OSI Approved :: MIT License", 48 | keywords="design-by-contract precondition postcondition validation", 49 | packages=find_packages(exclude=["tests*"]), 50 | install_requires=[ 51 | "asttokens>=2,<3", 52 | 'contextvars;python_version=="3.6"', 53 | "typing_extensions", 54 | ], 55 | extras_require={ 56 | "dev": [ 57 | 'pylint==2.17.5;python_version>="3.7"', 58 | "tox>=3.0.0", 59 | "pydocstyle>=6.3.0,<7", 60 | "coverage>=6.5.0,<7", 61 | "docutils>=0.14,<1", 62 | "pygments>=2.2.0,<3", 63 | "dpcontracts==0.6.0", 64 | "tabulate>=0.8.7,<1", 65 | "py-cpuinfo>=5.0.0,<6", 66 | "typeguard>=2,<5", 67 | "astor==0.8.1", 68 | "numpy>=1,<2", 69 | 'mypy==1.5.1;python_version>="3.8"', 70 | 'black==23.9.1;python_version>="3.8"', 71 | 'deal>=4,<5;python_version>="3.8"', 72 | 'asyncstdlib==3.9.1;python_version>="3.8"', 73 | ] 74 | }, 75 | py_modules=["icontract"], 76 | package_data={"icontract": ["py.typed"]}, 77 | ) 78 | -------------------------------------------------------------------------------- /style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | spaces_before_comment = 2 4 | split_before_logical_operator = true 5 | column_limit = 120 6 | coalesce_brackets = true 7 | 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test icontract.""" 2 | -------------------------------------------------------------------------------- /tests/error.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Manipulate the error text.""" 3 | import re 4 | 5 | _LOCATION_RE = re.compile( 6 | r"\AFile [^\n]+, line [0-9]+ in [a-zA-Z_0-9]+:\n(.*)\Z", 7 | flags=re.MULTILINE | re.DOTALL, 8 | ) 9 | 10 | 11 | def wo_mandatory_location(text: str) -> str: 12 | r""" 13 | Strip the location of the contract from the text of the error. 14 | 15 | :param text: text of the error 16 | :return: text without the location prefix 17 | :raise AssertionError: if the location is not present in the text of the error. 18 | 19 | >>> wo_mandatory_location(text='File /some/file.py, line 233 in some_module:\nsome\ntext') 20 | 'some\ntext' 21 | 22 | >>> wo_mandatory_location(text='a text') 23 | Traceback (most recent call last): 24 | ... 25 | AssertionError: Expected the text to match \AFile [^\n]+, line [0-9]+ in [a-zA-Z_0-9]+:\n(.*)\Z, but got: 'a text' 26 | """ 27 | mtch = _LOCATION_RE.match(text) 28 | if not mtch: 29 | raise AssertionError( 30 | "Expected the text to match {}, but got: {!r}".format( 31 | _LOCATION_RE.pattern, text 32 | ) 33 | ) 34 | 35 | return mtch.group(1) 36 | -------------------------------------------------------------------------------- /tests/mock.py: -------------------------------------------------------------------------------- 1 | """Provide mock structures used accross the tests.""" 2 | from typing import List, Union 3 | 4 | 5 | class NumpyArray: 6 | """Represent a class that mocks a numpy.array and it's behavior on less-then operator.""" 7 | 8 | def __init__(self, values: List[Union[int, bool]]) -> None: 9 | """Initialize with the given values.""" 10 | self.values = values 11 | 12 | def __lt__(self, other: int) -> "NumpyArray": 13 | """Map the value to each comparison with ``other``.""" 14 | return NumpyArray(values=[value < other for value in self.values]) 15 | 16 | def __gt__(self, other: int) -> "NumpyArray": 17 | """Map the value to each comparison with ``other``.""" 18 | return NumpyArray(values=[value > other for value in self.values]) 19 | 20 | def __bool__(self) -> bool: 21 | """Raise a ValueError.""" 22 | raise ValueError( 23 | "The truth value of an array with more than one element is ambiguous." 24 | ) 25 | 26 | def all(self) -> bool: 27 | """Return True if all values are True.""" 28 | return all(self.values) 29 | 30 | def __repr__(self) -> str: 31 | """Represent with the constructor.""" 32 | return "NumpyArray({!r})".format(self.values) 33 | -------------------------------------------------------------------------------- /tests/test_checkers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=invalid-name 5 | # pylint: disable=unused-argument 6 | 7 | import functools 8 | import unittest 9 | from typing import Optional 10 | 11 | import icontract._checkers 12 | from icontract._globals import CallableT 13 | 14 | 15 | def decorator_plus_1(func: CallableT) -> CallableT: 16 | def wrapper(*args, **kwargs): # type: ignore 17 | return func(*args, **kwargs) + 1 18 | 19 | functools.update_wrapper(wrapper=wrapper, wrapped=func) 20 | 21 | return wrapper # type: ignore 22 | 23 | 24 | def decorator_plus_2(func: CallableT) -> CallableT: 25 | def wrapper(*args, **kwargs): # type: ignore 26 | return func(*args, **kwargs) + 2 27 | 28 | functools.update_wrapper(wrapper=wrapper, wrapped=func) 29 | 30 | return wrapper # type: ignore 31 | 32 | 33 | class TestUnwindDecoratorStack(unittest.TestCase): 34 | def test_wo_decorators(self) -> None: 35 | def func() -> int: 36 | return 0 37 | 38 | self.assertListEqual( 39 | [0], 40 | [a_func() for a_func in icontract._checkers._walk_decorator_stack(func)], 41 | ) 42 | 43 | def test_with_single_decorator(self) -> None: 44 | @decorator_plus_1 45 | def func() -> int: 46 | return 0 47 | 48 | self.assertListEqual( 49 | [1, 0], 50 | [a_func() for a_func in icontract._checkers._walk_decorator_stack(func)], 51 | ) 52 | 53 | def test_with_double_decorator(self) -> None: 54 | @decorator_plus_2 55 | @decorator_plus_1 56 | def func() -> int: 57 | return 0 58 | 59 | self.assertListEqual( 60 | [3, 1, 0], 61 | [a_func() for a_func in icontract._checkers._walk_decorator_stack(func)], 62 | ) 63 | 64 | 65 | class TestResolveKwargs(unittest.TestCase): 66 | def test_that_extra_args_raise_correct_type_error(self) -> None: 67 | @icontract.require(lambda: True) 68 | def some_func(x: int, y: int) -> None: 69 | pass 70 | 71 | type_error = None # type: Optional[TypeError] 72 | try: 73 | some_func(1, 2, 3) # type: ignore 74 | except TypeError as error: 75 | type_error = error 76 | 77 | assert type_error is not None 78 | self.assertRegex( 79 | str(type_error), 80 | r"^([a-zA-Z_0-9<>.]+\.)?some_func\(\) takes 2 positional arguments but 3 were given$", 81 | ) 82 | 83 | def test_that_result_in_kwargs_raises_an_error(self) -> None: 84 | @icontract.ensure(lambda result: result > 0) 85 | def some_func(*args, **kwargs) -> int: # type: ignore 86 | return -1 87 | 88 | type_error = None # type: Optional[TypeError] 89 | 90 | try: 91 | some_func(result=-1) 92 | except TypeError as error: 93 | type_error = error 94 | 95 | assert type_error is not None 96 | 97 | self.assertEqual( 98 | "Unexpected argument 'result' in a function decorated with postconditions.", 99 | str(type_error), 100 | ) 101 | 102 | def test_that_OLD_in_kwargs_raises_an_error(self) -> None: 103 | @icontract.ensure(lambda result: result > 0) 104 | def some_func(*args, **kwargs) -> int: # type: ignore 105 | return -1 106 | 107 | type_error = None # type: Optional[TypeError] 108 | 109 | try: 110 | some_func(OLD=-1) 111 | except TypeError as error: 112 | type_error = error 113 | 114 | assert type_error is not None 115 | 116 | self.assertEqual( 117 | "Unexpected argument 'OLD' in a function decorated with postconditions.", 118 | str(type_error), 119 | ) 120 | 121 | 122 | if __name__ == "__main__": 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /tests/test_error.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | # pylint: disable=unused-variable 5 | 6 | import unittest 7 | from typing import Optional 8 | 9 | import icontract 10 | 11 | import tests.error 12 | 13 | 14 | class TestNoneSpecified(unittest.TestCase): 15 | def test_that_it_works(self) -> None: 16 | @icontract.require(lambda x: x > 0) 17 | def some_func(x: int) -> None: 18 | pass 19 | 20 | violation_error = None # type: Optional[icontract.ViolationError] 21 | try: 22 | some_func(x=-1) 23 | except icontract.ViolationError as err: 24 | violation_error = err 25 | 26 | assert violation_error is not None 27 | self.assertEqual( 28 | "x > 0: x was -1", tests.error.wo_mandatory_location(str(violation_error)) 29 | ) 30 | 31 | 32 | class TestSpecifiedAsFunction(unittest.TestCase): 33 | def test_lambda(self) -> None: 34 | @icontract.require( 35 | lambda x: x > 0, 36 | error=lambda x: ValueError("x must be positive: {}".format(x)), 37 | ) 38 | def some_func(x: int) -> None: 39 | pass 40 | 41 | value_error = None # type: Optional[ValueError] 42 | try: 43 | some_func(x=-1) 44 | except ValueError as err: 45 | value_error = err 46 | 47 | assert value_error is not None 48 | self.assertEqual("x must be positive: -1", str(value_error)) 49 | 50 | def test_separate_function(self) -> None: 51 | def error_func(x: int) -> ValueError: 52 | return ValueError("x must be positive: {}".format(x)) 53 | 54 | @icontract.require(lambda x: x > 0, error=error_func) 55 | def some_func(x: int) -> None: 56 | pass 57 | 58 | value_error = None # type: Optional[ValueError] 59 | try: 60 | some_func(x=-1) 61 | except ValueError as err: 62 | value_error = err 63 | 64 | assert value_error is not None 65 | self.assertEqual("x must be positive: -1", str(value_error)) 66 | 67 | def test_separate_method(self) -> None: 68 | class Errorer: 69 | def error_func(self, x: int) -> ValueError: 70 | return ValueError("x must be positive: {}".format(x)) 71 | 72 | errorer = Errorer() 73 | 74 | @icontract.require(lambda x: x > 0, error=errorer.error_func) 75 | def some_func(x: int) -> None: 76 | pass 77 | 78 | value_error = None # type: Optional[ValueError] 79 | try: 80 | some_func(x=-1) 81 | except ValueError as err: 82 | value_error = err 83 | 84 | assert value_error is not None 85 | self.assertEqual("x must be positive: -1", str(value_error)) 86 | 87 | def test_report_if_result_is_not_base_exception(self) -> None: 88 | @icontract.require(lambda x: x > 0, error=lambda x: "x must be positive") # type: ignore 89 | def some_func(x: int) -> None: 90 | pass 91 | 92 | type_error = None # type: Optional[TypeError] 93 | try: 94 | some_func(x=-1) 95 | except TypeError as err: 96 | type_error = err 97 | 98 | assert type_error is not None 99 | self.assertRegex( 100 | str(type_error), 101 | r"^The exception returned by the contract's error does not inherit from BaseException\.$", 102 | ) 103 | 104 | 105 | class TestSpecifiedAsType(unittest.TestCase): 106 | def test_valid_exception(self) -> None: 107 | @icontract.require(lambda x: x > 0, error=ValueError) 108 | def some_func(x: int) -> None: 109 | pass 110 | 111 | value_error = None # type: Optional[ValueError] 112 | try: 113 | some_func(x=-1) 114 | except ValueError as err: 115 | value_error = err 116 | 117 | assert value_error is not None 118 | self.assertEqual( 119 | "x > 0: x was -1", tests.error.wo_mandatory_location(str(value_error)) 120 | ) 121 | 122 | 123 | class TestSpecifiedAsInstance(unittest.TestCase): 124 | def test_valid_exception(self) -> None: 125 | @icontract.require(lambda x: x > 0, error=ValueError("negative x")) 126 | def some_func(x: int) -> None: 127 | pass 128 | 129 | value_error = None # type: Optional[ValueError] 130 | try: 131 | some_func(x=-1) 132 | except ValueError as err: 133 | value_error = err 134 | 135 | assert value_error is not None 136 | self.assertEqual("negative x", str(value_error)) 137 | 138 | def test_repeated_raising(self) -> None: 139 | @icontract.require(lambda x: x > 0, error=ValueError("negative x")) 140 | def some_func(x: int) -> None: 141 | pass 142 | 143 | value_error = None # type: Optional[ValueError] 144 | try: 145 | some_func(x=-1) 146 | except ValueError as err: 147 | value_error = err 148 | 149 | assert value_error is not None 150 | self.assertEqual("negative x", str(value_error)) 151 | 152 | # Repeat 153 | value_error = None 154 | try: 155 | some_func(x=-1) 156 | except ValueError as err: 157 | value_error = err 158 | 159 | assert value_error is not None 160 | self.assertEqual("negative x", str(value_error)) 161 | 162 | 163 | class TestSpecifiedAsInvalidType(unittest.TestCase): 164 | def test_in_precondition(self) -> None: 165 | class A: 166 | pass 167 | 168 | value_error = None # type: Optional[ValueError] 169 | try: 170 | 171 | @icontract.require(lambda x: x > 0, error=A) # type: ignore 172 | def some_func(x: int) -> None: 173 | pass 174 | 175 | except ValueError as err: 176 | value_error = err 177 | 178 | assert value_error is not None 179 | self.assertRegex( 180 | str(value_error), 181 | r"The error of the contract is given as a type, " 182 | r"but the type does not inherit from BaseException: ", 183 | ) 184 | 185 | def test_in_postcondition(self) -> None: 186 | class A: 187 | pass 188 | 189 | value_error = None # type: Optional[ValueError] 190 | try: 191 | 192 | @icontract.ensure(lambda result: result > 0, error=A) # type: ignore 193 | def some_func() -> int: 194 | return -1 195 | 196 | except ValueError as err: 197 | value_error = err 198 | 199 | assert value_error is not None 200 | self.assertRegex( 201 | str(value_error), 202 | r"The error of the contract is given as a type, " 203 | r"but the type does not inherit from BaseException: ", 204 | ) 205 | 206 | def test_in_invariant(self) -> None: 207 | value_error = None # type: Optional[ValueError] 208 | try: 209 | 210 | class A: 211 | pass 212 | 213 | @icontract.invariant(lambda self: self.x > 0, error=A) # type: ignore 214 | class B: 215 | def __init__(self) -> None: 216 | self.x = -1 217 | 218 | except ValueError as err: 219 | value_error = err 220 | 221 | assert value_error is not None 222 | self.assertRegex( 223 | str(value_error), 224 | r"The error of the contract is given as a type, " 225 | r"but the type does not inherit from BaseException: ", 226 | ) 227 | 228 | 229 | class TestSpecifiedAsInstanceOfInvalidType(unittest.TestCase): 230 | def test_in_precondition(self) -> None: 231 | class A: 232 | def __init__(self, msg: str) -> None: 233 | self.msg = msg 234 | 235 | value_error = None # type: Optional[ValueError] 236 | try: 237 | 238 | @icontract.require(lambda x: x > 0, error=A("something went wrong")) # type: ignore 239 | def some_func(x: int) -> None: 240 | pass 241 | 242 | except ValueError as err: 243 | value_error = err 244 | 245 | assert value_error is not None 246 | self.assertRegex( 247 | str(value_error), 248 | r"^The error of the contract must be either a callable \(a function or a method\), " 249 | r"a class \(subclass of BaseException\) or an instance of BaseException, " 250 | r"but got: <.*\.A object at 0x.*>$", 251 | ) 252 | 253 | def test_in_postcondition(self) -> None: 254 | class A: 255 | def __init__(self, msg: str) -> None: 256 | self.msg = msg 257 | 258 | value_error = None # type: Optional[ValueError] 259 | try: 260 | 261 | @icontract.ensure(lambda result: result > 0, error=A("something went wrong")) # type: ignore 262 | def some_func() -> int: 263 | return -1 264 | 265 | except ValueError as err: 266 | value_error = err 267 | 268 | assert value_error is not None 269 | self.assertRegex( 270 | str(value_error), 271 | r"^The error of the contract must be either a callable \(a function or a method\), " 272 | r"a class \(subclass of BaseException\) or an instance of BaseException, " 273 | r"but got: <.*\.A object at 0x.*>$", 274 | ) 275 | 276 | def test_in_invariant(self) -> None: 277 | class A: 278 | def __init__(self, msg: str) -> None: 279 | self.msg = msg 280 | 281 | value_error = None # type: Optional[ValueError] 282 | try: 283 | 284 | @icontract.invariant(lambda self: self.x > 0, error=A("something went wrong")) # type: ignore 285 | class B: 286 | def __init__(self) -> None: 287 | self.x = -1 288 | 289 | except ValueError as err: 290 | value_error = err 291 | 292 | assert value_error is not None 293 | self.assertRegex( 294 | str(value_error), 295 | r"^The error of the contract must be either a callable \(a function or a method\), " 296 | r"a class \(subclass of BaseException\) or an instance of BaseException, " 297 | r"but got: <.*\.A object at 0x.*>$", 298 | ) 299 | 300 | 301 | if __name__ == "__main__": 302 | unittest.main() 303 | -------------------------------------------------------------------------------- /tests/test_for_integrators.py: -------------------------------------------------------------------------------- 1 | """Test logic that can be potentially used by the integrators such as third-party libraries.""" 2 | 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=invalid-name,unnecessary-lambda 5 | 6 | import ast 7 | import unittest 8 | from typing import List, MutableMapping, Any, Optional 9 | 10 | import icontract._checkers 11 | import icontract._represent 12 | 13 | 14 | @icontract.require(lambda x: x > 0) 15 | @icontract.snapshot( 16 | lambda cumulative: None if len(cumulative) == 0 else cumulative[-1], "last" 17 | ) 18 | @icontract.snapshot(lambda cumulative: len(cumulative), "len_cumulative") 19 | @icontract.ensure(lambda cumulative, OLD: len(cumulative) == OLD.len_cumulative + 1) 20 | @icontract.ensure( 21 | lambda x, cumulative, OLD: OLD.last is None or OLD.last + x == cumulative[-1] 22 | ) 23 | @icontract.ensure( 24 | lambda x, cumulative, OLD: OLD.last is not None or x == cumulative[-1] 25 | ) 26 | def func_with_contracts(x: int, cumulative: List[int]) -> None: 27 | if len(cumulative) == 0: 28 | cumulative.append(x) 29 | else: 30 | cumulative.append(x + cumulative[-1]) 31 | 32 | 33 | def func_without_contracts() -> None: 34 | pass 35 | 36 | 37 | @icontract.invariant(lambda self: self.x > 0) 38 | class ClassWithInvariants(icontract.DBC): 39 | def __init__(self) -> None: 40 | self.x = 1 41 | 42 | 43 | class TestInitial(unittest.TestCase): 44 | def test_that_there_is_no_checker_if_no_contracts(self) -> None: 45 | checker = icontract._checkers.find_checker(func=func_without_contracts) 46 | self.assertIsNone(checker) 47 | 48 | 49 | class TestPreconditions(unittest.TestCase): 50 | def test_evaluating(self) -> None: 51 | checker = icontract._checkers.find_checker(func=func_with_contracts) 52 | assert checker is not None 53 | 54 | preconditions = checker.__preconditions__ # type: ignore 55 | assert isinstance(preconditions, list) 56 | assert all(isinstance(group, list) for group in preconditions) 57 | assert all( 58 | isinstance(contract, icontract._types.Contract) 59 | for group in preconditions 60 | for contract in group 61 | ) 62 | 63 | ## 64 | # Evaluate manually preconditions 65 | ## 66 | 67 | kwargs = {"x": 4, "cumulative": [2]} 68 | 69 | success = True 70 | # We have to check preconditions in groups in case they are weakened 71 | for group in preconditions: 72 | success = True 73 | for contract in group: 74 | condition_kwargs = icontract._checkers.select_condition_kwargs( 75 | contract=contract, resolved_kwargs=kwargs 76 | ) 77 | 78 | success = contract.condition(**condition_kwargs) 79 | if not success: 80 | break 81 | 82 | if success: 83 | break 84 | 85 | assert success 86 | 87 | def test_adding(self) -> None: 88 | def some_func(x: int) -> None: # pylint: disable=unused-argument 89 | return 90 | 91 | checker = icontract._checkers.find_checker(func=some_func) 92 | assert checker is None 93 | 94 | wrapped = checker = icontract._checkers.decorate_with_checker(func=some_func) 95 | 96 | # The contract needs to have its own error specified since it is not added as a decorator, 97 | # so the module ``icontract._represent`` will be confused. 98 | icontract._checkers.add_precondition_to_checker( 99 | checker=checker, 100 | contract=icontract._types.Contract( 101 | condition=lambda x: x > 0, 102 | error=lambda x: icontract.ViolationError( 103 | "x must be positive, but got: {}".format(x) 104 | ), 105 | ), 106 | ) 107 | 108 | violation_error = None # type: Optional[icontract.ViolationError] 109 | try: 110 | wrapped(x=-1) 111 | except icontract.ViolationError as err: 112 | violation_error = err 113 | 114 | assert violation_error is not None 115 | 116 | self.assertEqual("x must be positive, but got: -1", str(violation_error)) 117 | 118 | 119 | class TestPostconditions(unittest.TestCase): 120 | def test_evaluating(self) -> None: 121 | checker = icontract._checkers.find_checker(func=func_with_contracts) 122 | assert checker is not None 123 | 124 | # Retrieve postconditions 125 | postconditions = checker.__postconditions__ # type: ignore 126 | assert isinstance(postconditions, list) 127 | assert all( 128 | isinstance(contract, icontract._types.Contract) 129 | for contract in postconditions 130 | ) 131 | 132 | # Retrieve snapshots 133 | snapshots = checker.__postcondition_snapshots__ # type: ignore 134 | assert isinstance(snapshots, list) 135 | assert all( 136 | isinstance(snapshot, icontract._types.Snapshot) for snapshot in snapshots 137 | ) 138 | 139 | ## 140 | # Evaluate manually postconditions 141 | ## 142 | 143 | cumulative = [2] 144 | kwargs = {"x": 4, "cumulative": cumulative} # kwargs **before** the call 145 | 146 | # Capture OLD 147 | old_as_mapping = dict() # type: MutableMapping[str, Any] 148 | for snap in snapshots: 149 | snap_kwargs = icontract._checkers.select_capture_kwargs( 150 | a_snapshot=snap, resolved_kwargs=kwargs 151 | ) 152 | 153 | old_as_mapping[snap.name] = snap.capture(**snap_kwargs) 154 | 155 | old = icontract._checkers.Old(mapping=old_as_mapping) 156 | 157 | # Simulate the call 158 | cumulative.append(6) 159 | 160 | # Evaluate the postconditions 161 | kwargs["OLD"] = old 162 | 163 | success = True 164 | for contract in postconditions: 165 | condition_kwargs = icontract._checkers.select_condition_kwargs( 166 | contract=contract, resolved_kwargs=kwargs 167 | ) 168 | 169 | success = contract.condition(**condition_kwargs) 170 | 171 | if not success: 172 | break 173 | 174 | assert success 175 | 176 | def test_adding(self) -> None: 177 | def some_func(lst: List[int]) -> None: 178 | # This will break the post-condition, see below. 179 | lst.append(1984) 180 | 181 | checker = icontract._checkers.find_checker(func=some_func) 182 | assert checker is None 183 | 184 | wrapped = checker = icontract._checkers.decorate_with_checker(func=some_func) 185 | 186 | # The contract needs to have its own error specified since it is not added as a decorator, 187 | # so the module ``icontract._represent`` will be confused. 188 | icontract._checkers.add_postcondition_to_checker( 189 | checker=checker, 190 | contract=icontract._types.Contract( 191 | condition=lambda OLD, lst: OLD.len_lst == len(lst), 192 | error=icontract.ViolationError("The size of lst must not change."), 193 | ), 194 | ) 195 | 196 | icontract._checkers.add_snapshot_to_checker( 197 | checker=checker, 198 | snapshot=icontract._types.Snapshot( 199 | capture=lambda lst: len(lst), name="len_lst" 200 | ), 201 | ) 202 | 203 | violation_error = None # type: Optional[icontract.ViolationError] 204 | try: 205 | lst = [1, 2, 3] 206 | wrapped(lst=lst) 207 | except icontract.ViolationError as err: 208 | violation_error = err 209 | 210 | assert violation_error is not None 211 | 212 | self.assertEqual("The size of lst must not change.", str(violation_error)) 213 | 214 | 215 | class TestInvariants(unittest.TestCase): 216 | def test_reading(self) -> None: 217 | instance = ClassWithInvariants() 218 | assert instance.x == 1 # Test assumption 219 | 220 | invariants = ClassWithInvariants.__invariants__ # type: ignore 221 | assert isinstance(invariants, list) 222 | assert all( 223 | isinstance(invariant, icontract._types.Contract) for invariant in invariants 224 | ) 225 | 226 | invariants = instance.__invariants__ # type: ignore 227 | assert isinstance(invariants, list) 228 | assert all( 229 | isinstance(invariant, icontract._types.Contract) for invariant in invariants 230 | ) 231 | 232 | success = True 233 | for contract in invariants: 234 | success = contract.condition(self=instance) 235 | 236 | if not success: 237 | break 238 | 239 | assert success 240 | 241 | 242 | class TestRepresentation(unittest.TestCase): 243 | def test_condition_text(self) -> None: 244 | checker = icontract._checkers.find_checker(func=func_with_contracts) 245 | assert checker is not None 246 | 247 | # Retrieve postconditions 248 | contract = checker.__postconditions__[0] # type: ignore 249 | assert isinstance(contract, icontract._types.Contract) 250 | 251 | assert icontract._represent.is_lambda(a_function=contract.condition) 252 | 253 | lambda_inspection = icontract._represent.inspect_lambda_condition( 254 | condition=contract.condition 255 | ) 256 | 257 | assert lambda_inspection is not None 258 | 259 | self.assertEqual( 260 | "OLD.last is not None or x == cumulative[-1]", lambda_inspection.text 261 | ) 262 | 263 | assert isinstance(lambda_inspection.node, ast.Lambda) 264 | 265 | def test_condition_representation(self) -> None: 266 | checker = icontract._checkers.find_checker(func=func_with_contracts) 267 | assert checker is not None 268 | 269 | # Retrieve postconditions 270 | contract = checker.__postconditions__[0] # type: ignore 271 | assert isinstance(contract, icontract._types.Contract) 272 | 273 | text = icontract._represent.represent_condition(contract.condition) 274 | self.assertEqual( 275 | "lambda x, cumulative, OLD: OLD.last is not None or x == cumulative[-1]", 276 | text, 277 | ) 278 | 279 | 280 | if __name__ == "__main__": 281 | unittest.main() 282 | -------------------------------------------------------------------------------- /tests/test_globals.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | 3 | import unittest 4 | 5 | import icontract 6 | 7 | 8 | class TestSlow(unittest.TestCase): 9 | def test_slow_set(self) -> None: 10 | self.assertTrue( 11 | icontract.SLOW, 12 | "icontract.SLOW was not set. Please check if you set the environment variable ICONTRACT_SLOW " 13 | "before running this test.", 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /tests/test_mypy_decorators.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | import pathlib 3 | import subprocess 4 | import sys 5 | import tempfile 6 | import unittest 7 | 8 | if sys.version_info < (3, 8): 9 | raise unittest.SkipTest( 10 | ( 11 | "Mypy since 1.5 dropped support for Python 3.7 and " 12 | "you are running Python {}, so skipping." 13 | ).format(sys.version_info) 14 | ) 15 | 16 | 17 | class TestMypyDecorators(unittest.TestCase): 18 | def test_functions(self) -> None: 19 | with tempfile.TemporaryDirectory(prefix="mypy_fail_case_") as tmpdir: 20 | content = '''\ 21 | """Implement a fail case for mypy to test that the types are preserved with the decorators.""" 22 | 23 | import icontract 24 | 25 | @icontract.require(lambda x: x > 0) 26 | def f1(x: int) -> int: 27 | return x 28 | f1("this is wrong") 29 | 30 | @icontract.ensure(lambda result: result > 0) 31 | def f2(x: int) -> int: 32 | return x 33 | f2("this is wrong") 34 | 35 | @icontract.snapshot(lambda x: x) 36 | def f3(x: int) -> int: 37 | return x 38 | f3("this is wrong") 39 | ''' 40 | 41 | pth = pathlib.Path(tmpdir) / "source.py" 42 | pth.write_text(content, encoding="utf-8") 43 | 44 | with subprocess.Popen( 45 | ["mypy", "--strict", str(pth)], 46 | universal_newlines=True, 47 | stdout=subprocess.PIPE, 48 | ) as proc: 49 | out, err = proc.communicate() 50 | 51 | self.assertIsNone(err) 52 | self.assertEqual( 53 | """\ 54 | {path}:8: error: Argument 1 to "f1" has incompatible type "str"; expected "int" [arg-type] 55 | {path}:13: error: Argument 1 to "f2" has incompatible type "str"; expected "int" [arg-type] 56 | {path}:18: error: Argument 1 to "f3" has incompatible type "str"; expected "int" [arg-type] 57 | Found 3 errors in 1 file (checked 1 source file) 58 | """.format( 59 | path=pth 60 | ), 61 | out, 62 | ) 63 | 64 | def test_class_type_when_decorated_with_invariant(self) -> None: 65 | with tempfile.TemporaryDirectory(prefix="mypy_fail_case_") as tmpdir: 66 | content = '''\ 67 | """Implement a passing case for mypy to test that the type of class is preserved.""" 68 | 69 | import icontract 70 | 71 | class SomeClass: 72 | pass 73 | 74 | reveal_type(SomeClass) 75 | 76 | @icontract.invariant(lambda self: self.x > 0) 77 | class Decorated: 78 | def __init__(self) -> None: 79 | self.x = 1 80 | 81 | reveal_type(Decorated) 82 | ''' 83 | 84 | pth = pathlib.Path(tmpdir) / "source.py" 85 | pth.write_text(content, encoding="utf-8") 86 | 87 | with subprocess.Popen( 88 | ["mypy", "--strict", str(pth)], 89 | universal_newlines=True, 90 | stdout=subprocess.PIPE, 91 | ) as proc: 92 | out, err = proc.communicate() 93 | 94 | self.assertIsNone(err) 95 | self.assertEqual( 96 | """\ 97 | {path}:8: note: Revealed type is "def () -> source.SomeClass" 98 | {path}:15: note: Revealed type is "def () -> source.Decorated" 99 | Success: no issues found in 1 source file 100 | """.format( 101 | path=pth 102 | ), 103 | out, 104 | ) 105 | 106 | def test_that_mypy_complains_when_decorating_non_type_with_invariant(self) -> None: 107 | with tempfile.TemporaryDirectory(prefix="mypy_fail_case_") as tmpdir: 108 | content = '''\ 109 | """Provide a fail case to test that mypy complains when we decorate a non-type with invariant.""" 110 | 111 | import icontract 112 | 113 | @icontract.invariant(lambda: True) 114 | def some_func() -> None: 115 | pass 116 | ''' 117 | 118 | pth = pathlib.Path(tmpdir) / "source.py" 119 | pth.write_text(content, encoding="utf-8") 120 | 121 | with subprocess.Popen( 122 | ["mypy", "--strict", str(pth)], 123 | universal_newlines=True, 124 | stdout=subprocess.PIPE, 125 | ) as proc: 126 | out, err = proc.communicate() 127 | 128 | self.assertIsNone(err) 129 | self.assertEqual( 130 | """\ 131 | {path}:5: error: Value of type variable "ClassT" of "__call__" of "invariant" cannot be "Callable[[], None]" [type-var] 132 | Found 1 error in 1 file (checked 1 source file) 133 | """.format( 134 | path=pth 135 | ), 136 | out, 137 | ) 138 | 139 | 140 | if __name__ == "__main__": 141 | unittest.main() 142 | -------------------------------------------------------------------------------- /tests/test_recompute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pylint: disable=missing-docstring,invalid-name 3 | # pylint: disable=unused-argument 4 | import ast 5 | import re 6 | import textwrap 7 | import unittest 8 | 9 | import astor 10 | 11 | # noinspection PyProtectedMember 12 | import icontract._recompute 13 | 14 | 15 | class TestTranslationForTracingAll(unittest.TestCase): 16 | @staticmethod 17 | def translate_all_expression(input_source_code: str) -> str: 18 | """ 19 | Parse the input source code and translate it to a module with a tracing function. 20 | 21 | :param input_source_code: source code containing a single ``all`` expression 22 | :return: 23 | source code of the module with a tracing function. 24 | All non-deterministic bits are erased from it. 25 | """ 26 | node = ast.parse(input_source_code) 27 | assert isinstance(node, ast.Module) 28 | expr_node = node.body[0] 29 | assert isinstance(expr_node, ast.Expr) 30 | call_node = expr_node.value 31 | assert isinstance(call_node, ast.Call) 32 | generator_exp = call_node.args[0] 33 | assert isinstance(generator_exp, ast.GeneratorExp) 34 | 35 | # We set only SOME_GLOBAL_CONSTANT in ``name_to_value``. In ``_recompute`` module 36 | # all the contract arguments will be set including also all the built-ins and other 37 | # global variables. 38 | 39 | module_node = icontract._recompute._translate_all_expression_to_a_module( 40 | generator_exp=generator_exp, 41 | generated_function_name="some_func", 42 | name_to_value={"SOME_GLOBAL_CONSTANT": 10}, 43 | ) 44 | 45 | got = astor.to_source(module_node) 46 | 47 | # We need to replace the UUID of the result variable for reproducibility. 48 | got = re.sub( 49 | r"icontract_tracing_all_result_[a-zA-Z0-9]+", 50 | "icontract_tracing_all_result", 51 | got, 52 | ) 53 | 54 | assert isinstance(got, str) 55 | return got 56 | 57 | def test_global_variable(self) -> None: 58 | input_source_code = textwrap.dedent( 59 | """\ 60 | all( 61 | x > SOME_GLOBAL_CONSTANT 62 | for x in lst 63 | ) 64 | """ 65 | ) 66 | 67 | got_source_code = TestTranslationForTracingAll.translate_all_expression( 68 | input_source_code=input_source_code 69 | ) 70 | 71 | # Please see ``TestTranslationForTracingAll.translate_all_expression`` and the note about ``name_to_value`` 72 | # if you wonder why ``lst`` is not in the arguments. 73 | self.assertEqual( 74 | textwrap.dedent( 75 | """\ 76 | def some_func(SOME_GLOBAL_CONSTANT): 77 | for x in lst: 78 | icontract_tracing_all_result = (x > 79 | SOME_GLOBAL_CONSTANT) 80 | if icontract_tracing_all_result: 81 | pass 82 | else: 83 | return ( 84 | icontract_tracing_all_result, 85 | (('x', x),)) 86 | return icontract_tracing_all_result, None 87 | """ 88 | ), 89 | got_source_code, 90 | ) 91 | 92 | def test_translation_two_fors_and_two_ifs(self) -> None: 93 | input_source_code = textwrap.dedent( 94 | """\ 95 | all( 96 | cell > SOME_GLOBAL_CONSTANT 97 | for i, row in enumerate(matrix) 98 | if i > 0 99 | for j, cell in enumerate(row) 100 | if i == j 101 | ) 102 | """ 103 | ) 104 | 105 | got_source_code = TestTranslationForTracingAll.translate_all_expression( 106 | input_source_code=input_source_code 107 | ) 108 | 109 | # Please see ``TestTranslationForTracingAll.translate_all_expression`` and the note about ``name_to_value`` 110 | # if you wonder why ``matrix`` is not in the arguments. 111 | self.assertEqual( 112 | textwrap.dedent( 113 | """\ 114 | def some_func(SOME_GLOBAL_CONSTANT): 115 | for i, row in enumerate(matrix): 116 | if i > 0: 117 | for j, cell in enumerate(row): 118 | if i == j: 119 | (icontract_tracing_all_result 120 | ) = cell > SOME_GLOBAL_CONSTANT 121 | if (icontract_tracing_all_result 122 | ): 123 | pass 124 | else: 125 | return ( 126 | icontract_tracing_all_result 127 | , (('i', i), ('row', row), ('j', j), ('cell', 128 | cell))) 129 | return icontract_tracing_all_result, None 130 | """ 131 | ), 132 | got_source_code, 133 | ) 134 | 135 | def test_nested_all(self) -> None: 136 | # Nesting is not recursively followed by design. Only the outer-most all expression should be traced. 137 | 138 | input_source_code = textwrap.dedent( 139 | """\ 140 | all( 141 | all(cell > SOME_GLOBAL_CONSTANT for cell in row) 142 | for row in matrix 143 | ) 144 | """ 145 | ) 146 | 147 | got_source_code = TestTranslationForTracingAll.translate_all_expression( 148 | input_source_code=input_source_code 149 | ) 150 | 151 | # Please see ``TestTranslationForTracingAll.translate_all_expression`` and the note about ``name_to_value`` 152 | # if you wonder why ``matrix`` is not in the arguments. 153 | self.assertEqual( 154 | textwrap.dedent( 155 | """\ 156 | def some_func(SOME_GLOBAL_CONSTANT): 157 | for row in matrix: 158 | icontract_tracing_all_result = all( 159 | cell > SOME_GLOBAL_CONSTANT for cell in row) 160 | if icontract_tracing_all_result: 161 | pass 162 | else: 163 | return ( 164 | icontract_tracing_all_result, 165 | (('row', row),)) 166 | return icontract_tracing_all_result, None 167 | """ 168 | ), 169 | got_source_code, 170 | ) 171 | 172 | 173 | if __name__ == "__main__": 174 | unittest.main() 175 | -------------------------------------------------------------------------------- /tests/test_recursion.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=unnecessary-lambda 3 | import unittest 4 | from typing import List 5 | 6 | import icontract 7 | 8 | 9 | class TestPrecondition(unittest.TestCase): 10 | def test_ok(self) -> None: 11 | order = [] # type: List[str] 12 | 13 | @icontract.require(lambda: another_func()) 14 | @icontract.require(lambda: yet_another_func()) 15 | def some_func() -> bool: 16 | order.append(some_func.__name__) 17 | return True 18 | 19 | @icontract.require(lambda: some_func()) 20 | @icontract.require( 21 | lambda: yet_yet_another_func() 22 | ) # pylint: disable=unnecessary-lambda 23 | def another_func() -> bool: 24 | order.append(another_func.__name__) 25 | return True 26 | 27 | def yet_another_func() -> bool: 28 | order.append(yet_another_func.__name__) 29 | return True 30 | 31 | def yet_yet_another_func() -> bool: 32 | order.append(yet_yet_another_func.__name__) 33 | return True 34 | 35 | some_func() 36 | 37 | self.assertListEqual( 38 | [ 39 | "yet_another_func", 40 | "yet_yet_another_func", 41 | "some_func", 42 | "another_func", 43 | "some_func", 44 | ], 45 | order, 46 | ) 47 | 48 | def test_recover_after_exception(self) -> None: 49 | order = [] # type: List[str] 50 | some_func_should_raise = True 51 | 52 | class CustomError(Exception): 53 | pass 54 | 55 | @icontract.require(lambda: another_func()) 56 | @icontract.require(lambda: yet_another_func()) 57 | def some_func() -> bool: 58 | order.append(some_func.__name__) 59 | if some_func_should_raise: 60 | raise CustomError("some_func_should_raise") 61 | return True 62 | 63 | @icontract.require(lambda: some_func()) 64 | @icontract.require(lambda: yet_yet_another_func()) 65 | def another_func() -> bool: 66 | order.append(another_func.__name__) 67 | return True 68 | 69 | def yet_another_func() -> bool: 70 | order.append(yet_another_func.__name__) 71 | return True 72 | 73 | def yet_yet_another_func() -> bool: 74 | order.append(yet_yet_another_func.__name__) 75 | return True 76 | 77 | try: 78 | some_func() 79 | except CustomError: 80 | pass 81 | 82 | self.assertListEqual( 83 | ["yet_another_func", "yet_yet_another_func", "some_func"], order 84 | ) 85 | 86 | # Reset for the next experiment 87 | order = [] 88 | some_func_should_raise = False 89 | 90 | some_func() 91 | 92 | self.assertListEqual( 93 | [ 94 | "yet_another_func", 95 | "yet_yet_another_func", 96 | "some_func", 97 | "another_func", 98 | "some_func", 99 | ], 100 | order, 101 | ) 102 | 103 | 104 | class TestPostcondition(unittest.TestCase): 105 | def test_ok(self) -> None: 106 | order = [] # type: List[str] 107 | another_func_should_raise = True 108 | 109 | class CustomError(Exception): 110 | pass 111 | 112 | @icontract.ensure(lambda: another_func()) 113 | @icontract.ensure(lambda: yet_another_func()) 114 | def some_func() -> bool: 115 | order.append(some_func.__name__) 116 | return True 117 | 118 | @icontract.ensure(lambda: some_func()) 119 | @icontract.ensure(lambda: yet_yet_another_func()) 120 | def another_func() -> bool: 121 | order.append(another_func.__name__) 122 | if another_func_should_raise: 123 | raise CustomError("some_func_should_raise") 124 | 125 | return True 126 | 127 | def yet_another_func() -> bool: 128 | order.append(yet_another_func.__name__) 129 | return True 130 | 131 | def yet_yet_another_func() -> bool: 132 | order.append(yet_yet_another_func.__name__) 133 | return True 134 | 135 | try: 136 | some_func() 137 | except CustomError: 138 | pass 139 | 140 | self.assertListEqual(["some_func", "yet_another_func", "another_func"], order) 141 | 142 | # Reset for the next experiments 143 | order = [] 144 | another_func_should_raise = False 145 | 146 | some_func() 147 | 148 | self.assertListEqual( 149 | [ 150 | "some_func", 151 | "yet_another_func", 152 | "another_func", 153 | "yet_yet_another_func", 154 | "some_func", 155 | ], 156 | order, 157 | ) 158 | 159 | def test_recover_after_exception(self) -> None: 160 | order = [] # type: List[str] 161 | 162 | @icontract.ensure(lambda: another_func()) 163 | @icontract.ensure(lambda: yet_another_func()) 164 | def some_func() -> bool: 165 | order.append(some_func.__name__) 166 | return True 167 | 168 | @icontract.ensure(lambda: some_func()) 169 | @icontract.ensure(lambda: yet_yet_another_func()) 170 | def another_func() -> bool: 171 | order.append(another_func.__name__) 172 | return True 173 | 174 | def yet_another_func() -> bool: 175 | order.append(yet_another_func.__name__) 176 | return True 177 | 178 | def yet_yet_another_func() -> bool: 179 | order.append(yet_yet_another_func.__name__) 180 | return True 181 | 182 | some_func() 183 | 184 | self.assertListEqual( 185 | [ 186 | "some_func", 187 | "yet_another_func", 188 | "another_func", 189 | "yet_yet_another_func", 190 | "some_func", 191 | ], 192 | order, 193 | ) 194 | 195 | 196 | class TestInvariant(unittest.TestCase): 197 | def test_ok(self) -> None: 198 | order = [] # type: List[str] 199 | 200 | @icontract.invariant(lambda self: self.some_func()) 201 | class SomeClass(icontract.DBC): 202 | def __init__(self) -> None: 203 | order.append("__init__") 204 | 205 | def some_func(self) -> bool: 206 | order.append("some_func") 207 | return True 208 | 209 | def another_func(self) -> bool: 210 | order.append("another_func") 211 | return True 212 | 213 | some_instance = SomeClass() 214 | self.assertListEqual(["__init__", "some_func"], order) 215 | 216 | # Reset for the next experiment 217 | order = [] 218 | 219 | some_instance.another_func() 220 | self.assertListEqual(["some_func", "another_func", "some_func"], order) 221 | 222 | def test_recover_after_exception(self) -> None: 223 | order = [] # type: List[str] 224 | some_func_should_raise = False 225 | 226 | class CustomError(Exception): 227 | pass 228 | 229 | @icontract.invariant(lambda self: self.some_func()) 230 | class SomeClass(icontract.DBC): 231 | def __init__(self) -> None: 232 | order.append("__init__") 233 | 234 | def some_func(self) -> bool: 235 | order.append("some_func") 236 | if some_func_should_raise: 237 | raise CustomError("some_func_should_raise") 238 | 239 | return True 240 | 241 | def another_func(self) -> bool: 242 | order.append("another_func") 243 | return True 244 | 245 | some_instance = SomeClass() 246 | self.assertListEqual(["__init__", "some_func"], order) 247 | 248 | # Reset for the next experiment 249 | order = [] 250 | some_func_should_raise = True 251 | 252 | try: 253 | some_instance.another_func() 254 | except CustomError: 255 | pass 256 | 257 | self.assertListEqual(["some_func"], order) 258 | 259 | # Reset for the next experiment 260 | order = [] 261 | some_func_should_raise = False 262 | 263 | some_instance.another_func() 264 | self.assertListEqual(["some_func", "another_func", "some_func"], order) 265 | 266 | def test_member_function_call_in_constructor(self) -> None: 267 | order = [] # type: List[str] 268 | 269 | @icontract.invariant(lambda self: self.some_attribute > 0) 270 | class SomeClass(icontract.DBC): 271 | def __init__(self) -> None: 272 | order.append("__init__ enters") 273 | self.some_attribute = self.some_func() 274 | order.append("__init__ exits") 275 | 276 | def some_func(self) -> int: 277 | order.append("some_func") 278 | return 3 279 | 280 | _ = SomeClass() 281 | self.assertListEqual(["__init__ enters", "some_func", "__init__ exits"], order) 282 | 283 | 284 | if __name__ == "__main__": 285 | unittest.main() 286 | -------------------------------------------------------------------------------- /tests/test_threading.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring,unnecessary-lambda 2 | import threading 3 | import unittest 4 | 5 | import icontract 6 | 7 | 8 | @icontract.require(lambda: other_func()) # type: ignore 9 | def some_func() -> bool: 10 | return True 11 | 12 | 13 | @icontract.require(lambda: some_func()) 14 | def other_func() -> bool: 15 | return True 16 | 17 | 18 | class TestThreading(unittest.TestCase): 19 | def test_two_threads(self) -> None: 20 | """ 21 | Test that icontract can run in a multi-threaded environment. 22 | 23 | This is a regression test. The threading.local() can be only set 24 | immutable values. See 25 | http://slinkp.com/python-thread-locals-20171201.html for more details. 26 | 27 | Originally, this caused the exception 28 | `AttributeError: '_thread._local' object has no attribute [...]`. 29 | """ 30 | 31 | class Worker(threading.Thread): 32 | def run(self) -> None: 33 | some_func() 34 | 35 | worker, another_worker = Worker(), Worker() 36 | worker.start() 37 | another_worker.start() 38 | worker.join() 39 | another_worker.join() 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/test_typeguard.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | 5 | import textwrap 6 | import unittest 7 | from typing import Optional 8 | 9 | import typeguard 10 | 11 | import icontract 12 | import tests.error 13 | 14 | 15 | class TestPrecondition(unittest.TestCase): 16 | def test_both_precondition_and_typeguard_ok(self) -> None: 17 | @icontract.require(lambda x: x > 0) 18 | @typeguard.typechecked 19 | def some_func(x: int) -> None: 20 | pass 21 | 22 | some_func(1) 23 | 24 | def test_precondition_ok_and_typeguard_fails(self) -> None: 25 | class A: 26 | def is_ok(self) -> bool: 27 | return True 28 | 29 | class B: 30 | def is_ok(self) -> bool: 31 | return True 32 | 33 | @icontract.require(lambda x: x.is_ok()) 34 | @typeguard.typechecked 35 | def some_func(x: A) -> None: 36 | pass 37 | 38 | b = B() 39 | type_check_error = None # type: Optional[typeguard.TypeCheckError] 40 | try: 41 | some_func(b) # type: ignore 42 | except typeguard.TypeCheckError as err: 43 | type_check_error = err 44 | 45 | self.assertIsNotNone(type_check_error) 46 | 47 | def test_precondition_fails_and_typeguard_ok(self) -> None: 48 | @icontract.require(lambda x: x > 0) 49 | @typeguard.typechecked 50 | def some_func(x: int) -> None: 51 | pass 52 | 53 | violation_error = None # type: Optional[icontract.ViolationError] 54 | try: 55 | some_func(-10) 56 | except icontract.ViolationError as err: 57 | violation_error = err 58 | 59 | self.assertIsNotNone(violation_error) 60 | self.assertEqual( 61 | "x > 0: x was -10", tests.error.wo_mandatory_location(str(violation_error)) 62 | ) 63 | 64 | 65 | class TestInvariant(unittest.TestCase): 66 | def test_both_invariant_and_typeguard_ok(self) -> None: 67 | @icontract.invariant(lambda self: self.x > 0) 68 | @typeguard.typechecked 69 | class A: 70 | def __init__(self, x: int) -> None: 71 | self.x = x 72 | 73 | def do_something(self, y: int) -> None: 74 | self.x += y 75 | 76 | a = A(x=1) 77 | a.do_something(y=2) 78 | 79 | def test_invariant_ok_and_typeguard_fails(self) -> None: 80 | class A: 81 | def is_ok(self) -> bool: 82 | return True 83 | 84 | class B: 85 | def is_ok(self) -> bool: 86 | return True 87 | 88 | @icontract.invariant(lambda self: self.x.is_ok()) 89 | @typeguard.typechecked 90 | class C: 91 | def __init__(self, a: A) -> None: 92 | self.a = a 93 | 94 | b = B() 95 | 96 | type_check_error = None # type: Optional[typeguard.TypeCheckError] 97 | try: 98 | _ = C(a=b) # type: ignore 99 | except typeguard.TypeCheckError as err: 100 | type_check_error = err 101 | 102 | self.assertIsNotNone(type_check_error) 103 | 104 | def test_invariant_fails_and_typeguard_ok(self) -> None: 105 | @icontract.invariant(lambda self: self.x > 0) 106 | @typeguard.typechecked 107 | class A: 108 | def __init__(self, x: int) -> None: 109 | self.x = x 110 | 111 | def __repr__(self) -> str: 112 | return "an instance of {}".format(self.__class__.__name__) 113 | 114 | violation_error = None # type: Optional[icontract.ViolationError] 115 | try: 116 | _ = A(-1) 117 | except icontract.ViolationError as err: 118 | violation_error = err 119 | 120 | self.assertIsNotNone(violation_error) 121 | self.assertEqual( 122 | textwrap.dedent( 123 | """\ 124 | self.x > 0: 125 | self was an instance of A 126 | self.x was -1""" 127 | ), 128 | tests.error.wo_mandatory_location(str(violation_error)), 129 | ) 130 | 131 | 132 | class TestInheritance(unittest.TestCase): 133 | def test_both_invariant_and_typeguard_ok(self) -> None: 134 | @icontract.invariant(lambda self: self.x > 0) 135 | @typeguard.typechecked 136 | class A(icontract.DBC): 137 | def __init__(self, x: int) -> None: 138 | self.x = x 139 | 140 | def do_something(self, y: int) -> None: 141 | self.x += y 142 | 143 | class B(A): 144 | pass 145 | 146 | b = B(x=1) 147 | b.do_something(y=2) 148 | 149 | def test_invariant_ok_and_typeguard_fails(self) -> None: 150 | class A: 151 | def is_ok(self) -> bool: 152 | return True 153 | 154 | class B: 155 | def is_ok(self) -> bool: 156 | return True 157 | 158 | @icontract.invariant(lambda self: self.x.is_ok()) 159 | @typeguard.typechecked 160 | class C(icontract.DBC): 161 | def __init__(self, a: A) -> None: 162 | self.a = a 163 | 164 | class D(C): 165 | pass 166 | 167 | b = B() 168 | 169 | type_check_error = None # type: Optional[typeguard.TypeCheckError] 170 | try: 171 | _ = D(a=b) # type: ignore 172 | except typeguard.TypeCheckError as err: 173 | type_check_error = err 174 | 175 | self.assertIsNotNone(type_check_error) 176 | 177 | def test_invariant_fails_and_typeguard_ok(self) -> None: 178 | @icontract.invariant(lambda self: self.x > 0) 179 | @typeguard.typechecked 180 | class A: 181 | def __init__(self, x: int) -> None: 182 | self.x = x 183 | 184 | def __repr__(self) -> str: 185 | return "an instance of {}".format(self.__class__.__name__) 186 | 187 | class B(A): 188 | def __repr__(self) -> str: 189 | return "an instance of {}".format(self.__class__.__name__) 190 | 191 | violation_error = None # type: Optional[icontract.ViolationError] 192 | try: 193 | _ = B(-1) 194 | except icontract.ViolationError as err: 195 | violation_error = err 196 | 197 | self.assertIsNotNone(violation_error) 198 | self.assertEqual( 199 | textwrap.dedent( 200 | """\ 201 | self.x > 0: 202 | self was an instance of B 203 | self.x was -1""" 204 | ), 205 | tests.error.wo_mandatory_location(str(violation_error)), 206 | ) 207 | 208 | 209 | if __name__ == "__main__": 210 | unittest.main() 211 | -------------------------------------------------------------------------------- /tests_3_6/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Python 3.6-specific features. 3 | 4 | For example, one such feature is literal string interpolation. 5 | """ 6 | 7 | import sys 8 | 9 | if sys.version_info < (3, 6): 10 | 11 | def load_tests(loader, suite, pattern): # pylint: disable=unused-argument 12 | """Ignore all the tests for lower Python versions.""" 13 | return suite 14 | -------------------------------------------------------------------------------- /tests_3_6/test_represent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pylint: disable=missing-docstring,invalid-name 3 | # pylint: disable=unused-argument 4 | 5 | import textwrap 6 | import unittest 7 | from typing import Optional # pylint: disable=unused-import 8 | 9 | import icontract._represent 10 | import tests.error 11 | import tests.mock 12 | 13 | 14 | class TestLiteralStringInterpolation(unittest.TestCase): 15 | def test_plain_string(self) -> None: 16 | # pylint: disable=f-string-without-interpolation 17 | @icontract.require(lambda x: f"something" == "") # type: ignore 18 | def func(x: float) -> float: 19 | return x 20 | 21 | violation_err = None # type: Optional[icontract.ViolationError] 22 | try: 23 | func(x=0) 24 | except icontract.ViolationError as err: 25 | violation_err = err 26 | 27 | self.assertIsNotNone(violation_err) 28 | self.assertEqual( 29 | textwrap.dedent( 30 | """\ 31 | f"something" == "": 32 | f"something" was 'something' 33 | x was 0""" 34 | ), 35 | tests.error.wo_mandatory_location(str(violation_err)), 36 | ) 37 | 38 | def test_simple_interpolation(self) -> None: 39 | @icontract.require(lambda x: f"{x}" == "") 40 | def func(x: float) -> float: 41 | return x 42 | 43 | violation_err = None # type: Optional[icontract.ViolationError] 44 | try: 45 | func(x=0) 46 | except icontract.ViolationError as err: 47 | violation_err = err 48 | 49 | self.assertIsNotNone(violation_err) 50 | self.assertEqual( 51 | textwrap.dedent( 52 | """\ 53 | f"{x}" == "": 54 | f"{x}" was '0' 55 | x was 0""" 56 | ), 57 | tests.error.wo_mandatory_location(str(violation_err)), 58 | ) 59 | 60 | def test_string_formatting(self) -> None: 61 | @icontract.require(lambda x: f"{x!s}" == "") 62 | def func(x: float) -> float: 63 | return x 64 | 65 | violation_err = None # type: Optional[icontract.ViolationError] 66 | try: 67 | func(x=1.984) 68 | except icontract.ViolationError as err: 69 | violation_err = err 70 | 71 | self.assertIsNotNone(violation_err) 72 | self.assertEqual( 73 | textwrap.dedent( 74 | """\ 75 | f"{x!s}" == "": 76 | f"{x!s}" was '1.984' 77 | x was 1.984""" 78 | ), 79 | tests.error.wo_mandatory_location(str(violation_err)), 80 | ) 81 | 82 | def test_repr_formatting(self) -> None: 83 | @icontract.require(lambda x: f"{x!r}" == "") 84 | def func(x: float) -> float: 85 | return x 86 | 87 | violation_err = None # type: Optional[icontract.ViolationError] 88 | try: 89 | func(x=1.984) 90 | except icontract.ViolationError as err: 91 | violation_err = err 92 | 93 | self.assertIsNotNone(violation_err) 94 | self.assertEqual( 95 | textwrap.dedent( 96 | """\ 97 | f"{x!r}" == "": 98 | f"{x!r}" was '1.984' 99 | x was 1.984""" 100 | ), 101 | tests.error.wo_mandatory_location(str(violation_err)), 102 | ) 103 | 104 | def test_ascii_formatting(self) -> None: 105 | @icontract.require(lambda x: f"{x!a}" == "") 106 | def func(x: float) -> float: 107 | return x 108 | 109 | violation_err = None # type: Optional[icontract.ViolationError] 110 | try: 111 | func(x=1.984) 112 | except icontract.ViolationError as err: 113 | violation_err = err 114 | 115 | self.assertIsNotNone(violation_err) 116 | self.assertEqual( 117 | textwrap.dedent( 118 | """\ 119 | f"{x!a}" == "": 120 | f"{x!a}" was '1.984' 121 | x was 1.984""" 122 | ), 123 | tests.error.wo_mandatory_location(str(violation_err)), 124 | ) 125 | 126 | def test_format_spec(self) -> None: 127 | @icontract.require(lambda x: f"{x:.3}" == "") 128 | def func(x: float) -> float: 129 | return x 130 | 131 | violation_err = None # type: Optional[icontract.ViolationError] 132 | try: 133 | func(x=1.984) 134 | except icontract.ViolationError as err: 135 | violation_err = err 136 | 137 | self.assertIsNotNone(violation_err) 138 | self.assertEqual( 139 | textwrap.dedent( 140 | """\ 141 | f"{x:.3}" == "": 142 | f"{x:.3}" was '1.98' 143 | x was 1.984""" 144 | ), 145 | tests.error.wo_mandatory_location(str(violation_err)), 146 | ) 147 | 148 | def test_conversion_and_format_spec(self) -> None: 149 | @icontract.require(lambda x: f"{x!r:.3}" == "") 150 | def func(x: float) -> float: 151 | return x 152 | 153 | violation_err = None # type: Optional[icontract.ViolationError] 154 | try: 155 | func(x=1.984) 156 | except icontract.ViolationError as err: 157 | violation_err = err 158 | 159 | self.assertIsNotNone(violation_err) 160 | self.assertEqual( 161 | textwrap.dedent( 162 | """\ 163 | f"{x!r:.3}" == "": 164 | f"{x!r:.3}" was '1.9' 165 | x was 1.984""" 166 | ), 167 | tests.error.wo_mandatory_location(str(violation_err)), 168 | ) 169 | 170 | 171 | if __name__ == "__main__": 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /tests_3_7/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Python 3.-specific features. 3 | 4 | For example, one such feature is decorator ``dataclasses.dataclass``. 5 | """ 6 | 7 | import sys 8 | 9 | if sys.version_info < (3, 7): 10 | 11 | def load_tests(loader, suite, pattern): # pylint: disable=unused-argument 12 | """Ignore all the tests for lower Python versions.""" 13 | return suite 14 | -------------------------------------------------------------------------------- /tests_3_7/test_invariant.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | 5 | import dataclasses 6 | import textwrap 7 | import unittest 8 | from typing import NamedTuple, Optional # pylint: disable=unused-import 9 | 10 | import icontract 11 | import tests.error 12 | 13 | 14 | class TestOK(unittest.TestCase): 15 | def test_on_dataclass(self) -> None: 16 | @icontract.invariant(lambda self: self.first > 0) 17 | @dataclasses.dataclass 18 | class RightHalfPlanePoint: 19 | first: int 20 | second: int 21 | 22 | _ = RightHalfPlanePoint(1, 0) 23 | 24 | self.assertEqual( 25 | "Create and return a new object. See help(type) for accurate signature.", 26 | RightHalfPlanePoint.__new__.__doc__, 27 | ) 28 | 29 | def test_on_dataclass_with_field(self) -> None: 30 | @icontract.invariant(lambda self: self.x > 0) 31 | @dataclasses.dataclass 32 | class Foo: 33 | x: int = dataclasses.field(default=42) 34 | 35 | _ = Foo() 36 | 37 | 38 | class TestViolation(unittest.TestCase): 39 | def test_on_dataclass(self) -> None: 40 | @icontract.invariant(lambda self: self.second > 0) 41 | @icontract.invariant(lambda self: self.first > 0) 42 | @dataclasses.dataclass 43 | class RightHalfPlanePoint: 44 | first: int 45 | second: int 46 | 47 | violation_error = None # type: Optional[icontract.ViolationError] 48 | try: 49 | _ = RightHalfPlanePoint(1, -1) 50 | except icontract.ViolationError as err: 51 | violation_error = err 52 | 53 | self.assertIsNotNone(violation_error) 54 | self.assertEqual( 55 | textwrap.dedent( 56 | """\ 57 | self.second > 0: 58 | self was TestViolation.test_on_dataclass..RightHalfPlanePoint(first=1, second=-1) 59 | self.second was -1""" 60 | ), 61 | tests.error.wo_mandatory_location(str(violation_error)), 62 | ) 63 | 64 | def test_on_dataclass_with_field(self) -> None: 65 | @icontract.invariant(lambda self: self.x < 0) 66 | @dataclasses.dataclass 67 | class Foo: 68 | x: int = dataclasses.field(default=-1) 69 | 70 | violation_error = None # type: Optional[icontract.ViolationError] 71 | try: 72 | _ = Foo(3) 73 | except icontract.ViolationError as err: 74 | violation_error = err 75 | 76 | self.assertIsNotNone(violation_error) 77 | self.assertEqual( 78 | textwrap.dedent( 79 | """\ 80 | self.x < 0: 81 | self was TestViolation.test_on_dataclass_with_field..Foo(x=3) 82 | self.x was 3""" 83 | ), 84 | tests.error.wo_mandatory_location(str(violation_error)), 85 | ) 86 | 87 | def test_on_dataclass_with_field_default_violating(self) -> None: 88 | @icontract.invariant(lambda self: self.x < 0) 89 | @dataclasses.dataclass 90 | class Foo: 91 | x: int = dataclasses.field(default=42) 92 | 93 | violation_error = None # type: Optional[icontract.ViolationError] 94 | try: 95 | _ = Foo() 96 | except icontract.ViolationError as err: 97 | violation_error = err 98 | 99 | self.assertIsNotNone(violation_error) 100 | self.assertEqual( 101 | textwrap.dedent( 102 | """\ 103 | self.x < 0: 104 | self was TestViolation.test_on_dataclass_with_field_default_violating..Foo(x=42) 105 | self.x was 42""" 106 | ), 107 | tests.error.wo_mandatory_location(str(violation_error)), 108 | ) 109 | 110 | 111 | class TestCheckOn(unittest.TestCase): 112 | def test_setattr_with_dataclass(self) -> None: 113 | @icontract.invariant( 114 | lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.SETATTR 115 | ) 116 | @dataclasses.dataclass 117 | class A: 118 | x: int = 10 119 | 120 | def __repr__(self) -> str: 121 | return "an instance of {}".format(self.__class__.__name__) 122 | 123 | a = A() 124 | 125 | violation_error = None # type: Optional[icontract.ViolationError] 126 | try: 127 | a.x = -1 128 | except icontract.ViolationError as err: 129 | violation_error = err 130 | 131 | self.assertIsNotNone(violation_error) 132 | self.assertEqual( 133 | textwrap.dedent( 134 | """\ 135 | self.x > 0: 136 | self was an instance of A 137 | self.x was -1""" 138 | ), 139 | tests.error.wo_mandatory_location(str(violation_error)), 140 | ) 141 | 142 | 143 | if __name__ == "__main__": 144 | unittest.main() 145 | -------------------------------------------------------------------------------- /tests_3_8/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Python 3.8-specific features. 3 | 4 | For example, one such feature is walrus operator used in named expressions. 5 | We have to exclude these tests running on prior versions of Python since the syntax would be considered 6 | invalid. 7 | """ 8 | 9 | import sys 10 | 11 | if sys.version_info < (3, 8): 12 | 13 | def load_tests(loader, suite, pattern): # pylint: disable=unused-argument 14 | """Ignore all the tests for lower Python versions.""" 15 | return suite 16 | -------------------------------------------------------------------------------- /tests_3_8/async/__init__.py: -------------------------------------------------------------------------------- 1 | # The module ``unittest`` supports async only from 3.8 on. 2 | # That is why we had to move this test to 3.8 specific tests. 3 | -------------------------------------------------------------------------------- /tests_3_8/async/separately_test_concurrent.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=unnecessary-lambda 3 | # pylint: disable=disallowed-name 4 | import asyncio 5 | import random 6 | 7 | import icontract 8 | 9 | 10 | async def main() -> None: 11 | # NOTE (mristin, 2023-01-26): 12 | # This is a regression test for #255. 13 | # 14 | # We have to run it as a separate script since unittest.IsolatedAsyncioTestCase 15 | # messes up the contextvars in such a way that they are shared among the coroutines. 16 | 17 | async def is_between_0_and_100(value: int) -> bool: 18 | sleep_time = random.randint(1, 3) 19 | await asyncio.sleep(sleep_time) 20 | return 0 <= value < 100 21 | 22 | @icontract.require( 23 | lambda bar: is_between_0_and_100(bar), 24 | error=lambda bar: icontract.ViolationError( 25 | f"bar between 0 and 100, but got {bar}" 26 | ), 27 | ) 28 | async def is_less_than_42(bar: int) -> bool: 29 | sleep_time = random.randint(1, 5) 30 | await asyncio.sleep(sleep_time) 31 | return bar < 42 32 | 33 | results_or_errors = await asyncio.gather( 34 | is_less_than_42(0), # Should return True 35 | is_less_than_42(101), # Should violate the pre-condition 36 | is_less_than_42(-1), # Should violate the pre-condition 37 | return_exceptions=True, 38 | ) 39 | 40 | assert len(results_or_errors) == 3 41 | assert results_or_errors[0] 42 | 43 | assert isinstance(results_or_errors[1], icontract.ViolationError) 44 | assert "bar between 0 and 100, but got 101" == str(results_or_errors[1]) 45 | 46 | assert isinstance(results_or_errors[2], icontract.ViolationError) 47 | assert "bar between 0 and 100, but got -1" == str(results_or_errors[2]) 48 | 49 | 50 | if __name__ == "__main__": 51 | asyncio.run(main()) 52 | -------------------------------------------------------------------------------- /tests_3_8/async/test_coroutine_example.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | 3 | import dataclasses 4 | import unittest 5 | from typing import List 6 | 7 | import asyncstdlib as a 8 | 9 | import icontract 10 | 11 | 12 | class TestCoroutines(unittest.IsolatedAsyncioTestCase): 13 | async def test_mock_backend_example(self) -> None: 14 | # This is an example of a backend system. 15 | # This test demonstrates how contracts can be used with coroutines. 16 | 17 | async def has_author(identifier: str) -> bool: 18 | return identifier in ["Margaret Cavendish", "Jane Austen"] 19 | 20 | async def has_category(category: str) -> bool: 21 | return category in await get_categories() 22 | 23 | async def get_categories() -> List[str]: 24 | return ["sci-fi", "romance"] 25 | 26 | @dataclasses.dataclass 27 | class Book: 28 | identifier: str 29 | author: str 30 | 31 | @icontract.require(lambda categories: a.map(has_category, categories)) 32 | @icontract.ensure( 33 | lambda result: a.all( 34 | a.await_each(has_author(book.author) for book in result) 35 | ) 36 | ) 37 | async def list_books(categories: List[str]) -> List[Book]: 38 | result = [] # type: List[Book] 39 | for category in categories: 40 | if category == "sci-fi": 41 | result.extend( 42 | [ 43 | Book( 44 | identifier="The Blazing World", 45 | author="Margaret Cavendish", 46 | ) 47 | ] 48 | ) 49 | elif category == "romance": 50 | result.extend( 51 | [Book(identifier="Pride and Prejudice", author="Jane Austen")] 52 | ) 53 | else: 54 | raise AssertionError(category) 55 | 56 | return result 57 | 58 | sci_fi_books = await list_books(categories=["sci-fi"]) 59 | self.assertListEqual( 60 | ["The Blazing World"], [book.identifier for book in sci_fi_books] 61 | ) 62 | -------------------------------------------------------------------------------- /tests_3_8/async/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # The module ``unittest`` supports async only from 3.8 on. 2 | # That is why we had to move this test to 3.8 specific tests. 3 | 4 | # pylint: disable=missing-docstring, invalid-name, unnecessary-lambda 5 | import unittest 6 | from typing import Optional, List 7 | 8 | import icontract 9 | 10 | 11 | class TestSyncFunctionAsyncConditionFail(unittest.IsolatedAsyncioTestCase): 12 | def test_precondition(self) -> None: 13 | async def x_greater_zero(x: int) -> bool: 14 | return x > 0 15 | 16 | @icontract.require(x_greater_zero) 17 | def some_func(x: int) -> int: 18 | return x * 10 19 | 20 | value_error = None # type: Optional[ValueError] 21 | try: 22 | _ = some_func(100) 23 | except ValueError as err: 24 | value_error = err 25 | 26 | self.assertIsNotNone(value_error) 27 | self.assertRegex( 28 | str(value_error), 29 | r"^Unexpected coroutine \(async\) condition <.*> for a sync function <.*\.some_func at .*>.", 30 | ) 31 | 32 | def test_postcondition(self) -> None: 33 | async def result_greater_zero(result: int) -> bool: 34 | return result > 0 35 | 36 | @icontract.ensure(result_greater_zero) 37 | def some_func() -> int: 38 | return 100 39 | 40 | value_error = None # type: Optional[ValueError] 41 | try: 42 | _ = some_func() 43 | except ValueError as err: 44 | value_error = err 45 | 46 | self.assertIsNotNone(value_error) 47 | self.assertRegex( 48 | str(value_error), 49 | r"^Unexpected coroutine \(async\) condition <.*> for a sync function <.*\.some_func at .*>.", 50 | ) 51 | 52 | def test_snapshot(self) -> None: 53 | async def capture_len_lst(lst: List[int]) -> int: 54 | return len(lst) 55 | 56 | @icontract.snapshot(capture_len_lst, name="len_lst") 57 | @icontract.ensure(lambda OLD, lst: OLD.len_lst + 1 == len(lst)) 58 | def some_func(lst: List[int]) -> None: 59 | lst.append(1984) 60 | 61 | value_error = None # type: Optional[ValueError] 62 | try: 63 | some_func([1]) 64 | except ValueError as err: 65 | value_error = err 66 | 67 | self.assertIsNotNone(value_error) 68 | self.assertRegex( 69 | str(value_error), 70 | r"^Unexpected coroutine \(async\) snapshot capture " 71 | r"for a sync function \.", 72 | ) 73 | 74 | 75 | class TestSyncFunctionConditionCoroutineFail(unittest.IsolatedAsyncioTestCase): 76 | def test_precondition(self) -> None: 77 | async def x_greater_zero(x: int) -> bool: 78 | return x > 0 79 | 80 | @icontract.require(lambda x: x_greater_zero(x)) 81 | def some_func(x: int) -> int: 82 | return x * 10 83 | 84 | value_error = None # type: Optional[ValueError] 85 | try: 86 | _ = some_func(100) 87 | except ValueError as err: 88 | value_error = err 89 | 90 | assert value_error is not None 91 | 92 | self.assertRegex( 93 | str(value_error), 94 | r"^Unexpected coroutine resulting from the condition for a sync function \.$", 95 | ) 96 | 97 | def test_postcondition(self) -> None: 98 | async def result_greater_zero(result: int) -> bool: 99 | return result > 0 100 | 101 | @icontract.ensure(lambda result: result_greater_zero(result)) 102 | def some_func() -> int: 103 | return 100 104 | 105 | value_error = None # type: Optional[ValueError] 106 | try: 107 | _ = some_func() 108 | except ValueError as err: 109 | value_error = err 110 | 111 | assert value_error is not None 112 | 113 | self.assertRegex( 114 | str(value_error), 115 | r"^Unexpected coroutine resulting from the condition for a sync function \.$", 116 | ) 117 | 118 | def test_snapshot(self) -> None: 119 | async def capture_len_lst(lst: List[int]) -> int: 120 | return len(lst) 121 | 122 | @icontract.snapshot(lambda lst: capture_len_lst(lst), name="len_lst") 123 | @icontract.ensure(lambda OLD, lst: OLD.len_lst + 1 == len(lst)) 124 | def some_func(lst: List[int]) -> None: 125 | lst.append(1984) 126 | 127 | value_error = None # type: Optional[ValueError] 128 | try: 129 | some_func([1]) 130 | except ValueError as err: 131 | value_error = err 132 | 133 | assert value_error is not None 134 | self.assertRegex( 135 | str(value_error), 136 | r"^Unexpected coroutine resulting " 137 | r"from the snapshot capture of a sync function .$", 138 | ) 139 | 140 | 141 | class TestAsyncInvariantsFail(unittest.IsolatedAsyncioTestCase): 142 | def test_that_async_invariants_reported(self) -> None: 143 | async def some_async_invariant(self: "A") -> bool: 144 | return self.x > 0 145 | 146 | value_error = None # type: Optional[ValueError] 147 | try: 148 | # pylint: disable=unused-variable 149 | @icontract.invariant(some_async_invariant) 150 | class A: 151 | def __init__(self) -> None: 152 | self.x = 100 153 | 154 | except ValueError as error: 155 | value_error = error 156 | 157 | assert value_error is not None 158 | 159 | self.assertEqual( 160 | "Async conditions are not possible in invariants as sync methods such as __init__ have to be wrapped.", 161 | str(value_error), 162 | ) 163 | 164 | 165 | if __name__ == "__main__": 166 | unittest.main() 167 | -------------------------------------------------------------------------------- /tests_3_8/async/test_invariant.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | 4 | import unittest 5 | from typing import Optional 6 | 7 | import icontract 8 | 9 | import tests.error 10 | 11 | 12 | class TestAsyncMethod(unittest.IsolatedAsyncioTestCase): 13 | async def test_ok(self) -> None: 14 | @icontract.invariant(lambda self: self.x > 0) 15 | class A: 16 | def __init__(self) -> None: 17 | self.x = 100 18 | 19 | async def some_func(self) -> int: 20 | self.x = 200 21 | return self.x 22 | 23 | def another_func(self) -> int: 24 | self.x = 300 25 | return self.x 26 | 27 | a = A() 28 | result = await a.some_func() 29 | self.assertEqual(200, result) 30 | 31 | result = a.another_func() 32 | self.assertEqual(300, result) 33 | 34 | async def test_fail(self) -> None: 35 | @icontract.invariant(lambda self: self.x > 0) 36 | class A: 37 | def __init__(self) -> None: 38 | self.x = 100 39 | 40 | async def some_func(self) -> None: 41 | self.x = -1 42 | 43 | a = A() 44 | violation_error = None # type: Optional[icontract.ViolationError] 45 | try: 46 | await a.some_func() 47 | except icontract.ViolationError as err: 48 | violation_error = err 49 | 50 | self.assertIsNotNone(violation_error) 51 | self.assertTrue( 52 | tests.error.wo_mandatory_location(str(violation_error)).startswith( 53 | "self.x > 0" 54 | ) 55 | ) 56 | -------------------------------------------------------------------------------- /tests_3_8/async/test_postcondition.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | # pylint: disable=unnecessary-lambda 5 | 6 | import unittest 7 | from typing import Optional, List 8 | 9 | import icontract 10 | 11 | import tests.error 12 | import tests.mock 13 | 14 | 15 | class TestAsyncFunctionSyncCondition(unittest.IsolatedAsyncioTestCase): 16 | async def test_ok(self) -> None: 17 | @icontract.ensure(lambda result: result > 0) 18 | async def some_func() -> int: 19 | return 100 20 | 21 | result = await some_func() 22 | self.assertEqual(100, result) 23 | 24 | async def test_ok_with_snapshot(self) -> None: 25 | @icontract.snapshot(lambda lst: len(lst), name="len_lst") 26 | @icontract.ensure(lambda OLD, result: result > OLD.len_lst) 27 | async def some_func(lst: List[int]) -> int: 28 | lst.append(1984) 29 | return len(lst) 30 | 31 | result = await some_func(lst=[1, 2, 3]) 32 | self.assertEqual(4, result) 33 | 34 | async def test_fail(self) -> None: 35 | @icontract.ensure(lambda result: result > 0) 36 | async def some_func() -> int: 37 | return -100 38 | 39 | violation_error = None # type: Optional[icontract.ViolationError] 40 | try: 41 | _ = await some_func() 42 | except icontract.ViolationError as err: 43 | violation_error = err 44 | 45 | self.assertIsNotNone(violation_error) 46 | self.assertEqual( 47 | "result > 0: result was -100", 48 | tests.error.wo_mandatory_location(str(violation_error)), 49 | ) 50 | 51 | 52 | class TestAsyncFunctionAsyncCondition(unittest.IsolatedAsyncioTestCase): 53 | async def test_ok(self) -> None: 54 | async def result_greater_zero(result: int) -> bool: 55 | return result > 0 56 | 57 | @icontract.ensure(result_greater_zero) 58 | async def some_func() -> int: 59 | return 100 60 | 61 | result = await some_func() 62 | self.assertEqual(100, result) 63 | 64 | async def test_snapshot(self) -> None: 65 | async def capture_len_lst(lst: List[int]) -> int: 66 | return len(lst) 67 | 68 | @icontract.snapshot(capture_len_lst, name="len_lst") 69 | @icontract.ensure(lambda OLD, lst: OLD.len_lst + 1 == len(lst)) 70 | async def some_func(lst: List[int]) -> None: 71 | lst.append(1984) 72 | 73 | lst = [] # type: List[int] 74 | await some_func(lst=lst) 75 | self.assertListEqual([1984], lst) 76 | 77 | async def test_fail(self) -> None: 78 | async def result_greater_zero(result: int) -> bool: 79 | return result > 0 80 | 81 | @icontract.ensure(result_greater_zero) 82 | async def some_func() -> int: 83 | return -100 84 | 85 | violation_error = None # type: Optional[icontract.ViolationError] 86 | try: 87 | _ = await some_func() 88 | except icontract.ViolationError as err: 89 | violation_error = err 90 | 91 | self.assertIsNotNone(violation_error) 92 | self.assertEqual( 93 | "result_greater_zero: result was -100", 94 | tests.error.wo_mandatory_location(str(violation_error)), 95 | ) 96 | 97 | 98 | class TestCoroutine(unittest.IsolatedAsyncioTestCase): 99 | async def test_ok(self) -> None: 100 | async def some_condition() -> bool: 101 | return True 102 | 103 | @icontract.ensure(lambda: some_condition()) 104 | async def some_func() -> None: 105 | pass 106 | 107 | await some_func() 108 | 109 | async def test_fail(self) -> None: 110 | async def some_condition() -> bool: 111 | return False 112 | 113 | @icontract.ensure( 114 | lambda: some_condition(), error=lambda: icontract.ViolationError("hihi") 115 | ) 116 | async def some_func() -> None: 117 | pass 118 | 119 | violation_error = None # type: Optional[icontract.ViolationError] 120 | try: 121 | await some_func() 122 | except icontract.ViolationError as err: 123 | violation_error = err 124 | 125 | self.assertIsNotNone(violation_error) 126 | self.assertEqual("hihi", str(violation_error)) 127 | 128 | async def test_reported_if_no_error_is_specified_as_we_can_not_recompute_coroutine_functions( 129 | self, 130 | ) -> None: 131 | async def some_condition() -> bool: 132 | return False 133 | 134 | @icontract.ensure(lambda: some_condition()) 135 | async def some_func() -> None: 136 | pass 137 | 138 | runtime_error = None # type: Optional[RuntimeError] 139 | try: 140 | await some_func() 141 | except RuntimeError as err: 142 | runtime_error = err 143 | 144 | assert runtime_error is not None 145 | assert runtime_error.__cause__ is not None 146 | assert isinstance(runtime_error.__cause__, ValueError) 147 | 148 | value_error = runtime_error.__cause__ 149 | 150 | self.assertRegex( 151 | str(value_error), 152 | r"^Unexpected coroutine function as a condition of a contract\. " 153 | r"You must specify your own error if the condition of your contract is a coroutine function\.", 154 | ) 155 | 156 | async def test_snapshot(self) -> None: 157 | async def some_capture() -> int: 158 | return 1984 159 | 160 | @icontract.snapshot(lambda: some_capture(), name="hoho") 161 | @icontract.ensure(lambda OLD: OLD.hoho == 1984) 162 | async def some_func() -> None: 163 | pass 164 | 165 | await some_func() 166 | 167 | 168 | class TestInvalid(unittest.IsolatedAsyncioTestCase): 169 | async def test_invalid_postcondition_arguments(self) -> None: 170 | @icontract.ensure(lambda b, result: b > result) 171 | async def some_function(a: int) -> None: # pylint: disable=unused-variable 172 | pass 173 | 174 | type_err = None # type: Optional[TypeError] 175 | try: 176 | await some_function(a=13) 177 | except TypeError as err: 178 | type_err = err 179 | 180 | self.assertIsNotNone(type_err) 181 | self.assertEqual( 182 | "The argument(s) of the contract condition have not been set: ['b']. " 183 | "Does the original function define them? Did you supply them in the call?", 184 | tests.error.wo_mandatory_location(str(type_err)), 185 | ) 186 | 187 | async def test_conflicting_result_argument(self) -> None: 188 | @icontract.ensure(lambda a, result: a > result) 189 | async def some_function( 190 | a: int, result: int 191 | ) -> None: # pylint: disable=unused-variable 192 | pass 193 | 194 | type_err = None # type: Optional[TypeError] 195 | try: 196 | await some_function(a=13, result=2) 197 | except TypeError as err: 198 | type_err = err 199 | 200 | self.assertIsNotNone(type_err) 201 | self.assertEqual( 202 | "Unexpected argument 'result' in a function decorated with postconditions.", 203 | str(type_err), 204 | ) 205 | 206 | async def test_conflicting_OLD_argument(self) -> None: 207 | @icontract.snapshot(lambda a: a[:]) 208 | @icontract.ensure(lambda OLD, a: a == OLD.a) 209 | async def some_function( 210 | a: List[int], OLD: int 211 | ) -> None: # pylint: disable=unused-variable 212 | pass 213 | 214 | type_err = None # type: Optional[TypeError] 215 | try: 216 | await some_function(a=[13], OLD=2) 217 | except TypeError as err: 218 | type_err = err 219 | 220 | self.assertIsNotNone(type_err) 221 | self.assertEqual( 222 | "Unexpected argument 'OLD' in a function decorated with postconditions.", 223 | str(type_err), 224 | ) 225 | 226 | async def test_error_with_invalid_arguments(self) -> None: 227 | @icontract.ensure( 228 | lambda result: result > 0, 229 | error=lambda z, result: ValueError( 230 | "x is {}, result is {}".format(z, result) 231 | ), 232 | ) 233 | async def some_func(x: int) -> int: 234 | return x 235 | 236 | type_error = None # type: Optional[TypeError] 237 | try: 238 | await some_func(x=0) 239 | except TypeError as err: 240 | type_error = err 241 | 242 | self.assertIsNotNone(type_error) 243 | self.assertEqual( 244 | "The argument(s) of the contract error have not been set: ['z']. " 245 | "Does the original function define them? Did you supply them in the call?", 246 | tests.error.wo_mandatory_location(str(type_error)), 247 | ) 248 | 249 | async def test_no_boolyness(self) -> None: 250 | @icontract.ensure(lambda: tests.mock.NumpyArray([True, False])) 251 | async def some_func() -> None: 252 | pass 253 | 254 | value_error = None # type: Optional[ValueError] 255 | try: 256 | await some_func() 257 | except ValueError as err: 258 | value_error = err 259 | 260 | self.assertIsNotNone(value_error) 261 | self.assertEqual( 262 | "Failed to negate the evaluation of the condition.", 263 | tests.error.wo_mandatory_location(str(value_error)), 264 | ) 265 | 266 | 267 | if __name__ == "__main__": 268 | unittest.main() 269 | -------------------------------------------------------------------------------- /tests_3_8/async/test_precondition.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | # pylint: disable=unnecessary-lambda 5 | 6 | import unittest 7 | from typing import Optional 8 | 9 | import icontract 10 | import tests.error 11 | 12 | 13 | class TestAsyncFunctionSyncCondition(unittest.IsolatedAsyncioTestCase): 14 | async def test_ok(self) -> None: 15 | @icontract.require(lambda x: x > 0) 16 | async def some_func(x: int) -> int: 17 | return x * 10 18 | 19 | result = await some_func(1) 20 | self.assertEqual(10, result) 21 | 22 | async def test_fail(self) -> None: 23 | @icontract.require(lambda x: x > 0) 24 | async def some_func(x: int) -> int: 25 | return x * 10 26 | 27 | violation_error = None # type: Optional[icontract.ViolationError] 28 | try: 29 | _ = await some_func(-1) 30 | except icontract.ViolationError as err: 31 | violation_error = err 32 | 33 | self.assertIsNotNone(violation_error) 34 | self.assertEqual( 35 | "x > 0: x was -1", tests.error.wo_mandatory_location(str(violation_error)) 36 | ) 37 | 38 | 39 | class TestAsyncFunctionAsyncCondition(unittest.IsolatedAsyncioTestCase): 40 | async def test_ok(self) -> None: 41 | async def x_greater_zero(x: int) -> bool: 42 | return x > 0 43 | 44 | @icontract.require(x_greater_zero) 45 | async def some_func(x: int) -> int: 46 | return x * 10 47 | 48 | result = await some_func(1) 49 | self.assertEqual(10, result) 50 | 51 | async def test_fail(self) -> None: 52 | async def x_greater_zero(x: int) -> bool: 53 | return x > 0 54 | 55 | @icontract.require(x_greater_zero) 56 | async def some_func(x: int) -> int: 57 | return x * 10 58 | 59 | violation_error = None # type: Optional[icontract.ViolationError] 60 | try: 61 | _ = await some_func(-1) 62 | except icontract.ViolationError as err: 63 | violation_error = err 64 | 65 | self.assertIsNotNone(violation_error) 66 | self.assertEqual( 67 | "x_greater_zero: x was -1", 68 | tests.error.wo_mandatory_location(str(violation_error)), 69 | ) 70 | 71 | 72 | class TestCoroutine(unittest.IsolatedAsyncioTestCase): 73 | async def test_ok(self) -> None: 74 | async def some_condition() -> bool: 75 | return True 76 | 77 | @icontract.require(lambda: some_condition()) 78 | async def some_func() -> None: 79 | pass 80 | 81 | await some_func() 82 | 83 | async def test_fail(self) -> None: 84 | async def some_condition() -> bool: 85 | return False 86 | 87 | @icontract.require( 88 | lambda: some_condition(), error=lambda: icontract.ViolationError("hihi") 89 | ) 90 | async def some_func() -> None: 91 | pass 92 | 93 | violation_error = None # type: Optional[icontract.ViolationError] 94 | try: 95 | await some_func() 96 | except icontract.ViolationError as err: 97 | violation_error = err 98 | 99 | self.assertIsNotNone(violation_error) 100 | self.assertEqual("hihi", str(violation_error)) 101 | 102 | async def test_reported_if_no_error_is_specified_as_we_can_not_recompute_coroutine_functions( 103 | self, 104 | ) -> None: 105 | async def some_condition() -> bool: 106 | return False 107 | 108 | @icontract.require(lambda: some_condition()) 109 | async def some_func() -> None: 110 | pass 111 | 112 | runtime_error = None # type: Optional[RuntimeError] 113 | try: 114 | await some_func() 115 | except RuntimeError as err: 116 | runtime_error = err 117 | 118 | assert runtime_error is not None 119 | assert runtime_error.__cause__ is not None 120 | assert isinstance(runtime_error.__cause__, ValueError) 121 | 122 | value_error = runtime_error.__cause__ 123 | 124 | self.assertRegex( 125 | str(value_error), 126 | r"^Unexpected coroutine function as a condition of a contract\. " 127 | r"You must specify your own error if the condition of your contract is a coroutine function\.", 128 | ) 129 | 130 | 131 | if __name__ == "__main__": 132 | unittest.main() 133 | -------------------------------------------------------------------------------- /tests_3_8/async/test_recursion.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=unnecessary-lambda 3 | import unittest 4 | from typing import List 5 | 6 | import icontract 7 | 8 | 9 | class TestPrecondition(unittest.IsolatedAsyncioTestCase): 10 | async def test_ok(self) -> None: 11 | order = [] # type: List[str] 12 | 13 | @icontract.require(lambda: another_func()) 14 | @icontract.require(lambda: yet_another_func()) 15 | async def some_func() -> bool: 16 | order.append(some_func.__name__) 17 | return True 18 | 19 | @icontract.require(lambda: some_func()) 20 | @icontract.require(lambda: yet_yet_another_func()) 21 | async def another_func() -> bool: 22 | order.append(another_func.__name__) 23 | return True 24 | 25 | async def yet_another_func() -> bool: 26 | order.append(yet_another_func.__name__) 27 | return True 28 | 29 | async def yet_yet_another_func() -> bool: 30 | order.append(yet_yet_another_func.__name__) 31 | return True 32 | 33 | await some_func() 34 | 35 | self.assertListEqual( 36 | [ 37 | "yet_another_func", 38 | "yet_yet_another_func", 39 | "some_func", 40 | "another_func", 41 | "some_func", 42 | ], 43 | order, 44 | ) 45 | 46 | async def test_recover_after_exception(self) -> None: 47 | order = [] # type: List[str] 48 | some_func_should_raise = True 49 | 50 | class CustomError(Exception): 51 | pass 52 | 53 | @icontract.require(lambda: another_func()) # pylint: disable=unnecessary-lambda 54 | @icontract.require( 55 | lambda: yet_another_func() 56 | ) # pylint: disable=unnecessary-lambda 57 | async def some_func() -> bool: 58 | order.append(some_func.__name__) 59 | if some_func_should_raise: 60 | raise CustomError("some_func_should_raise") 61 | return True 62 | 63 | @icontract.require(lambda: some_func()) # pylint: disable=unnecessary-lambda 64 | @icontract.require( 65 | lambda: yet_yet_another_func() 66 | ) # pylint: disable=unnecessary-lambda 67 | async def another_func() -> bool: 68 | order.append(another_func.__name__) 69 | return True 70 | 71 | async def yet_another_func() -> bool: 72 | order.append(yet_another_func.__name__) 73 | return True 74 | 75 | async def yet_yet_another_func() -> bool: 76 | order.append(yet_yet_another_func.__name__) 77 | return True 78 | 79 | try: 80 | await some_func() 81 | except CustomError: 82 | pass 83 | 84 | self.assertListEqual( 85 | ["yet_another_func", "yet_yet_another_func", "some_func"], order 86 | ) 87 | 88 | # Reset for the next experiment 89 | order = [] 90 | some_func_should_raise = False 91 | 92 | await some_func() 93 | 94 | self.assertListEqual( 95 | [ 96 | "yet_another_func", 97 | "yet_yet_another_func", 98 | "some_func", 99 | "another_func", 100 | "some_func", 101 | ], 102 | order, 103 | ) 104 | 105 | 106 | class TestPostcondition(unittest.IsolatedAsyncioTestCase): 107 | async def test_ok(self) -> None: 108 | order = [] # type: List[str] 109 | another_func_should_raise = True 110 | 111 | class CustomError(Exception): 112 | pass 113 | 114 | @icontract.ensure(lambda: another_func()) # pylint: disable=unnecessary-lambda 115 | @icontract.ensure( 116 | lambda: yet_another_func() 117 | ) # pylint: disable=unnecessary-lambda 118 | async def some_func() -> bool: 119 | order.append(some_func.__name__) 120 | return True 121 | 122 | @icontract.ensure(lambda: some_func()) # pylint: disable=unnecessary-lambda 123 | @icontract.ensure( 124 | lambda: yet_yet_another_func() 125 | ) # pylint: disable=unnecessary-lambda 126 | async def another_func() -> bool: 127 | order.append(another_func.__name__) 128 | if another_func_should_raise: 129 | raise CustomError("some_func_should_raise") 130 | 131 | return True 132 | 133 | async def yet_another_func() -> bool: 134 | order.append(yet_another_func.__name__) 135 | return True 136 | 137 | async def yet_yet_another_func() -> bool: 138 | order.append(yet_yet_another_func.__name__) 139 | return True 140 | 141 | try: 142 | await some_func() 143 | except CustomError: 144 | pass 145 | 146 | self.assertListEqual(["some_func", "yet_another_func", "another_func"], order) 147 | 148 | # Reset for the next experiments 149 | order = [] 150 | another_func_should_raise = False 151 | 152 | await some_func() 153 | 154 | self.assertListEqual( 155 | [ 156 | "some_func", 157 | "yet_another_func", 158 | "another_func", 159 | "yet_yet_another_func", 160 | "some_func", 161 | ], 162 | order, 163 | ) 164 | 165 | async def test_recover_after_exception(self) -> None: 166 | order = [] # type: List[str] 167 | 168 | @icontract.ensure(lambda: another_func()) # pylint: disable=unnecessary-lambda 169 | @icontract.ensure( 170 | lambda: yet_another_func() 171 | ) # pylint: disable=unnecessary-lambda 172 | async def some_func() -> bool: 173 | order.append(some_func.__name__) 174 | return True 175 | 176 | @icontract.ensure(lambda: some_func()) # pylint: disable=unnecessary-lambda 177 | @icontract.ensure( 178 | lambda: yet_yet_another_func() 179 | ) # pylint: disable=unnecessary-lambda 180 | async def another_func() -> bool: 181 | order.append(another_func.__name__) 182 | return True 183 | 184 | async def yet_another_func() -> bool: 185 | order.append(yet_another_func.__name__) 186 | return True 187 | 188 | async def yet_yet_another_func() -> bool: 189 | order.append(yet_yet_another_func.__name__) 190 | return True 191 | 192 | await some_func() 193 | 194 | self.assertListEqual( 195 | [ 196 | "some_func", 197 | "yet_another_func", 198 | "another_func", 199 | "yet_yet_another_func", 200 | "some_func", 201 | ], 202 | order, 203 | ) 204 | 205 | 206 | if __name__ == "__main__": 207 | unittest.main() 208 | -------------------------------------------------------------------------------- /tests_3_8/test_error.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | # pylint: disable=unused-variable 5 | 6 | import textwrap 7 | import unittest 8 | from typing import Optional 9 | 10 | import icontract 11 | 12 | import tests.error 13 | 14 | 15 | class TestNoneSpecified(unittest.TestCase): 16 | def test_that_original_call_arguments_do_not_shadow_condition_variables_in_the_generated_message( 17 | self, 18 | ) -> None: 19 | # ``y`` in the condition shadows the ``y`` in the arguments, but the condition lambda does not refer to 20 | # the original ``y``. 21 | @icontract.require(lambda x: (y := x + 3, x > 0)[1]) 22 | def some_func(x: int, y: int) -> None: 23 | pass 24 | 25 | violation_error = None # type: Optional[icontract.ViolationError] 26 | try: 27 | some_func(x=-1, y=-1000) 28 | except icontract.ViolationError as err: 29 | violation_error = err 30 | 31 | assert violation_error is not None 32 | self.assertEqual( 33 | textwrap.dedent( 34 | """\ 35 | (y := x + 3, x > 0)[1]: 36 | (y := x + 3, x > 0)[1] was False 37 | x was -1 38 | y was 2""" 39 | ), 40 | tests.error.wo_mandatory_location(str(violation_error)), 41 | ) 42 | 43 | 44 | if __name__ == "__main__": 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /tests_3_8/test_invariant.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=invalid-name 3 | # pylint: disable=unused-argument 4 | 5 | import textwrap 6 | import unittest 7 | from typing import NamedTuple, Optional # pylint: disable=unused-import 8 | 9 | import icontract 10 | import tests.error 11 | 12 | 13 | class TestOK(unittest.TestCase): 14 | def test_on_named_tuple(self) -> None: 15 | # This test is related to the issue #171. 16 | # 17 | # The test could not be executed under Python 3.6 as the ``inspect`` module 18 | # could not figure out the type of getters. 19 | @icontract.invariant(lambda self: self.first > 0) 20 | class RightHalfPlanePoint(NamedTuple): 21 | first: int 22 | second: int 23 | 24 | _ = RightHalfPlanePoint(1, 0) 25 | 26 | self.assertEqual( 27 | "Create new instance of RightHalfPlanePoint(first, second)", 28 | RightHalfPlanePoint.__new__.__doc__, 29 | ) 30 | 31 | 32 | class TestViolation(unittest.TestCase): 33 | def test_on_named_tuple(self) -> None: 34 | # This test is related to the issue #171. 35 | # 36 | # The test could not be executed under Python 3.6 as the ``inspect`` module 37 | # could not figure out the type of getters. 38 | @icontract.invariant(lambda self: self.second > 0) 39 | @icontract.invariant(lambda self: self.first > 0) 40 | class RightHalfPlanePoint(NamedTuple): 41 | first: int 42 | second: int 43 | 44 | violation_error = None # type: Optional[icontract.ViolationError] 45 | try: 46 | _ = RightHalfPlanePoint(1, -1) 47 | except icontract.ViolationError as err: 48 | violation_error = err 49 | 50 | self.assertIsNotNone(violation_error) 51 | self.assertEqual( 52 | textwrap.dedent( 53 | """\ 54 | self.second > 0: 55 | self was RightHalfPlanePoint(first=1, second=-1) 56 | self.second was -1""" 57 | ), 58 | tests.error.wo_mandatory_location(str(violation_error)), 59 | ) 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /tests_3_8/test_represent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # pylint: disable=missing-docstring,invalid-name 3 | # pylint: disable=unused-argument 4 | import textwrap 5 | import unittest 6 | from typing import Optional # pylint: disable=unused-import 7 | 8 | import icontract._recompute 9 | import icontract._represent 10 | import tests.error 11 | import tests.mock 12 | 13 | 14 | class TestReprValues(unittest.TestCase): 15 | def test_named_expression(self) -> None: 16 | @icontract.require( 17 | lambda x: (t := x + 1) and t > 1 18 | ) # pylint: disable=undefined-variable 19 | def func(x: int) -> int: 20 | return x 21 | 22 | violation_err = None # type: Optional[icontract.ViolationError] 23 | try: 24 | func(x=0) 25 | except icontract.ViolationError as err: 26 | violation_err = err 27 | 28 | self.assertIsNotNone(violation_err) 29 | self.assertEqual( 30 | textwrap.dedent( 31 | """\ 32 | (t := x + 1) and t > 1: 33 | t was 1 34 | x was 0""" 35 | ), 36 | tests.error.wo_mandatory_location(str(violation_err)), 37 | ) 38 | 39 | 40 | if __name__ == "__main__": 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /tests_with_others/test_deal.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=broad-except 3 | # pylint: disable=invalid-name 4 | 5 | import unittest 6 | from typing import Optional 7 | 8 | import deal 9 | 10 | 11 | class TestDeal(unittest.TestCase): 12 | def test_recursion_handled_in_preconditions(self) -> None: 13 | @deal.pre(lambda _: another_func()) # type: ignore 14 | @deal.pre(lambda _: yet_another_func()) 15 | def some_func() -> bool: 16 | return True 17 | 18 | @deal.pre(lambda _: some_func()) 19 | @deal.pre(lambda _: yet_yet_another_func()) 20 | def another_func() -> bool: 21 | return True 22 | 23 | def yet_another_func() -> bool: 24 | return True 25 | 26 | def yet_yet_another_func() -> bool: 27 | return True 28 | 29 | cause_err = None # type: Optional[BaseException] 30 | try: 31 | some_func() 32 | except Exception as err: 33 | cause_err = err.__cause__ 34 | 35 | self.assertIsNone(cause_err, "Deal can deal with the recursive contracts.") 36 | 37 | def test_inheritance_of_postconditions_incorrect(self) -> None: 38 | class A: 39 | @deal.post(lambda result: result % 2 == 0) 40 | def some_func(self) -> int: 41 | return 2 42 | 43 | class B(A): 44 | @deal.post(lambda result: result % 3 == 0) 45 | def some_func(self) -> int: 46 | # The result 9 satisfies the postcondition of B.some_func, but not A.some_func. 47 | return 9 48 | 49 | b = B() 50 | # The correct behavior would be to throw an exception here. 51 | b.some_func() 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /tests_with_others/test_dpcontracts.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | # pylint: disable=broad-except 3 | # pylint: disable=invalid-name 4 | import unittest 5 | from typing import Optional 6 | 7 | import dpcontracts 8 | 9 | 10 | class TestDpcontracts(unittest.TestCase): 11 | def test_recursion_unhandled_in_preconditions(self) -> None: 12 | @dpcontracts.require("must another_func", lambda args: another_func()) # type: ignore 13 | @dpcontracts.require("must yet_another_func", lambda args: yet_another_func()) # type: ignore 14 | def some_func() -> bool: 15 | return True 16 | 17 | @dpcontracts.require("must some_func", lambda args: some_func()) # type: ignore 18 | @dpcontracts.require("must yet_yet_another_func", lambda args: yet_yet_another_func()) # type: ignore 19 | def another_func() -> bool: 20 | return True 21 | 22 | def yet_another_func() -> bool: 23 | return True 24 | 25 | def yet_yet_another_func() -> bool: 26 | return True 27 | 28 | cause_err = None # type: Optional[BaseException] 29 | try: 30 | some_func() 31 | except Exception as err: 32 | cause_err = err.__cause__ 33 | 34 | self.assertIsNotNone(cause_err) 35 | self.assertIsInstance(cause_err, RecursionError) 36 | 37 | def test_inheritance_of_postconditions_incorrect(self) -> None: 38 | class A: 39 | @dpcontracts.ensure("dummy contract", lambda args, result: result % 2 == 0) # type: ignore 40 | def some_func(self) -> int: 41 | return 2 42 | 43 | class B(A): 44 | @dpcontracts.ensure("dummy contract", lambda args, result: result % 3 == 0) # type: ignore 45 | def some_func(self) -> int: 46 | # The result 9 satisfies the postcondition of B.some_func, but not A.some_func. 47 | return 9 48 | 49 | b = B() 50 | # The correct behavior would be to throw an exception here. 51 | b.some_func() 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,py310,py311 3 | 4 | [testenv] 5 | deps = .[dev] 6 | commands = 7 | python3 {toxinidir}/precommit.py 8 | 9 | setenv = 10 | COVERAGE_FILE={envbindir}/.coverage 11 | --------------------------------------------------------------------------------