Skip to content

Commit

Permalink
Merge a60c3db into ba81509
Browse files Browse the repository at this point in the history
  • Loading branch information
orsinium committed Sep 29, 2020
2 parents ba81509 + a60c3db commit 4ddb218
Show file tree
Hide file tree
Showing 49 changed files with 1,098 additions and 534 deletions.
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -18,6 +18,8 @@
* [External validators support.][validators]
* [Contracts for importing modules.][module_load]
* [Can be enabled or disabled on production.][runtime]
* [Colorless](colorless): annotate only what you want. Hence, easy integration into an existing project.
* Partial execution: linter executes contracts to statically check possible values.

[values]: https://deal.readthedocs.io/basic/values.html
[exceptions]: https://deal.readthedocs.io/basic/exceptions.html
Expand All @@ -26,6 +28,7 @@
[validators]: https://deal.readthedocs.io/details/validators.html
[module_load]: https://deal.readthedocs.io/details/module_load.html
[runtime]: https://deal.readthedocs.io/basic/runtime.html
[colorless]: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/

## Deal in 30 seconds

Expand Down
12 changes: 6 additions & 6 deletions deal/_aliases.py
Expand Up @@ -47,7 +47,7 @@ def pre(
```
[wiki]: https://en.wikipedia.org/wiki/Precondition
[value]: ../basic/values.md
[value]: https://deal.readthedocs.io/basic/values.html
"""
cls = _decorators.Pre[_CallableType]
return cls(validator=validator, message=message, exception=exception)
Expand Down Expand Up @@ -90,7 +90,7 @@ def post(
```
[wiki]: https://en.wikipedia.org/wiki/Postcondition
[value]: ../basic/values.md
[value]: https://deal.readthedocs.io/basic/values.html
"""
cls = _decorators.Post[_CallableType]
return cls(validator=validator, message=message, exception=exception)
Expand Down Expand Up @@ -136,7 +136,7 @@ def ensure(
```
[wiki]: https://en.wikipedia.org/wiki/Postcondition
[value]: ../basic/values.md
[value]: https://deal.readthedocs.io/basic/values.html
"""
cls = _decorators.Ensure[_CallableType]
return cls(validator=validator, message=message, exception=exception)
Expand Down Expand Up @@ -184,7 +184,7 @@ def raises(
```
[exception] ../basic/exceptions.md
[exception] https://deal.readthedocs.io/basic/exceptions.html
"""
cls = _decorators.Raises[_CallableType]
return cls(*exceptions, message=message, exception=exception)
Expand Down Expand Up @@ -245,7 +245,7 @@ def reason(
```
[exception]: ../basic/exceptions.md
[exception]: https://deal.readthedocs.io/basic/exceptions.html
"""
cls = _decorators.Reason[_CallableType]
return cls(event=event, validator=validator, message=message, exception=exception)
Expand Down Expand Up @@ -308,7 +308,7 @@ def inv(
```
[wiki]: https://en.wikipedia.org/wiki/Class_invariant
[value]: ../basic/values.md
[value]: https://deal.readthedocs.io/basic/values.html
"""
cls = _decorators.Invariant[_CallableType]
return cls( # type: ignore
Expand Down
51 changes: 51 additions & 0 deletions deal/_cli/_common.py
@@ -0,0 +1,51 @@
# built-in
from pathlib import Path
from typing import Iterator

import pygments
from pygments.formatters import TerminalFormatter
from pygments.lexers import PythonLexer


COLORS = dict(
red='\033[91m',
green='\033[92m',
yellow='\033[93m',
blue='\033[94m',
magenta='\033[95m',
end='\033[0m',
)
NOCOLORS = dict(
red='',
green='',
yellow='',
blue='',
magenta='',
end='',
)


def highlight(source: str) -> str:
source = pygments.highlight(
code=source,
lexer=PythonLexer(),
formatter=TerminalFormatter(),
)
return source.rstrip()


def get_paths(path: Path) -> Iterator[Path]:
"""Recursively yields python files.
"""
if not path.exists():
raise FileNotFoundError(str(path))
if path.is_file():
if path.suffix == '.py':
yield path
return
for subpath in path.iterdir():
if subpath.name[0] == '.':
continue
if subpath.name == '__pycache__':
continue
yield from get_paths(subpath)
61 changes: 31 additions & 30 deletions deal/_cli/_lint.py
Expand Up @@ -8,38 +8,14 @@

# app
from ..linter import Checker
from ._common import get_paths, highlight, COLORS, NOCOLORS


COLORS = dict(
red='\033[91m',
green='\033[92m',
yellow='\033[93m',
blue='\033[94m',
magenta='\033[95m',
end='\033[0m',
)
TEMPLATE = ' {blue}{row}{end}:{blue}{col}{end} {magenta}{code}{end} {yellow}{text}{end}'
VALUE = ' {magenta}({value}){end}'
POINTER = '{magenta}^{end}'


def get_paths(path: Path) -> Iterator[Path]:
"""Recursively yields python files.
"""
if not path.exists():
raise FileNotFoundError(str(path))
if path.is_file():
if path.suffix == '.py':
yield path
return
for subpath in path.iterdir():
if subpath.name[0] == '.':
continue
if subpath.name == '__pycache__':
continue
yield from get_paths(subpath)


def get_errors(paths: Iterable[Union[str, Path]]) -> Iterator[dict]:
for arg in paths:
for path in get_paths(Path(arg)):
Expand All @@ -64,34 +40,59 @@ def get_errors(paths: Iterable[Union[str, Path]]) -> Iterator[dict]:
def get_parser() -> ArgumentParser:
parser = ArgumentParser(prog='python3 -m deal lint')
parser.add_argument('--json', action='store_true', help='json output')
parser.add_argument('--nocolor', action='store_true', help='colorless output')
parser.add_argument('paths', nargs='*', default='.')
return parser


def lint_command(argv: Sequence[str]) -> int:
"""Run linter against the given files.
```bash
python3 -m deal lint project/
```
Options:
+ `--json`: output violations as [json per line][ndjson].
+ `--nocolor`: output violations in human-friendly format but without colors.
Useful for running linter on CI.
Exit code is equal to the found violations count.
See [linter][linter] documentation for more details.
[ndjson]: http://ndjson.org/
[linter]: https://deal.readthedocs.io/basic/linter.html
"""
parser = get_parser()
args = parser.parse_args(argv)
prev = None
errors = list(get_errors(paths=args.paths))
colors = COLORS
if args.nocolor:
colors = NOCOLORS
for error in errors:
if args.json:
print(json.dumps(error))
continue

# print file path
if error['path'] != prev:
print('{green}{path}{end}'.format(**COLORS, **error))
print('{green}{path}{end}'.format(**colors, **error))
prev = error['path']

# print message
line = TEMPLATE.format(**COLORS, **error)
line = TEMPLATE.format(**colors, **error)
if error['value']:
line += VALUE.format(**COLORS, **error)
line += VALUE.format(**colors, **error)
print(line)

# print code line
pointer = ' ' * error['col'] + POINTER.format(**COLORS)
content = error['content'] + '\n' + pointer
pointer = ' ' * error['col'] + POINTER.format(**colors)
content = error['content']
if not args.nocolor:
content = highlight(content)
content += '\n' + pointer
content = indent(dedent(content), prefix=' ')
print(content)
return len(errors)
25 changes: 23 additions & 2 deletions deal/_cli/_stub.py
@@ -1,19 +1,40 @@
# built-in
from argparse import ArgumentParser
from pathlib import Path
from typing import Sequence
from typing import List, Sequence

# app
from ..linter import StubsManager, generate_stub
from ._common import get_paths


def stub_command(argv: Sequence[str]) -> int:
"""Generate stub files for the given Python files.
```bash
python3 -m deal stub project/
```
Options:
+ `--iterations`: how many time run stub generation against files.
Every new iteration uses results from the previous ones, improving the result.
Default: 1.
Exit code is 0. See [stubs][stubs] documentation for more details.
[stubs]: https://deal.readthedocs.io/details/stubs.html
"""
parser = ArgumentParser(prog='python3 -m deal stub')
parser.add_argument('--iterations', type=int, default=1)
parser.add_argument('paths', nargs='+')
args = parser.parse_args(argv)

paths = [Path(path) for path in args.paths]
paths: List[Path] = []
for arg in args.paths:
for path in get_paths(Path(arg)):
paths.append(path)

roots = list(StubsManager.default_paths) + list(set(paths))
stubs = StubsManager(paths=roots)

Expand Down
63 changes: 44 additions & 19 deletions deal/_cli/_test.py
Expand Up @@ -8,21 +8,16 @@
from traceback import format_exception
from typing import Iterator, Sequence, TextIO

import pygments
from pygments.formatters import TerminalFormatter
from pygments.lexers import PythonTracebackLexer

# app
from .._testing import cases
from ..linter._contract import Category
from ..linter._extractors.pre import format_call_args
from ..linter._func import Func


COLORS = dict(
red='\033[91m',
green='\033[92m',
yellow='\033[93m',
blue='\033[94m',
magenta='\033[95m',
end='\033[0m',
)
from ._common import get_paths, COLORS


@contextmanager
Expand Down Expand Up @@ -51,10 +46,19 @@ def get_func_names(path: Path) -> Iterator[str]:
yield func.name


def color_exception(text: str) -> str:
text = text.replace('deal._exceptions.', '')
return pygments.highlight(
code=text,
lexer=PythonTracebackLexer(),
formatter=TerminalFormatter(),
)


def print_exception(stream: TextIO) -> None:
lines = format_exception(*sys.exc_info())
text = indent(text=''.join(lines), prefix=' ')
text = '{red}{text}{end}'.format(text=text, **COLORS)
text = color_exception(''.join(lines))
text = indent(text=text, prefix=' ').rstrip()
print(text, file=stream)


Expand Down Expand Up @@ -89,6 +93,26 @@ def run_tests(path: Path, root: Path, count: int, stream: TextIO = sys.stdout) -
def test_command(
argv: Sequence[str], root: Path = None, stream: TextIO = sys.stdout,
) -> int:
"""Generate and run tests against pure functions.
```bash
python3 -m deal test project/
```
Function must be decorated by one of the following to be run:
+ `@deal.pure`
+ `@deal.has()` (without arguments)
Options:
+ `--count`: how many input values combinations should be checked.
Exit code is equal to count of failed test cases.
See [tests][tests] documentation for more details.
[tests]: https://deal.readthedocs.io/basic/tests.html
"""
if root is None: # pragma: no cover
root = Path()
parser = ArgumentParser(prog='python3 -m deal test')
Expand All @@ -97,11 +121,12 @@ def test_command(
args = parser.parse_args(argv)

failed = 0
for path in args.paths:
failed += run_tests(
path=Path(path),
root=root,
count=args.count,
stream=stream,
)
for arg in args.paths:
for path in get_paths(Path(arg)):
failed += run_tests(
path=Path(path),
root=root,
count=args.count,
stream=stream,
)
return failed

0 comments on commit 4ddb218

Please sign in to comment.