python-constricter 0.4.3__py3-none-any.whl → 0.4.5__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/_version.py +1 -1
- constricter/cli/command.py +16 -7
- constricter/cli/config.py +7 -5
- constricter/cli/explain.py +8 -0
- constricter/cli/lsp/connection.py +5 -10
- constricter/cli/lsp/edits.py +1 -1
- constricter/cli/lsp/guard.py +10 -7
- constricter/cli/lsp/hints.py +23 -18
- constricter/cli/lsp/protocol.py +25 -14
- constricter/cli/lsp/remembered.py +9 -9
- constricter/cli/lsp/stored.py +6 -5
- constricter/cli/options.py +10 -5
- constricter/cli/reading.py +5 -0
- constricter/cli/report.py +14 -7
- constricter/cli/rounds.py +91 -22
- constricter/cli/runs.py +2 -1
- constricter/cli/schedule.py +28 -103
- constricter/cli/workers.py +10 -23
- constricter/fix/core/fixes.py +5 -1
- constricter/fix/core/imports.py +93 -56
- constricter/fix/core/inherited.py +4 -4
- constricter/fix/core/known.py +96 -35
- constricter/fix/core/signatures.py +2 -2
- constricter/fix/index/across.py +173 -0
- constricter/fix/index/annotated.py +1 -1
- constricter/fix/index/awaits.py +11 -11
- constricter/fix/index/beyond.py +5 -5
- constricter/fix/index/callers.py +37 -7
- constricter/fix/index/consistent.py +11 -20
- constricter/fix/index/crossed.py +144 -32
- constricter/fix/index/decorated.py +21 -8
- constricter/fix/index/fixtures.py +7 -5
- constricter/fix/index/linked.py +39 -34
- constricter/fix/index/loose.py +9 -9
- constricter/fix/index/managed.py +9 -9
- constricter/fix/index/modules.py +333 -156
- constricter/fix/index/offers.py +84 -16
- constricter/fix/index/order.py +6 -10
- constricter/fix/index/origins.py +33 -2
- constricter/fix/index/own_methods.py +59 -11
- constricter/fix/index/own_overloads.py +34 -10
- constricter/fix/index/own_stubs.py +95 -0
- constricter/fix/index/own_types.py +32 -14
- constricter/fix/index/params.py +6 -5
- constricter/fix/index/plain.py +139 -27
- constricter/fix/index/project.py +157 -703
- constricter/fix/index/sides.py +10 -74
- constricter/fix/index/spelling.py +702 -0
- constricter/fix/index/strings.py +1 -1
- constricter/fix/index/stubs/atoms.py +42 -26
- constricter/fix/index/stubs/chained.py +2 -2
- constricter/fix/index/stubs/classnames.py +9 -5
- constricter/fix/index/stubs/declared.py +57 -6
- constricter/fix/index/stubs/installed.py +253 -26
- constricter/fix/index/stubs/names.py +8 -10
- constricter/fix/index/stubs/read.py +6 -6
- constricter/fix/index/stubs/signatures.py +63 -21
- constricter/fix/index/stubs/stubbed.py +6 -9
- constricter/fix/index/taken.py +10 -6
- constricter/fix/index/tuples.py +7 -10
- constricter/fix/index/used.py +130 -0
- constricter/fix/libraries/library.py +10 -10
- constricter/fix/libraries/operators.py +185 -0
- constricter/fix/libraries/overloads.py +11 -175
- constricter/fix/libraries/receivers.py +2 -10
- constricter/fix/libraries/stdlib.py +62 -18
- constricter/fix/tables/attributes.json +151 -56
- constricter/fix/tables/awaited.json +1 -0
- constricter/fix/tables/classes.json +4 -0
- constricter/fix/tables/method_overloads.json +7 -6
- constricter/fix/tables/method_signatures.json +14 -0
- constricter/fix/tables/methods.json +68 -58
- constricter/fix/tables/returns.json +3 -0
- constricter/fix/tables/source.json +1 -1
- constricter/fix/values/aliased.py +8 -18
- constricter/fix/values/bodies.py +41 -4
- constricter/fix/values/callables.py +14 -21
- constricter/fix/values/called.py +20 -23
- constricter/fix/values/classvars.py +357 -46
- constricter/fix/values/decided.py +2 -2
- constricter/fix/values/documented.py +440 -0
- constricter/fix/values/doubts.py +85 -31
- constricter/fix/values/entered.py +12 -18
- constricter/fix/values/fills.py +20 -22
- constricter/fix/values/guesses.py +102 -31
- constricter/fix/values/hinted.py +7 -7
- constricter/fix/values/inference.py +133 -58
- constricter/fix/values/loops.py +3 -5
- constricter/fix/values/members.py +108 -35
- constricter/fix/values/narrowed.py +14 -12
- constricter/fix/values/ordered.py +1 -1
- constricter/fix/values/reads.py +72 -25
- constricter/fix/values/refused.py +3 -2
- constricter/fix/values/returned.py +153 -57
- constricter/fix/values/returns.py +135 -5
- constricter/fix/values/shapes.py +13 -14
- constricter/fix/values/stored.py +25 -23
- constricter/{rules → fix/values}/tables.py +67 -20
- constricter/fix/values/targets.py +18 -5
- constricter/fix/values/unchecked.py +2 -2
- constricter/notebook.py +2 -2
- constricter/offences.py +94 -44
- constricter/plugins/flake8.py +31 -47
- constricter/plugins/options.py +41 -0
- constricter/plugins/pylint.py +29 -63
- constricter/recording.py +63 -22
- constricter/rules/attributes.py +19 -4
- constricter/rules/binding.py +32 -24
- constricter/rules/calls.py +44 -16
- constricter/rules/checker.py +118 -32
- constricter/rules/late.py +20 -305
- constricter/rules/narrowing.py +3 -3
- constricter/rules/parameters.py +94 -0
- constricter/rules/rebinding.py +1 -1
- constricter/rules/rebound.py +296 -0
- constricter/rules/recorded.py +2 -2
- constricter/rules/redundant.py +2 -2
- constricter/rules/returning.py +49 -29
- constricter/rules/scope.py +54 -19
- constricter/rules/scoped.py +4 -4
- constricter/rules/scoping.py +13 -10
- constricter/rules/settings.py +45 -45
- constricter/rules/signatures.py +77 -0
- constricter/rules/widened.py +10 -7
- constricter/source/__init__.py +1 -0
- constricter/{rules → source}/annotations.py +34 -10
- constricter/source/concrete.py +247 -0
- constricter/{rules → source}/declarations.py +199 -100
- constricter/{rules → source}/decorators.py +11 -29
- constricter/{rules → source}/flow.py +1 -1
- constricter/source/inert.py +194 -0
- constricter/{rules → source}/keys.py +1 -1
- constricter/{rules → source}/parsed.py +1 -1
- constricter/{rules → source}/syntax.py +99 -28
- constricter/source/usable.py +63 -0
- constricter/trace.py +10 -7
- constricter/{cli/traced.py → traced.py} +86 -14
- {python_constricter-0.4.3.dist-info → python_constricter-0.4.5.dist-info}/METADATA +19 -8
- python_constricter-0.4.5.dist-info/RECORD +183 -0
- python_constricter-0.4.3.dist-info/RECORD +0 -169
- /constricter/{rules → source}/quoted.py +0 -0
- /constricter/{rules → source}/walked.py +0 -0
- {python_constricter-0.4.3.dist-info → python_constricter-0.4.5.dist-info}/WHEEL +0 -0
- {python_constricter-0.4.3.dist-info → python_constricter-0.4.5.dist-info}/entry_points.txt +0 -0
- {python_constricter-0.4.3.dist-info → python_constricter-0.4.5.dist-info}/licenses/LICENSE.md +0 -0
constricter/_version.py
CHANGED
constricter/cli/command.py
CHANGED
|
@@ -17,7 +17,7 @@ from constricter.cli import baseline, collecting, schedule
|
|
|
17
17
|
from constricter.cli.lsp.protocol import HintError
|
|
18
18
|
from constricter.cli.options import Mode, Options, Output
|
|
19
19
|
from constricter.cli.paths import STDIN, python_files
|
|
20
|
-
from constricter.cli.reading import encoding_of, read_text
|
|
20
|
+
from constricter.cli.reading import READ_ERRORS, encoding_of, read_text
|
|
21
21
|
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
22
22
|
from constricter.cli.rounds import checked_all, settled
|
|
23
23
|
from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
|
|
@@ -31,8 +31,8 @@ from constricter.offences import (
|
|
|
31
31
|
Edit,
|
|
32
32
|
Offence,
|
|
33
33
|
)
|
|
34
|
-
from constricter.rules import parsed
|
|
35
34
|
from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
|
|
35
|
+
from constricter.source import parsed
|
|
36
36
|
|
|
37
37
|
if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
38
38
|
from types import ModuleType
|
|
@@ -42,7 +42,6 @@ if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
|
42
42
|
EXIT_CLEAN: Final = 0
|
|
43
43
|
EXIT_FOUND: Final = 1
|
|
44
44
|
EXIT_ERROR: Final = 2
|
|
45
|
-
_ALL: Final = 100 # percent
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
|
|
@@ -199,11 +198,21 @@ def _read_checked(
|
|
|
199
198
|
checked: Checked
|
|
200
199
|
try:
|
|
201
200
|
raw, checked = _checked(path, name, checks, outside)
|
|
202
|
-
except
|
|
203
|
-
return "", Checked([], Returns()),
|
|
201
|
+
except READ_ERRORS as error:
|
|
202
|
+
return "", Checked([], Returns()), _error(name, error)
|
|
204
203
|
return raw, checked, ""
|
|
205
204
|
|
|
206
205
|
|
|
206
|
+
def _error(name: Path, error: Exception) -> str:
|
|
207
|
+
"""Say why a file couldn't be read or parsed.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The message.
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
return f"{name}: error: {error}"
|
|
214
|
+
|
|
215
|
+
|
|
207
216
|
def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
|
|
208
217
|
"""Turn the offences in `raw`, the text of `name`, into the results to report.
|
|
209
218
|
|
|
@@ -326,8 +335,8 @@ def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
|
|
|
326
335
|
name: Path = options.input.name(path)
|
|
327
336
|
try:
|
|
328
337
|
return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
|
|
329
|
-
except
|
|
330
|
-
return CoverageRun(error=
|
|
338
|
+
except READ_ERRORS as error:
|
|
339
|
+
return CoverageRun(error=_error(name, error))
|
|
331
340
|
|
|
332
341
|
|
|
333
342
|
def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
constricter/cli/config.py
CHANGED
|
@@ -220,11 +220,13 @@ def _checkers(value: _Toml) -> list[str] | None:
|
|
|
220
220
|
"""Read `infer-with`: a checker, or a list of them, each one it knows.
|
|
221
221
|
|
|
222
222
|
Returns:
|
|
223
|
-
Them,
|
|
223
|
+
Them, each once in the order they're preferred, as `--infer-with` gives them; `None` for
|
|
224
|
+
anything else.
|
|
224
225
|
|
|
225
226
|
"""
|
|
226
227
|
checkers: list[str] | None = [value] if isinstance(value, str) else _strings(value)
|
|
227
|
-
|
|
228
|
+
known: bool = bool(checkers) and all(checker in SERVERS for checker in checkers or ())
|
|
229
|
+
return list(dict.fromkeys(checkers)) if checkers and known else None
|
|
228
230
|
|
|
229
231
|
|
|
230
232
|
def _levels(value: _Toml) -> dict[str, str] | None:
|
|
@@ -247,7 +249,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
|
|
|
247
249
|
return None if None in lists.values() else {key: list(item or []) for key, item in lists.items()}
|
|
248
250
|
|
|
249
251
|
|
|
250
|
-
def
|
|
252
|
+
def _path(value: _Toml, root: Path) -> str | None:
|
|
251
253
|
"""Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
|
|
252
254
|
|
|
253
255
|
Returns:
|
|
@@ -302,8 +304,8 @@ def config_defaults(start: Path) -> dict[str, Default]:
|
|
|
302
304
|
return {}
|
|
303
305
|
readers: dict[str, Callable[[_Toml], Default | None]] = {
|
|
304
306
|
**_READERS,
|
|
305
|
-
"baseline": partial(
|
|
306
|
-
"infer-from": partial(
|
|
307
|
+
"baseline": partial(_path, root=path.parent),
|
|
308
|
+
"infer-from": partial(_path, root=path.parent),
|
|
307
309
|
}
|
|
308
310
|
defaults: dict[str, Default] = {}
|
|
309
311
|
key: str
|
constricter/cli/explain.py
CHANGED
|
@@ -15,6 +15,7 @@ from constricter.offences import (
|
|
|
15
15
|
UNANNOTATED,
|
|
16
16
|
UNANNOTATED_ATTRIBUTE,
|
|
17
17
|
UNANNOTATED_MEMBER,
|
|
18
|
+
UNANNOTATED_PARAMETER,
|
|
18
19
|
UNANNOTATED_RETURN,
|
|
19
20
|
UNTYPED_TARGET,
|
|
20
21
|
UNUSED_UNION_MEMBER,
|
|
@@ -95,6 +96,13 @@ _WHY: Final = {
|
|
|
95
96
|
"returns nothing. Off unless selected by its full code (`--extend-select LVA014`): ruff's ANN201\n"
|
|
96
97
|
"reports it too; this is for the fix."
|
|
97
98
|
),
|
|
99
|
+
UNANNOTATED_PARAMETER: (
|
|
100
|
+
"A parameter with no annotation leaves the function's body to whatever a type checker\n"
|
|
101
|
+
"infers of it. `--fix` writes what typed it from outside, a guess: what every call passes it,\n"
|
|
102
|
+
"the one class with every member the function reads of it, its pytest fixture's value, or\n"
|
|
103
|
+
"the one type the checked files annotate its name with. Off unless selected by its full code\n"
|
|
104
|
+
"(`--extend-select LVA015`): ruff's ANN001 reports it too; this is for the fix."
|
|
105
|
+
),
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
|
|
@@ -5,7 +5,6 @@ The files it has open, the hint requests sent and not yet answered, and what it
|
|
|
5
5
|
|
|
6
6
|
import contextlib
|
|
7
7
|
import itertools
|
|
8
|
-
import json
|
|
9
8
|
import os
|
|
10
9
|
import queue
|
|
11
10
|
import re
|
|
@@ -329,7 +328,7 @@ class Connection:
|
|
|
329
328
|
|
|
330
329
|
"""
|
|
331
330
|
self.last += 1
|
|
332
|
-
self._send({
|
|
331
|
+
self._send({_ID: self.last, _METHOD: method, "params": params})
|
|
333
332
|
return self.last
|
|
334
333
|
|
|
335
334
|
def _collect(self, numbers: set[int], method: str) -> dict[int, _Json]:
|
|
@@ -444,7 +443,7 @@ class Connection:
|
|
|
444
443
|
def _answer(self, message: _Object) -> None:
|
|
445
444
|
"""Answer one of the server's own requests: its settings, and nothing else it asks for."""
|
|
446
445
|
method: str = str(message[_METHOD])
|
|
447
|
-
reply: _Object = {
|
|
446
|
+
reply: _Object = {_ID: message[_ID]}
|
|
448
447
|
if method == _CONFIGURATION:
|
|
449
448
|
items: list[_Json] = cast("list[_Json]", cast("_Object", message["params"])["items"])
|
|
450
449
|
reply["result"] = [protocol.settings(cast("_Object", item).get("section")) for item in items]
|
|
@@ -456,22 +455,18 @@ class Connection:
|
|
|
456
455
|
|
|
457
456
|
def notify(self, method: str, params: _Json) -> None:
|
|
458
457
|
"""Send a notification: no answer comes."""
|
|
459
|
-
self._send({"
|
|
458
|
+
self._send({"method": method, "params": params})
|
|
460
459
|
|
|
461
460
|
def _send(self, message: _Object) -> None:
|
|
462
|
-
"""Frame and send one message.
|
|
461
|
+
"""Frame and send one message (see `protocol.write`).
|
|
463
462
|
|
|
464
463
|
Raises:
|
|
465
464
|
ExitedError: The server has exited.
|
|
466
465
|
|
|
467
466
|
"""
|
|
468
|
-
body: bytes = json.dumps(message).encode()
|
|
469
467
|
with self.writing: # one message at a time: the reader answers the server's own meanwhile
|
|
470
468
|
try:
|
|
471
|
-
protocol.write(
|
|
472
|
-
cast("IO[bytes]", self.process.stdin),
|
|
473
|
-
f"Content-Length: {len(body)}\r\n\r\n".encode() + body,
|
|
474
|
-
)
|
|
469
|
+
protocol.write(cast("IO[bytes]", self.process.stdin), message)
|
|
475
470
|
except (OSError, ValueError) as error: # it has gone, or its input was closed to stop it
|
|
476
471
|
failure: str = f"{self.name} exited: {error}"
|
|
477
472
|
raise ExitedError(failure) from error
|
constricter/cli/lsp/edits.py
CHANGED
|
@@ -19,7 +19,7 @@ from urllib.request import url2pathname
|
|
|
19
19
|
from constricter.cli.lsp.protocol import Json, Object
|
|
20
20
|
from constricter.fix.core.known import Offered
|
|
21
21
|
from constricter.fix.index.modules import SUFFIX, module_name
|
|
22
|
-
from constricter.
|
|
22
|
+
from constricter.source import parsed
|
|
23
23
|
|
|
24
24
|
_Where: TypeAlias = tuple[int, int] # a line (from 1) and a UTF-8 byte column, as `ast` counts
|
|
25
25
|
_Locate: TypeAlias = Callable[[Object], _Where | None] # a position's, or `None` past the file's end
|
constricter/cli/lsp/guard.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Run a type checker's language server for `--infer-with`, so it can't outlive constricter.
|
|
2
2
|
|
|
3
|
-
python .../constricter/cli/guard.py GRACE EXECUTABLE [ARGUMENT ...]
|
|
3
|
+
python .../constricter/cli/lsp/guard.py GRACE EXECUTABLE [ARGUMENT ...]
|
|
4
4
|
|
|
5
5
|
It imports only the standard library, so it runs by its path whatever the Python can import.
|
|
6
6
|
|
|
@@ -65,20 +65,23 @@ def main(argv: Sequence[str], stdin: IO[bytes] | None = None, stdout: IO[bytes]
|
|
|
65
65
|
|
|
66
66
|
def _pass_on(source: IO[bytes], target: IO[bytes]) -> None:
|
|
67
67
|
"""Pass the server's output on until it ends (or constricter's end of it has gone)."""
|
|
68
|
-
chunk: bytes
|
|
69
68
|
with contextlib.suppress(OSError):
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
_copied(source, target)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _copied(source: IO[bytes], target: IO[bytes]) -> None:
|
|
73
|
+
"""Copy `source` to `target` until it ends, each chunk as it comes: the reader waits on it."""
|
|
74
|
+
chunk: bytes
|
|
75
|
+
for chunk in iter(lambda: _read(source), b""):
|
|
76
|
+
_write(target, chunk)
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
def relay_to(source: IO[bytes], server: "subprocess.Popen[bytes]", grace: float) -> None:
|
|
75
80
|
"""Pass `source` on to the server's input; when it ends, end the server's, and give it `grace` seconds."""
|
|
76
81
|
target: IO[bytes] = cast("IO[bytes]", server.stdin)
|
|
77
|
-
chunk: bytes
|
|
78
82
|
# The server has gone (or this is exiting, its input closed): nothing more to pass on.
|
|
79
83
|
with contextlib.suppress(OSError, ValueError):
|
|
80
|
-
|
|
81
|
-
_write(target, chunk)
|
|
84
|
+
_copied(source, target)
|
|
82
85
|
with contextlib.suppress(OSError): # what's left for a server that has gone is dropped
|
|
83
86
|
target.close()
|
|
84
87
|
try:
|
constricter/cli/lsp/hints.py
CHANGED
|
@@ -94,11 +94,7 @@ class Session:
|
|
|
94
94
|
|
|
95
95
|
def __exit__(self, *_details: object) -> None:
|
|
96
96
|
"""Shut every server down, at once."""
|
|
97
|
-
|
|
98
|
-
if running := [server for checker in self.checkers for server in checker.servers]:
|
|
99
|
-
pool: ThreadPoolExecutor
|
|
100
|
-
with ThreadPoolExecutor(len(running)) as pool:
|
|
101
|
-
_ = list(pool.map(Connection.close, running))
|
|
97
|
+
_closed([server for checker in self.checkers for server in checker.servers])
|
|
102
98
|
|
|
103
99
|
def hints(self, files: Mapping[Path, str]) -> dict[Path, tuple[Hints, ...]]:
|
|
104
100
|
"""Ask every checker for the variable-type hints in `files` (each path's current text).
|
|
@@ -306,7 +302,7 @@ class Checker: # pylint: disable=too-many-instance-attributes
|
|
|
306
302
|
batches: queue.SimpleQueue[dict[Path, str]] = queue.SimpleQueue()
|
|
307
303
|
start: int
|
|
308
304
|
for start in range(0, len(fresh), _BATCH):
|
|
309
|
-
batch: list[Path] = fresh[start:
|
|
305
|
+
batch: list[Path] = fresh[start : start + _BATCH]
|
|
310
306
|
batches.put({path: files[path] for path in batch})
|
|
311
307
|
return [partial(self._work, index, own.get(index, {}), batches) for index in range(len(self.servers))]
|
|
312
308
|
|
|
@@ -337,11 +333,7 @@ class Checker: # pylint: disable=too-many-instance-attributes
|
|
|
337
333
|
except HintError as failure: # it has gone since its last answers: past its cap, say
|
|
338
334
|
if not self._restartable(failure):
|
|
339
335
|
raise
|
|
340
|
-
|
|
341
|
-
path: text for sent, _ in pending for path, text in sent.items()
|
|
342
|
-
}
|
|
343
|
-
pending.clear()
|
|
344
|
-
found.update(self._alone(index, unanswered | batch))
|
|
336
|
+
found.update(self._requeued(index, pending, batch))
|
|
345
337
|
continue
|
|
346
338
|
if len(pending) > 1:
|
|
347
339
|
found.update(self._answers(index, pending))
|
|
@@ -367,12 +359,21 @@ class Checker: # pylint: disable=too-many-instance-attributes
|
|
|
367
359
|
except HintError as failure:
|
|
368
360
|
if not self._restartable(failure):
|
|
369
361
|
raise
|
|
370
|
-
|
|
371
|
-
pending.clear()
|
|
372
|
-
return self._alone(index, unanswered)
|
|
362
|
+
return self._requeued(index, pending, {})
|
|
373
363
|
del pending[0]
|
|
374
364
|
return found
|
|
375
365
|
|
|
366
|
+
def _requeued(self, index: int, pending: list[Sent], more: Mapping[Path, str]) -> Found:
|
|
367
|
+
"""Ask server `index`, which failed, about each pending file and each of `more` alone (see `_alone`).
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Their hints.
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
unanswered: dict[Path, str] = {path: text for batch, _ in pending for path, text in batch.items()}
|
|
374
|
+
pending.clear()
|
|
375
|
+
return self._alone(index, unanswered | dict(more))
|
|
376
|
+
|
|
376
377
|
def _alone(self, index: int, files: Mapping[Path, str]) -> Found:
|
|
377
378
|
"""Restart server `index`, which hung or exited past its cap, and ask it about each of `files` alone.
|
|
378
379
|
|
|
@@ -410,10 +411,7 @@ class Checker: # pylint: disable=too-many-instance-attributes
|
|
|
410
411
|
|
|
411
412
|
def stop(self) -> None:
|
|
412
413
|
"""Shut its servers down, at once: the next round starts others, which open each file anew."""
|
|
413
|
-
|
|
414
|
-
# None where its turn had no file to ask about (a round with none, or every one abandoned).
|
|
415
|
-
with ThreadPoolExecutor(max(1, len(self.servers))) as pool:
|
|
416
|
-
_ = list(pool.map(Connection.close, self.servers))
|
|
414
|
+
_closed(self.servers)
|
|
417
415
|
self.servers.clear()
|
|
418
416
|
self.assigned.clear()
|
|
419
417
|
|
|
@@ -496,3 +494,10 @@ def command(checker: str) -> list[str]:
|
|
|
496
494
|
)
|
|
497
495
|
raise HintError(message)
|
|
498
496
|
return [found, *server.args]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _closed(servers: Sequence[Connection]) -> None:
|
|
500
|
+
"""Shut `servers` down, at once: none where a turn had no file to ask about."""
|
|
501
|
+
pool: ThreadPoolExecutor
|
|
502
|
+
with ThreadPoolExecutor(max(1, len(servers))) as pool:
|
|
503
|
+
_ = list(pool.map(Connection.close, servers))
|
constricter/cli/lsp/protocol.py
CHANGED
|
@@ -15,6 +15,7 @@ Json: TypeAlias = dict[str, "Json"] | list["Json"] | str | int | float | bool |
|
|
|
15
15
|
Object: TypeAlias = dict[str, Json]
|
|
16
16
|
_HEADER_END: Final = b"\r\n"
|
|
17
17
|
_LENGTH: Final = "content-length"
|
|
18
|
+
_JSONRPC: Final = "2.0" # the version every message names, first
|
|
18
19
|
_TYPE_HINT: Final = 1 # `InlayHintKind.Type`
|
|
19
20
|
# Only variable types: argument names and return types are hints too, but nothing here.
|
|
20
21
|
_BASEDPYRIGHT_SECTION: Final = "basedpyright.analysis"
|
|
@@ -91,14 +92,7 @@ def executable(checker: str) -> str | None:
|
|
|
91
92
|
Its path, or `None` if it isn't installed.
|
|
92
93
|
|
|
93
94
|
"""
|
|
94
|
-
|
|
95
|
-
found: list[str] = list(
|
|
96
|
-
dict.fromkeys(
|
|
97
|
-
path
|
|
98
|
-
for path in (shutil.which(name), shutil.which(name, path=str(Path(sys.executable).parent)))
|
|
99
|
-
if path is not None
|
|
100
|
-
),
|
|
101
|
-
)
|
|
95
|
+
found: list[str] = _found(checker)
|
|
102
96
|
return next((path for path in found if _runs(path)), found[0] if found else None)
|
|
103
97
|
|
|
104
98
|
|
|
@@ -106,11 +100,27 @@ def runs(checker: str) -> bool:
|
|
|
106
100
|
"""Check that `checker` is installed, and runs.
|
|
107
101
|
|
|
108
102
|
Returns:
|
|
109
|
-
Whether it does.
|
|
103
|
+
Whether it does: any of its servers found runs.
|
|
110
104
|
|
|
111
105
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
return any(_runs(path) for path in _found(checker))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _found(checker: str) -> list[str]:
|
|
110
|
+
"""Find `checker`'s language servers: on `PATH`, then beside this Python.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Their paths, each once.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
name: str = SERVERS[checker].executable
|
|
117
|
+
return list(
|
|
118
|
+
dict.fromkeys(
|
|
119
|
+
path
|
|
120
|
+
for path in (shutil.which(name), shutil.which(name, path=str(Path(sys.executable).parent)))
|
|
121
|
+
if path is not None
|
|
122
|
+
),
|
|
123
|
+
)
|
|
114
124
|
|
|
115
125
|
|
|
116
126
|
def _runs(path: str) -> bool:
|
|
@@ -144,9 +154,10 @@ class ExitedError(HintError):
|
|
|
144
154
|
"""The type checker's server exited while it was being asked: past its heap's cap, say."""
|
|
145
155
|
|
|
146
156
|
|
|
147
|
-
def write(stream: IO[bytes],
|
|
148
|
-
"""
|
|
149
|
-
|
|
157
|
+
def write(stream: IO[bytes], sent: Object) -> None:
|
|
158
|
+
"""Frame `sent` (see `message`), write it to `stream`, and flush it: the server reads it now."""
|
|
159
|
+
body: bytes = json.dumps({"jsonrpc": _JSONRPC, **sent}).encode()
|
|
160
|
+
_ = stream.write(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)
|
|
150
161
|
stream.flush()
|
|
151
162
|
|
|
152
163
|
|
|
@@ -29,9 +29,7 @@ def _marker(checker: str, command: Sequence[str], named: str) -> Path:
|
|
|
29
29
|
Its path in the cache.
|
|
30
30
|
|
|
31
31
|
"""
|
|
32
|
-
made: str =
|
|
33
|
-
with contextlib.suppress(OSError):
|
|
34
|
-
made = build(checker, Path(command[0]))
|
|
32
|
+
made: str = build(checker, Path(command[0]))
|
|
35
33
|
key: str = "\0".join((checker, command[0], made, named))
|
|
36
34
|
return cache_directory().with_name("hung") / hashlib.sha256(key.encode("utf-8", "replace")).hexdigest()
|
|
37
35
|
|
|
@@ -43,15 +41,17 @@ def build(checker: str, executable: Path) -> str:
|
|
|
43
41
|
|
|
44
42
|
Returns:
|
|
45
43
|
The distribution's folder's name, which holds its version; else the executable's size and when
|
|
46
|
-
it changed.
|
|
44
|
+
it changed; empty where neither can be read.
|
|
47
45
|
|
|
48
46
|
"""
|
|
49
47
|
pattern: str
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
with contextlib.suppress(OSError):
|
|
49
|
+
for pattern in _DISTRIBUTIONS:
|
|
50
|
+
found: list[Path]
|
|
51
|
+
if found := sorted(executable.parent.parent.glob(f"{pattern}/{checker}-*.dist-info")):
|
|
52
|
+
return found[0].name
|
|
53
|
+
return f"{executable.stat().st_size}:{executable.stat().st_mtime_ns}"
|
|
54
|
+
return ""
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def hung(checker: str, command: Sequence[str], text: str, path: Path | None = None) -> bool:
|
constricter/cli/lsp/stored.py
CHANGED
|
@@ -24,7 +24,7 @@ from collections.abc import Iterable, Mapping, Sequence
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Final, TypeAlias, TypedDict, cast
|
|
26
26
|
|
|
27
|
-
from constricter.cli.lsp import connection, protocol, remembered
|
|
27
|
+
from constricter.cli.lsp import connection, edits, protocol, remembered
|
|
28
28
|
from constricter.cli.lsp.connection import FileHints, Found
|
|
29
29
|
from constricter.fix.core.known import Offered
|
|
30
30
|
from constricter.fix.index.stubs.installed import cache_directory, search_path
|
|
@@ -56,11 +56,12 @@ def key(checker: str, command: Sequence[str], root: Path, files: Mapping[Path, s
|
|
|
56
56
|
A digest of everything they rest on (see the module docstring).
|
|
57
57
|
|
|
58
58
|
"""
|
|
59
|
-
build: str =
|
|
60
|
-
with contextlib.suppress(OSError):
|
|
61
|
-
build = remembered.build(checker, Path(command[0]))
|
|
59
|
+
build: str = remembered.build(checker, Path(command[0]))
|
|
62
60
|
texts: list[str] = [checker, *command, build, *(os.environ.get(name, "") for name in _VARIABLES)]
|
|
63
|
-
|
|
61
|
+
# The code that asks and reads them, `edits` the one that writes what the store keeps.
|
|
62
|
+
parts: list[bytes] = [
|
|
63
|
+
Path(cast("str", code.__file__)).read_bytes() for code in (connection, protocol, edits)
|
|
64
|
+
]
|
|
64
65
|
asked: dict[str, str] = {_relative(path, root): text for path, text in files.items()}
|
|
65
66
|
name: str
|
|
66
67
|
for name in sorted(asked):
|
constricter/cli/options.py
CHANGED
|
@@ -7,10 +7,10 @@ from collections.abc import Callable, Sequence
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Final, cast
|
|
10
|
+
from typing import Final, Self, cast
|
|
11
11
|
|
|
12
|
-
from constricter import __version__
|
|
13
|
-
from constricter.cli import baseline
|
|
12
|
+
from constricter import __version__, traced
|
|
13
|
+
from constricter.cli import baseline
|
|
14
14
|
from constricter.cli.config import (
|
|
15
15
|
DEFAULT_BASELINE,
|
|
16
16
|
config_defaults,
|
|
@@ -33,6 +33,7 @@ from constricter.offences import (
|
|
|
33
33
|
OPT_IN,
|
|
34
34
|
PLAIN_BASES,
|
|
35
35
|
UNANNOTATED_ATTRIBUTE,
|
|
36
|
+
UNANNOTATED_PARAMETER,
|
|
36
37
|
UNANNOTATED_RETURN,
|
|
37
38
|
VAGUE,
|
|
38
39
|
Checks,
|
|
@@ -637,7 +638,7 @@ class Options: # pylint: disable=too-many-instance-attributes
|
|
|
637
638
|
infer_in_turn: bool = False # ask one checker at a time, not all at once (`--infer-in-turn`)
|
|
638
639
|
|
|
639
640
|
@classmethod
|
|
640
|
-
def parse(cls, argv: Sequence[str] | None) ->
|
|
641
|
+
def parse(cls, argv: Sequence[str] | None) -> Self:
|
|
641
642
|
"""Parse `argv` over the defaults `pyproject.toml` sets; `--explain` prints and exits.
|
|
642
643
|
|
|
643
644
|
Returns:
|
|
@@ -679,6 +680,7 @@ class Options: # pylint: disable=too-many-instance-attributes
|
|
|
679
680
|
final=CAN_BE_FINAL in {*_select(args), *cast("list[str]", args.extend_select)},
|
|
680
681
|
attributes=UNANNOTATED_ATTRIBUTE in {*_select(args), *cast("list[str]", args.extend_select)},
|
|
681
682
|
returns=UNANNOTATED_RETURN in {*_select(args), *cast("list[str]", args.extend_select)},
|
|
683
|
+
parameters=UNANNOTATED_PARAMETER in {*_select(args), *cast("list[str]", args.extend_select)},
|
|
682
684
|
fixes=FixPolicy(
|
|
683
685
|
frozenset(cast("list[str]", args.fix_select)),
|
|
684
686
|
frozenset(cast("list[str]", args.fix_ignore)),
|
|
@@ -744,7 +746,10 @@ def _maximal(args: argparse.Namespace) -> argparse.Namespace:
|
|
|
744
746
|
args.per_path_levels = {}
|
|
745
747
|
args.all_scopes = True
|
|
746
748
|
# LVA014 only selected by itself: it's ruff's ANN201 again, for its fix.
|
|
747
|
-
args.extend_select = [
|
|
749
|
+
args.extend_select = [
|
|
750
|
+
*cast("list[str]", args.extend_select),
|
|
751
|
+
*sorted(OPT_IN - {UNANNOTATED_RETURN, UNANNOTATED_PARAMETER}),
|
|
752
|
+
]
|
|
748
753
|
return args
|
|
749
754
|
|
|
750
755
|
|
constricter/cli/reading.py
CHANGED
|
@@ -4,10 +4,15 @@ import io
|
|
|
4
4
|
import sys
|
|
5
5
|
import tokenize
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Final
|
|
7
8
|
|
|
8
9
|
from constricter import notebook
|
|
9
10
|
from constricter.cli.paths import STDIN
|
|
10
11
|
|
|
12
|
+
# What reading or parsing a file raises: UnicodeDecodeError is a ValueError; a bad encoding
|
|
13
|
+
# declaration, a SyntaxError.
|
|
14
|
+
READ_ERRORS: Final = (OSError, ValueError, SyntaxError)
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
def encoding_of(path: Path, data: bytes) -> str:
|
|
13
18
|
"""Find the encoding of `path`, whose bytes are `data`: a notebook's is UTF-8 (it's JSON).
|
constricter/cli/report.py
CHANGED
|
@@ -166,12 +166,21 @@ def _sarif(results: Sequence[Result]) -> dict[str, _Json]:
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
def _located(r: Result) -> str:
|
|
170
|
+
"""Say where `r` is: its path, its cell in a notebook, its line and its column (from 1).
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
It.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
cell: str = "" if r.offence.cell is None else f"cell {r.offence.cell}:"
|
|
177
|
+
return f"{r.path}:{cell}{r.offence.line}:{r.offence.col + 1}"
|
|
178
|
+
|
|
179
|
+
|
|
169
180
|
def _text(results: Sequence[Result]) -> Iterator[str]:
|
|
170
181
|
r: Result
|
|
171
182
|
for r in results:
|
|
172
|
-
|
|
173
|
-
where: str = f"{r.path}:{cell}{r.offence.line}:{r.offence.col + 1}"
|
|
174
|
-
yield f"{where}: {r.severity}: {r.offence.code} {r.offence.message}"
|
|
183
|
+
yield f"{_located(r)}: {r.severity}: {r.offence.code} {r.offence.message}"
|
|
175
184
|
|
|
176
185
|
|
|
177
186
|
def _full(results: Sequence[Result]) -> Iterator[str]:
|
|
@@ -186,8 +195,7 @@ def _full(results: Sequence[Result]) -> Iterator[str]:
|
|
|
186
195
|
for r in results:
|
|
187
196
|
yield from _text([r])
|
|
188
197
|
source: str = r.source
|
|
189
|
-
|
|
190
|
-
start: int = len(source.encode()[: r.offence.col].decode(errors="ignore"))
|
|
198
|
+
start: int = _column(r) # the caret goes under the name's first character
|
|
191
199
|
width: int = len(r.offence.name) if source[start:].startswith(r.offence.name) else 1
|
|
192
200
|
number: str = str(r.offence.line)
|
|
193
201
|
gutter: str = " " * len(number)
|
|
@@ -374,8 +382,7 @@ def fix_reasons(results: Sequence[Result]) -> Iterator[str]:
|
|
|
374
382
|
r: Result
|
|
375
383
|
for r in results:
|
|
376
384
|
if r.offence.fix is not None:
|
|
377
|
-
|
|
378
|
-
where: str = f"{r.path}:{cell}{r.offence.line}:{r.offence.col + 1}"
|
|
385
|
+
where: str = _located(r)
|
|
379
386
|
flag: str = "--likely" if r.offence.likely else "--unsafe-fixes"
|
|
380
387
|
guess: str = f" (a guess: {flag})" if r.offence.unsafe and not r.fixed else ""
|
|
381
388
|
kinds: str = ", ".join(sorted(r.offence.edit.kinds if r.offence.edit else ()))
|