python-constricter 0.4.1__cp314-cp314-win_amd64.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 +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp314-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp314-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp314-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp314-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp314-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp314-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp314-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp314-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp314-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp314-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp314-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp314-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp314-win_amd64.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The `constricter` command (see README)."""
|
|
3
|
+
|
|
4
|
+
import codecs
|
|
5
|
+
import contextlib
|
|
6
|
+
import difflib
|
|
7
|
+
import importlib
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable, Sequence
|
|
11
|
+
from dataclasses import replace
|
|
12
|
+
from functools import partial
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
|
|
15
|
+
|
|
16
|
+
from constricter import notebook
|
|
17
|
+
from constricter.cli import baseline, collecting, schedule
|
|
18
|
+
from constricter.cli.lsp.protocol import HintError
|
|
19
|
+
from constricter.cli.options import Mode, Options, Output
|
|
20
|
+
from constricter.cli.paths import STDIN, python_files
|
|
21
|
+
from constricter.cli.reading import encoding_of, read_text
|
|
22
|
+
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
23
|
+
from constricter.cli.rounds import checked_all, settled
|
|
24
|
+
from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
|
|
25
|
+
from constricter.cli.workers import kept
|
|
26
|
+
from constricter.fix.core import fixes
|
|
27
|
+
from constricter.fix.core.known import Outside, Returns
|
|
28
|
+
from constricter.noqa import lines, unsuppressed
|
|
29
|
+
from constricter.offences import (
|
|
30
|
+
DEFAULT_CHECKS,
|
|
31
|
+
Checks,
|
|
32
|
+
Edit,
|
|
33
|
+
Offence,
|
|
34
|
+
)
|
|
35
|
+
from constricter.rules import parsed
|
|
36
|
+
from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
39
|
+
from constricter.cli.lsp import hints
|
|
40
|
+
|
|
41
|
+
EXIT_CLEAN: Final = 0
|
|
42
|
+
EXIT_FOUND: Final = 1
|
|
43
|
+
EXIT_ERROR: Final = 2
|
|
44
|
+
_ALL: Final = 100 # percent
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
|
|
48
|
+
"""Extract the Python in `raw`: a notebook's code cells, joined.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The source, and each line's cell if it's a notebook.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
return notebook.parse(raw, str(name)) if name.suffix == notebook.SUFFIX else (raw, [])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def checked_text(
|
|
58
|
+
raw: str,
|
|
59
|
+
name: Path,
|
|
60
|
+
checks: Checks = DEFAULT_CHECKS,
|
|
61
|
+
*,
|
|
62
|
+
outside: Outside | None = None,
|
|
63
|
+
) -> Checked:
|
|
64
|
+
"""Check `raw`, the text of `name`: the offences no `# noqa` suppresses.
|
|
65
|
+
|
|
66
|
+
A notebook's code cells are checked as one module, and each offence placed in its cell.
|
|
67
|
+
Raises `ValueError` for a `.ipynb` that isn't a notebook.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The unsuppressed offences, a notebook's placed in their cells, and what its functions return
|
|
71
|
+
(see `Checked`).
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
source: str
|
|
75
|
+
where: list[notebook.Line]
|
|
76
|
+
source, where = _source(raw, name)
|
|
77
|
+
checked: Checked = checked_source(source, str(name), checks, outside=outside)
|
|
78
|
+
offences: list[Offence] = unsuppressed(checked.offences, lines(source))
|
|
79
|
+
return checked._replace(offences=[_placed(o, where) for o in offences] if where else offences)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _placed(offence: Offence, where: list[notebook.Line]) -> Offence:
|
|
83
|
+
"""Place an offence from a notebook's joined module in its cell, its declaration's line too.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The offence, its `line` (and a `Edit.DECLARE` fix's statement line) counted in its `cell`; a
|
|
87
|
+
fix that would add an import isn't offered.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
line: notebook.Line = where[offence.line - 1]
|
|
91
|
+
placed: Offence = replace(offence, line=line.line, cell=line.cell)
|
|
92
|
+
# A notebook's cells have no import block.
|
|
93
|
+
if offence.edit is not None and (offence.edit.imports or offence.edit.guarded):
|
|
94
|
+
return replace(placed, edit=None)
|
|
95
|
+
if offence.edit is not None and offence.edit.edit is Edit.DECLARE:
|
|
96
|
+
statement: int = where[offence.edit.span[0] - 1].line
|
|
97
|
+
placed = replace(placed, edit=offence.edit._replace(span=(statement, offence.edit.span[1])))
|
|
98
|
+
return placed
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# One changed part of a fixed file (the file, or a notebook's cell): its label, old and new lines.
|
|
102
|
+
_Change: TypeAlias = tuple[str, list[str], list[str]]
|
|
103
|
+
# One JSON value a coverage report holds, and one file's row of them.
|
|
104
|
+
_Scalar: TypeAlias = str | int | float
|
|
105
|
+
_Row: TypeAlias = dict[str, _Scalar]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Fixed(NamedTuple):
|
|
109
|
+
"""`raw`, fixed: its new text, and each changed part (the file, or a notebook's cell)."""
|
|
110
|
+
|
|
111
|
+
text: str
|
|
112
|
+
changes: list[_Change] # each part's label and old and new lines
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fixed(raw: str, name: Path, offences: Sequence[Offence]) -> Fixed:
|
|
116
|
+
"""Add each fixable offence's annotation to `raw`, the text of `name`.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The new text, and each changed part.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
if name.suffix == notebook.SUFFIX:
|
|
123
|
+
text: str
|
|
124
|
+
cells: list[notebook.Cell]
|
|
125
|
+
text, cells = notebook.fix(raw, offences)
|
|
126
|
+
return Fixed(text, [(f"{name}:cell {c.number}", c.old, c.new) for c in cells])
|
|
127
|
+
old: list[str] = lines(raw)
|
|
128
|
+
new: list[str] = fixes.apply(old, offences)
|
|
129
|
+
return Fixed("".join(new), [(str(name), old, new)] if new != old else [])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _diff(raw: str, name: Path, offences: Sequence[Offence]) -> str:
|
|
133
|
+
return "".join(
|
|
134
|
+
"".join(difflib.unified_diff(old, new, label, label))
|
|
135
|
+
for label, old, new in _fixed(raw, name, offences).changes
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def fix_file(path: Path, offences: Sequence[Offence]) -> int:
|
|
140
|
+
"""Add each fixable offence's annotation to `path` (a notebook's, in its cells), in its encoding.
|
|
141
|
+
|
|
142
|
+
Raises `UnicodeEncodeError`, leaving the file as it was, when an annotation can't be written in
|
|
143
|
+
the file's encoding (a PEP 263 declaration's).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
How many offences were fixed.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
count: int
|
|
150
|
+
if not (count := sum(1 for o in offences if o.edit is not None)):
|
|
151
|
+
return 0
|
|
152
|
+
data: bytes = path.read_bytes()
|
|
153
|
+
encoding: str = encoding_of(path, data)
|
|
154
|
+
_ = path.write_bytes(_fixed(data.decode(encoding), path, offences).text.encode(encoding))
|
|
155
|
+
return count
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _shown_lines(raw: str, name: Path) -> dict[tuple[int | None, int], str]:
|
|
159
|
+
"""Map each line of `raw` (a notebook's, in its cells) to its text, as offences are placed.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Each line's text, by its cell (`None` outside a notebook) and line number.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
source: str
|
|
166
|
+
where: list[notebook.Line]
|
|
167
|
+
source, where = _source(raw, name)
|
|
168
|
+
return {
|
|
169
|
+
(where[index].cell if where else None, where[index].line if where else index + 1): text
|
|
170
|
+
for index, text in enumerate(source.splitlines())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _checked(path: Path, name: Path, checks: Checks, outside: Outside) -> tuple[str, Checked]:
|
|
175
|
+
"""Read `path` and check it as `name`; raises what reading or parsing it does.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Its text, and what checking it found.
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
raw: str = read_text(path)
|
|
182
|
+
return raw, checked_text(raw, name, checks, outside=outside)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _read_checked(
|
|
186
|
+
path: Path,
|
|
187
|
+
name: Path,
|
|
188
|
+
checks: Checks,
|
|
189
|
+
outside: Outside,
|
|
190
|
+
) -> tuple[str, Checked, str]:
|
|
191
|
+
"""Read and check `path`, turning a read or parse error into a message instead of raising.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Its text and what checking it found (nothing on error), and the error message (empty on success).
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
raw: str
|
|
198
|
+
checked: Checked
|
|
199
|
+
try:
|
|
200
|
+
raw, checked = _checked(path, name, checks, outside)
|
|
201
|
+
except (OSError, ValueError, SyntaxError) as error: # UnicodeDecodeError is a ValueError
|
|
202
|
+
return "", Checked([], Returns()), f"{name}: error: {error}"
|
|
203
|
+
return raw, checked, ""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
|
|
207
|
+
"""Turn the offences in `raw`, the text of `name`, into the results to report.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Each one the options don't filter out, with its source line and its fix as a text edit.
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
shown: dict[tuple[int | None, int], str] = _shown_lines(raw, name)
|
|
214
|
+
# A notebook's cells have no file lines for an edit to point at.
|
|
215
|
+
text: list[str] = [] if name.suffix == notebook.SUFFIX else lines(raw)
|
|
216
|
+
return [
|
|
217
|
+
r._replace(
|
|
218
|
+
source=shown.get((r.offence.cell, r.offence.line), ""),
|
|
219
|
+
replacements=fixes.replacements(text, r.offence) if text else (),
|
|
220
|
+
)
|
|
221
|
+
for r in options.filter.results(name, offences)
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _check_path(path: Path, outside: Outside, options: Options) -> CheckRun:
|
|
226
|
+
"""Check (and fix, or diff) one file, given what's known of it from outside it.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
What it found; a file that can't be read or parsed is an error.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
name: Path = options.input.name(path)
|
|
233
|
+
raw: str
|
|
234
|
+
checked: Checked
|
|
235
|
+
error: str
|
|
236
|
+
raw, checked, error = _read_checked(path, name, options.checks, outside)
|
|
237
|
+
if error:
|
|
238
|
+
return CheckRun(error=error)
|
|
239
|
+
return replace(
|
|
240
|
+
_handled(path, (raw, name), checked.offences, options),
|
|
241
|
+
returned=checked.returned,
|
|
242
|
+
calls=checked.calls,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], options: Options) -> CheckRun:
|
|
247
|
+
"""Report, fix or diff the offences found in `path` (`text`: its text, and its name).
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
What was found, and fixed.
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
raw: str
|
|
254
|
+
name: Path
|
|
255
|
+
raw, name = text
|
|
256
|
+
baselined: int
|
|
257
|
+
offences, baselined = options.filter.unbaselined(name, offences)
|
|
258
|
+
results: list[Result] = _results(raw, name, offences, options)
|
|
259
|
+
fixing: list[Offence] = [
|
|
260
|
+
r.offence for r in results if r.offence.edit is not None and options.guesses.applies(r.offence)
|
|
261
|
+
]
|
|
262
|
+
if options.mode is Mode.DIFF:
|
|
263
|
+
return CheckRun(text=_diff(raw, name, fixing))
|
|
264
|
+
if options.mode is not Mode.FIX:
|
|
265
|
+
return CheckRun(results, baselined)
|
|
266
|
+
left: list[Result] = [r for r in results if r.offence not in fixing]
|
|
267
|
+
if path == STDIN: # the fixed source goes to stdout
|
|
268
|
+
return CheckRun(left, baselined, len(fixing), _fixed(raw, name, fixing).text)
|
|
269
|
+
fixed: int
|
|
270
|
+
try:
|
|
271
|
+
fixed = fix_file(path, fixing)
|
|
272
|
+
except UnicodeEncodeError as failure: # the file is left as it was, its offences unfixed
|
|
273
|
+
# Its canonical name: PyPy reports `latin1` where CPython says `latin-1`.
|
|
274
|
+
encoding: str = codecs.lookup(failure.encoding).name
|
|
275
|
+
message: str = f"an annotation can't be written in its encoding, {encoding}; left as it was"
|
|
276
|
+
return CheckRun(results, baselined, error=f"{name}: error: {message}")
|
|
277
|
+
if fixed:
|
|
278
|
+
_ = parsed.take(raw) # the tree of the text it had: no check will ask for it again
|
|
279
|
+
return CheckRun(
|
|
280
|
+
left,
|
|
281
|
+
baselined,
|
|
282
|
+
fixed,
|
|
283
|
+
made=[r._replace(replacements=(), fixed=True) for r in results if r.offence in fixing],
|
|
284
|
+
rounds=(_added(raw, name, fixing),),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _added(raw: str, name: Path, fixing: Sequence[Offence]) -> Added:
|
|
289
|
+
"""Find the lines fixing `fixing` adds to `raw`, the text of `name` (see `fixes.inserted`).
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Each one's cell (`None` outside a notebook) and the line it goes before; a notebook's cells
|
|
293
|
+
get declarations alone.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
if name.suffix != notebook.SUFFIX:
|
|
297
|
+
return tuple((None, line) for line in fixes.inserted(lines(raw), fixing))
|
|
298
|
+
declared: list[tuple[int, int]] = [
|
|
299
|
+
(o.cell or 0, o.edit.span[0]) for o in fixing if o.edit is not None and o.edit.edit is Edit.DECLARE
|
|
300
|
+
]
|
|
301
|
+
return tuple(sorted(declared))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _baseline_path(path: Path, outside: Outside, options: Options) -> BaselineRun:
|
|
305
|
+
"""Check one file, unfiltered, for --write-baseline.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Every offence found; a file that can't be read or parsed is an error.
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
name: Path = options.input.name(path)
|
|
312
|
+
checked: Checked
|
|
313
|
+
error: str
|
|
314
|
+
_, checked, error = _read_checked(path, name, options.checks, outside)
|
|
315
|
+
return BaselineRun(error=error) if error else BaselineRun(checked.offences, returned=checked.returned)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
|
|
319
|
+
"""Count one file's typed first bindings.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
The counts; a file that can't be read or parsed is an error.
|
|
323
|
+
|
|
324
|
+
"""
|
|
325
|
+
name: Path = options.input.name(path)
|
|
326
|
+
try:
|
|
327
|
+
return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
|
|
328
|
+
except (OSError, ValueError, SyntaxError) as error:
|
|
329
|
+
return CoverageRun(error=f"{name}: error: {error}")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
333
|
+
"""Check every file (see `_check_rounds`), its rounds by one set of worker processes.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
The names, and what each file found.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
schedule.forget()
|
|
340
|
+
with kept():
|
|
341
|
+
return _check_rounds(options)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
345
|
+
"""Check every file (`--jobs` at a time), in order.
|
|
346
|
+
|
|
347
|
+
With `--infer-with`, the type checker's server runs throughout: it's asked for every file's
|
|
348
|
+
hints first, and with `--fix`, each file a round changed is asked again and checked again, as its
|
|
349
|
+
new annotations change what the checker infers (see `settled`). A baseline records offences,
|
|
350
|
+
and coverage counts annotations: hints change neither, only what `--fix` offers.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The names, and what each file found.
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
paths: list[Path] = list(
|
|
357
|
+
python_files(options.input.paths, options.input.exclude, tracked=options.input.gitignore),
|
|
358
|
+
)
|
|
359
|
+
check: Callable[[Path, Outside], FileRun]
|
|
360
|
+
if options.mode is Mode.COVERAGE:
|
|
361
|
+
check = partial(_cover_path, options=options)
|
|
362
|
+
elif options.mode is Mode.WRITE_BASELINE:
|
|
363
|
+
check = partial(_baseline_path, options=options)
|
|
364
|
+
else:
|
|
365
|
+
check = partial(_check_path, options=options)
|
|
366
|
+
names: list[Path] = [options.input.name(path) for path in paths]
|
|
367
|
+
if options.mode in {Mode.COVERAGE, Mode.WRITE_BASELINE}:
|
|
368
|
+
return names, checked_all(paths, check, options)[0]
|
|
369
|
+
if not options.infer_with:
|
|
370
|
+
return names, settled(paths, check, options, None)
|
|
371
|
+
session: hints.Session
|
|
372
|
+
# Imported only for `--infer-with`: it's slow to import, for every run's start.
|
|
373
|
+
with cast("type[hints.Session]", importlib.import_module("constricter.cli.lsp.hints").Session)(
|
|
374
|
+
options.infer_with,
|
|
375
|
+
Path.cwd(),
|
|
376
|
+
options.jobs,
|
|
377
|
+
options.infer_memory,
|
|
378
|
+
kept=not options.infer_fresh,
|
|
379
|
+
) as session:
|
|
380
|
+
runs: list[FileRun] = settled(paths, check, options, session)
|
|
381
|
+
checker: str
|
|
382
|
+
path: Path
|
|
383
|
+
for checker, path in session.abandoned:
|
|
384
|
+
name: Path = options.input.name(path)
|
|
385
|
+
_ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
|
|
386
|
+
return names, runs
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _report(options: Options, runs: Sequence[FileRun], files: int) -> int:
|
|
390
|
+
"""Print the results (`runs` is check/fix/diff mode's: every other mode has its own printing).
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
The exit status.
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
checked: Sequence[CheckRun] = cast("Sequence[CheckRun]", runs)
|
|
397
|
+
results: list[Result] = [result for run in checked for result in run.results]
|
|
398
|
+
output: Output = options.output
|
|
399
|
+
text: bool = output.fmt in {Format.TEXT, Format.FULL}
|
|
400
|
+
# With `--show-fixes`, the fixes `--fix` made too: after the rest, as text or JSON.
|
|
401
|
+
shown: list[Result] = [*results, *(made for run in checked if output.show_fixes for made in run.made)]
|
|
402
|
+
line: str
|
|
403
|
+
for line in (
|
|
404
|
+
statistics(results)
|
|
405
|
+
if text and output.statistics
|
|
406
|
+
else render(output.fmt, shown if output.fmt is Format.JSON else results)
|
|
407
|
+
):
|
|
408
|
+
_ = sys.stdout.write(f"{line}\n")
|
|
409
|
+
for line in fix_reasons(shown) if text and output.show_fixes else ():
|
|
410
|
+
_ = sys.stdout.write(f"{line}\n")
|
|
411
|
+
errors: int = sum(r.offence.is_error(r.level) for r in results)
|
|
412
|
+
if text and not output.quiet:
|
|
413
|
+
parts: list[str] = [
|
|
414
|
+
f"Found {errors} error(s) and {len(results) - errors} warning(s) in {files} file(s)",
|
|
415
|
+
]
|
|
416
|
+
if options.mode is Mode.FIX:
|
|
417
|
+
parts.append(f"fixed {sum(run.fixed for run in checked)}")
|
|
418
|
+
guesses: int
|
|
419
|
+
if guesses := sum(r.offence.unsafe for r in results):
|
|
420
|
+
likely: int = sum(r.offence.likely for r in results)
|
|
421
|
+
parts.append(
|
|
422
|
+
f"{guesses} more with --unsafe-fixes" + f" ({likely} with --likely)" * bool(likely),
|
|
423
|
+
)
|
|
424
|
+
if options.filter.baseline_file is not None:
|
|
425
|
+
parts.append(f"{sum(run.baselined for run in checked)} baselined")
|
|
426
|
+
_ = sys.stdout.write("; ".join(parts) + ".\n")
|
|
427
|
+
return EXIT_FOUND if errors else EXIT_CLEAN
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _coverage(options: Options, paths: Sequence[Path], runs: Sequence[FileRun]) -> int:
|
|
431
|
+
"""Print each file's and the total annotation coverage (`runs` is --coverage mode's).
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
The exit status.
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
covered: Sequence[CoverageRun] = cast("Sequence[CoverageRun]", runs)
|
|
438
|
+
counted: list[tuple[Path, Coverage]] = [
|
|
439
|
+
(path, run.coverage) for path, run in zip(paths, covered, strict=True) if run.coverage is not None
|
|
440
|
+
]
|
|
441
|
+
total: Coverage = Coverage(
|
|
442
|
+
sum(c.typed for _, c in counted),
|
|
443
|
+
sum(c.total for _, c in counted),
|
|
444
|
+
sum(c.widened for _, c in counted),
|
|
445
|
+
)
|
|
446
|
+
if options.output.fmt is Format.JSON:
|
|
447
|
+
report: dict[str, _Scalar | list[_Row]] = {
|
|
448
|
+
**_counts(total),
|
|
449
|
+
"files": [{"path": str(path), **_counts(c)} for path, c in counted],
|
|
450
|
+
}
|
|
451
|
+
_ = sys.stdout.write(json.dumps(report, indent=2) + "\n")
|
|
452
|
+
else:
|
|
453
|
+
path: Path
|
|
454
|
+
c: Coverage
|
|
455
|
+
for path, c in counted:
|
|
456
|
+
_ = sys.stdout.write(f"{path}: {_typed(c)}\n")
|
|
457
|
+
_ = sys.stdout.write(f"Total: {_typed(total)} in {len(counted)} file(s).\n")
|
|
458
|
+
threshold: float | None = options.output.fail_under
|
|
459
|
+
return EXIT_FOUND if threshold is not None and total.percent < threshold else EXIT_CLEAN
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _counts(found: Coverage) -> _Row:
|
|
463
|
+
"""Name a coverage's counts, for `--format=json`.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Them, by name.
|
|
467
|
+
|
|
468
|
+
"""
|
|
469
|
+
return {
|
|
470
|
+
"typed": found.typed,
|
|
471
|
+
"total": found.total,
|
|
472
|
+
"widened": found.widened,
|
|
473
|
+
"percent": round(found.percent, 1),
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _typed(found: Coverage) -> str:
|
|
478
|
+
"""Write a coverage as text: `3/4 typed (75.0%)`, then what a widening annotated, if anything.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
It.
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
wider: str = f", {found.widened} widened" if found.widened else ""
|
|
485
|
+
return f"{found.typed}/{found.total} typed ({found.percent:.1f}%){wider}"
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _run(options: Options) -> int:
|
|
489
|
+
"""Check, fix, diff, count or baseline, printing what that finds.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
The exit status.
|
|
493
|
+
|
|
494
|
+
"""
|
|
495
|
+
names: list[Path]
|
|
496
|
+
runs: list[FileRun]
|
|
497
|
+
with collecting.by_hand():
|
|
498
|
+
try:
|
|
499
|
+
names, runs = _check_all(options)
|
|
500
|
+
except HintError as error:
|
|
501
|
+
_ = sys.stderr.write(f"constricter: error: {error}\n")
|
|
502
|
+
return EXIT_ERROR
|
|
503
|
+
_ = sys.stderr.write("".join(f"{run.error}\n" for run in runs if run.error))
|
|
504
|
+
failed: bool = any(run.error for run in runs)
|
|
505
|
+
status: int
|
|
506
|
+
if options.mode is Mode.WRITE_BASELINE and options.filter.baseline_file is not None:
|
|
507
|
+
file: Path = options.filter.baseline_file
|
|
508
|
+
baselined: Sequence[BaselineRun] = cast("Sequence[BaselineRun]", runs)
|
|
509
|
+
found: dict[str, list[Offence]] = {
|
|
510
|
+
baseline.key(n, file): run.found for n, run in zip(names, baselined, strict=True)
|
|
511
|
+
}
|
|
512
|
+
_ = sys.stdout.write(f"Wrote {baseline.write(file, found)} offence(s) to {file}.\n")
|
|
513
|
+
status = EXIT_CLEAN
|
|
514
|
+
elif options.mode is Mode.COVERAGE:
|
|
515
|
+
status = _coverage(options, names, runs)
|
|
516
|
+
elif options.mode is Mode.DIFF:
|
|
517
|
+
checked: Sequence[CheckRun] = cast("Sequence[CheckRun]", runs)
|
|
518
|
+
diffs: str = "".join(run.text for run in checked)
|
|
519
|
+
_ = sys.stdout.write(diffs)
|
|
520
|
+
status = EXIT_FOUND if diffs else EXIT_CLEAN
|
|
521
|
+
elif options.mode is Mode.FIX and options.input.paths == [STDIN] and runs and not failed:
|
|
522
|
+
fixed: CheckRun = cast("CheckRun", runs[0])
|
|
523
|
+
_ = sys.stdout.write(fixed.text) # standard input, fixed, is the whole output
|
|
524
|
+
status = EXIT_FOUND if any(r.offence.is_error(r.level) for r in fixed.results) else EXIT_CLEAN
|
|
525
|
+
else:
|
|
526
|
+
status = _report(options, runs, len(names))
|
|
527
|
+
return EXIT_ERROR if failed else status
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
531
|
+
"""Run the command.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Its exit status.
|
|
535
|
+
|
|
536
|
+
"""
|
|
537
|
+
options: Options = Options.parse(argv)
|
|
538
|
+
file: Path | None = options.output.output_file
|
|
539
|
+
stream: TextIO
|
|
540
|
+
with (
|
|
541
|
+
file.open("w", encoding="utf-8", newline="\n")
|
|
542
|
+
if file
|
|
543
|
+
else contextlib.nullcontext(sys.stdout) as stream,
|
|
544
|
+
contextlib.redirect_stdout(stream),
|
|
545
|
+
):
|
|
546
|
+
status: int = _run(options)
|
|
547
|
+
return EXIT_CLEAN if options.output.exit_zero and status == EXIT_FOUND else status
|