diff --git a/.gitignore b/.gitignore index 6b959603..629d2fe5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ htmlcov/ README.rst docs/build/ /setup.py +.dephell_report/ diff --git a/deal/core.py b/deal/core.py index c6d9eba9..09643044 100644 --- a/deal/core.py +++ b/deal/core.py @@ -7,7 +7,6 @@ from typing import Callable, Type from . import exceptions -from .schemes import is_scheme from .state import state @@ -28,30 +27,34 @@ def __init__(self, validator, *, message: str = None, def validate(self, *args, **kwargs) -> None: """ - Step 4 (6 for invariant). Process contract (validator) + Step 4. Process contract (validator) """ - # Schemes validation interface - if is_scheme(self.validator): - params = getcallargs(self.function, *args, **kwargs) - params.update(kwargs) - validator = self.validator(data=params, request=None) - if validator.is_valid(): - return - raise self.exception(validator.errors) - # Simple validation interface if hasattr(self.validator, 'is_valid'): - validator = self.validator(*args, **kwargs) - # is valid - if validator.is_valid(): - return - # is invalid - if hasattr(validator, 'errors'): - raise self.exception(validator.errors) - if hasattr(validator, '_errors'): - raise self.exception(validator._errors) - raise self.exception + self._vaa_validation(*args, **kwargs) + else: + self._simple_validation(*args, **kwargs) + + def _vaa_validation(self, *args, **kwargs) -> None: + params = kwargs.copy() + if hasattr(self, 'function'): + # detect original function + function = self.function + while hasattr(function, '__wrapped__'): + function = function.__wrapped__ + # assign *args to real names + params.update(getcallargs(function, *args, **kwargs)) + # drop args-kwargs, we already put them on the right places + for bad_name in ('args', 'kwargs'): + if bad_name in params and bad_name not in kwargs: + del params[bad_name] + + validator = self.validator(data=params) + if validator.is_valid(): + return + raise self.exception(validator.errors) + def _simple_validation(self, *args, **kwargs) -> None: validation_result = self.validator(*args, **kwargs) # is invalid (validator return error message) if isinstance(validation_result, str): @@ -171,6 +174,18 @@ def __setattr__(self, name: str, value): class Invariant(_Base): exception = exceptions.InvContractError + def validate(self, obj) -> None: + """ + Step 6. Process contract (validator) + """ + + if hasattr(self.validator, 'is_valid') and hasattr(obj, '__dict__'): + kwargs = obj.__dict__.copy() + kwargs.pop('_disable_patching', '') + self._vaa_validation(**kwargs) + else: + self._simple_validation(obj) + def validate_chain(self, *args, **kwargs) -> None: self.validate(*args, **kwargs) self.child_validator(*args, **kwargs) diff --git a/deal/schemes.py b/deal/schemes.py index 02ac51c7..aa714208 100644 --- a/deal/schemes.py +++ b/deal/schemes.py @@ -1,19 +1,8 @@ class Scheme: - def __init__(self, data, request=None): + def __init__(self, data): self.data = data def is_valid(self) -> bool: raise NotImplementedError # pragma: no cover - - -def is_scheme(obj) -> bool: - if not hasattr(obj, 'mro'): - return False - if Scheme in obj.mro(): - return True - for parent in obj.mro(): - if parent.__module__.startswith('djburger.'): - return True - return False diff --git a/docs/validators.md b/docs/validators.md index b313e9c2..1fb15a43 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -21,87 +21,20 @@ f(4) # PreContractError: name must be str ``` -## Simple validator +## External validators -It's almost like Django Forms, except initialization: +For a complex validation you can wrap your contract into [vaa](https://github.com/life4/vaa). It supports Marshmallow, WTForms, PyScheme etc. For example: ```python -class Validator: - def __init__(self, x): - self.x = x +import deal +import marshmallow +import vaa - def is_valid(self): - if self.x <= 0: - self.errors = ['x must be > 0'] - return False - return True - -@deal.pre(Validator) -def f(x): - return x * 2 - -f(5) -# 10 - -f(-5) -# PreContractError: ['x must be > 0'] -``` - -## Scheme - -Scheme is like simple validator, but `data` attribute contains dictionary with all passed arguments: - -```python -class NameScheme(Scheme): - def is_valid(self): - if not isinstance(self.data['name'], str): - self.errors = ['name must be str'] - return False - return True - - -@deal.pre(NameScheme) -def f(name): - return name * 2 - -f('Chris') -# 'ChrisChris' - -f(3) -# PreContractError: ['name must be str'] -``` - -Scheme automatically detect all arguments names: - -```python -class Printer(Scheme): - def is_valid(self): - print(self.data) - return True - - -@deal.pre(Printer) -def f(a, b, c=4, *args, **kwargs): - pass - -f(1, b=2, e=6) -{'args': (), 'a': 1, 'b': 2, 'c': 4, 'e': 6, 'kwargs': {'e': 6}} - -f(1, 2, 3, 4, 5, 6) -{'a': 1, 'b': 2, 'c': 3, 'args': (4, 5, 6), 'kwargs': {}} -``` - -## Marshmallow, WTForms, PyScheme etc. - -You can use any validators from [djburger](https://github.com/orsinium/djburger). See [validators documentation](https://djburger.readthedocs.io/en/latest/validators.html) and [list of supported external libraries](https://github.com/orsinium/djburger#external-libraries-support). For example, deal + djburger + [marshmallow](https://marshmallow.readthedocs.io/en/latest/): - -```python -import djburger, marshmallow - -class Scheme(djburger.v.b.Marshmallow): +@vaa.marshmallow +class Schema(marshmallow.Schema): name = marshmallow.fields.Str() -@deal.pre(Scheme) +@deal.pre(Schema) def func(name): return name * 2 @@ -111,5 +44,3 @@ func('Chris') func(123) # PreContractError: {'name': ['Not a valid string.']} ``` - -Djburger is Django independent. You can use it in any python projects. diff --git a/pyproject.toml b/pyproject.toml index 17ea19b6..75f6c905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ from = {format = "pip", path = "requirements-flake.txt"} python = ">=3.6" command = "flake8" - # generate report: # dephell venv run --env=tests coverage report --show-missing [tool.dephell.tests] @@ -61,11 +60,12 @@ classifiers=[ [tool.poetry.dependencies] python = ">=3.5" +vaa = ">=0.1.4" [tool.poetry.dev-dependencies] coverage = "*" -djburger = "*" marshmallow = "*" +pytest = "*" urllib3 = "*" m2r = "*" @@ -74,5 +74,16 @@ sphinx = "*" sphinx-rtd-theme = "*" [tool.poetry.extras] -tests = ["coverage", "djburger", "marshmallow", "urllib3"] -docs = ["urllib3", "m2r", "recommonmark", "sphinx", "sphinx-rtd-theme"] +tests = [ + "coverage", + "marshmallow", + "pytest", + "urllib3", +] +docs = [ + "m2r", + "recommonmark", + "sphinx-rtd-theme", + "sphinx", + "urllib3", +] diff --git a/tests.py b/tests.py index ffc183ec..d628e985 100644 --- a/tests.py +++ b/tests.py @@ -1,14 +1,11 @@ -import djburger +import unittest + import marshmallow import urllib3 +import vaa import deal -from deal.schemes import is_scheme - -try: - import unittest2 as unittest -except ImportError: - import unittest +import pytest class PreTest(unittest.TestCase): @@ -95,50 +92,6 @@ def _test_validator(self, validator): except deal.PreContractError as e: self.assertEqual(e.args[0], 'TEST') - def test_django_style(self): - class Validator: - def __init__(self, x): - self.x = x - - def is_valid(self): - if self.x <= 0: - self.errors = 'TEST' - return False - return True - - self._test_validator(Validator) - - def test_django_style_hidden_attr(self): - class Validator: - def __init__(self, x): - self.x = x - - def is_valid(self): - if self.x <= 0: - self._errors = 'TEST' - return False - return True - - self._test_validator(Validator) - - def test_django_style_without_attr(self): - class Validator: - def __init__(self, x): - self.x = x - - def is_valid(self): - if self.x <= 0: - return False - return True - - func = deal.pre(Validator)(lambda x: x) - with self.subTest(text='good'): - self.assertEqual(func(4), 4) - - with self.subTest(text='error'): - with self.assertRaises(deal.PreContractError): - func(-2) - def test_error_returning(self): func = deal.pre(lambda x: x > 0 or 'TEST')(lambda x: x) with self.subTest(text='good'): @@ -257,17 +210,10 @@ class A: class MarshmallowSchemeTests(unittest.TestCase): def setUp(self): - class _Scheme(djburger.v.b.Marshmallow): + class _Scheme(marshmallow.Schema): name = marshmallow.fields.Str() - self.Scheme = _Scheme - def test_detecting(self): - with self.subTest('is scheme'): - self.assertTrue(is_scheme(self.Scheme)) - with self.subTest('is func'): - self.assertFalse(is_scheme(deal.pre)) - with self.subTest('is class'): - self.assertFalse(is_scheme(deal.InvContractError)) + self.Scheme = vaa.marshmallow(_Scheme) def test_validation(self): @deal.pre(self.Scheme) @@ -287,6 +233,69 @@ def func(name): except deal.PreContractError as e: self.assertEqual(e.args[0], {'name': ['Not a valid string.']}) + def test_pre_chain(self): + @deal.pre(self.Scheme) + @deal.pre(lambda name: name != 'Oleg') + def func(name): + return name * 2 + + with self.subTest('simple call'): + self.assertEqual(func('Chris'), 'ChrisChris') + + with self.subTest('not passed first validation'): + with self.assertRaises(deal.PreContractError): + func(123) + + with self.subTest('not passed second validation'): + with self.assertRaises(deal.PreContractError): + func('Oleg') + + def test_invariant(self): + @deal.inv(self.Scheme) + class User: + name = '' + + user = User() + + with self.subTest('simple call'): + user.name = 'Chris' + + with self.subTest('not passed validation'): + with self.assertRaises(deal.InvContractError): + user.name = 123 + + with self.subTest('error message'): + try: + user.name = 123 + except deal.InvContractError as e: + self.assertEqual(e.args[0], {'name': ['Not a valid string.']}) + + def test_invariant_chain(self): + @deal.inv(lambda user: user.name != 'Oleg') + @deal.inv(self.Scheme) + @deal.inv(lambda user: user.name != 'Chris') + class User: + name = '' + + user = User() + with self.subTest('simple call'): + user.name = 'Gram' + + user = User() + with self.subTest('not passed first validation'): + with self.assertRaises(deal.InvContractError): + user.name = 'Oleg' + + user = User() + with self.subTest('not passed second validation'): + with self.assertRaises(deal.InvContractError): + user.name = 123 + + user = User() + with self.subTest('not passed third validation'): + with self.assertRaises(deal.InvContractError): + user.name = 'Chris' + def test_arg_passing(self): @deal.pre(self.Scheme) def func(name): @@ -474,4 +483,4 @@ def func(a, b): if __name__ == '__main__': - unittest.main() + pytest.main(['tests.py'])