Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve partial execution #57

Merged
merged 6 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# https://coverage.readthedocs.io/en/coverage-5.0.4/config.html
[run]
# branch = True
omit =
deal/linter/_template.py
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ __pycache__/
dist/

# coverage
.coverage*
.coverage
coverage.xml
htmlcov/

Expand Down
26 changes: 17 additions & 9 deletions deal/linter/_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -21,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'):
Expand Down Expand Up @@ -67,12 +66,20 @@ 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
# 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,
Expand All @@ -81,7 +88,8 @@ 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='<ast>', mode='exec')

def run(self, *args, **kwargs):
Expand Down
44 changes: 27 additions & 17 deletions deal/linter/_func.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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']:
Expand All @@ -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,
Expand All @@ -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 # type: ignore

# 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,
Expand Down
5 changes: 2 additions & 3 deletions deal/linter/_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions deal/linter/_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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 = ...

base = Base(validator=contract) # type: ignore
if func is not Ellipsis:
base.function = func

try:
base.validate(*args, **kwargs) # type: ignore # noqa: F821
except ContractError as exc:
result = False
if exc.args:
result = exc.args[0]
else:
result = True
40 changes: 40 additions & 0 deletions tests/test_linter/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/test_linter/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down