Skip to content

Commit

Permalink
Merge pull request #13 from life4/vaa
Browse files Browse the repository at this point in the history
Vaa
  • Loading branch information
orsinium committed Oct 2, 2019
2 parents 7ee5dd6 + 9ba7ec1 commit 351e664
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 175 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ htmlcov/
README.rst
docs/build/
/setup.py
.dephell_report/
57 changes: 36 additions & 21 deletions deal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from typing import Callable, Type

from . import exceptions
from .schemes import is_scheme
from .state import state


Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 1 addition & 12 deletions deal/schemes.py
Original file line number Diff line number Diff line change
@@ -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
85 changes: 8 additions & 77 deletions docs/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
19 changes: 15 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 = "*"
Expand All @@ -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",
]

0 comments on commit 351e664

Please sign in to comment.