Skip to content

Commit

Permalink
Merge 9494dde into bad6707
Browse files Browse the repository at this point in the history
  • Loading branch information
orsinium committed Oct 9, 2019
2 parents bad6707 + 9494dde commit 7b9f101
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 53 deletions.
8 changes: 6 additions & 2 deletions deal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@


# app
from ._aliases import chain, ensure, inv, invariant, offline, post, pre, pure, raises, require, safe, silent
from ._aliases import (
chain, ensure, inv, invariant, offline, post,
pre, pure, raises, reason, require, safe, silent
)
from ._exceptions import * # noQA
from ._schemes import Scheme
from ._state import reset, switch
Expand All @@ -39,15 +42,16 @@
'chain',
'ensure',
'inv',
'invariant',
'offline',
'post',
'pre',
'pure',
'raises',
'reason',
'safe',
'silent',

# aliases
'invariant',
'require',
]
19 changes: 10 additions & 9 deletions deal/_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
from typing import Callable

# app
from ._decorators import Ensure, Invariant, Offline, Post, Pre, Raises, Silent
from . import _decorators
from ._types import ExceptionType


require = pre = Pre
post = Post
ensure = Ensure
inv = invariant = Invariant
raises = Raises
require = pre = _decorators.Pre
post = _decorators.Post
ensure = _decorators.Ensure
inv = invariant = _decorators.Invariant
raises = _decorators.Raises
reason = _decorators.Reason


# makes braces for decorator are optional
Expand All @@ -22,9 +23,9 @@ def _optional(_contract, _func: Callable = None, *, message: str = None,
return _contract(message=message, exception=exception, debug=debug)


offline = partial(_optional, Offline)
safe = partial(_optional, Raises)
silent = partial(_optional, Silent)
offline = partial(_optional, _decorators.Offline)
safe = partial(_optional, _decorators.Raises)
silent = partial(_optional, _decorators.Silent)


def chain(*contracts) -> Callable[[Callable], Callable]:
Expand Down
2 changes: 2 additions & 0 deletions deal/_decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .post import Post
from .pre import Pre
from .raises import Raises
from .reason import Reason
from .silent import Silent


Expand All @@ -15,5 +16,6 @@
'Post',
'Pre',
'Raises',
'Reason',
'Silent',
]
12 changes: 8 additions & 4 deletions deal/_decorators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Base:
exception: ExceptionType = ContractError

def __init__(self, validator: Callable, *, message: str = None,
exception: Type[Exception] = None, debug: bool = False):
exception: Type[ExceptionType] = None, debug: bool = False):
"""
Step 1. Set contract (validator).
"""
Expand All @@ -22,7 +22,7 @@ def __init__(self, validator: Callable, *, message: str = None,
if exception:
self.exception = exception
if message:
self.exception = self.exception(message)
self.exception = self.exception(message) # type: ignore

def validate(self, *args, **kwargs) -> None:
"""
Expand All @@ -40,7 +40,7 @@ def _vaa_validation(self, *args, **kwargs) -> None:
# detect original function
function = self.function
while hasattr(function, '__wrapped__'):
function = function.__wrapped__
function = function.__wrapped__ # type: ignore
# assign *args to real names
params.update(getcallargs(function, *args, **kwargs))
# drop args-kwargs, we already put them on the right places
Expand All @@ -51,12 +51,16 @@ def _vaa_validation(self, *args, **kwargs) -> None:
validator = self.validator(data=params)
if validator.is_valid():
return
if isinstance(self.exception, Exception):
raise self.exception
raise self.exception(validator.errors)

def _simple_validation(self, *args, **kwargs) -> None:
validation_result = self.validator(*args, **kwargs)
# is invalid (validator return error message)
# is invalid (validator returns error message)
if isinstance(validation_result, str):
if isinstance(self.exception, Exception):
raise self.exception
raise self.exception(validation_result)
# is valid (truely result)
if validation_result:
Expand Down
4 changes: 2 additions & 2 deletions deal/_decorators/raises.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# built-in
from typing import Tuple
from typing import Tuple, Type

# app
from .._exceptions import ContractError, RaisesContractError
Expand All @@ -14,7 +14,7 @@ def __init__(self, *exceptions, message: str = None, exception: ExceptionType =
"""
Step 1. Set allowed exceptions list.
"""
self.exceptions: Tuple[Exception, ...] = exceptions
self.exceptions: Tuple[Type[Exception], ...] = exceptions
super().__init__(
validator=None,
message=message,
Expand Down
37 changes: 37 additions & 0 deletions deal/_decorators/reason.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# built-in
from typing import Callable

# app
from .._exceptions import ReasonContractError
from .._types import ExceptionType
from .base import Base


class Reason(Base):
exception: ExceptionType = ReasonContractError

def __init__(self, event: Exception, validator: Callable, *,
message: str = None, exception: ExceptionType = None, debug: bool = False):
"""
Step 1. Set allowed exceptions list.
"""
self.event = event
super().__init__(
validator=validator,
message=message,
exception=exception,
debug=debug,
)

def patched_function(self, *args, **kwargs):
"""
Step 3. Wrapped function calling.
"""
try:
return self.function(*args, **kwargs)
except self.event as origin:
try:
self.validate(*args, **kwargs)
except self.exception:
raise self.exception from origin
raise
4 changes: 4 additions & 0 deletions deal/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class RaisesContractError(ContractError):
pass


class ReasonContractError(ContractError):
pass


class OfflineContractError(ContractError):
pass

Expand Down
23 changes: 13 additions & 10 deletions deal/_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@

# app
from ._decorators import Pre, Raises
from ._types import ArgsKwargsType, ExceptionType
from ._types import ArgsKwargsType


class TestCase(typing.NamedTuple):
args: typing.Tuple[typing.Any, ...]
kwargs: typing.Dict[str, typing.Any]
func: typing.Callable
exceptions: typing.Tuple[ExceptionType, ...]
exceptions: typing.Tuple[typing.Type[Exception], ...]

def __call__(self) -> typing.Any:
"""Calls the given test case returning the called functions result on success or
Expand All @@ -25,7 +25,7 @@ def __call__(self) -> typing.Any:
try:
result = self.func(*self.args, **self.kwargs)
except self.exceptions:
return typing.NoReturn
return typing.NoReturn # type: ignore
self._check_result(result)
return result

Expand All @@ -40,19 +40,22 @@ def _check_result(self, result: typing.Any) -> None:
)


def get_excs(func: typing.Callable) -> typing.Iterator[ExceptionType]:
def get_excs(func: typing.Callable) -> typing.Iterator[typing.Type[Exception]]:
while True:
if func.__closure__:
for cell in func.__closure__:
if getattr(func, '__closure__', None):
for cell in func.__closure__: # type: ignore
obj = cell.cell_contents
if isinstance(obj, Raises):
yield from obj.exceptions
elif isinstance(obj, Pre):
yield obj.exception
if isinstance(obj.exception, Exception):
yield type(obj.exception)
else:
yield obj.exception

if not hasattr(func, '__wrapped__'):
if not hasattr(func, '__wrapped__'): # type: ignore
return
func = func.__wrapped__
func = func.__wrapped__ # type: ignore


def get_examples(func: typing.Callable, kwargs: typing.Dict[str, typing.Any],
Expand All @@ -65,7 +68,7 @@ def get_examples(func: typing.Callable, kwargs: typing.Dict[str, typing.Any],
def pass_along_variables(*args, **kwargs) -> ArgsKwargsType:
return args, kwargs

pass_along_variables.__signature__ = signature(func)
pass_along_variables.__signature__ = signature(func) # type: ignore
pass_along_variables.__annotations__ = getattr(func, '__annotations__', {})
strategy = hypothesis.strategies.builds(pass_along_variables, **kwargs)
examples = []
Expand Down
20 changes: 18 additions & 2 deletions docs/decorators/ensure.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ double(0)

## Motivation

Perfect for complex task that easy to check. For example:
Ensure allows you to simplify testing, easier check hypothesis, tell more about the function behavior. It works perfect for [P vs NP](https://en.wikipedia.org/wiki/P_versus_NP_problem) like problems. In other words, for complex task when checking result correctness (even partial checking only for some cases) is much easier then calculation itself. For example:

```python
from typing import List
Expand All @@ -38,4 +38,20 @@ def index_of(items: List[int], item: int) -> int:
raise LookupError
```

It allows you to simplify testing, easier check hypothesis, tell more about the function behavior.
Also, it's ok if you can check only some simple cases. For example, function `map` applies given function to the list. Let's check that count of returned elements is the same as the count of given elements:

```python
from typing import Callable, List

@deal.ensure(lambda: items, func, result: len(result) == len(items))
def map(items: List[str], func: Callable[[str], str]) -> List[str]:
...
```

Or if function `choice` returns random element from the list, we can't from one run check result randomness, but can't ensure that result is an element from the list:

```python
@deal.ensure(lambda items, result: result in items)
def choice(items: List[str]) -> str:
...
```
26 changes: 7 additions & 19 deletions docs/decorators/post.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,23 @@ always_positive_sum(2, -3, -4)

## Motivation

Make constraints about function response.
Post-condition allows to make additional constraints about function result. Use type annotations to limit types of result and post-conditions to limit possible values inside given types. Let's see a few examples.

Bad:
If function `count` returns count of elements that equal to given element, result is always non-negative.

```python
def get_usernames(role):
if role != 'admin':
return dict(code=403)
return dict(records=['oleg', 'greg', 'admin'])

def some_other_code(role):
response = get_usernames(role)
if 'code' not in response:
response['code'] = 200
@deal.post(lambda result: result >= 0)
def count(items: List[str], item: str) -> int:
...
```

Good:
Or you can make promise that your response always contains some specific fields:

```python
@deal.post(lambda resp: type(resp) is dict)
@deal.post(lambda resp: 'code' in resp)
@deal.post(lambda resp: 'records' in resp)
@deal.post(lambda result: 'code' in result)
@deal.post(lambda result: 'records' in result)
def get_usernames(role):
if role != 'admin':
return dict(code=403, records=[])
return dict(code=200, records=['oleg', 'greg', 'admin'])

def some_other_code(role):
response = get_usernames(role)
...
```
4 changes: 3 additions & 1 deletion docs/decorators/raises.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ divide()

## Motivation

Exceptions are the most explicit part of Python. Any code can raise any exception. None of the tools can say you which exceptions can be raised in some function. However, sometimes you can infer it yourself and say it to other people. And `deal.raises` will remind you if function has raised something that you forgot to specify.
Exceptions are the most explicit part of Python. Any code can raise any exception. None of the tools can say you which exceptions can be raised in some function. However, sometimes you can infer it yourself and say it to other people. And `@deal.raises` will remain you if function has raised something that you forgot to specify.

Also, it's the most important decorator for [autotesting](../testing.html). Deal won't fail tests for exceptions that was marked as allowed with `@deal.raises`.

Bad:

Expand Down
24 changes: 24 additions & 0 deletions docs/decorators/reason.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# reason

Checks condition if exception was raised.

```python
@deal.reason(ZeroDivisionError, lambda a, b: b == 0)
def divide(a, b):
return a / b
```

## Motivation

This is the [@deal.ensure](./ensure.html) for exceptions. It works perfect when it's easy to check correctness of conditions when exception is raised.

For example, if function `index_of` returns index of the first element that equal to the given element and raises `LookupError` if element is not found, we can check that if `LookupError` is raised, element not in the list:

```python
@deal.reason(LookupError, lambda items, item: item not in items)
def index_of(items: List[int], item: int) -> int:
for index, el in enumerate(items):
if el == item:
return index
raise LookupError
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
decorators/offline
decorators/pure
decorators/raises
decorators/reason
decorators/safe
decorators/silent
Expand Down
5 changes: 3 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
Deal can automatically test your functions. First of all, your function has to be prepared:

1. All function arguments are type-annotated.
1. All exceptions that function can raise are specified in [@deal.raises](decorators/raises).
1. All pre-conditions are specified with [@deal.pre](decorators/pre).
1. All exceptions that function can raise are specified in [@deal.raises](decorators/raises.html).
1. All pre-conditions are specified with [@deal.pre](decorators/pre.html).

Then use `deal.cases` to get test cases for the function. Every case is a callable object that gets no arguments. Calling it will call the original function with suppressing allowed exceptions.

Expand Down Expand Up @@ -103,6 +103,7 @@ contract_for_index_of = deal.chain(
),
# LookupError will be raised if no elements found
deal.raises(LookupError),
deal.reason(LookupError, lambda items, item: item not in items)
)
```

Expand Down

0 comments on commit 7b9f101

Please sign in to comment.