Skip to content

Commit 816be6a

Browse files
authored
libs: Add envoy.code.check[python*] (#264)
Signed-off-by: Ryan Northey <ryan@synca.io>
1 parent f1636e6 commit 816be6a

37 files changed

+1848
-645
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,19 @@ pypi: https://pypi.org/project/envoy.base.utils
110110
---
111111

112112

113-
#### [envoy.code_format.python_check](envoy.code_format.python_check)
113+
#### [envoy.code.check](envoy.code.check)
114114

115-
version: 0.0.10.dev0
115+
version: 0.0.1.dev0
116116

117-
pypi: https://pypi.org/project/envoy.code_format.python_check
117+
pypi: https://pypi.org/project/envoy.code.check
118118

119119
##### requirements:
120120

121121
- [abstracts](https://pypi.org/project/abstracts) >=0.0.12
122-
- [aio.core](https://pypi.org/project/aio.core) >=0.3.0
122+
- [aio.core](https://pypi.org/project/aio.core) >=0.5.0
123123
- [aio.run.checker](https://pypi.org/project/aio.run.checker) >=0.3.0
124+
- [aiofiles](https://pypi.org/project/aiofiles)
125+
- [envoy.base.utils](https://pypi.org/project/envoy.base.utils) >=0.0.13
124126
- [flake8](https://pypi.org/project/flake8)
125127
- [pep8-naming](https://pypi.org/project/pep8-naming)
126128
- [yapf](https://pypi.org/project/yapf)

deps/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ colorama
1010
coloredlogs
1111
docutils~=0.16.0
1212
envoy.base.utils>=0.1.0
13-
envoy.code_format.python_check>=0.0.5
1413
envoy.dependency.pip_check>=0.0.5
1514
envoy.distribution.distrotest>=0.0.7
1615
envoy.distribution.release>=0.0.6

envoy.code.check/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
pytooling_package("envoy.code.check")

envoy.code.check/README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
envoy.code.check
3+
================
4+
5+
Code checker used in Envoy proxy's CI

envoy.code.check/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.0.1-dev
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
pytooling_library(
3+
"envoy.code.check",
4+
dependencies=[
5+
"//deps:abstracts",
6+
"//deps:aio.core",
7+
"//deps:aio.run.checker",
8+
"//deps:envoy.base.utils",
9+
"//deps:flake8",
10+
"//deps:yapf",
11+
],
12+
sources=[
13+
"__init__.py",
14+
"abstract/__init__.py",
15+
"abstract/base.py",
16+
"abstract/checker.py",
17+
"abstract/flake8.py",
18+
"abstract/yapf.py",
19+
"checker.py",
20+
"cmd.py",
21+
"exceptions.py",
22+
"typing.py",
23+
],
24+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
from . import abstract, exceptions, typing
3+
from .abstract import (
4+
ACodeCheck,
5+
ACodeChecker,
6+
AFlake8Check,
7+
AYapfCheck)
8+
from .checker import (
9+
CodeChecker,
10+
Flake8Check,
11+
YapfCheck)
12+
from .cmd import run, main
13+
from . import checker
14+
15+
16+
__all__ = (
17+
"abstract",
18+
"ACodeCheck",
19+
"ACodeChecker",
20+
"AFlake8Check",
21+
"AYapfCheck",
22+
"checker",
23+
"exceptions",
24+
"CodeChecker",
25+
"Flake8Check",
26+
"main",
27+
"run",
28+
"typing",
29+
"YapfCheck")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
from .base import ACodeCheck
3+
from .checker import ACodeChecker
4+
from .flake8 import AFlake8Check
5+
from .yapf import AYapfCheck
6+
from . import (
7+
base,
8+
checker,
9+
flake8,
10+
yapf)
11+
12+
13+
__all__ = (
14+
"ACodeCheck",
15+
"ACodeChecker",
16+
"AFlake8Check",
17+
"AYapfCheck",
18+
"base",
19+
"checker",
20+
"flake8",
21+
"yapf")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
from typing import Dict, List, Set
3+
4+
import abstracts
5+
6+
from aio.core.directory import ADirectory
7+
from aio.core.functional import async_property
8+
9+
10+
class ACodeCheck(metaclass=abstracts.Abstraction):
11+
12+
def __init__(self, directory: ADirectory, fix: bool = False) -> None:
13+
self.directory = directory
14+
self._fix = fix
15+
16+
@async_property(cache=True)
17+
async def absolute_paths(self) -> Set[str]:
18+
return self.directory.absolute_paths(await self.files)
19+
20+
@async_property
21+
@abstracts.interfacemethod
22+
async def checker_files(self) -> Set[str]:
23+
raise NotImplementedError
24+
25+
@async_property(cache=True)
26+
async def files(self) -> Set[str]:
27+
files = await self.directory.files
28+
return (
29+
files & await self.checker_files
30+
if files
31+
else files)
32+
33+
@property
34+
def fix(self) -> bool:
35+
return self._fix
36+
37+
@async_property
38+
@abstracts.interfacemethod
39+
async def problem_files(self) -> Dict[str, List[str]]:
40+
raise NotImplementedError
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Abstract code checker."""
2+
3+
import abc
4+
import argparse
5+
import pathlib
6+
import re
7+
from functools import cached_property
8+
from typing import Dict, Optional, Pattern, Tuple, Type
9+
10+
import abstracts
11+
12+
from aio.core import directory as _directory
13+
from aio.run import checker
14+
15+
from envoy.code.check import abstract
16+
17+
18+
# This is excluding at least some of the things that `.gitignore` would.
19+
GREP_EXCLUDE_GLOBS = (r"\#*", r"\.#*", r"*~")
20+
GREP_EXCLUDE_DIR_GLOBS = (r"build", r"build*", r"generated", r"\.*", r"src")
21+
22+
23+
class ACodeChecker(
24+
checker.Checker,
25+
metaclass=abstracts.Abstraction):
26+
"""Code checker."""
27+
28+
checks = ("python_yapf", "python_flake8")
29+
30+
@property
31+
def all_files(self) -> bool:
32+
return self.args.all_files
33+
34+
@property
35+
def exclude_from_grep(self) -> Tuple[str, ...]:
36+
"""Globs to exclude when grepping, ignored if `git grep` is used."""
37+
return GREP_EXCLUDE_GLOBS if self.all_files else ()
38+
39+
@property
40+
def exclude_dirs_from_grep(self) -> Tuple[str, ...]:
41+
"""Glob directories to exclude when grepping, ignored if `git grep` is
42+
used."""
43+
return GREP_EXCLUDE_DIR_GLOBS if self.all_files else ()
44+
45+
@property
46+
def changed_since(self) -> Optional[str]:
47+
return self.args.since
48+
49+
@cached_property
50+
def directory(self) -> "_directory.ADirectory":
51+
"""Greppable directory - optionally in a git repo, depending on whether
52+
we want to look at all files.
53+
"""
54+
return self.directory_class(self.path, **self.directory_kwargs)
55+
56+
@property
57+
def directory_class(self) -> Type["_directory.ADirectory"]:
58+
return (
59+
self.fs_directory_class
60+
if self.all_files
61+
else self.git_directory_class)
62+
63+
@property
64+
def directory_kwargs(self) -> Dict:
65+
kwargs: Dict = dict(
66+
exclude_matcher=self.grep_excluding_re,
67+
path_matcher=self.grep_matching_re,
68+
exclude=self.exclude_from_grep,
69+
exclude_dirs=self.exclude_dirs_from_grep)
70+
if not self.all_files:
71+
kwargs["changed"] = self.changed_since
72+
return kwargs
73+
74+
@cached_property
75+
def flake8(self) -> "abstract.AFlake8Check":
76+
"""Flake8 checker."""
77+
return self.flake8_class(self.directory, fix=self.fix)
78+
79+
@property # type:ignore
80+
@abstracts.interfacemethod
81+
def flake8_class(self) -> Type["abstract.AFlake8Check"]:
82+
raise NotImplementedError
83+
84+
@property # type:ignore
85+
@abstracts.interfacemethod
86+
def fs_directory_class(self) -> Type["_directory.ADirectory"]:
87+
raise NotImplementedError
88+
89+
@property # type:ignore
90+
@abstracts.interfacemethod
91+
def git_directory_class(self) -> Type["_directory.AGitDirectory"]:
92+
raise NotImplementedError
93+
94+
@property
95+
def grep_excluding_re(self) -> Optional[Pattern[str]]:
96+
return self._grep_re(self.args.excluding)
97+
98+
@property
99+
def grep_matching_re(self) -> Optional[Pattern[str]]:
100+
return self._grep_re(self.args.matching)
101+
102+
@property
103+
@abc.abstractmethod
104+
def path(self) -> pathlib.Path:
105+
return super().path
106+
107+
@cached_property
108+
def yapf(self) -> "abstract.AYapfCheck":
109+
"""YAPF checker."""
110+
return self.yapf_class(self.directory, fix=self.fix)
111+
112+
@property # type:ignore
113+
@abstracts.interfacemethod
114+
def yapf_class(self) -> Type["abstract.AYapfCheck"]:
115+
raise NotImplementedError
116+
117+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
118+
super().add_arguments(parser)
119+
parser.add_argument("-a", "--all_files", action="store_true")
120+
parser.add_argument("-m", "--matching", action="append")
121+
parser.add_argument("-x", "--excluding", action="append")
122+
parser.add_argument("-s", "--since")
123+
124+
async def check_python_flake8(self) -> None:
125+
"""Check for flake8 issues."""
126+
await self._code_check(self.flake8)
127+
128+
async def check_python_yapf(self) -> None:
129+
"""Check for yapf issues."""
130+
await self._code_check(self.yapf)
131+
132+
@checker.preload(
133+
when=["python_flake8"])
134+
async def preload_flake8(self) -> None:
135+
await self.flake8.problem_files
136+
137+
@checker.preload(
138+
when=["python_yapf"])
139+
async def preload_yapf(self) -> None:
140+
await self.yapf.problem_files
141+
142+
async def _code_check(self, check: "abstract.ACodeCheck") -> None:
143+
problem_files = await check.problem_files
144+
for path in sorted(await check.files):
145+
if path in problem_files:
146+
self.error(
147+
self.active_check,
148+
problem_files[path])
149+
else:
150+
self.succeed(
151+
self.active_check,
152+
[f"🗸 {path}"])
153+
154+
def _grep_re(self, arg: Optional[str]) -> Optional[Pattern[str]]:
155+
# When using system `grep` we want to filter out at least some
156+
# of the files that .gitignore would.
157+
# TODO: use globs on cli and covert to re here
158+
return (
159+
re.compile("|".join(arg))
160+
if arg
161+
else None)

0 commit comments

Comments
 (0)