python-constricter 0.2.4__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constricter/__init__.py +1 -1
- constricter/cli/collecting.py +47 -0
- constricter/cli/command.py +116 -133
- constricter/cli/config.py +8 -15
- constricter/cli/hints.py +9 -103
- constricter/cli/options.py +1 -1
- constricter/cli/paths.py +31 -0
- constricter/cli/protocol.py +110 -0
- constricter/cli/runs.py +42 -0
- constricter/cli/schedule.py +222 -0
- constricter/cli/workers.py +154 -0
- constricter/fix/doubts.py +387 -0
- constricter/fix/fills.py +47 -45
- constricter/fix/fixes.py +60 -20
- constricter/fix/guesses.py +173 -63
- constricter/fix/imports.py +76 -25
- constricter/fix/inference.py +163 -197
- constricter/fix/known.py +91 -5
- constricter/fix/library.py +80 -0
- constricter/fix/members.py +207 -0
- constricter/fix/modules.py +252 -0
- constricter/fix/narrowed.py +164 -0
- constricter/fix/order.py +145 -0
- constricter/fix/overloads.py +394 -0
- constricter/fix/project.py +452 -207
- constricter/fix/returned.py +516 -55
- constricter/fix/returns.py +22 -29
- constricter/fix/stdlib.py +303 -215
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +675 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1939 -0
- constricter/fix/tables/method_overloads.json +353 -0
- constricter/fix/tables/method_signatures.json +655 -0
- constricter/fix/tables/methods.json +857 -0
- constricter/fix/tables/overloads.json +373 -0
- constricter/fix/tables/returns.json +1328 -0
- constricter/fix/tables/source.json +4 -0
- constricter/fix/tables/type_parameters.json +175 -0
- constricter/fix/targets.py +64 -8
- constricter/offences.py +5 -0
- constricter/rules/annotations.py +130 -11
- constricter/rules/binding.py +241 -0
- constricter/rules/checker.py +319 -330
- constricter/rules/flow.py +8 -2
- constricter/rules/late.py +288 -0
- constricter/rules/narrowing.py +48 -1
- constricter/rules/parsed.py +91 -0
- constricter/rules/rebinding.py +140 -0
- constricter/rules/redundant.py +2 -8
- constricter/rules/scope.py +211 -215
- constricter/rules/syntax.py +99 -4
- constricter/rules/walked.py +88 -18
- {python_constricter-0.2.4.dist-info → python_constricter-0.2.7.dist-info}/METADATA +9 -8
- python_constricter-0.2.7.dist-info/RECORD +75 -0
- python_constricter-0.2.4.dist-info/RECORD +0 -48
- {python_constricter-0.2.4.dist-info → python_constricter-0.2.7.dist-info}/WHEEL +0 -0
- {python_constricter-0.2.4.dist-info → python_constricter-0.2.7.dist-info}/entry_points.txt +0 -0
- {python_constricter-0.2.4.dist-info → python_constricter-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
constricter/__init__.py
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The garbage collector, as the command runs it: by hand, once per file checked.
|
|
3
|
+
|
|
4
|
+
A check keeps every file's tree (see `parsed`), and allocates a great deal for each file, little of it
|
|
5
|
+
cyclic. Left to itself, the collector looked through the kept trees again and again as the check
|
|
6
|
+
allocated: a third of the standard library's check (34s, 21s without). So while the command runs,
|
|
7
|
+
it's off: what indexing built is frozen (never looked through again), and after each file, only
|
|
8
|
+
what's been allocated since (the young generations) is collected, which bounds what a cycle can hold.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import gc
|
|
13
|
+
from collections.abc import Generator
|
|
14
|
+
from typing import Final
|
|
15
|
+
|
|
16
|
+
_YOUNG: Final = 1 # the generations collected after each file: 0 and 1, never the frozen or the old
|
|
17
|
+
_FREEZES: Final = hasattr(gc, "freeze") # PyPy's collector has no freezing
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextlib.contextmanager
|
|
21
|
+
def by_hand() -> Generator[None]:
|
|
22
|
+
"""Turn automatic collection off for the command's run, and back as it was after.
|
|
23
|
+
|
|
24
|
+
Yields:
|
|
25
|
+
Nothing: the run.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
was: bool = gc.isenabled()
|
|
29
|
+
gc.disable()
|
|
30
|
+
try:
|
|
31
|
+
yield
|
|
32
|
+
finally:
|
|
33
|
+
if _FREEZES:
|
|
34
|
+
gc.unfreeze()
|
|
35
|
+
if was:
|
|
36
|
+
gc.enable()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def indexed() -> None:
|
|
40
|
+
"""Freeze what indexing built (the kept trees, the index): it lives until the check takes it."""
|
|
41
|
+
if _FREEZES:
|
|
42
|
+
gc.freeze()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sweep() -> None:
|
|
46
|
+
"""Collect what the file just checked left behind (see the module docstring)."""
|
|
47
|
+
_ = gc.collect(_YOUNG)
|
constricter/cli/command.py
CHANGED
|
@@ -4,25 +4,27 @@
|
|
|
4
4
|
import codecs
|
|
5
5
|
import contextlib
|
|
6
6
|
import difflib
|
|
7
|
+
import importlib
|
|
7
8
|
import io
|
|
8
|
-
import itertools
|
|
9
9
|
import json
|
|
10
10
|
import sys
|
|
11
11
|
import tokenize
|
|
12
|
-
from collections.abc import Callable,
|
|
13
|
-
from
|
|
14
|
-
from dataclasses import dataclass, field, replace
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from dataclasses import replace
|
|
15
14
|
from functools import partial
|
|
16
15
|
from pathlib import Path
|
|
17
|
-
from typing import Final, NamedTuple, TextIO, TypeAlias, cast
|
|
16
|
+
from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
|
|
18
17
|
|
|
19
18
|
from constricter import notebook
|
|
20
|
-
from constricter.cli import baseline,
|
|
19
|
+
from constricter.cli import baseline, collecting, schedule
|
|
21
20
|
from constricter.cli.options import Mode, Options, Output
|
|
22
21
|
from constricter.cli.paths import STDIN, python_files
|
|
22
|
+
from constricter.cli.protocol import HintError
|
|
23
23
|
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
24
|
+
from constricter.cli.runs import BaselineRun, CheckRun, CoverageRun, FileRun
|
|
25
|
+
from constricter.cli.workers import Workers
|
|
24
26
|
from constricter.fix import fixes, project
|
|
25
|
-
from constricter.fix.known import Hints, Outside
|
|
27
|
+
from constricter.fix.known import Hints, Outside, Returns
|
|
26
28
|
from constricter.noqa import lines, unsuppressed
|
|
27
29
|
from constricter.offences import (
|
|
28
30
|
DEFAULT_CHECKS,
|
|
@@ -30,7 +32,10 @@ from constricter.offences import (
|
|
|
30
32
|
Edit,
|
|
31
33
|
Offence,
|
|
32
34
|
)
|
|
33
|
-
from constricter.rules.checker import Coverage, annotation_coverage,
|
|
35
|
+
from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
38
|
+
from constricter.cli import hints
|
|
34
39
|
|
|
35
40
|
EXIT_CLEAN: Final = 0
|
|
36
41
|
EXIT_FOUND: Final = 1
|
|
@@ -74,30 +79,29 @@ def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
|
|
|
74
79
|
return notebook.parse(raw, str(name)) if name.suffix == notebook.SUFFIX else (raw, [])
|
|
75
80
|
|
|
76
81
|
|
|
77
|
-
def
|
|
82
|
+
def checked_text(
|
|
78
83
|
raw: str,
|
|
79
84
|
name: Path,
|
|
80
85
|
checks: Checks = DEFAULT_CHECKS,
|
|
81
86
|
*,
|
|
82
87
|
outside: Outside | None = None,
|
|
83
|
-
) ->
|
|
84
|
-
"""
|
|
88
|
+
) -> Checked:
|
|
89
|
+
"""Check `raw`, the text of `name`: the offences no `# noqa` suppresses.
|
|
85
90
|
|
|
86
91
|
A notebook's code cells are checked as one module, and each offence placed in its cell.
|
|
87
92
|
Raises `ValueError` for a `.ipynb` that isn't a notebook.
|
|
88
93
|
|
|
89
94
|
Returns:
|
|
90
|
-
The unsuppressed offences, a notebook's placed in their cells
|
|
95
|
+
The unsuppressed offences, a notebook's placed in their cells, and what its functions return
|
|
96
|
+
(see `Checked`).
|
|
91
97
|
|
|
92
98
|
"""
|
|
93
99
|
source: str
|
|
94
100
|
where: list[notebook.Line]
|
|
95
101
|
source, where = _source(raw, name)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
return [_placed(o, where) for o in offences] if where else offences
|
|
102
|
+
checked: Checked = checked_source(source, str(name), checks, outside=outside)
|
|
103
|
+
offences: list[Offence] = unsuppressed(checked.offences, lines(source))
|
|
104
|
+
return checked._replace(offences=[_placed(o, where) for o in offences] if where else offences)
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def _placed(offence: Offence, where: list[notebook.Line]) -> Offence:
|
|
@@ -110,7 +114,8 @@ def _placed(offence: Offence, where: list[notebook.Line]) -> Offence:
|
|
|
110
114
|
"""
|
|
111
115
|
line: notebook.Line = where[offence.line - 1]
|
|
112
116
|
placed: Offence = replace(offence, line=line.line, cell=line.cell)
|
|
113
|
-
|
|
117
|
+
# A notebook's cells have no import block.
|
|
118
|
+
if offence.edit is not None and (offence.edit.imports or offence.edit.guarded):
|
|
114
119
|
return replace(placed, edit=None)
|
|
115
120
|
if offence.edit is not None and offence.edit.edit is Edit.DECLARE:
|
|
116
121
|
statement: int = where[offence.edit.span[0] - 1].line
|
|
@@ -175,34 +180,6 @@ def fix_file(path: Path, offences: Sequence[Offence]) -> int:
|
|
|
175
180
|
return count
|
|
176
181
|
|
|
177
182
|
|
|
178
|
-
@dataclass(frozen=True)
|
|
179
|
-
class _CheckRun:
|
|
180
|
-
"""What checking one file found (and fixed, or would fix), in check/fix/diff mode."""
|
|
181
|
-
|
|
182
|
-
results: list[Result] = field(default_factory=list[Result])
|
|
183
|
-
baselined: int = 0
|
|
184
|
-
fixed: int = 0 # --fix: how many offences it fixed
|
|
185
|
-
text: str = "" # --diff: the diff; --fix on standard input: the fixed source
|
|
186
|
-
error: str = ""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@dataclass(frozen=True)
|
|
190
|
-
class _BaselineRun:
|
|
191
|
-
"""One file's offences, unfiltered, for --write-baseline."""
|
|
192
|
-
|
|
193
|
-
found: list[Offence] = field(default_factory=list[Offence])
|
|
194
|
-
error: str = ""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
@dataclass(frozen=True)
|
|
198
|
-
class _CoverageRun:
|
|
199
|
-
"""One file's annotation coverage, for --coverage."""
|
|
200
|
-
|
|
201
|
-
coverage: Coverage | None = None
|
|
202
|
-
error: str = ""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
_FileRun: TypeAlias = _CheckRun | _BaselineRun | _CoverageRun
|
|
206
183
|
HINT_ROUNDS: Final = 4 # with `--fix --infer-with`: how many times each file is fixed, at most
|
|
207
184
|
|
|
208
185
|
|
|
@@ -222,15 +199,15 @@ def _shown_lines(raw: str, name: Path) -> dict[tuple[int | None, int], str]:
|
|
|
222
199
|
}
|
|
223
200
|
|
|
224
201
|
|
|
225
|
-
def _checked(path: Path, name: Path, checks: Checks, outside: Outside) -> tuple[str,
|
|
202
|
+
def _checked(path: Path, name: Path, checks: Checks, outside: Outside) -> tuple[str, Checked]:
|
|
226
203
|
"""Read `path` and check it as `name`; raises what reading or parsing it does.
|
|
227
204
|
|
|
228
205
|
Returns:
|
|
229
|
-
Its text, and
|
|
206
|
+
Its text, and what checking it found.
|
|
230
207
|
|
|
231
208
|
"""
|
|
232
209
|
raw: str = _read(path)
|
|
233
|
-
return raw,
|
|
210
|
+
return raw, checked_text(raw, name, checks, outside=outside)
|
|
234
211
|
|
|
235
212
|
|
|
236
213
|
def _read_checked(
|
|
@@ -238,20 +215,20 @@ def _read_checked(
|
|
|
238
215
|
name: Path,
|
|
239
216
|
checks: Checks,
|
|
240
217
|
outside: Outside,
|
|
241
|
-
) -> tuple[str,
|
|
218
|
+
) -> tuple[str, Checked, str]:
|
|
242
219
|
"""Read and check `path`, turning a read or parse error into a message instead of raising.
|
|
243
220
|
|
|
244
221
|
Returns:
|
|
245
|
-
Its text and
|
|
222
|
+
Its text and what checking it found (nothing on error), and the error message (empty on success).
|
|
246
223
|
|
|
247
224
|
"""
|
|
248
225
|
raw: str
|
|
249
|
-
|
|
226
|
+
checked: Checked
|
|
250
227
|
try:
|
|
251
|
-
raw,
|
|
228
|
+
raw, checked = _checked(path, name, checks, outside)
|
|
252
229
|
except (OSError, ValueError, SyntaxError) as error: # UnicodeDecodeError is a ValueError
|
|
253
|
-
return "", [], f"{name}: error: {error}"
|
|
254
|
-
return raw,
|
|
230
|
+
return "", Checked([], Returns()), f"{name}: error: {error}"
|
|
231
|
+
return raw, checked, ""
|
|
255
232
|
|
|
256
233
|
|
|
257
234
|
def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
|
|
@@ -273,7 +250,7 @@ def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options
|
|
|
273
250
|
]
|
|
274
251
|
|
|
275
252
|
|
|
276
|
-
def _check_path(path: Path, outside: Outside, options: Options) ->
|
|
253
|
+
def _check_path(path: Path, outside: Outside, options: Options) -> CheckRun:
|
|
277
254
|
"""Check (and fix, or diff) one file, given what's known of it from outside it.
|
|
278
255
|
|
|
279
256
|
Returns:
|
|
@@ -282,11 +259,24 @@ def _check_path(path: Path, outside: Outside, options: Options) -> _CheckRun:
|
|
|
282
259
|
"""
|
|
283
260
|
name: Path = options.input.name(path)
|
|
284
261
|
raw: str
|
|
285
|
-
|
|
262
|
+
checked: Checked
|
|
286
263
|
error: str
|
|
287
|
-
raw,
|
|
264
|
+
raw, checked, error = _read_checked(path, name, options.checks, outside)
|
|
288
265
|
if error:
|
|
289
|
-
return
|
|
266
|
+
return CheckRun(error=error)
|
|
267
|
+
return replace(_handled(path, (raw, name), checked.offences, options), returned=checked.returned)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], options: Options) -> CheckRun:
|
|
271
|
+
"""Report, fix or diff the offences found in `path` (`text`: its text, and its name).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
What was found, and fixed.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
raw: str
|
|
278
|
+
name: Path
|
|
279
|
+
raw, name = text
|
|
290
280
|
baselined: int
|
|
291
281
|
offences, baselined = options.filter.unbaselined(name, offences)
|
|
292
282
|
results: list[Result] = _results(raw, name, offences, options)
|
|
@@ -295,22 +285,22 @@ def _check_path(path: Path, outside: Outside, options: Options) -> _CheckRun:
|
|
|
295
285
|
r.offence for r in results if r.offence.edit is not None and (unsafe or not r.offence.unsafe)
|
|
296
286
|
]
|
|
297
287
|
if options.mode is Mode.DIFF:
|
|
298
|
-
return
|
|
288
|
+
return CheckRun(text=_diff(raw, name, fixing))
|
|
299
289
|
if options.mode is not Mode.FIX:
|
|
300
|
-
return
|
|
290
|
+
return CheckRun(results, baselined)
|
|
301
291
|
left: list[Result] = [r for r in results if r.offence not in fixing]
|
|
302
292
|
if path == STDIN: # the fixed source goes to stdout
|
|
303
|
-
return
|
|
293
|
+
return CheckRun(left, baselined, len(fixing), _fixed(raw, name, fixing).text)
|
|
304
294
|
try:
|
|
305
|
-
return
|
|
295
|
+
return CheckRun(left, baselined, fix_file(path, fixing))
|
|
306
296
|
except UnicodeEncodeError as failure: # the file is left as it was, its offences unfixed
|
|
307
297
|
# Its canonical name: PyPy reports `latin1` where CPython says `latin-1`.
|
|
308
298
|
encoding: str = codecs.lookup(failure.encoding).name
|
|
309
299
|
message: str = f"an annotation can't be written in its encoding, {encoding}; left as it was"
|
|
310
|
-
return
|
|
300
|
+
return CheckRun(results, baselined, error=f"{name}: error: {message}")
|
|
311
301
|
|
|
312
302
|
|
|
313
|
-
def _baseline_path(path: Path, outside: Outside, options: Options) ->
|
|
303
|
+
def _baseline_path(path: Path, outside: Outside, options: Options) -> BaselineRun:
|
|
314
304
|
"""Check one file, unfiltered, for --write-baseline.
|
|
315
305
|
|
|
316
306
|
Returns:
|
|
@@ -318,13 +308,13 @@ def _baseline_path(path: Path, outside: Outside, options: Options) -> _BaselineR
|
|
|
318
308
|
|
|
319
309
|
"""
|
|
320
310
|
name: Path = options.input.name(path)
|
|
321
|
-
|
|
311
|
+
checked: Checked
|
|
322
312
|
error: str
|
|
323
|
-
_,
|
|
324
|
-
return
|
|
313
|
+
_, checked, error = _read_checked(path, name, options.checks, outside)
|
|
314
|
+
return BaselineRun(error=error) if error else BaselineRun(checked.offences, returned=checked.returned)
|
|
325
315
|
|
|
326
316
|
|
|
327
|
-
def _cover_path(path: Path, _outside: Outside, options: Options) ->
|
|
317
|
+
def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
|
|
328
318
|
"""Count one file's typed first bindings.
|
|
329
319
|
|
|
330
320
|
Returns:
|
|
@@ -333,12 +323,12 @@ def _cover_path(path: Path, _outside: Outside, options: Options) -> _CoverageRun
|
|
|
333
323
|
"""
|
|
334
324
|
name: Path = options.input.name(path)
|
|
335
325
|
try:
|
|
336
|
-
return
|
|
326
|
+
return CoverageRun(annotation_coverage(_source(_read(path), name)[0], options.checks))
|
|
337
327
|
except (OSError, ValueError, SyntaxError) as error:
|
|
338
|
-
return
|
|
328
|
+
return CoverageRun(error=f"{name}: error: {error}")
|
|
339
329
|
|
|
340
330
|
|
|
341
|
-
def _check_all(options: Options) -> tuple[list[Path], list[
|
|
331
|
+
def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
342
332
|
"""Check every file (`--jobs` at a time), in order.
|
|
343
333
|
|
|
344
334
|
With `--infer-with`, the type checker's server runs throughout: it's asked for every file's
|
|
@@ -352,7 +342,7 @@ def _check_all(options: Options) -> tuple[list[Path], list[_FileRun]]:
|
|
|
352
342
|
|
|
353
343
|
"""
|
|
354
344
|
paths: list[Path] = list(python_files(options.input.paths, options.input.exclude))
|
|
355
|
-
check: Callable[[Path, Outside],
|
|
345
|
+
check: Callable[[Path, Outside], FileRun]
|
|
356
346
|
if options.mode is Mode.COVERAGE:
|
|
357
347
|
check = partial(_cover_path, options=options)
|
|
358
348
|
elif options.mode is Mode.WRITE_BASELINE:
|
|
@@ -363,20 +353,26 @@ def _check_all(options: Options) -> tuple[list[Path], list[_FileRun]]:
|
|
|
363
353
|
if not options.infer_with or options.mode in {Mode.COVERAGE, Mode.WRITE_BASELINE}:
|
|
364
354
|
return names, _checked_all(paths, check, options)[0]
|
|
365
355
|
session: hints.Session
|
|
366
|
-
with
|
|
367
|
-
|
|
356
|
+
# Imported only for `--infer-with`: it's slow to import, for every run's start.
|
|
357
|
+
with cast("type[hints.Session]", importlib.import_module("constricter.cli.hints").Session)(
|
|
358
|
+
options.infer_with,
|
|
359
|
+
Path.cwd(),
|
|
360
|
+
options.jobs,
|
|
361
|
+
options.infer_memory,
|
|
362
|
+
) as session:
|
|
363
|
+
runs: list[FileRun]
|
|
368
364
|
modules: project.Index
|
|
369
365
|
runs, modules = _checked_all(paths, check, options, session)
|
|
370
366
|
# The files the last round changed: only their hints can have changed.
|
|
371
367
|
again: list[int] = [
|
|
372
368
|
index
|
|
373
369
|
for index, run in enumerate(runs)
|
|
374
|
-
if options.mode is Mode.FIX and isinstance(run,
|
|
370
|
+
if options.mode is Mode.FIX and isinstance(run, CheckRun) and run.fixed and paths[index] != STDIN
|
|
375
371
|
]
|
|
376
372
|
for _ in range(HINT_ROUNDS - 1):
|
|
377
373
|
if not again:
|
|
378
374
|
break
|
|
379
|
-
redone: list[
|
|
375
|
+
redone: list[FileRun] = _checked_all(
|
|
380
376
|
[paths[i] for i in again],
|
|
381
377
|
check,
|
|
382
378
|
options,
|
|
@@ -384,20 +380,20 @@ def _check_all(options: Options) -> tuple[list[Path], list[_FileRun]]:
|
|
|
384
380
|
modules,
|
|
385
381
|
)[0]
|
|
386
382
|
index: int
|
|
387
|
-
run:
|
|
383
|
+
run: FileRun
|
|
388
384
|
for index, run in zip(again, redone, strict=True):
|
|
389
|
-
runs[index] = _merged(
|
|
390
|
-
again = [index for index, run in zip(again, redone, strict=True) if cast("
|
|
385
|
+
runs[index] = _merged(runs[index], run)
|
|
386
|
+
again = [index for index, run in zip(again, redone, strict=True) if cast("CheckRun", run).fixed]
|
|
391
387
|
return names, runs
|
|
392
388
|
|
|
393
389
|
|
|
394
390
|
def _checked_all(
|
|
395
391
|
paths: Sequence[Path],
|
|
396
|
-
check: Callable[[Path, Outside],
|
|
392
|
+
check: Callable[[Path, Outside], FileRun],
|
|
397
393
|
options: Options,
|
|
398
|
-
session: hints.Session | None = None,
|
|
394
|
+
session: "hints.Session | None" = None,
|
|
399
395
|
modules: project.Index | None = None,
|
|
400
|
-
) -> tuple[list[
|
|
396
|
+
) -> tuple[list[FileRun], project.Index]:
|
|
401
397
|
"""Check `paths` (`--jobs` at a time), with the `session`'s hints, and `modules` (else indexed).
|
|
402
398
|
|
|
403
399
|
Returns:
|
|
@@ -406,30 +402,35 @@ def _checked_all(
|
|
|
406
402
|
"""
|
|
407
403
|
hinted: dict[Path, tuple[Hints, ...]] = {} if session is None else session.hints(_texts(paths))
|
|
408
404
|
coverage: bool = options.mode is Mode.COVERAGE # needs nothing from the other files
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
stack: contextlib.ExitStack
|
|
406
|
+
with contextlib.ExitStack() as stack:
|
|
407
|
+
pool: Workers | None = None
|
|
408
|
+
if options.jobs != 1 and len(paths) > 1:
|
|
409
|
+
pool = stack.enter_context(Workers(paths, options.jobs))
|
|
410
|
+
if modules is None and pool:
|
|
411
|
+
modules = project.Index({}, []) if coverage else pool.index()
|
|
412
|
+
elif modules is None:
|
|
411
413
|
modules = project.Index({}, []) if coverage else project.index(paths)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def _merged(before: _CheckRun, after: _CheckRun) -> _CheckRun:
|
|
426
|
-
"""Join a file's two `--fix` rounds: what's left is the later round's, what's fixed is both's.
|
|
414
|
+
collecting.indexed()
|
|
415
|
+
return schedule.checked(
|
|
416
|
+
paths,
|
|
417
|
+
check,
|
|
418
|
+
pool,
|
|
419
|
+
(modules, hinted),
|
|
420
|
+
_merged if options.mode is Mode.FIX else None,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _merged(before: FileRun, after: FileRun) -> FileRun:
|
|
425
|
+
"""Join a file's two `--fix` rounds' `CheckRun`s: what's left is the later's, what's fixed is both's.
|
|
427
426
|
|
|
428
427
|
Returns:
|
|
429
428
|
The joined run.
|
|
430
429
|
|
|
431
430
|
"""
|
|
432
|
-
|
|
431
|
+
first: CheckRun = cast("CheckRun", before)
|
|
432
|
+
later: CheckRun = cast("CheckRun", after)
|
|
433
|
+
return replace(later, fixed=first.fixed + later.fixed)
|
|
433
434
|
|
|
434
435
|
|
|
435
436
|
def _texts(paths: Sequence[Path]) -> dict[Path, str]:
|
|
@@ -451,33 +452,14 @@ def _texts(paths: Sequence[Path]) -> dict[Path, str]:
|
|
|
451
452
|
return texts
|
|
452
453
|
|
|
453
454
|
|
|
454
|
-
def
|
|
455
|
-
modules: project.Index,
|
|
456
|
-
paths: Sequence[Path],
|
|
457
|
-
hinted: Mapping[Path, tuple[Hints, ...]],
|
|
458
|
-
) -> list[Outside]:
|
|
459
|
-
"""Find what's known of each file from outside it: what it imports from the others, and its hints.
|
|
460
|
-
|
|
461
|
-
Returns:
|
|
462
|
-
Each file's, in order.
|
|
463
|
-
|
|
464
|
-
"""
|
|
465
|
-
found: list[Outside] = []
|
|
466
|
-
path: Path
|
|
467
|
-
for path in paths:
|
|
468
|
-
imported: project.Imported = project.imported(modules, path)
|
|
469
|
-
found.append(Outside(imported.calls, imported.classes, hinted.get(path, ())))
|
|
470
|
-
return found
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def _report(options: Options, runs: Sequence[_FileRun], files: int) -> int:
|
|
455
|
+
def _report(options: Options, runs: Sequence[FileRun], files: int) -> int:
|
|
474
456
|
"""Print the results (`runs` is check/fix/diff mode's: every other mode has its own printing).
|
|
475
457
|
|
|
476
458
|
Returns:
|
|
477
459
|
The exit status.
|
|
478
460
|
|
|
479
461
|
"""
|
|
480
|
-
checked: Sequence[
|
|
462
|
+
checked: Sequence[CheckRun] = cast("Sequence[CheckRun]", runs)
|
|
481
463
|
results: list[Result] = [result for run in checked for result in run.results]
|
|
482
464
|
output: Output = options.output
|
|
483
465
|
text: bool = output.fmt in {Format.TEXT, Format.FULL}
|
|
@@ -502,14 +484,14 @@ def _report(options: Options, runs: Sequence[_FileRun], files: int) -> int:
|
|
|
502
484
|
return EXIT_FOUND if errors else EXIT_CLEAN
|
|
503
485
|
|
|
504
486
|
|
|
505
|
-
def _coverage(options: Options, paths: Sequence[Path], runs: Sequence[
|
|
487
|
+
def _coverage(options: Options, paths: Sequence[Path], runs: Sequence[FileRun]) -> int:
|
|
506
488
|
"""Print each file's and the total annotation coverage (`runs` is --coverage mode's).
|
|
507
489
|
|
|
508
490
|
Returns:
|
|
509
491
|
The exit status.
|
|
510
492
|
|
|
511
493
|
"""
|
|
512
|
-
covered: Sequence[
|
|
494
|
+
covered: Sequence[CoverageRun] = cast("Sequence[CoverageRun]", runs)
|
|
513
495
|
counted: list[tuple[Path, Coverage]] = [
|
|
514
496
|
(path, run.coverage) for path, run in zip(paths, covered, strict=True) if run.coverage is not None
|
|
515
497
|
]
|
|
@@ -545,18 +527,19 @@ def _run(options: Options) -> int:
|
|
|
545
527
|
|
|
546
528
|
"""
|
|
547
529
|
names: list[Path]
|
|
548
|
-
runs: list[
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
530
|
+
runs: list[FileRun]
|
|
531
|
+
with collecting.by_hand():
|
|
532
|
+
try:
|
|
533
|
+
names, runs = _check_all(options)
|
|
534
|
+
except HintError as error:
|
|
535
|
+
_ = sys.stderr.write(f"constricter: error: {error}\n")
|
|
536
|
+
return EXIT_ERROR
|
|
554
537
|
_ = sys.stderr.write("".join(f"{run.error}\n" for run in runs if run.error))
|
|
555
538
|
failed: bool = any(run.error for run in runs)
|
|
556
539
|
status: int
|
|
557
540
|
if options.mode is Mode.WRITE_BASELINE and options.filter.baseline_file is not None:
|
|
558
541
|
file: Path = options.filter.baseline_file
|
|
559
|
-
baselined: Sequence[
|
|
542
|
+
baselined: Sequence[BaselineRun] = cast("Sequence[BaselineRun]", runs)
|
|
560
543
|
found: dict[str, list[Offence]] = {
|
|
561
544
|
baseline.key(n, file): run.found for n, run in zip(names, baselined, strict=True)
|
|
562
545
|
}
|
|
@@ -565,12 +548,12 @@ def _run(options: Options) -> int:
|
|
|
565
548
|
elif options.mode is Mode.COVERAGE:
|
|
566
549
|
status = _coverage(options, names, runs)
|
|
567
550
|
elif options.mode is Mode.DIFF:
|
|
568
|
-
checked: Sequence[
|
|
551
|
+
checked: Sequence[CheckRun] = cast("Sequence[CheckRun]", runs)
|
|
569
552
|
diffs: str = "".join(run.text for run in checked)
|
|
570
553
|
_ = sys.stdout.write(diffs)
|
|
571
554
|
status = EXIT_FOUND if diffs else EXIT_CLEAN
|
|
572
555
|
elif options.mode is Mode.FIX and options.input.paths == [STDIN] and runs and not failed:
|
|
573
|
-
fixed:
|
|
556
|
+
fixed: CheckRun = cast("CheckRun", runs[0])
|
|
574
557
|
_ = sys.stdout.write(fixed.text) # standard input, fixed, is the whole output
|
|
575
558
|
status = EXIT_FOUND if any(r.offence.is_error(r.level) for r in fixed.results) else EXIT_CLEAN
|
|
576
559
|
else:
|
constricter/cli/config.py
CHANGED
|
@@ -7,7 +7,7 @@ from functools import partial
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
9
9
|
|
|
10
|
-
from constricter.cli.
|
|
10
|
+
from constricter.cli.protocol import SERVERS
|
|
11
11
|
from constricter.jsonc import is_int
|
|
12
12
|
from constricter.offences import FIX_KINDS, LEVELS, MESSAGES
|
|
13
13
|
|
|
@@ -165,24 +165,17 @@ def _levels(value: _Toml) -> dict[str, str] | None:
|
|
|
165
165
|
return None if None in levels.values() else {glob: str(level) for glob, level in levels.items()}
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def
|
|
169
|
-
|
|
170
|
-
return None
|
|
171
|
-
ignores: dict[str, list[str] | None] = {glob: _codes(codes) for glob, codes in value.items()}
|
|
172
|
-
return None if None in ignores.values() else {glob: list(codes or []) for glob, codes in ignores.items()}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _narrower(value: _Toml) -> dict[str, list[str]] | None:
|
|
176
|
-
"""Read a type hierarchy: each type, and the list of types it's narrower than.
|
|
168
|
+
def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str, list[str]] | None:
|
|
169
|
+
"""Read a table of lists, each as `read` reads one: `per-file-ignores`' codes, `narrower`'s types.
|
|
177
170
|
|
|
178
171
|
Returns:
|
|
179
|
-
It, or `None` if `value` isn't a table of
|
|
172
|
+
It, or `None` if `value` isn't a table, or `read` can't read one of its values.
|
|
180
173
|
|
|
181
174
|
"""
|
|
182
175
|
if not isinstance(value, dict):
|
|
183
176
|
return None
|
|
184
|
-
|
|
185
|
-
return None if None in
|
|
177
|
+
lists: dict[str, list[str] | None] = {key: read(item) for key, item in value.items()}
|
|
178
|
+
return None if None in lists.values() else {key: list(item or []) for key, item in lists.items()}
|
|
186
179
|
|
|
187
180
|
|
|
188
181
|
def _baseline(value: _Toml, root: Path) -> str | None:
|
|
@@ -211,8 +204,8 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
211
204
|
"type-comments": _flag,
|
|
212
205
|
"all-scopes": _flag,
|
|
213
206
|
"per-path-levels": _levels,
|
|
214
|
-
"per-file-ignores":
|
|
215
|
-
"narrower":
|
|
207
|
+
"per-file-ignores": partial(_lists, read=_codes),
|
|
208
|
+
"narrower": partial(_lists, read=_strings), # a type hierarchy: each type, and those it's narrower than
|
|
216
209
|
"infer-with": _checkers,
|
|
217
210
|
"infer-memory": _gigabytes,
|
|
218
211
|
}
|