python-constricter 0.4.2__py3-none-any.whl → 0.4.4__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 +0 -1
- constricter/__main__.py +3 -3
- constricter/_version.py +1 -2
- constricter/cli/__init__.py +0 -1
- constricter/cli/baseline.py +0 -1
- constricter/cli/collecting.py +6 -4
- constricter/cli/command.py +38 -15
- constricter/cli/config.py +7 -6
- constricter/cli/explain.py +21 -1
- constricter/cli/lsp/__init__.py +0 -1
- constricter/cli/lsp/connection.py +19 -22
- constricter/cli/lsp/edits.py +1 -2
- constricter/cli/lsp/guard.py +10 -8
- constricter/cli/lsp/hints.py +165 -53
- constricter/cli/lsp/protocol.py +39 -16
- constricter/cli/lsp/remembered.py +9 -10
- constricter/cli/lsp/stored.py +6 -6
- constricter/cli/options.py +24 -7
- constricter/cli/paths.py +0 -1
- constricter/cli/reading.py +5 -1
- constricter/cli/report.py +15 -8
- constricter/cli/rounds.py +72 -27
- constricter/cli/runs.py +34 -2
- constricter/cli/schedule.py +48 -100
- constricter/cli/workers.py +10 -24
- constricter/empty.py +0 -1
- constricter/fix/__init__.py +0 -1
- constricter/fix/core/__init__.py +0 -1
- constricter/fix/core/asked.py +0 -1
- constricter/fix/core/fixes.py +16 -4
- constricter/fix/core/imports.py +83 -58
- constricter/fix/core/inherited.py +4 -5
- constricter/fix/core/known.py +40 -27
- constricter/fix/core/signatures.py +2 -3
- constricter/fix/index/__init__.py +0 -1
- constricter/fix/index/across.py +145 -0
- constricter/fix/index/annotated.py +83 -0
- constricter/fix/index/awaits.py +11 -12
- constricter/fix/index/beyond.py +5 -6
- constricter/fix/index/callers.py +37 -8
- constricter/fix/index/consistent.py +133 -0
- constricter/fix/index/crossed.py +84 -35
- constricter/fix/index/decorated.py +21 -9
- constricter/fix/index/fixtures.py +21 -12
- constricter/fix/index/linked.py +29 -30
- constricter/fix/index/loose.py +9 -10
- constricter/fix/index/managed.py +9 -10
- constricter/fix/index/modules.py +90 -41
- constricter/fix/index/offers.py +10 -17
- constricter/fix/index/order.py +6 -11
- constricter/fix/index/origins.py +7 -3
- constricter/fix/index/own_methods.py +3 -4
- constricter/fix/index/own_overloads.py +9 -5
- constricter/fix/index/own_types.py +11 -15
- constricter/fix/index/params.py +6 -6
- constricter/fix/index/plain.py +3 -4
- constricter/fix/index/project.py +99 -660
- constricter/fix/index/sides.py +10 -75
- constricter/fix/index/spelling.py +706 -0
- constricter/fix/index/strings.py +1 -2
- constricter/fix/index/stubs/__init__.py +0 -1
- constricter/fix/index/stubs/atoms.py +36 -25
- constricter/fix/index/stubs/chained.py +2 -3
- constricter/fix/index/stubs/classnames.py +3 -4
- constricter/fix/index/stubs/declared.py +2 -3
- constricter/fix/index/stubs/installed.py +134 -43
- constricter/fix/index/stubs/names.py +8 -11
- constricter/fix/index/stubs/read.py +6 -7
- constricter/fix/index/stubs/signatures.py +11 -14
- constricter/fix/index/stubs/stubbed.py +10 -12
- constricter/fix/index/taken.py +10 -7
- constricter/fix/index/tuples.py +7 -11
- constricter/fix/libraries/__init__.py +0 -1
- constricter/fix/libraries/generics.py +50 -0
- constricter/fix/libraries/library.py +60 -22
- constricter/fix/libraries/opened.py +0 -1
- constricter/fix/libraries/operators.py +185 -0
- constricter/fix/libraries/overloads.py +90 -224
- constricter/fix/libraries/receivers.py +2 -11
- constricter/fix/libraries/stdlib.py +55 -11
- constricter/fix/libraries/walks.py +0 -1
- constricter/fix/tables/source.json +1 -1
- constricter/fix/values/__init__.py +0 -1
- constricter/fix/values/aliased.py +8 -19
- constricter/fix/values/bodies.py +41 -5
- constricter/fix/values/callables.py +14 -22
- constricter/fix/values/called.py +20 -24
- constricter/fix/values/classvars.py +34 -32
- constricter/fix/values/decided.py +2 -3
- constricter/fix/values/displays.py +0 -1
- constricter/fix/values/doubts.py +50 -31
- constricter/fix/values/entered.py +12 -19
- constricter/fix/values/fills.py +176 -33
- constricter/fix/values/guesses.py +34 -34
- constricter/fix/values/hinted.py +181 -41
- constricter/fix/values/inference.py +87 -55
- constricter/fix/values/loops.py +5 -8
- constricter/fix/values/members.py +142 -20
- constricter/fix/values/narrowed.py +283 -20
- constricter/fix/values/operated.py +0 -1
- constricter/fix/values/ordered.py +1 -2
- constricter/fix/values/reads.py +388 -0
- constricter/fix/values/refused.py +87 -0
- constricter/fix/values/returned.py +46 -48
- constricter/fix/values/returns.py +2 -2
- constricter/fix/values/shapes.py +30 -20
- constricter/fix/values/stored.py +64 -35
- constricter/{rules → fix/values}/tables.py +4 -5
- constricter/fix/values/targets.py +18 -6
- constricter/fix/values/unchecked.py +27 -14
- constricter/jsonc.py +0 -1
- constricter/noqa.py +0 -1
- constricter/notebook.py +2 -3
- constricter/offences.py +102 -41
- constricter/overrides.py +0 -1
- constricter/plugins/__init__.py +0 -1
- constricter/plugins/flake8.py +50 -50
- constricter/plugins/options.py +41 -0
- constricter/plugins/pylint.py +36 -65
- constricter/recording.py +63 -23
- constricter/rules/__init__.py +0 -1
- constricter/rules/attributes.py +258 -0
- constricter/rules/binding.py +149 -57
- constricter/rules/calls.py +105 -22
- constricter/rules/checker.py +115 -32
- constricter/rules/late.py +46 -312
- constricter/rules/narrowing.py +3 -4
- constricter/rules/parameters.py +94 -0
- constricter/rules/rebinding.py +1 -2
- constricter/rules/rebound.py +296 -0
- constricter/rules/recorded.py +8 -4
- constricter/rules/redundant.py +2 -3
- constricter/rules/returning.py +251 -0
- constricter/rules/scope.py +186 -38
- constricter/rules/scoped.py +11 -6
- constricter/rules/scoping.py +9 -11
- constricter/rules/settings.py +69 -40
- constricter/rules/signatures.py +77 -0
- constricter/rules/widened.py +10 -8
- constricter/source/__init__.py +1 -0
- constricter/{rules → source}/annotations.py +2 -3
- constricter/{rules → source}/declarations.py +170 -14
- constricter/{rules → source}/decorators.py +11 -30
- constricter/{rules → source}/flow.py +1 -2
- constricter/{rules → source}/keys.py +1 -2
- constricter/{rules → source}/parsed.py +2 -3
- constricter/{rules → source}/quoted.py +0 -1
- constricter/{rules → source}/syntax.py +99 -46
- constricter/{rules → source}/walked.py +17 -11
- constricter/trace.py +104 -35
- constricter/{cli/traced.py → traced.py} +54 -9
- {python_constricter-0.4.2.dist-info → python_constricter-0.4.4.dist-info}/METADATA +39 -11
- python_constricter-0.4.4.dist-info/RECORD +177 -0
- python_constricter-0.4.2.dist-info/RECORD +0 -162
- {python_constricter-0.4.2.dist-info → python_constricter-0.4.4.dist-info}/WHEEL +0 -0
- {python_constricter-0.4.2.dist-info → python_constricter-0.4.4.dist-info}/entry_points.txt +0 -0
- {python_constricter-0.4.2.dist-info → python_constricter-0.4.4.dist-info}/licenses/LICENSE.md +0 -0
constricter/__init__.py
CHANGED
constricter/__main__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""`python -m constricter`, and the `constricter` script."""
|
|
3
2
|
|
|
4
3
|
import contextlib
|
|
@@ -13,9 +12,10 @@ def run() -> None:
|
|
|
13
12
|
|
|
14
13
|
Not by the interpreter's own exit, which frees every parsed file and the index an object at a
|
|
15
14
|
time: a fifth of a check of a thousand files. What the command wrote is flushed first; nothing
|
|
16
|
-
else waits on the exit (its workers have ended, its output file is closed).
|
|
15
|
+
else waits on the exit (its workers have ended, its output file is closed). Nor is the collector
|
|
16
|
+
turned on again for it (`leaving`).
|
|
17
17
|
"""
|
|
18
|
-
status: int = main()
|
|
18
|
+
status: int = main(leaving=True)
|
|
19
19
|
with contextlib.suppress(OSError, ValueError): # a reader that left, a stream closed already
|
|
20
20
|
_ = sys.stdout.flush()
|
|
21
21
|
_ = sys.stderr.flush()
|
constricter/_version.py
CHANGED
constricter/cli/__init__.py
CHANGED
constricter/cli/baseline.py
CHANGED
constricter/cli/collecting.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""The garbage collector, as the command runs it: by hand, once per file checked.
|
|
3
2
|
|
|
4
3
|
A check keeps every file's tree (see `parsed`), and allocates a great deal for each file, little of it
|
|
@@ -24,9 +23,12 @@ _YOUNGER: Final = not sysconfig.get_config_var("Py_GIL_DISABLED")
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@contextlib.contextmanager
|
|
27
|
-
def by_hand() -> Generator[None]:
|
|
26
|
+
def by_hand(*, leaving: bool = False) -> Generator[None]:
|
|
28
27
|
"""Turn automatic collection off for the command's run, and back as it was after.
|
|
29
28
|
|
|
29
|
+
Not for a process that leaves once the run's done (`leaving`): turned on again, the collector
|
|
30
|
+
looked through all the run left alive, at once (a check of pandas: 3.4s, for nothing).
|
|
31
|
+
|
|
30
32
|
Yields:
|
|
31
33
|
Nothing: the run.
|
|
32
34
|
|
|
@@ -36,9 +38,9 @@ def by_hand() -> Generator[None]:
|
|
|
36
38
|
try:
|
|
37
39
|
yield
|
|
38
40
|
finally:
|
|
39
|
-
if _FREEZES:
|
|
41
|
+
if _FREEZES and not leaving:
|
|
40
42
|
gc.unfreeze()
|
|
41
|
-
if was:
|
|
43
|
+
if was and not leaving:
|
|
42
44
|
gc.enable()
|
|
43
45
|
|
|
44
46
|
|
constricter/cli/command.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""The `constricter` command (see README)."""
|
|
3
2
|
|
|
4
3
|
import codecs
|
|
@@ -18,7 +17,7 @@ from constricter.cli import baseline, collecting, schedule
|
|
|
18
17
|
from constricter.cli.lsp.protocol import HintError
|
|
19
18
|
from constricter.cli.options import Mode, Options, Output
|
|
20
19
|
from constricter.cli.paths import STDIN, python_files
|
|
21
|
-
from constricter.cli.reading import encoding_of, read_text
|
|
20
|
+
from constricter.cli.reading import READ_ERRORS, encoding_of, read_text
|
|
22
21
|
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
23
22
|
from constricter.cli.rounds import checked_all, settled
|
|
24
23
|
from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
|
|
@@ -32,16 +31,17 @@ from constricter.offences import (
|
|
|
32
31
|
Edit,
|
|
33
32
|
Offence,
|
|
34
33
|
)
|
|
35
|
-
from constricter.rules import parsed
|
|
36
34
|
from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
|
|
35
|
+
from constricter.source import parsed
|
|
37
36
|
|
|
38
37
|
if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
38
|
+
from types import ModuleType
|
|
39
|
+
|
|
39
40
|
from constricter.cli.lsp import hints
|
|
40
41
|
|
|
41
42
|
EXIT_CLEAN: Final = 0
|
|
42
43
|
EXIT_FOUND: Final = 1
|
|
43
44
|
EXIT_ERROR: Final = 2
|
|
44
|
-
_ALL: Final = 100 # percent
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
|
|
@@ -198,11 +198,21 @@ def _read_checked(
|
|
|
198
198
|
checked: Checked
|
|
199
199
|
try:
|
|
200
200
|
raw, checked = _checked(path, name, checks, outside)
|
|
201
|
-
except
|
|
202
|
-
return "", Checked([], Returns()),
|
|
201
|
+
except READ_ERRORS as error:
|
|
202
|
+
return "", Checked([], Returns()), _error(name, error)
|
|
203
203
|
return raw, checked, ""
|
|
204
204
|
|
|
205
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
|
+
|
|
206
216
|
def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
|
|
207
217
|
"""Turn the offences in `raw`, the text of `name`, into the results to report.
|
|
208
218
|
|
|
@@ -325,8 +335,8 @@ def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
|
|
|
325
335
|
name: Path = options.input.name(path)
|
|
326
336
|
try:
|
|
327
337
|
return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
|
|
328
|
-
except
|
|
329
|
-
return CoverageRun(error=
|
|
338
|
+
except READ_ERRORS as error:
|
|
339
|
+
return CoverageRun(error=_error(name, error))
|
|
330
340
|
|
|
331
341
|
|
|
332
342
|
def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
@@ -370,12 +380,17 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
370
380
|
return names, settled(paths, check, options, None)
|
|
371
381
|
session: hints.Session
|
|
372
382
|
# Imported only for `--infer-with`: it's slow to import, for every run's start.
|
|
373
|
-
|
|
383
|
+
hinting: ModuleType = importlib.import_module("constricter.cli.lsp.hints")
|
|
384
|
+
asking: hints.Asking = cast("type[hints.Asking]", hinting.Asking)(
|
|
385
|
+
kept=not options.infer_fresh,
|
|
386
|
+
in_turn=options.infer_in_turn,
|
|
387
|
+
)
|
|
388
|
+
with cast("type[hints.Session]", hinting.Session)(
|
|
374
389
|
options.infer_with,
|
|
375
390
|
Path.cwd(),
|
|
376
391
|
options.jobs,
|
|
377
392
|
options.infer_memory,
|
|
378
|
-
|
|
393
|
+
asking,
|
|
379
394
|
) as session:
|
|
380
395
|
runs: list[FileRun] = settled(paths, check, options, session)
|
|
381
396
|
checker: str
|
|
@@ -383,6 +398,11 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
383
398
|
for checker, path in session.abandoned:
|
|
384
399
|
name: Path = options.input.name(path)
|
|
385
400
|
_ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
|
|
401
|
+
read: int
|
|
402
|
+
rounds: int
|
|
403
|
+
for checker, read, rounds in session.reused:
|
|
404
|
+
told: str = f"{checker}'s hints for {read} of {rounds} rounds were read from the store"
|
|
405
|
+
_ = sys.stderr.write(f"constricter: note: {told} (`--infer-fresh` asks again)\n")
|
|
386
406
|
return names, runs
|
|
387
407
|
|
|
388
408
|
|
|
@@ -485,8 +505,8 @@ def _typed(found: Coverage) -> str:
|
|
|
485
505
|
return f"{found.typed}/{found.total} typed ({found.percent:.1f}%){wider}"
|
|
486
506
|
|
|
487
507
|
|
|
488
|
-
def _run(options: Options) -> int:
|
|
489
|
-
"""Check, fix, diff, count or baseline, printing what that finds.
|
|
508
|
+
def _run(options: Options, *, leaving: bool) -> int:
|
|
509
|
+
"""Check, fix, diff, count or baseline, printing what that finds (`leaving`: see `main`).
|
|
490
510
|
|
|
491
511
|
Returns:
|
|
492
512
|
The exit status.
|
|
@@ -494,7 +514,7 @@ def _run(options: Options) -> int:
|
|
|
494
514
|
"""
|
|
495
515
|
names: list[Path]
|
|
496
516
|
runs: list[FileRun]
|
|
497
|
-
with collecting.by_hand():
|
|
517
|
+
with collecting.by_hand(leaving=leaving):
|
|
498
518
|
try:
|
|
499
519
|
names, runs = _check_all(options)
|
|
500
520
|
except HintError as error:
|
|
@@ -527,9 +547,12 @@ def _run(options: Options) -> int:
|
|
|
527
547
|
return EXIT_ERROR if failed else status
|
|
528
548
|
|
|
529
549
|
|
|
530
|
-
def main(argv: Sequence[str] | None = None) -> int:
|
|
550
|
+
def main(argv: Sequence[str] | None = None, *, leaving: bool = False) -> int:
|
|
531
551
|
"""Run the command.
|
|
532
552
|
|
|
553
|
+
`leaving`: the process leaves once it's done (`constricter.__main__.run`), its collector as the
|
|
554
|
+
run left it (see `collecting.by_hand`).
|
|
555
|
+
|
|
533
556
|
Returns:
|
|
534
557
|
Its exit status.
|
|
535
558
|
|
|
@@ -543,5 +566,5 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
543
566
|
else contextlib.nullcontext(sys.stdout) as stream,
|
|
544
567
|
contextlib.redirect_stdout(stream),
|
|
545
568
|
):
|
|
546
|
-
status: int = _run(options)
|
|
569
|
+
status: int = _run(options, leaving=leaving)
|
|
547
570
|
return EXIT_CLEAN if options.output.exit_zero and status == EXIT_FOUND else status
|
constricter/cli/config.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""The command's defaults, from the nearest `pyproject.toml`'s `[tool.constricter]` table."""
|
|
3
2
|
|
|
4
3
|
import re
|
|
@@ -221,11 +220,13 @@ def _checkers(value: _Toml) -> list[str] | None:
|
|
|
221
220
|
"""Read `infer-with`: a checker, or a list of them, each one it knows.
|
|
222
221
|
|
|
223
222
|
Returns:
|
|
224
|
-
Them,
|
|
223
|
+
Them, each once in the order they're preferred, as `--infer-with` gives them; `None` for
|
|
224
|
+
anything else.
|
|
225
225
|
|
|
226
226
|
"""
|
|
227
227
|
checkers: list[str] | None = [value] if isinstance(value, str) else _strings(value)
|
|
228
|
-
|
|
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
|
|
229
230
|
|
|
230
231
|
|
|
231
232
|
def _levels(value: _Toml) -> dict[str, str] | None:
|
|
@@ -248,7 +249,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
|
|
|
248
249
|
return None if None in lists.values() else {key: list(item or []) for key, item in lists.items()}
|
|
249
250
|
|
|
250
251
|
|
|
251
|
-
def
|
|
252
|
+
def _path(value: _Toml, root: Path) -> str | None:
|
|
252
253
|
"""Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
|
|
253
254
|
|
|
254
255
|
Returns:
|
|
@@ -303,8 +304,8 @@ def config_defaults(start: Path) -> dict[str, Default]:
|
|
|
303
304
|
return {}
|
|
304
305
|
readers: dict[str, Callable[[_Toml], Default | None]] = {
|
|
305
306
|
**_READERS,
|
|
306
|
-
"baseline": partial(
|
|
307
|
-
"infer-from": partial(
|
|
307
|
+
"baseline": partial(_path, root=path.parent),
|
|
308
|
+
"infer-from": partial(_path, root=path.parent),
|
|
308
309
|
}
|
|
309
310
|
defaults: dict[str, Default] = {}
|
|
310
311
|
key: str
|
constricter/cli/explain.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""Each code's rationale and fix, for `constricter --explain`."""
|
|
3
2
|
|
|
4
3
|
from typing import Final
|
|
@@ -14,7 +13,10 @@ from constricter.offences import (
|
|
|
14
13
|
OPT_IN,
|
|
15
14
|
REDUNDANT_TYPE,
|
|
16
15
|
UNANNOTATED,
|
|
16
|
+
UNANNOTATED_ATTRIBUTE,
|
|
17
17
|
UNANNOTATED_MEMBER,
|
|
18
|
+
UNANNOTATED_PARAMETER,
|
|
19
|
+
UNANNOTATED_RETURN,
|
|
18
20
|
UNTYPED_TARGET,
|
|
19
21
|
UNUSED_UNION_MEMBER,
|
|
20
22
|
VAGUE_TYPE,
|
|
@@ -83,6 +85,24 @@ _WHY: Final = {
|
|
|
83
85
|
"(not from a nested function either) could say so: `limit: Final = 10`. Off unless selected\n"
|
|
84
86
|
"by its full code (`--extend-select LVA012`): most locals are bound once."
|
|
85
87
|
),
|
|
88
|
+
UNANNOTATED_ATTRIBUTE: (
|
|
89
|
+
"An instance attribute a plain class's method first stores (`self.count = 0`), declared\n"
|
|
90
|
+
"nowhere in the class, is typed by whatever a type checker infers from that store. Annotate\n"
|
|
91
|
+
"it there: `self.count: int = 0`. Off unless selected by its full code (`--extend-select LVA013`)."
|
|
92
|
+
),
|
|
93
|
+
UNANNOTATED_RETURN: (
|
|
94
|
+
"A plain function with no return annotation leaves its callers to whatever a type checker\n"
|
|
95
|
+
"infers. `--fix` writes what its own `return`s give (`-> int`), or `-> None` for one that\n"
|
|
96
|
+
"returns nothing. Off unless selected by its full code (`--extend-select LVA014`): ruff's ANN201\n"
|
|
97
|
+
"reports it too; this is for the fix."
|
|
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
|
+
),
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
|
constricter/cli/lsp/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""A running language server, and the JSON-RPC conversation with it (`Connection`), for `hints`.
|
|
3
2
|
|
|
4
3
|
The files it has open, the hint requests sent and not yet answered, and what it has said.
|
|
@@ -6,7 +5,6 @@ The files it has open, the hint requests sent and not yet answered, and what it
|
|
|
6
5
|
|
|
7
6
|
import contextlib
|
|
8
7
|
import itertools
|
|
9
|
-
import json
|
|
10
8
|
import os
|
|
11
9
|
import queue
|
|
12
10
|
import re
|
|
@@ -22,7 +20,7 @@ from pathlib import Path
|
|
|
22
20
|
from typing import IO, Final, NamedTuple, TypeAlias, cast
|
|
23
21
|
|
|
24
22
|
from constricter.cli.lsp import edits, guard, protocol
|
|
25
|
-
from constricter.cli.lsp.protocol import HintError, HungError
|
|
23
|
+
from constricter.cli.lsp.protocol import ExitedError, HintError, HungError
|
|
26
24
|
from constricter.fix.core.known import Offered
|
|
27
25
|
|
|
28
26
|
_Json: TypeAlias = protocol.Json
|
|
@@ -79,22 +77,24 @@ class _Documents:
|
|
|
79
77
|
texts: dict[str, str] = field(default_factory=dict[str, str])
|
|
80
78
|
|
|
81
79
|
|
|
82
|
-
class
|
|
80
|
+
class Asked(NamedTuple):
|
|
83
81
|
"""Hint requests sent, not yet answered: each request's file, and each file's lines."""
|
|
84
82
|
|
|
85
83
|
numbers: dict[int, Path]
|
|
86
84
|
lines: dict[Path, list[str]]
|
|
87
85
|
|
|
88
86
|
|
|
89
|
-
Sent: TypeAlias = tuple[dict[Path, str],
|
|
87
|
+
Sent: TypeAlias = tuple[dict[Path, str], Asked] # a batch of files, and its hint requests
|
|
90
88
|
|
|
91
89
|
|
|
92
90
|
class Connection:
|
|
93
91
|
"""A running language server, and the JSON-RPC conversation with it."""
|
|
94
92
|
|
|
95
|
-
def __init__(self, argv: Sequence[str], root: Path) -> None:
|
|
93
|
+
def __init__(self, argv: Sequence[str], root: Path, environment: Mapping[str, str] | None = None) -> None:
|
|
96
94
|
"""Start the server (`argv`) and initialize it, with `root` as its one workspace folder.
|
|
97
95
|
|
|
96
|
+
In `environment`, or else this process's.
|
|
97
|
+
|
|
98
98
|
Raises:
|
|
99
99
|
HintError: It can't be started, or fails to initialize.
|
|
100
100
|
|
|
@@ -115,6 +115,7 @@ class Connection:
|
|
|
115
115
|
stdout=subprocess.PIPE,
|
|
116
116
|
stderr=subprocess.DEVNULL,
|
|
117
117
|
cwd=root,
|
|
118
|
+
env=environment,
|
|
118
119
|
),
|
|
119
120
|
)
|
|
120
121
|
self.inbox: _Inbox = _Inbox()
|
|
@@ -188,7 +189,7 @@ class Connection:
|
|
|
188
189
|
return False
|
|
189
190
|
return True
|
|
190
191
|
|
|
191
|
-
def ask(self, files: Mapping[Path, str]) -> "
|
|
192
|
+
def ask(self, files: Mapping[Path, str]) -> "Asked":
|
|
192
193
|
"""Open each file (or send its new text), and ask for its hints over the whole of it.
|
|
193
194
|
|
|
194
195
|
It doesn't wait for them. Each file is left open: basedpyright (1.40) fails the next file's
|
|
@@ -204,7 +205,7 @@ class Connection:
|
|
|
204
205
|
text: str
|
|
205
206
|
for path, text in files.items():
|
|
206
207
|
self._opened(path.resolve().as_uri(), text)
|
|
207
|
-
return
|
|
208
|
+
return Asked({self._ask_hints(path, lines[path]): path for path in files}, lines)
|
|
208
209
|
|
|
209
210
|
def _ask_hints(self, path: Path, lines: Sequence[str]) -> int:
|
|
210
211
|
"""Ask for an open file's hints over the whole of it, without waiting for them.
|
|
@@ -221,7 +222,7 @@ class Connection:
|
|
|
221
222
|
},
|
|
222
223
|
)
|
|
223
224
|
|
|
224
|
-
def answers(self, asked: "
|
|
225
|
+
def answers(self, asked: "Asked") -> Found:
|
|
225
226
|
"""Wait for the answers to hint requests `ask` sent, asking again for those the server dropped.
|
|
226
227
|
|
|
227
228
|
Returns:
|
|
@@ -327,7 +328,7 @@ class Connection:
|
|
|
327
328
|
|
|
328
329
|
"""
|
|
329
330
|
self.last += 1
|
|
330
|
-
self._send({
|
|
331
|
+
self._send({_ID: self.last, _METHOD: method, "params": params})
|
|
331
332
|
return self.last
|
|
332
333
|
|
|
333
334
|
def _collect(self, numbers: set[int], method: str) -> dict[int, _Json]:
|
|
@@ -391,7 +392,7 @@ class Connection:
|
|
|
391
392
|
It.
|
|
392
393
|
|
|
393
394
|
Raises:
|
|
394
|
-
|
|
395
|
+
ExitedError: The server exited (`_waited` raises `HungError` where it took too long).
|
|
395
396
|
|
|
396
397
|
"""
|
|
397
398
|
# `_waited` raises rather than end: there is always a next message, or an error.
|
|
@@ -400,7 +401,7 @@ class Connection:
|
|
|
400
401
|
)
|
|
401
402
|
if message is None:
|
|
402
403
|
error: str = f"{self.name} exited while answering `{method}`"
|
|
403
|
-
raise
|
|
404
|
+
raise ExitedError(error)
|
|
404
405
|
return message
|
|
405
406
|
|
|
406
407
|
def _waited(self, method: str) -> _Object | None:
|
|
@@ -442,7 +443,7 @@ class Connection:
|
|
|
442
443
|
def _answer(self, message: _Object) -> None:
|
|
443
444
|
"""Answer one of the server's own requests: its settings, and nothing else it asks for."""
|
|
444
445
|
method: str = str(message[_METHOD])
|
|
445
|
-
reply: _Object = {
|
|
446
|
+
reply: _Object = {_ID: message[_ID]}
|
|
446
447
|
if method == _CONFIGURATION:
|
|
447
448
|
items: list[_Json] = cast("list[_Json]", cast("_Object", message["params"])["items"])
|
|
448
449
|
reply["result"] = [protocol.settings(cast("_Object", item).get("section")) for item in items]
|
|
@@ -454,22 +455,18 @@ class Connection:
|
|
|
454
455
|
|
|
455
456
|
def notify(self, method: str, params: _Json) -> None:
|
|
456
457
|
"""Send a notification: no answer comes."""
|
|
457
|
-
self._send({"
|
|
458
|
+
self._send({"method": method, "params": params})
|
|
458
459
|
|
|
459
460
|
def _send(self, message: _Object) -> None:
|
|
460
|
-
"""Frame and send one message.
|
|
461
|
+
"""Frame and send one message (see `protocol.write`).
|
|
461
462
|
|
|
462
463
|
Raises:
|
|
463
|
-
|
|
464
|
+
ExitedError: The server has exited.
|
|
464
465
|
|
|
465
466
|
"""
|
|
466
|
-
body: bytes = json.dumps(message).encode()
|
|
467
467
|
with self.writing: # one message at a time: the reader answers the server's own meanwhile
|
|
468
468
|
try:
|
|
469
|
-
protocol.write(
|
|
470
|
-
cast("IO[bytes]", self.process.stdin),
|
|
471
|
-
f"Content-Length: {len(body)}\r\n\r\n".encode() + body,
|
|
472
|
-
)
|
|
469
|
+
protocol.write(cast("IO[bytes]", self.process.stdin), message)
|
|
473
470
|
except (OSError, ValueError) as error: # it has gone, or its input was closed to stop it
|
|
474
471
|
failure: str = f"{self.name} exited: {error}"
|
|
475
|
-
raise
|
|
472
|
+
raise ExitedError(failure) from error
|
constricter/cli/lsp/edits.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""`--infer-with`: what a hint's own edits would write (see `Offered`).
|
|
3
2
|
|
|
4
3
|
An editor that accepts an inlay hint applies its `textEdits`: the annotation, spelled as the file can
|
|
@@ -20,7 +19,7 @@ from urllib.request import url2pathname
|
|
|
20
19
|
from constricter.cli.lsp.protocol import Json, Object
|
|
21
20
|
from constricter.fix.core.known import Offered
|
|
22
21
|
from constricter.fix.index.modules import SUFFIX, module_name
|
|
23
|
-
from constricter.
|
|
22
|
+
from constricter.source import parsed
|
|
24
23
|
|
|
25
24
|
_Where: TypeAlias = tuple[int, int] # a line (from 1) and a UTF-8 byte column, as `ast` counts
|
|
26
25
|
_Locate: TypeAlias = Callable[[Object], _Where | None] # a position's, or `None` past the file's end
|
constricter/cli/lsp/guard.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
1
|
"""Run a type checker's language server for `--infer-with`, so it can't outlive constricter.
|
|
3
2
|
|
|
4
|
-
python .../constricter/cli/guard.py GRACE EXECUTABLE [ARGUMENT ...]
|
|
3
|
+
python .../constricter/cli/lsp/guard.py GRACE EXECUTABLE [ARGUMENT ...]
|
|
5
4
|
|
|
6
5
|
It imports only the standard library, so it runs by its path whatever the Python can import.
|
|
7
6
|
|
|
@@ -66,20 +65,23 @@ def main(argv: Sequence[str], stdin: IO[bytes] | None = None, stdout: IO[bytes]
|
|
|
66
65
|
|
|
67
66
|
def _pass_on(source: IO[bytes], target: IO[bytes]) -> None:
|
|
68
67
|
"""Pass the server's output on until it ends (or constricter's end of it has gone)."""
|
|
69
|
-
chunk: bytes
|
|
70
68
|
with contextlib.suppress(OSError):
|
|
71
|
-
|
|
72
|
-
|
|
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)
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
def relay_to(source: IO[bytes], server: "subprocess.Popen[bytes]", grace: float) -> None:
|
|
76
80
|
"""Pass `source` on to the server's input; when it ends, end the server's, and give it `grace` seconds."""
|
|
77
81
|
target: IO[bytes] = cast("IO[bytes]", server.stdin)
|
|
78
|
-
chunk: bytes
|
|
79
82
|
# The server has gone (or this is exiting, its input closed): nothing more to pass on.
|
|
80
83
|
with contextlib.suppress(OSError, ValueError):
|
|
81
|
-
|
|
82
|
-
_write(target, chunk)
|
|
84
|
+
_copied(source, target)
|
|
83
85
|
with contextlib.suppress(OSError): # what's left for a server that has gone is dropped
|
|
84
86
|
target.close()
|
|
85
87
|
try:
|