Values¶
deal.pre¶
Precondition – condition that must be true before the function is executed.
@deal.pre(lambda *args: all(arg > 0 for arg in args))
def sum_positive(*args):
return sum(args)
sum_positive(1, 2, 3, 4)
# 10
sum_positive(1, 2, -3, 4)
# PreContractError: expected all(arg > 0 for arg in args) (where args=(1, 2, -3, 4))
deal.post¶
Postcondition – condition that must be true after the function was executed. Raises PostContractError
otherwise.
@deal.post(lambda x: x > 0)
def always_positive_sum(*args):
return sum(args)
always_positive_sum(2, -3, 4)
# 3
always_positive_sum(2, -3, -4)
# PostContractError:
Post-condition allows you to make additional constraints about a function result. Use type annotations to limit types of results and post-conditions to limit possible values inside given types.
deal.ensure¶
Ensure is a postcondition that accepts not only result, but also function arguments. Must be true after function executed.
@deal.ensure(lambda x, result: x != result)
def double(x):
return x * 2
double(2)
# 4
double(0)
# PostContractError: expected x != result (where result=0, x=0)
Ensure is the shining star of property-based testing. It works perfect for P vs NP like problems. In other words, for complex task when checking result correctness (even partial checking only for some cases) is much easier then the calculation itself.
deal.inv¶
Invariant – condition that can be relied upon to be true during execution of a program.
Invariant check condition in the next cases:
Before class method execution.
After class method execution.
After some class attribute setting.
@deal.inv(lambda post: post.likes >= 0)
class Post:
likes = 0
post = Post()
post.likes = 10
post.likes = -10
# InvContractError: expected post.likes >= 0
type(post)
# deal.core.PostInvarianted
assert¶
Good old assert statement is also kind of a contract. It is good for checking intermediate state inside a function. Also, it is similar to other contracts since deal mimics assert
behavior: all contracts are disabled on production and raise AssertionError in case of the contract violation. Also, deal linter checks assert
statements to be True.
def do_something(a):
result = something_else(a)
assert result > 0
return another_thing(result)
Exceptions¶
Every contract type raises it’s own exception type, inherited from ContractError
(which is inherited from built-in AssertionError
):
contract |
exception |
---|---|
pre |
PreContractError |
post |
PostContractError |
ensure |
PostContractError |
inv |
InvContractError |
Custom exception for any contract can be specified by exception
argument:
@deal.pre(lambda role: role in ('user', 'admin'), exception=LookupError)
def change_role(role):
print(f'now you are {role}!')
change_role('superuser')
# LookupError:
However, thumb-up rule is to avoid catching exceptions from contracts. Contracts aren’t part of business logic, but are validation. Hence, a contract error means a business logic violation has occurred and execution should be stopped to avoid doing something not predicted and even dangerous.
Chaining contracts¶
You can chain any contracts:
@deal.pre(lambda x: x > 0)
@deal.pre(lambda x: x < 10)
def f(x):
return x * 2
f(5)
# 10
f(-1)
# PreContractError: expected x > 0 (where x=-1)
f(12)
# PreContractError: expected x < 10 (where x=12)
@deal.post
and @deal.ensure
contracts are resolved from bottom to top. All other contracts are resolved from top to bottom. This is because of how wrapping works: before calling function we go down the contracts list, and after calling the function we go back up the call stack.
Generators and async functions¶
Contracts mostly support generators (yield
) and async functions:
contract |
yield |
async |
---|---|---|
pre |
yes |
yes |
post |
yes (checks every yielded value) |
yes |
ensure |
yes (checks every yielded value) |
yes |
inv |
partially (before execution) |
partially (before execution) |