diff --git a/.travis.yml b/.travis.yml index d6225465..e992766c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,11 @@ language: python dist: xenial +# do not run Travis for PR's twice (as for push and as for PR) +branches: + only: + - master + before_install: # show a little bit more information about environment - sudo apt-get install -y tree diff --git a/deal/aliases.py b/deal/aliases.py index 8f8f5539..e7124bd2 100644 --- a/deal/aliases.py +++ b/deal/aliases.py @@ -1,6 +1,6 @@ from functools import partial -from .core import Pre, Post, Invariant, Raises, Offline, Silent +from .core import Pre, Post, Invariant, Raises, Offline, Silent, Ensure __all__ = [ @@ -14,7 +14,8 @@ require = pre = Pre -ensure = post = Post +post = Post +ensure = Ensure inv = invariant = Invariant raises = Raises diff --git a/deal/core.py b/deal/core.py index 6c3cacdb..65ee0040 100644 --- a/deal/core.py +++ b/deal/core.py @@ -299,3 +299,19 @@ def patched_function(self, *args, **kwargs): finally: sys.stdout = true_stdout sys.stderr = true_stderr + + +class Ensure(_Base): + """ + Check both arguments and result (validator) after function processing. + Validate arguments and output result. + """ + exception = exceptions.PostContractError + + def patched_function(self, *args, **kwargs): + """ + Step 3. Wrapped function calling. + """ + result = self.function(*args, **kwargs) + self.validate(*args, result=result, **kwargs) + return result diff --git a/docs/decorators/ensure.md b/docs/decorators/ensure.md new file mode 100644 index 00000000..24538c3e --- /dev/null +++ b/docs/decorators/ensure.md @@ -0,0 +1,41 @@ +# ensure + +Ensure is a [postcondition](./post) that aceepts not only result, but also function arguments. Must be true after function executed. Raises `PostContractError` otherwise. + +```python +@deal.ensure(lambda x, result: x != result) +def double(x): + return x * 2 + +double(2) +# 4 + +double(0) +# PostContractError: +``` + +## Motivation + +Perfect for complex task that easy to check. For example: + +```python +from typing import List + +# element at this position matches item +@deal.ensure( + lambda items, item, result: items[result] == item, + message='invalid match', +) +# element at this position is the first match +@deal.ensure( + lambda items, item, result: not any(el == item for el in items[:result]), + message='not the first match', +) +def index_of(items: List[int], item: int) -> int: + for index, element in enumerate(items): + if element == item: + return index + raise LookupError +``` + +It allows you to simplify testing, easier check hypothesis, tell more about the function behavior. diff --git a/docs/index.md b/docs/index.md index 253d52ef..c087d55a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ decorators/pre decorators/post + decorators/ensure decorators/inv .. toctree:: diff --git a/tests.py b/tests.py index 7405ba52..99a08add 100644 --- a/tests.py +++ b/tests.py @@ -460,5 +460,27 @@ def test_main(self): func(-2) +class EnsureTest(unittest.TestCase): + def test_main(self): + @deal.ensure(lambda a, b, result: a > 0 and b > 0 and result != 'same number') + def func(a, b): + if a == b: + return 'same number' + else: + return 'different numbers' + + with self.subTest(text='good'): + self.assertEqual(func(1, 2), 'different numbers') + with self.subTest(text='argument error on a'): + with self.assertRaises(deal.PostContractError): + func(0, 1) + with self.subTest(text='argument error on b'): + with self.assertRaises(deal.PostContractError): + func(1, 0) + with self.subTest(text='result error'): + with self.assertRaises(deal.PostContractError): + func(1, 1) + + if __name__ == '__main__': pytest.main(['tests.py'])