Skip to main content

Provide design-by-contract with informative violation messages

Project description

icontract

https://travis-ci.com/Parquery/icontract.svg?branch=master https://coveralls.io/repos/github/Parquery/icontract/badge.svg?branch=master https://badge.fury.io/py/icontract.svg

icontract provides design-by-contract to Python3 with informative violation messages and inheritance.

Reladed Projects

There exist a couple of contract libraries. However, at the time of this writing (September 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.

To the best of our knowledge, there is currently no Python library that supports inheritance of the contracts in a correct way. icontract allows inheritance of the contracts and supports weakining of the preconditions as well as strengthening of the postconditions and invariants. Notably, weakining and strengthening of the contracts is a feature indispensable for modeling many non-trivial class hierarchies. Please see Section Inheritance.

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

To inherit the contracts of the parent class, the child class needs to either inherit from icontract.DBC or have a meta class set to icontract.DBCMeta.

When no contracts are specified in the child class, all contracts are inherited from the parent class as-are.

When the child class introduces additional preconditions or postconditions and invariants, these contracts are strengthened or weakened, respectively. icontract.DBCMeta allows you to specify the contracts not only on the concrete classes, but also on abstract classes.

Strengthening. If you specify additional invariants in the child class then the child class will need to satisfy all the invariants of its parent class as well as its own additional invariants. Analogously, if you specify additional postconditions to a function of the class, that function will need to satisfy both its own postconditions and the postconditions of the original parent function that it overrides.

Weakining. Adding preconditions to a function in the child class weakens the preconditions. The caller needs to provide either arguments that satisfy the preconditions associated with the function of the parent class or arguments that satisfy the preconditions of the function of the child class.

Abstract Classes. Since Python 3 does not allow multiple meta classes, icontract.DBCMeta inherits from abc.ABCMeta to allow combining contracts with abstract base classes.

The following example shows an abstract parent class and a child class that inherits and strengthens parent’s contracts:

>>> import abc
>>> import icontract

>>> @icontract.inv(lambda self: self.x > 0)
... class A(icontract.DBC):
...     def __init__(self) -> None:
...         self.x = 10
...
...     @abc.abstractmethod
...     @icontract.post(lambda y, result: result < y)
...     def func(self, y: int) -> int:
...         pass
...
...     def __repr__(self) -> str:
...         return "instance of A"

>>> @icontract.inv(lambda self: self.x < 100)
... class B(A):
...     def func(self, y: int) -> int:
...         # Break intentionally the postcondition
...         # for an illustration
...         return y + 1
...
...     def break_parent_invariant(self):
...         self.x = -1
...
...     def break_my_invariant(self):
...         self.x = 101
...
...     def __repr__(self) -> str:
...         return "instance of B"

# Break the parent's postcondition
>>> some_b = B()
>>> some_b.func(y=0)
Traceback (most recent call last):
    ...
icontract.ViolationError: result < y:
result was 1
y was 0

# Break the parent's invariant
>>> another_b = B()
>>> another_b.break_parent_invariant()
Traceback (most recent call last):
    ...
icontract.ViolationError: self.x > 0:
self was instance of B
self.x was -1

# Break the child's invariant
>>> yet_another_b = B()
>>> yet_another_b.break_my_invariant()
Traceback (most recent call last):
    ...
icontract.ViolationError: self.x < 100:
self was instance of B
self.x was 101

The following example shows how preconditions are weakened:

>>> class A(icontract.DBC):
...     @icontract.pre(lambda x: x % 2 == 0)
...     def func(self, x: int) -> None:
...         pass

>>> class B(A):
...     @icontract.pre(lambda x: x % 3 == 0)
...     def func(self, x: int) -> None:
...         pass

>>> b = B()

# The precondition of the parent is satisfied.
>>> b.func(x=2)

# The precondition of the child is satisfied,
# while the precondition of the parent is not.
# This is OK since the precondition has been
# weakened.
>>> b.func(x=3)

# None of the preconditions have been satisfied.
>>> b.func(x=5)
Traceback (most recent call last):
    ...
icontract.ViolationError: (x % 2) == 0: x was 5

Implementation Details

Decorator stack. The precondition and postcondition decorators have to be stacked together to allow for inheritance. Hence, when multiple precondition and postcondition decorators are given, the function is actually decorated only once with a precondition/postcondition checker while the contracts are stacked to the checker’s __preconditions__ and __postconditions__ attribute, respectively. The checker functions iterates through these two attributes to verify the contracts at run-time.

All the decorators in the function’s decorator stack are expected to call functools.update_wrapper(). Notably, we use __wrapped__ attribute to iterate through the decorator stack and find the checker function which is set with functools.update_wrapper(). Mind that this implies that preconditions and postconditions are verified at the inner-most decorator and not when outer preconditios and postconditions are defined.

Consider the following example:

@some_custom_decorator
@icontract.pre(lambda x: x > 0)
@another_custom_decorator
@icontract.pre(lambda x, y: y < x)
def some_func(x: int, y: int) -> None:
  # ...

The checker function will verify the two preconditions after both some_custom_decorator and another_custom_decorator have been applied, whily you would expect that the outer precondition (x > 0) is verified immediately after some_custom_decorator is applied.

To prevent bugs due to unexpected behavior, we recommend to always group preconditions and postconditions together.

Invariants. Since invariants are handled by a class decorator (in contrast to function decorators that handle preconditions and postconditions), they do not need to be stacked. The first invariant decorator wraps each public method of a class with a checker function. The invariants are added to the class’ __invariants__ attribute. At run-time, the checker function iterates through the __invariants__ attribute when it needs to actually verify the invariants.

Mind that we still expect each class decorator that decorates the class functions to use functools.update_wrapper() in order to be able to iterate through decorator stacks of the individual functions.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

icontract-1.5.0.tar.gz (18.3 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page