From a08566bb3ddb7bdca63c392f13126414d0661765 Mon Sep 17 00:00:00 2001 From: Gram Date: Thu, 7 May 2020 09:57:46 +0200 Subject: [PATCH 1/6] use base decorator in the linter contract template --- deal/linter/_contract.py | 10 ++++------ deal/linter/_template.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 deal/linter/_template.py diff --git a/deal/linter/_contract.py b/deal/linter/_contract.py index 8a9ca910..726e6ce5 100644 --- a/deal/linter/_contract.py +++ b/deal/linter/_contract.py @@ -2,15 +2,13 @@ import ast import builtins import enum +from pathlib import Path # external import astroid -TEMPLATE = """ -contract = PLACEHOLDER -result = contract(*args, **kwargs) -""" +TEMPLATE = (Path(__file__).parent / '_template.py').read_text() class Category(enum.Enum): @@ -72,7 +70,7 @@ def bytecode(self): # if contract is function, add it's definition and assign it's name # to `contract` variable. module.body = [contract] + module.body - module.body[1].value = ast.Name( + module.body[3].value = ast.Name( id=contract.name, lineno=1, col_offset=1, @@ -81,7 +79,7 @@ def bytecode(self): else: if isinstance(contract, ast.Expr): contract = contract.value - module.body[0].value = contract + module.body[2].value = contract return compile(module, filename='', mode='exec') def run(self, *args, **kwargs): diff --git a/deal/linter/_template.py b/deal/linter/_template.py new file mode 100644 index 00000000..4b55f069 --- /dev/null +++ b/deal/linter/_template.py @@ -0,0 +1,14 @@ +from deal import ContractError +from deal._decorators.base import Base + +contract = ... +base = Base(validator=contract) + +try: + base.validate(*args, **kwargs) # noqa: F821 +except ContractError as exc: + result = False + if exc.args: + result = exc.args[0] +else: + result = True From 13df5683de2fedfba5e02d1e9d9cd6bf9dfe61b0 Mon Sep 17 00:00:00 2001 From: Gram Date: Thu, 7 May 2020 10:57:36 +0200 Subject: [PATCH 2/6] inject function signature into contract --- deal/linter/_contract.py | 16 ++++++++++++--- deal/linter/_func.py | 44 ++++++++++++++++++++++++---------------- deal/linter/_template.py | 5 +++++ 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/deal/linter/_contract.py b/deal/linter/_contract.py index 726e6ce5..1cb6e7c9 100644 --- a/deal/linter/_contract.py +++ b/deal/linter/_contract.py @@ -19,14 +19,15 @@ class Category(enum.Enum): class Contract: - __slots__ = ('args', 'category') + __slots__ = ('args', 'category', 'func_args') - def __init__(self, args, category: Category): + def __init__(self, args, category: Category, func_args: ast.arguments = None): self.args = args self.category = category + self.func_args = func_args @property - def body(self): + def body(self) -> ast.AST: contract = self.args[0] # convert astroid node to ast node if hasattr(contract, 'as_string'): @@ -65,6 +66,14 @@ def exceptions(self) -> list: @property def bytecode(self): module = ast.parse(TEMPLATE) + + # inject function signature + if self.func_args is not None: + func = ast.parse('lambda:0').body[0].value + func.args = self.func_args + module.body[3].value = func + + # inject contract contract = self.body if isinstance(contract, ast.FunctionDef): # if contract is function, add it's definition and assign it's name @@ -80,6 +89,7 @@ def bytecode(self): if isinstance(contract, ast.Expr): contract = contract.value module.body[2].value = contract + return compile(module, filename='', mode='exec') def run(self, *args, **kwargs): diff --git a/deal/linter/_func.py b/deal/linter/_func.py index 47afd0d6..c7f84234 100644 --- a/deal/linter/_func.py +++ b/deal/linter/_func.py @@ -1,7 +1,7 @@ # built-in import ast from pathlib import Path -from typing import Iterable, List +from typing import Iterable, List, NamedTuple # external import astroid @@ -11,21 +11,14 @@ from ._extractors import get_contracts -TEMPLATE = """ -contract = PLACEHOLDER -result = contract(*args, **kwargs) -""" +class Func(NamedTuple): + name: str + args: ast.arguments + body: list + contracts: Iterable[Contract] - -class Func: - __slots__ = ('body', 'contracts', 'name', 'line', 'col') - - def __init__(self, *, body: list, contracts: Iterable[Contract], name: str, line: int, col: int): - self.body = body - self.contracts = contracts - self.name = name - self.line = line - self.col = col + line: int + col: int @classmethod def from_path(cls, path: Path) -> List['Func']: @@ -46,10 +39,15 @@ def from_ast(cls, tree: ast.Module) -> List['Func']: continue contracts = [] for category, args in get_contracts(expr.decorator_list): - contract = Contract(args=args, category=Category(category)) + contract = Contract( + args=args, + category=Category(category), + func_args=expr.args, + ) contracts.append(contract) funcs.append(cls( name=expr.name, + args=expr.args, body=expr.body, contracts=contracts, line=expr.lineno, @@ -63,13 +61,25 @@ def from_astroid(cls, tree: astroid.Module) -> List['Func']: for expr in tree.body: if not isinstance(expr, astroid.FunctionDef): continue + + # make signature + code = 'def f({}):0'.format(expr.args.as_string()) + func_args = ast.parse(code).body[0].args + + # collect contracts contracts = [] if expr.decorators: for category, args in get_contracts(expr.decorators.nodes): - contract = Contract(args=args, category=Category(category)) + contract = Contract( + args=args, + func_args=func_args, + category=Category(category), + ) contracts.append(contract) + funcs.append(cls( name=expr.name, + args=func_args, body=expr.body, contracts=contracts, line=expr.lineno, diff --git a/deal/linter/_template.py b/deal/linter/_template.py index 4b55f069..05582c4a 100644 --- a/deal/linter/_template.py +++ b/deal/linter/_template.py @@ -1,8 +1,13 @@ from deal import ContractError from deal._decorators.base import Base +# will be filled from the linter contract = ... +func = ... + base = Base(validator=contract) +if func is not Ellipsis: + base.function = func try: base.validate(*args, **kwargs) # noqa: F821 From 736bad64135ed009617eb4c7bf159e652a983c0a Mon Sep 17 00:00:00 2001 From: Gram Date: Thu, 7 May 2020 11:03:51 +0200 Subject: [PATCH 3/6] test a few cases --- tests/test_linter/test_contract.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_linter/test_contract.py b/tests/test_linter/test_contract.py index a04ee312..73731880 100644 --- a/tests/test_linter/test_contract.py +++ b/tests/test_linter/test_contract.py @@ -95,3 +95,43 @@ def f(x): c = func.contracts[0] assert c.run(1) is True assert c.run(-1) is False + + +def test_return_message(): + text = """ + import deal + + @deal.post(lambda x: x > 0 or 'oh no!') + def f(x): + return x + """ + text = dedent(text).strip() + funcs1 = Func.from_ast(ast.parse(text)) + assert len(funcs1) == 1 + funcs2 = Func.from_astroid(astroid.parse(text)) + assert len(funcs2) == 1 + for func in (funcs1[0], funcs2[0]): + assert len(func.contracts) == 1 + c = func.contracts[0] + assert c.run(1) is True + assert c.run(-1) == 'oh no!' + + +def test_simplified_signature(): + text = """ + import deal + + @deal.post(lambda _: _.a > _.b) + def f(a, b): + return a + b + """ + text = dedent(text).strip() + funcs1 = Func.from_ast(ast.parse(text)) + assert len(funcs1) == 1 + funcs2 = Func.from_astroid(astroid.parse(text)) + assert len(funcs2) == 1 + for func in (funcs1[0], funcs2[0]): + assert len(func.contracts) == 1 + c = func.contracts[0] + assert c.run(3, 2) is True + assert c.run(2, 3) is False From f5def9d7501380f713c6ed65f5afc3c7de91bb51 Mon Sep 17 00:00:00 2001 From: Gram Date: Thu, 7 May 2020 11:13:25 +0200 Subject: [PATCH 4/6] exclude template from coverage --- .coveragerc | 5 +++++ .gitignore | 2 +- deal/linter/_template.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..cda24be4 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +# https://coverage.readthedocs.io/en/coverage-5.0.4/config.html +[run] +# branch = True +omit = + deal/linter/_template.py diff --git a/.gitignore b/.gitignore index 1565a76c..d1f02065 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ __pycache__/ dist/ # coverage -.coverage* +.coverage coverage.xml htmlcov/ diff --git a/deal/linter/_template.py b/deal/linter/_template.py index 05582c4a..34aff6df 100644 --- a/deal/linter/_template.py +++ b/deal/linter/_template.py @@ -1,3 +1,5 @@ +# This file is excluded from coverage. + from deal import ContractError from deal._decorators.base import Base From 43e3364d08d889ff8cf6f0f950abcd4f95399e5f Mon Sep 17 00:00:00 2001 From: Gram Date: Thu, 7 May 2020 11:14:03 +0200 Subject: [PATCH 5/6] sort imports --- deal/linter/_rules.py | 5 ++--- deal/linter/_template.py | 2 ++ tests/test_linter/test_rules.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deal/linter/_rules.py b/deal/linter/_rules.py index fe626ab4..559bdcba 100644 --- a/deal/linter/_rules.py +++ b/deal/linter/_rules.py @@ -8,9 +8,8 @@ from ._contract import Category, Contract from ._error import Error from ._extractors import ( - get_exceptions, get_exceptions_stubs, get_globals, - get_imports, get_prints, get_returns, get_asserts, - has_returns, + get_asserts, get_exceptions, get_exceptions_stubs, get_globals, + get_imports, get_prints, get_returns, has_returns, ) from ._func import Func from ._stub import StubsManager diff --git a/deal/linter/_template.py b/deal/linter/_template.py index 34aff6df..ec334d4c 100644 --- a/deal/linter/_template.py +++ b/deal/linter/_template.py @@ -1,8 +1,10 @@ # This file is excluded from coverage. +# project from deal import ContractError from deal._decorators.base import Base + # will be filled from the linter contract = ... func = ... diff --git a/tests/test_linter/test_rules.py b/tests/test_linter/test_rules.py index 9d4ab7fd..5ab52185 100644 --- a/tests/test_linter/test_rules.py +++ b/tests/test_linter/test_rules.py @@ -8,7 +8,7 @@ # project from deal.linter._func import Func from deal.linter._rules import ( - CheckImports, CheckPrints, CheckPure, CheckRaises, CheckReturns, rules, CheckAsserts, + CheckAsserts, CheckImports, CheckPrints, CheckPure, CheckRaises, CheckReturns, rules, ) From fc63a81b55d4e0f856a467ece1c4219c0a48d402 Mon Sep 17 00:00:00 2001 From: Gram Date: Thu, 7 May 2020 11:24:53 +0200 Subject: [PATCH 6/6] fix typing --- deal/linter/_func.py | 2 +- deal/linter/_template.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deal/linter/_func.py b/deal/linter/_func.py index c7f84234..c6337a44 100644 --- a/deal/linter/_func.py +++ b/deal/linter/_func.py @@ -64,7 +64,7 @@ def from_astroid(cls, tree: astroid.Module) -> List['Func']: # make signature code = 'def f({}):0'.format(expr.args.as_string()) - func_args = ast.parse(code).body[0].args + func_args = ast.parse(code).body[0].args # type: ignore # collect contracts contracts = [] diff --git a/deal/linter/_template.py b/deal/linter/_template.py index ec334d4c..13cdc3f7 100644 --- a/deal/linter/_template.py +++ b/deal/linter/_template.py @@ -9,12 +9,12 @@ contract = ... func = ... -base = Base(validator=contract) +base = Base(validator=contract) # type: ignore if func is not Ellipsis: base.function = func try: - base.validate(*args, **kwargs) # noqa: F821 + base.validate(*args, **kwargs) # type: ignore # noqa: F821 except ContractError as exc: result = False if exc.args: