|
1 | 1 | #!/usr/bin/env python3 |
2 | | -""" |
3 | | -Runs precommit checks on the repository. |
4 | | -""" |
| 2 | +"""Run precommit checks on the repository.""" |
5 | 3 | import argparse |
6 | | -import concurrent.futures |
7 | | -import hashlib |
8 | 4 | import os |
9 | 5 | import pathlib |
| 6 | +import re |
10 | 7 | import subprocess |
11 | 8 | import sys |
12 | | -from typing import List, Union, Tuple # pylint: disable=unused-import |
13 | 9 |
|
14 | | -import yapf.yapflib.yapf_api |
15 | | - |
16 | | - |
17 | | -def compute_hash(text: str) -> str: |
18 | | - """ |
19 | | - :param text: to hash |
20 | | - :return: hash digest |
21 | | - """ |
22 | | - md5 = hashlib.md5() |
23 | | - md5.update(text.encode()) |
24 | | - return md5.hexdigest() |
25 | | - |
26 | | - |
27 | | -class Hasher: |
28 | | - """ |
29 | | - Hashes the source code files and reports if they differed to one of the previous hashings. |
30 | | - """ |
31 | | - |
32 | | - def __init__(self, source_dir: pathlib.Path, hash_dir: pathlib.Path) -> None: |
33 | | - self.source_dir = source_dir |
34 | | - self.hash_dir = hash_dir |
35 | | - |
36 | | - def __hash_dir(self, path: pathlib.Path) -> pathlib.Path: |
37 | | - """ |
38 | | - :param path: to a source file |
39 | | - :return: path to the file holding the hash of the source text |
40 | | - """ |
41 | | - if self.source_dir not in path.parents: |
42 | | - raise ValueError("Expected the path to be beneath the source directory {!r}, got: {!r}".format( |
43 | | - str(self.source_dir), str(path))) |
44 | | - |
45 | | - return self.hash_dir / path.relative_to(self.source_dir).parent / path.name |
46 | | - |
47 | | - def hash_differs(self, path: pathlib.Path) -> bool: |
48 | | - """ |
49 | | - :param path: to the source file |
50 | | - :return: True if the hash of the content differs to one of the previous hashings. |
51 | | - """ |
52 | | - hash_dir = self.__hash_dir(path=path) |
53 | | - |
54 | | - if not hash_dir.exists(): |
55 | | - return True |
56 | | - |
57 | | - prev_hashes = set([pth.name for pth in hash_dir.iterdir()]) |
58 | | - |
59 | | - new_hsh = compute_hash(text=path.read_text()) |
60 | | - |
61 | | - return not new_hsh in prev_hashes |
62 | | - |
63 | | - def update_hash(self, path: pathlib.Path) -> None: |
64 | | - """ |
65 | | - Hashes the file content and stores it on disk. |
66 | | -
|
67 | | - :param path: to the source file |
68 | | - :return: |
69 | | - """ |
70 | | - hash_dir = self.__hash_dir(path=path) |
71 | | - hash_dir.mkdir(exist_ok=True, parents=True) |
72 | | - |
73 | | - new_hsh = compute_hash(text=path.read_text()) |
74 | | - |
75 | | - pth = hash_dir / new_hsh |
76 | | - pth.write_text('passed') |
77 | 10 |
|
| 11 | +def main() -> int: |
| 12 | + """Execute the main routine.""" |
| 13 | + parser = argparse.ArgumentParser() |
| 14 | + parser.add_argument( |
| 15 | + "--overwrite", |
| 16 | + help="Overwrites the unformatted source files with the " |
| 17 | + "well-formatted code in place. If not set, " |
| 18 | + "an exception is raised if any of the files do not conform " |
| 19 | + "to the style guide.", |
| 20 | + action='store_true') |
78 | 21 |
|
79 | | -def check(path: pathlib.Path, py_dir: pathlib.Path, overwrite: bool) -> Union[None, str]: |
80 | | - """ |
81 | | - Runs all the checks on the given file. |
| 22 | + args = parser.parse_args() |
82 | 23 |
|
83 | | - :param path: to the source file |
84 | | - :param py_dir: path to the source files |
85 | | - :param overwrite: if True, overwrites the source file in place instead of reporting that it was not well-formatted. |
86 | | - :return: None if all checks passed. Otherwise, an error message. |
87 | | - """ |
88 | | - style_config = py_dir / 'style.yapf' |
| 24 | + overwrite = bool(args.overwrite) |
89 | 25 |
|
90 | | - report = [] |
| 26 | + repo_root = pathlib.Path(__file__).parent |
91 | 27 |
|
92 | | - # yapf |
93 | | - if not overwrite: |
94 | | - formatted, _, changed = yapf.yapflib.yapf_api.FormatFile( |
95 | | - filename=str(path), style_config=str(style_config), print_diff=True) |
| 28 | + # yapf: disable |
| 29 | + source_files = ( |
| 30 | + sorted((repo_root / "temppathlib").glob("**/*.py")) + |
| 31 | + sorted((repo_root / "tests").glob("**/*.py"))) |
| 32 | + # yapf: enable |
96 | 33 |
|
97 | | - if changed: |
98 | | - report.append("Failed to yapf {}:\n{}".format(path, formatted)) |
| 34 | + if overwrite: |
| 35 | + print('Removing trailing whitespace...') |
| 36 | + for pth in source_files: |
| 37 | + pth.write_text(re.sub(r'[ \t]+$', '', pth.read_text(), flags=re.MULTILINE)) |
| 38 | + |
| 39 | + print("YAPF'ing...") |
| 40 | + yapf_targets = ["tests", "temppathlib", "setup.py", "precommit.py"] |
| 41 | + if overwrite: |
| 42 | + # yapf: disable |
| 43 | + subprocess.check_call( |
| 44 | + ["yapf", "--in-place", "--style=style.yapf", "--recursive"] + |
| 45 | + yapf_targets, |
| 46 | + cwd=str(repo_root)) |
| 47 | + # yapf: enable |
99 | 48 | else: |
100 | | - yapf.yapflib.yapf_api.FormatFile(filename=str(path), style_config=str(style_config), in_place=True) |
101 | | - |
102 | | - # mypy |
103 | | - env = os.environ.copy() |
104 | | - env['PYTHONPATH'] = ":".join([py_dir.as_posix(), env.get("PYTHONPATH", "")]) |
105 | | - |
106 | | - proc = subprocess.Popen( |
107 | | - ['mypy', str(path), '--ignore-missing-imports'], |
108 | | - stdout=subprocess.PIPE, |
109 | | - stderr=subprocess.PIPE, |
110 | | - env=env, |
111 | | - universal_newlines=True) |
112 | | - stdout, stderr = proc.communicate() |
113 | | - if proc.returncode != 0: |
114 | | - report.append("Failed to mypy {}:\nOutput:\n{}\n\nError:\n{}".format(path, stdout, stderr)) |
115 | | - |
116 | | - # pylint |
117 | | - proc = subprocess.Popen( |
118 | | - ['pylint', str(path), '--rcfile={}'.format(py_dir / 'pylint.rc')], |
119 | | - stdout=subprocess.PIPE, |
120 | | - stderr=subprocess.PIPE, |
121 | | - universal_newlines=True) |
122 | | - |
123 | | - stdout, stderr = proc.communicate() |
124 | | - if proc.returncode != 0: |
125 | | - report.append("Failed to pylint {}:\nOutput:\n{}\n\nError:\n{}".format(path, stdout, stderr)) |
| 49 | + # yapf: disable |
| 50 | + subprocess.check_call( |
| 51 | + ["yapf", "--diff", "--style=style.yapf", "--recursive"] + |
| 52 | + yapf_targets, |
| 53 | + cwd=str(repo_root)) |
| 54 | + # yapf: enable |
126 | 55 |
|
127 | | - if len(report) > 0: |
128 | | - return "\n".join(report) |
129 | | - |
130 | | - return None |
| 56 | + print("Mypy'ing...") |
| 57 | + subprocess.check_call(["mypy", "--strict", "temppathlib", "tests"], cwd=str(repo_root)) |
131 | 58 |
|
| 59 | + print("Isort'ing...") |
| 60 | + # yapf: disable |
| 61 | + isort_files = map(str, source_files) |
| 62 | + # yapf: enable |
132 | 63 |
|
133 | | -def main() -> int: |
134 | | - """" |
135 | | - Main routine |
136 | | - """ |
137 | | - # pylint: disable=too-many-locals |
138 | | - parser = argparse.ArgumentParser() |
139 | | - parser.add_argument( |
140 | | - "--overwrite", |
141 | | - help="Overwrites the unformatted source files with the well-formatted code in place. " |
142 | | - "If not set, an exception is raised if any of the files do not conform to the style guide.", |
143 | | - action='store_true') |
| 64 | + # yapf: disable |
| 65 | + subprocess.check_call( |
| 66 | + ["isort", "--project", "temppathlib", '--line-width', '120'] + |
| 67 | + ([] if overwrite else ['--check-only']) + |
| 68 | + [str(pth) for pth in source_files]) |
| 69 | + # yapf: enable |
144 | 70 |
|
145 | | - parser.add_argument("--all", help="checks all the files even if they didn't change", action='store_true') |
| 71 | + print("Pydocstyle'ing...") |
| 72 | + subprocess.check_call(["pydocstyle", "temppathlib"], cwd=str(repo_root)) |
146 | 73 |
|
147 | | - args = parser.parse_args() |
| 74 | + print("Pylint'ing...") |
| 75 | + subprocess.check_call(["pylint", "--rcfile=pylint.rc", "tests", "temppathlib"], cwd=str(repo_root)) |
148 | 76 |
|
149 | | - overwrite = bool(args.overwrite) |
150 | | - check_all = bool(args.all) |
| 77 | + print("Testing...") |
| 78 | + env = os.environ.copy() |
| 79 | + env['ICONTRACT_SLOW'] = 'true' |
151 | 80 |
|
152 | | - py_dir = pathlib.Path(__file__).parent |
| 81 | + # yapf: disable |
| 82 | + subprocess.check_call( |
| 83 | + ["coverage", "run", |
| 84 | + "--source", "temppathlib", |
| 85 | + "-m", "unittest", "discover", "tests"], |
| 86 | + cwd=str(repo_root), |
| 87 | + env=env) |
| 88 | + # yapf: enable |
153 | 89 |
|
154 | | - hash_dir = py_dir / '.precommit_hashes' |
155 | | - hash_dir.mkdir(exist_ok=True) |
| 90 | + subprocess.check_call(["coverage", "report"]) |
156 | 91 |
|
157 | | - hasher = Hasher(source_dir=py_dir, hash_dir=hash_dir) |
| 92 | + print("Doctesting...") |
| 93 | + doctest_files = ([repo_root / "README.rst"] + sorted((repo_root / "temppathlib").glob("**/*.py"))) |
158 | 94 |
|
159 | | - # yapf: disable |
160 | | - pths = sorted( |
161 | | - list(py_dir.glob("*.py")) + |
162 | | - list((py_dir / 'tests').glob("*.py")) |
163 | | - ) |
164 | | - # yapf: enable |
| 95 | + for pth in doctest_files: |
| 96 | + subprocess.check_call([sys.executable, "-m", "doctest", str(pth)]) |
165 | 97 |
|
166 | | - # see which files changed: |
167 | | - pending_pths = [] # type: List[pathlib.Path] |
| 98 | + print("Checking setup.py sdist ...") |
| 99 | + subprocess.check_call([sys.executable, "setup.py", "sdist"], cwd=str(repo_root)) |
168 | 100 |
|
169 | | - if check_all: |
170 | | - pending_pths = pths |
171 | | - else: |
172 | | - for pth in pths: |
173 | | - if hasher.hash_differs(path=pth): |
174 | | - pending_pths.append(pth) |
175 | | - |
176 | | - print("There are {} file(s) that need to be individually checked...".format(len(pending_pths))) |
177 | | - |
178 | | - success = True |
179 | | - |
180 | | - futures_paths = [] # type: List[Tuple[concurrent.futures.Future, pathlib.Path]] |
181 | | - with concurrent.futures.ThreadPoolExecutor() as executor: |
182 | | - for pth in pending_pths: |
183 | | - future = executor.submit(fn=check, path=pth, py_dir=py_dir, overwrite=overwrite) |
184 | | - futures_paths.append((future, pth)) |
185 | | - |
186 | | - for future, pth in futures_paths: |
187 | | - report = future.result() |
188 | | - if report is None: |
189 | | - print("Passed all checks: {}".format(pth)) |
190 | | - hasher.update_hash(path=pth) |
191 | | - else: |
192 | | - print("One or more checks failed for {}:\n{}".format(pth, report)) |
193 | | - success = False |
194 | | - |
195 | | - success = subprocess.call(['python3', '-m', 'unittest', 'discover', (py_dir / 'tests').as_posix()]) == 0 and success |
196 | | - |
197 | | - if not success: |
198 | | - print("One or more checks failed.") |
199 | | - return 1 |
| 101 | + print("Checking with twine...") |
| 102 | + subprocess.check_call(["twine", "check", "dist/*"], cwd=str(repo_root)) |
200 | 103 |
|
201 | 104 | return 0 |
202 | 105 |
|
|
0 commit comments