Design by contract for Python with many validators support.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
deal +changelog Sep 7, 2018
.gitignore total coverage Jun 10, 2018
.travis.yml +coverage info Jun 10, 2018
LICENSE
MANIFEST.in Version 1.0 released! Jan 27, 2018
README.md
README.rst +changelog Sep 7, 2018
constraints.txt +travis for new tests Mar 24, 2018
deploy.sh Version 1.0 released! Jan 27, 2018
logo.png +logo Mar 22, 2018
logo.svg +TODO Mar 22, 2018
setup.cfg Version 1.0 released! Jan 27, 2018
setup.py +changelog Sep 7, 2018
tests.py total coverage Jun 10, 2018

README.md

Deal

Deal Logo

Build Status Coverage Status PyPI version Development Status Code size License

Deal -- python library for design by contract (DbC) programming.

This library contain 3 main conception from DbC:

  • Precondition -- condition that must be true before function is executed.
  • Postcondition -- condition that must be true after function executed.
  • Invariant -- condition that can be relied upon to be true during execution of a program. In this library invariant condition check in 3 cases:
    1. Before class method execution.
    2. After class method execution.
    3. After some class attribute setting.

Features

  • Functional declaration.
  • Custom exceptions.
  • Raising exceptions from contract.
  • Django Forms styled validators.
  • Attribute setting invariant validation.
  • Dynamically assigned attributes and methods invariant validation.

Installation

Stable:

pip install deal

Dev:

pip install -e git+https://github.com/orsinium/deal.git#egg=deal

TL;DR

  • @pre -- validate function arguments (pre-validation).
  • @post -- validate function result (post-validation).
  • @inv -- validate class methods before and after some method calling and after attribute setting.

Exceptions structure:

  • ContractError (inherited from built-in AssertionError)
    • PreContractError
    • PostContractError
    • InvContractError

Library decorators doesn't catch any exceptions raised from contracts.

Usage

Contract types

Pre (pre, require):

In [1]: from deal import pre, post, inv, Scheme

In [2]: @pre(lambda *args: all(map(lambda x: x > 0, args)))
   ...: def my_sum(*args):
   ...:     return sum(args)
   ...:

In [3]: my_sum(2, 3, 4)
Out[3]: 9

In [4]: my_sum(2, -3, 4)
PreContractError:

Post (post, ensure):

In [1]: @post(lambda x: x > 0)
   ...: def my_sum(*args):
   ...:     return sum(args)
   ...:

In [2]: my_sum(2, -3, 4)
Out[2]: 3

In [3]: my_sum(2, -3, -4)
PostContractError:

Inv (inv, invariant):

In [1]: @inv(lambda obj: obj.x > 0)
   ...: class A:
   ...:     x = 4
   ...:     

In [2]: a = A()

In [3]: a.x = 10

In [4]: a.x = -10
InvContractError:

In [5]: A
Out[5]: deal.core.AInvarianted

Customize error message

Custom message:

In [1]: @pre(lambda x: x > 0, "x must be > 0")
   ...: def f(x):
   ...:     return x * 2
   ...:

In [2]: f(-2)
PreContractError: x must be > 0

Custom exception:

In [1]: @pre(lambda x: x > 0, exception=AssertionError("x must be > 0"))
   ...: def f(x):
   ...:     return x * 2
   ...:

In [2]: f(-2)
AssertionError: x must be > 0

Return error message from contract:

In [1]: @pre(lambda x: x > 0 or "x must be > 0")
   ...: def f(x):
   ...:     return x * 2
   ...:

In [2]: f(-5)
PreContractError: x must be > 0

Validators

  1. Regular contract with errors returning:
In [1]: def contract(name):
   ...:     if not isinstance(name, str):
   ...:         return "name must be str"
   ...:     return True
   ...:

In [2]: @pre(contract)
   ...: def f(x):
   ...:     return x * 2
   ...:

In [3]: f('Chris')
Out[3]: 'ChrisChris'

In [4]: f(4)
PreContractError: name must be str
  1. Simple validator (nearly Django Forms style, except initialization):
In [1]: class Validator:
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...:         
   ...:     def is_valid(self):
   ...:         if self.x <= 0:
   ...:             self.errors = ['x must be > 0']
   ...:             return False
   ...:         return True
   ...:     

In [2]: @pre(Validator)
   ...: def f(x):
   ...:     return x * 2
   ...:

In [3]: f(5)
Out[3]: 10

In [4]: f(-5)
PreContractError: ['x must be > 0']
  1. Scheme like simple validator but data attribute contains dictionary with all passed arguments:
In [1]: class NameScheme(Scheme):
   ...:     def is_valid(self):
   ...:         if not isinstance(self.data['name'], str):
   ...:             self.errors = ['name must be str']
   ...:             return False
   ...:         return True
   ...:     

In [2]: @pre(NameScheme)
   ...: def f(name):
   ...:     return name * 2
   ...:

In [3]: f('Chris')
Out[3]: 'ChrisChris'

In [4]: f(3)
PreContractError: ['name must be str']

Scheme automatically detect all arguments names:

In [1]: class Printer(Scheme):
   ...:     def is_valid(self):
   ...:         print(self.data)
   ...:         return True
   ...:     

In [2]: @pre(Printer)
   ...: def f(a, b, c=4, *args, **kwargs):
   ...:     pass
   ...:

In [3]: f(1, b=2, e=6)
{'args': (), 'a': 1, 'b': 2, 'c': 4, 'e': 6, 'kwargs': {'e': 6}}

In [4]: f(1, 2, 3, 4, 5, 6)
{'a': 1, 'b': 2, 'c': 3, 'args': (4, 5, 6), 'kwargs': {}}
  1. You can use any validators from djburger. See validators documentation and list of supported external libraries. For example, deal + djburger + marshmallow:
In [1]: import djburger, marshmallow

In [2]: class Scheme(djburger.v.b.Marshmallow):
   ...:     name = marshmallow.fields.Str()
   ...:

In [3]: @pre(Scheme)
   ...: def func(name):
   ...:     return name * 2
   ...:

In [4]: func('Chris')
Out[4]: 'ChrisChris'

In [5]: func(123)
PreContractError: {'name': ['Not a valid string.']}

Djburger is Django independent. You can use it in any python projects.

Contracts chaining

You can chain any contracts:

In [1]: @pre(lambda x: x > 0)
   ...: @pre(lambda x: x < 10)
   ...: def f(x):
   ...:     return x * 2
   ...:

In [2]: f(5)
Out[2]: 10

In [3]: f(-1)
PreContractError:

In [3]: f(12)
PreContractError:

Chaining order:

  • @inv: from top to bottom.
  • @pre: from top to bottom.
  • @post: from bottom to top.

Disable contracts on production

If you want disable contracts on production, pass debug=True to decorator:

In [2]: from deal import pre, post, inv, Scheme
   ...: @post(lambda x: x > 0, debug=True)
   ...: def my_sum(*args):
   ...:     return sum(args)
   ...:

If you run python with -O option, contracts will be disabled. This is uses Python's __debug__ option:

The built-in variable __debug__ is True under normal circumstances, False when optimization is requested (command line option -O).

Perfomance

NOTICE: 1 µs == 1000 ns

@pre and @post:

In [1]: f = lambda x: x

In [2]: pre_f = pre(lambda x: True)(f)

In [3]: post_f = post(lambda x: True)(f)

In [4]: %timeit f(10)
92.3 ns ± 3.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [5]: %timeit pre_f(10)
2.07 µs ± 92.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [6]: %timeit post_f(10)
2.03 µs ± 18.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

+1 µs

@inv:

In [1]: class A:
   ...:     x = 4
   ...:     

In [2]: InvA = inv(lambda obj: True)(A)

In [3]: a = A()

In [4]: inv_a = InvA()

In [5]: %timeit a.x = 10
76.4 ns ± 1.36 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [6]: %timeit inv_a.x = 10
6.89 µs ± 408 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

+6 µs

Changelog

1.0. @pre, @post, @inv, error messages customization.

1.1. @inv chaining.

1.2. Travis CI, wrapper updating.

2.0. Schemes, djburger validators support.

2.1 @pre and @post can decorate class methods.

2.2 new debug option for optional disabling contracts on production.

Contributors