Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into issue-44
Browse files Browse the repository at this point in the history
  • Loading branch information
orsinium committed Apr 21, 2020
2 parents f9966e1 + 4da4ce9 commit a3d91f4
Show file tree
Hide file tree
Showing 54 changed files with 462 additions and 107 deletions.
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ env: ENV=pytest
python:
- "3.6.7"
- "3.7"
- "3.8-dev"
- "3.8"

# run flake8
matrix:
Expand All @@ -41,3 +41,8 @@ matrix:
env: ENV=flake8
script:
- dephell venv run --env=$ENV

- python: "3.7"
env: ENV=typing
script:
- dephell venv run --env=$ENV
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ That's nice `assert` statements in decorators style to validate function input,

CLassic DbC:

* [`@deal.pre`](https://deal.readthedocs.io/decorators/pre.html) -- validate function arguments (pre-condition)
* [`@deal.post`](https://deal.readthedocs.io/decorators/post.html) -- validate function return value (post-condition)
* [`@deal.ensure`](https://deal.readthedocs.io/decorators/ensure.html) -- post-condition that accepts not only result, but also function arguments.
* [`@deal.inv`](https://deal.readthedocs.io/decorators/inv.html) -- validate object internal state (invariant).
* [deal.pre](https://deal.readthedocs.io/decorators/pre.html) -- validate function arguments (pre-condition)
* [deal.post](https://deal.readthedocs.io/decorators/post.html) -- validate function return value (post-condition)
* [deal.ensure](https://deal.readthedocs.io/decorators/ensure.html) -- post-condition that accepts not only result, but also function arguments.
* [deal.inv](https://deal.readthedocs.io/decorators/inv.html) -- validate object internal state (invariant).

Take more control:

* [`@deal.module_load`](https://deal.readthedocs.io/decorators/module_load.html) -- check contracts at module initialization.
* [`@deal.offline`](https://deal.readthedocs.io/decorators/offline.html) -- forbid network requests
* [`@deal.raises`](https://deal.readthedocs.io/decorators/raises.html) -- allow only list of exceptions
* [`@deal.reason`](https://deal.readthedocs.io/decorators/reason.html) -- check function arguments that caused a given exception.
* [`@deal.silent`](https://deal.readthedocs.io/decorators/silent.html) -- forbid output into stderr/stdout.
* [deal.module_load](https://deal.readthedocs.io/decorators/module_load.html) -- check contracts at module initialization.
* [deal.offline](https://deal.readthedocs.io/decorators/offline.html) -- forbid network requests
* [deal.raises](https://deal.readthedocs.io/decorators/raises.html) -- allow only list of exceptions
* [deal.reason](https://deal.readthedocs.io/decorators/reason.html) -- check function arguments that caused a given exception.
* [deal.silent](https://deal.readthedocs.io/decorators/silent.html) -- forbid output into stderr/stdout.

Helpers:

Expand Down
6 changes: 3 additions & 3 deletions deal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

# main package info
__title__ = 'deal'
__version__ = '3.6.0'
__version__ = '3.6.1'
__author__ = 'Gram Orsinium'
__license__ = 'MIT'

Expand All @@ -26,9 +26,9 @@
pre, pure, raises, reason, require, safe, silent,
)
from ._exceptions import * # noQA
from ._imports import activate, module_load
from ._schemes import Scheme
from ._state import reset, switch
from ._imports import module_load, activate
from ._testing import TestCase, cases


Expand All @@ -46,14 +46,14 @@
'offline',
'post',
'pre',
'pure',
'raises',
'reason',
'safe',
'silent',

# aliases
'invariant',
'pure',
'require',

# module level
Expand Down
56 changes: 47 additions & 9 deletions deal/_decorators/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# built-in
import inspect
from asyncio import iscoroutinefunction
from contextlib import suppress
from functools import update_wrapper
from inspect import getcallargs, isgeneratorfunction
from typing import Callable, Type
from typing import Callable

# external
import vaa

# app
from .._exceptions import ContractError
Expand All @@ -13,18 +17,34 @@
class Base:
exception: ExceptionType = ContractError

def __init__(self, validator: Callable, *, message: str = None,
exception: Type[ExceptionType] = None, debug: bool = False):
def __init__(self, validator, *, message: str = None,
exception: ExceptionType = None, debug: bool = False):
"""
Step 1. Set contract (validator).
"""
self.validator = validator
self.validator = self._make_validator(validator, message=message)
self.debug = debug
if exception:
self.exception = exception
if message:
self.exception = self.exception(message) # type: ignore

@staticmethod
def _make_validator(validator, message: str = None):
if validator is None:
return None
# implicitly wrap in vaa all external validators
with suppress(TypeError):
return vaa.wrap(validator, simple=False)

# implicitly wrap in vaa.simple only funcs with one `_` argument.
if inspect.isfunction(validator):
params = inspect.signature(validator).parameters
if set(params) == {'_'}:
return vaa.simple(validator, error=message)

return validator

def validate(self, *args, **kwargs) -> None:
"""
Step 4. Process contract (validator)
Expand All @@ -37,24 +57,42 @@ def validate(self, *args, **kwargs) -> None:

def _vaa_validation(self, *args, **kwargs) -> None:
params = kwargs.copy()

# if it is a decorator for a function, convert positional args into named ones.
if hasattr(self, 'function'):
# detect original function
function = self.function
while hasattr(function, '__wrapped__'):
function = function.__wrapped__ # type: ignore
# assign *args to real names
params.update(getcallargs(function, *args, **kwargs))
params.update(inspect.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]

# validate
validator = self.validator(data=params)
if validator.is_valid():
return

# if no errors returned, raise the default exception
errors = validator.errors
if not errors:
raise self.exception

# Flatten single error without field to one simple str message.
# This is for better readability of simple validators.
if type(errors) is list:
if type(errors[0]) is vaa.Error:
if len(errors) == 1:
if errors[0].field is None:
errors = errors[0].message

# raise errors
if isinstance(self.exception, Exception):
raise type(self.exception)(validator.errors)
raise self.exception(validator.errors)
raise type(self.exception)(errors)
raise self.exception(errors)

def _simple_validation(self, *args, **kwargs) -> None:
validation_result = self.validator(*args, **kwargs)
Expand Down Expand Up @@ -101,6 +139,6 @@ def wrapped_generator(*args, **kwargs):

if iscoroutinefunction(function):
return update_wrapper(async_wrapped, function)
if isgeneratorfunction(function):
if inspect.isgeneratorfunction(function):
return update_wrapper(wrapped_generator, function)
return update_wrapper(wrapped, function)
6 changes: 3 additions & 3 deletions deal/_decorators/inv.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def __setattr__(self, name: str, value):
class Invariant(Base):
exception: ExceptionType = InvContractError

def validate(self, obj) -> None:
def validate(self, obj) -> None: # type: ignore
"""
Step 6. Process contract (validator)
"""
Expand All @@ -81,15 +81,15 @@ def validate_chain(self, *args, **kwargs) -> None:
self.validate(*args, **kwargs)
self.child_validator(*args, **kwargs)

def __call__(self, _class: type):
def __call__(self, _class: type): # type: ignore
"""
Step 2. Return wrapped class.
"""
# patch class parents and add method for validation

# if already invarianted
if hasattr(_class, '_validate_base'):
self.child_validator = _class._validate_base
self.child_validator = _class._validate_base # type: ignore
patched_class = type(
_class.__name__,
(_class, ),
Expand Down
2 changes: 1 addition & 1 deletion deal/_decorators/offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self, *, message: str = None, exception: ExceptionType = None, debu
Step 1. Init params.
"""
super().__init__(
validator=None,
validator=None, # type: ignore
message=message,
exception=exception,
debug=debug,
Expand Down
2 changes: 1 addition & 1 deletion deal/_decorators/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self, *exceptions, message: str = None, exception: ExceptionType =
"""
self.exceptions: Tuple[Type[Exception], ...] = exceptions
super().__init__(
validator=None,
validator=None, # type: ignore
message=message,
exception=exception,
debug=debug,
Expand Down
25 changes: 14 additions & 11 deletions deal/_imports.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# built-in
import ast
import sys
from _frozen_importlib_external import PathFinder
from types import ModuleType
from typing import Callable, Optional, List
from typing import Any, Callable, List, Optional

from .linter._extractors.common import get_name
# project
from _frozen_importlib_external import PathFinder

# app
from . import _aliases
from ._state import state
from .linter._extractors.common import get_name


def _enabled(debug: bool = False) -> bool:
Expand Down Expand Up @@ -60,10 +63,10 @@ def exec_module(self, module: ModuleType) -> None:

@staticmethod
def _get_contracts(tree: ast.Module) -> List[ast.AST]:
for node in tree.body:
if not type(node) is ast.Expr:
for node in tree.body: # type: Any
if type(node) is not ast.Expr:
continue
if not type(node.value) is ast.Call:
if type(node.value) is not ast.Call:
continue
if get_name(node.value.func) != 'deal.module_load':
continue
Expand All @@ -74,12 +77,12 @@ def _get_contracts(tree: ast.Module) -> List[ast.AST]:
def _exec_contract(cls, node: ast.AST) -> Optional[Callable]:
"""Get AST node and return a contract function
"""
if type(node) is ast.Call and not node.args:
return cls._exec_contract(node.func)
if type(node) is ast.Call and not node.args: # type: ignore
return cls._exec_contract(node.func) # type: ignore

if not isinstance(node, ast.Attribute):
return None
if node.value.id != 'deal':
if node.value.id != 'deal': # type: ignore
return None
contract = getattr(_aliases, node.attr, None)
if contract is None:
Expand Down Expand Up @@ -109,7 +112,7 @@ def activate(debug: bool = False) -> bool:
if DealFinder in sys.meta_path:
return False
index = sys.meta_path.index(PathFinder)
sys.meta_path[index] = DealFinder
sys.meta_path[index] = DealFinder # type: ignore
return True


Expand All @@ -118,6 +121,6 @@ def deactivate() -> bool:
"""
if DealFinder not in sys.meta_path:
return False
index = sys.meta_path.index(DealFinder)
index = sys.meta_path.index(DealFinder) # type: ignore
sys.meta_path[index] = PathFinder
return True
1 change: 1 addition & 0 deletions deal/linter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# app
from ._checker import Checker


Expand Down
3 changes: 3 additions & 0 deletions deal/linter/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# built-in
import sys

# app
from ._cli import main


if __name__ == '__main__':
exit(main(sys.argv[1:]))
29 changes: 20 additions & 9 deletions deal/linter/_checker.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
# built-in
import ast
import typing
from pathlib import Path

from astroid import AstroidSyntaxError

# app
from ._error import Error
from ._func import Func
from ._rules import rules, Required
from ._rules import Required, rules


class Checker:
name = 'deal'
version = '1.0.0'
_tree = None
_rules = rules

def __init__(self, tree: ast.AST, file_tokens=None, filename: str = 'stdin'):
def __init__(self, tree: ast.Module, file_tokens=None, filename: str = 'stdin'):
self._tree = tree
self._filename = filename

@property
def version(self):
import deal

return deal.__version__

def run(self) -> typing.Iterator[tuple]:
for error in self.get_errors():
yield tuple(error) + (type(self),) # type: ignore

def get_errors(self) -> typing.Iterator[Error]:
def get_funcs(self) -> typing.List['Func']:
if self._filename == 'stdin':
funcs = Func.from_ast(tree=self._tree)
else:
funcs = Func.from_path(path=Path(self._filename))
return Func.from_ast(tree=self._tree)
try:
return Func.from_path(path=Path(self._filename))
except AstroidSyntaxError:
return Func.from_ast(tree=self._tree)

for func in funcs:
def get_errors(self) -> typing.Iterator[Error]:
for func in self.get_funcs():
for rule in self._rules:
if rule.required != Required.FUNC:
continue
Expand Down
6 changes: 4 additions & 2 deletions deal/linter/_cli.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# built-in
import ast
import json
from argparse import ArgumentParser
from pathlib import Path
from textwrap import dedent, indent
from typing import Iterable, Iterator, Union
from typing import Iterable, Iterator, Sequence, Union

# app
from ._checker import Checker


Expand Down Expand Up @@ -65,7 +67,7 @@ def get_parser() -> ArgumentParser:
return parser


def main(argv: Iterable) -> int:
def main(argv: Sequence[str]) -> int:
parser = get_parser()
args = parser.parse_args(argv)
prev = None
Expand Down
Loading

0 comments on commit a3d91f4

Please sign in to comment.