Skip to content

Commit

Permalink
Merge pull request #57 from life4/simple-signature-lint
Browse files Browse the repository at this point in the history
Improve partial execution
  • Loading branch information
orsinium committed May 7, 2020
2 parents d4c428a + fc63a81 commit ceb4ac4
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .coveragerc
@@ -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
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
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
@@ -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
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
@@ -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
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
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

0 comments on commit ceb4ac4

Please sign in to comment.