Provide design-by-contract with informative violation messages
Project description
icontract
icontract provides design-by-contract to Python3 with informative violation messages.
There exist a couple of contract libraries. However, at the time of this writing (July 2018), they all required the programmer either to learn a new syntax (PyContracts) or to write redundant condition descriptions ( e.g., contracts, covenant, dpcontracts, pyadbc and pcd).
This library was strongly inspired by them, but we go a step further and use the meta programming library to infer violation messages from the code in order to promote dont-repeat-yourself principle (DRY) and spare the programmer the tedious task of repeating the message that was already written in code.
In the long run, we hope that design-by-contract will be adopted and integrated in the language. Consider this library a work-around till that happens. An ongoing discussion on how to bring design-by-contract into Python language can be followed on python-ideas mailing list.
Usage
icontract provides two function decorators, pre and post for pre-conditions and post-conditions, respectively. Additionally, it provides a class decorator, inv, to establish class invariants.
The condition argument specifies the contract and is usually written in lambda notation. In post-conditions, condition function receives a reserved parameter result corresponding to the result of the function. The condition can take as input a subset of arguments required by the wrapped function. This allows for very succinct conditions.
You can provide an optional description by passing in description argument.
Whenever a violation occurs, ViolationError is raised. Its message includes:
the human-readable representation of the condition,
description (if supplied) and
representation of all the values.
You can provide a custom representation function with the argument repr_args that needs to cover all the input arguments (including result in post-conditions) of the condition function and return a string. If no representation function was specified, the input arguments are represented by concatenation of __repr__ on each one of them.
If no custom representation function has been supplied, the representation of the values is obtained by re-executing the condition function programmatically by traversing its abstract syntax tree and filling the tree leaves with values held in the function frame. Mind that this re-execution will also re-execute all the functions. Therefore you need to make sure that all the function calls involved in the condition functions do not have any side effects.
>>> import icontract
>>> @icontract.pre(lambda x: x > 3)
... def some_func(x: int, y: int = 5)->None:
... pass
...
>>> some_func(x=5)
# Pre-condition violation
>>> some_func(x=1)
Traceback (most recent call last):
...
icontract.ViolationError: x > 3: x was 1
# Pre-condition violation with a description
>>> @icontract.pre(lambda x: x > 3, "x must not be small")
... def some_func(x: int, y: int = 5) -> None:
... pass
...
>>> some_func(x=1)
Traceback (most recent call last):
...
icontract.ViolationError: x must not be small: x > 3: x was 1
# Pre-condition violation with a custom representation function
>>> @icontract.pre(lambda x: x > 3, repr_args=lambda x: "x was 0x{:x}".format(x))
... def some_func(x: int, y: int = 5) -> None:
... pass
...
>>> some_func(x=1)
Traceback (most recent call last):
...
icontract.ViolationError: x > 3: x was 0x1
# Pre-condition violation with more complex values
>>> class B:
... def __init__(self) -> None:
... self.x = 7
...
... def y(self) -> int:
... return 2
...
... def __repr__(self) -> str:
... return "instance of B"
...
>>> class A:
... def __init__(self)->None:
... self.b = B()
...
... def __repr__(self) -> str:
... return "instance of A"
...
>>> SOME_GLOBAL_VAR = 13
>>> @icontract.pre(lambda a: a.b.x + a.b.y() > SOME_GLOBAL_VAR)
... def some_func(a: A) -> None:
... pass
...
>>> an_a = A()
>>> some_func(an_a)
Traceback (most recent call last):
...
icontract.ViolationError: (a.b.x + a.b.y()) > SOME_GLOBAL_VAR:
SOME_GLOBAL_VAR was 13
a was instance of A
a.b was instance of B
a.b.x was 7
a.b.y() was 2
# Post-condition
>>> @icontract.post(lambda result, x: result > x)
... def some_func(x: int, y: int = 5) -> int:
... return x - y
...
>>> some_func(x=10)
Traceback (most recent call last):
...
icontract.ViolationError: result > x:
result was 5
x was 10
Toggling Contracts
By default, the contracts are always checked at run-time. To disable them, run the interpreter in optimized mode (-O or -OO, see Python command-line options).
If you want to override this behavior, you can supply the the enabled argument to the contract:
>>> @icontract.pre(lambda x: x > 10, enabled=False)
... def some_func(x: int) -> int:
... return 123
...
# The pre-condition is breached, but the check was disabled:
>>> some_func(x=0)
123
icontract provides a global icontract.SLOW to provide a unified way to mark a plethora of contracts in large code bases. icontract.SLOW reflects the environment variable ICONTRACT_SLOW.
While you may want to keep most contracts running both during the development and in the production, contracts marked with icontract.SLOW should run only during the development (since they are too sluggish to execute in a real application).
If you want to enable contracts marked with icontract.SLOW, set the environment variable ICONTRACT_SLOW to a non-empty string.
Here is some example code:
# some_module.py
@icontract.pre(lambda x: x > 10, enabled=icontract.SLOW)
def some_func(x: int) -> int:
return 123
# in test_some_module.py
import unittest
class TestSomething(unittest.TestCase):
def test_some_func(self):
self.assertEqual(123, some_func(15))
if __name__ == '__main__':
unittest.main()
Run this bash command to execute the unit test with slow contracts:
$ ICONTRACT_SLOW=true python test_some_module.py
Invariants
Invariants are special contracts associated with an instance of a class. An invariant should hold after initialization and before and after a call to any public instance method. The invariants are the pivotal element of design-by-contract: they allow you to formally define properties of a data structures that you know will be maintained throughout the life time of every instance.
We consider the following methods to be “public”:
All methods not prefixed with _
All magic methods (prefix __ and suffix __)
Class methods can not observe the invariant since they are not associated with an instance of the class.
We exempt __repr__ method from observing the invariant since that function needs to be called when generating error messages.
The icontract invariants are implemented as class decorators.
The following examples shows various cases when an invariant is breached.
After the initialization:
>>> @icontract.inv(lambda self: self.x > 0)
... class SomeClass:
... def __init__(self) -> None:
... self.x = -1
...
... def __repr__(self) -> str:
... return "some instance"
...
>>> some_instance = SomeClass()
Traceback (most recent call last):
...
icontract.ViolationError: self.x > 0:
self was some instance
self.x was -1
Before the invocation of a public method:
>>> @icontract.inv(lambda self: self.x > 0)
... class SomeClass:
... def __init__(self) -> None:
... self.x = 100
...
... def some_method(self) -> None:
... self.x = 10
...
... def __repr__(self) -> str:
... return "some instance"
...
>>> some_instance = SomeClass()
>>> some_instance.x = -1
>>> some_instance.some_method()
Traceback (most recent call last):
...
icontract.ViolationError: self.x > 0:
self was some instance
self.x was -1
After the invocation of a public method:
>>> @icontract.inv(lambda self: self.x > 0)
... class SomeClass:
... def __init__(self) -> None:
... self.x = 100
...
... def some_method(self) -> None:
... self.x = -1
...
... def __repr__(self) -> str:
... return "some instance"
...
>>> some_instance = SomeClass()
>>> some_instance.some_method()
Traceback (most recent call last):
...
icontract.ViolationError: self.x > 0:
self was some instance
self.x was -1
After the invocation of a magic method:
>>> @icontract.inv(lambda self: self.x > 0)
... class SomeClass:
... def __init__(self) -> None:
... self.x = 100
...
... def __call__(self) -> None:
... self.x = -1
...
... def __repr__(self) -> str:
... return "some instance"
...
>>> some_instance = SomeClass()
>>> some_instance()
Traceback (most recent call last):
...
icontract.ViolationError: self.x > 0:
self was some instance
self.x was -1
Inheritance
Python 3 does not allow inheritance of function and class decorators. This makes it impossible to elegantly implement inheritance of invariants, pre and postconditions. We are still experimenting with approaches how to achieve that as painlessly as possible. Please let us know if you know how to deal with inheritance and contracts in a nice way.
Installation
Install icontract with pip:
pip3 install icontract
Development
Check out the repository.
In the repository root, create the virtual environment:
python3 -m venv venv3
Activate the virtual environment:
source venv3/bin/activate
Install the development dependencies:
pip3 install -e .[dev]
We use tox for testing and packaging the distribution. Run:
tox
We also provide a set of pre-commit checks that lint and check code for formatting. Run them locally from an activated virtual environment with development dependencies:
./precommit.py
The pre-commit script can also automatically format the code:
./precommit.py --overwrite
Versioning
We follow Semantic Versioning. The version X.Y.Z indicates:
X is the major version (backward-incompatible),
Y is the minor version (backward-compatible), and
Z is the patch version (backward-compatible bug fix).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file icontract-1.4.0.tar.gz
.
File metadata
- Download URL: icontract-1.4.0.tar.gz
- Upload date:
- Size: 13.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/40.2.0 requests-toolbelt/0.8.0 tqdm/4.25.0 CPython/3.5.2+
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ddd8df95b70edf8ff5fc4cf0fedacf3c5d2975b2d4d80d6e63fe383b39c871a4 |
|
MD5 | d7ae4e5b9b3d9f538d999eb2f4491ec0 |
|
BLAKE2b-256 | 7d9490856e49e74168107fdfac3d0ccedaf8e6ad0e79002ae5805bf872445aaa |