python-constricter 0.2.7__tar.gz → 0.2.9__tar.gz
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.
- {python_constricter-0.2.7 → python_constricter-0.2.9}/PKG-INFO +4 -2
- {python_constricter-0.2.7 → python_constricter-0.2.9}/README.md +3 -1
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/__init__.py +1 -1
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/command.py +82 -6
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/hints.py +51 -24
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/options.py +36 -2
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/protocol.py +57 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/runs.py +2 -1
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/schedule.py +9 -1
- python_constricter-0.2.9/constricter/fix/atoms.py +407 -0
- python_constricter-0.2.9/constricter/fix/callers.py +153 -0
- python_constricter-0.2.9/constricter/fix/classnames.py +107 -0
- python_constricter-0.2.9/constricter/fix/declared.py +426 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/doubts.py +9 -6
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/guesses.py +6 -2
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/imports.py +14 -1
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/inference.py +85 -4
- python_constricter-0.2.9/constricter/fix/installed.py +295 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/known.py +68 -3
- python_constricter-0.2.9/constricter/fix/library.py +164 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/members.py +9 -2
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/modules.py +132 -9
- python_constricter-0.2.9/constricter/fix/overloads.py +826 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/project.py +180 -40
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/returned.py +26 -1
- python_constricter-0.2.9/constricter/fix/signatures.py +85 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/stdlib.py +117 -40
- python_constricter-0.2.9/constricter/fix/stubbed.py +990 -0
- python_constricter-0.2.9/constricter/fix/tables/generic_attributes.json +55 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/method_signatures.json +149 -149
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/overloads.json +251 -125
- python_constricter-0.2.9/constricter/fix/tables/scalar_members.json +16 -0
- python_constricter-0.2.9/constricter/fix/tables/scalars.json +2702 -0
- python_constricter-0.2.9/constricter/fix/tables/subscriptable.json +76 -0
- python_constricter-0.2.9/constricter/fix/tables/variables.json +2938 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/offences.py +3 -1
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/binding.py +13 -3
- python_constricter-0.2.9/constricter/rules/calls.py +211 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/checker.py +22 -11
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/late.py +3 -2
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/scope.py +52 -18
- {python_constricter-0.2.7 → python_constricter-0.2.9}/pyproject.toml +6 -2
- python_constricter-0.2.7/constricter/fix/library.py +0 -80
- python_constricter-0.2.7/constricter/fix/overloads.py +0 -394
- {python_constricter-0.2.7 → python_constricter-0.2.9}/LICENSE.md +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/__main__.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/__init__.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/baseline.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/collecting.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/config.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/explain.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/guard.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/paths.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/report.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/cli/workers.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/__init__.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/fills.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/fixes.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/hinted.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/narrowed.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/opened.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/order.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/returns.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/aliases.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/attributes.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/bases.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/classes.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/method_overloads.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/methods.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/returns.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/source.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/tables/type_parameters.json +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/fix/targets.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/jsonc.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/noqa.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/notebook.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/plugins/__init__.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/plugins/flake8.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/plugins/pylint.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/py.typed +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/__init__.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/annotations.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/flow.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/narrowing.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/parsed.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/rebinding.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/redundant.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/syntax.py +0 -0
- {python_constricter-0.2.7 → python_constricter-0.2.9}/constricter/rules/walked.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-constricter
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Lint rules: every local variable is typed where it's first bound. A flake8 plugin, a pylint plugin and a standalone CLI.
|
|
5
5
|
Author: Ivy Duggan
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -173,6 +173,8 @@ Options:
|
|
|
173
173
|
| Option | CLI | `[tool.constricter]` | flake8 (CLI or config) | pylint |
|
|
174
174
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
|
|
175
175
|
| level | `--level` | `level` | `--constricter-level` | `constricter-level` |
|
|
176
|
+
| strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
|
|
177
|
+
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright and ty that's installed and runs | — | — | — |
|
|
176
178
|
| type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
|
|
177
179
|
| all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
|
|
178
180
|
| nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
|
|
@@ -262,7 +264,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
262
264
|
|
|
263
265
|
```yaml
|
|
264
266
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
265
|
-
rev: v0.2.
|
|
267
|
+
rev: v0.2.9
|
|
266
268
|
hooks:
|
|
267
269
|
- id: constricter
|
|
268
270
|
```
|
|
@@ -146,6 +146,8 @@ Options:
|
|
|
146
146
|
| Option | CLI | `[tool.constricter]` | flake8 (CLI or config) | pylint |
|
|
147
147
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
|
|
148
148
|
| level | `--level` | `level` | `--constricter-level` | `constricter-level` |
|
|
149
|
+
| strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
|
|
150
|
+
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright and ty that's installed and runs | — | — | — |
|
|
149
151
|
| type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
|
|
150
152
|
| all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
|
|
151
153
|
| nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
|
|
@@ -235,7 +237,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
235
237
|
|
|
236
238
|
```yaml
|
|
237
239
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
238
|
-
rev: v0.2.
|
|
240
|
+
rev: v0.2.9
|
|
239
241
|
hooks:
|
|
240
242
|
- id: constricter
|
|
241
243
|
```
|
|
@@ -9,7 +9,7 @@ import io
|
|
|
9
9
|
import json
|
|
10
10
|
import sys
|
|
11
11
|
import tokenize
|
|
12
|
-
from collections.abc import Callable, Sequence
|
|
12
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
13
13
|
from dataclasses import replace
|
|
14
14
|
from functools import partial
|
|
15
15
|
from pathlib import Path
|
|
@@ -23,8 +23,8 @@ from constricter.cli.protocol import HintError
|
|
|
23
23
|
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
24
24
|
from constricter.cli.runs import BaselineRun, CheckRun, CoverageRun, FileRun
|
|
25
25
|
from constricter.cli.workers import Workers
|
|
26
|
-
from constricter.fix import fixes, project
|
|
27
|
-
from constricter.fix.known import Hints, Outside, Returns
|
|
26
|
+
from constricter.fix import callers, fixes, installed, project
|
|
27
|
+
from constricter.fix.known import Callee, Hints, Outside, Returns
|
|
28
28
|
from constricter.noqa import lines, unsuppressed
|
|
29
29
|
from constricter.offences import (
|
|
30
30
|
DEFAULT_CHECKS,
|
|
@@ -41,6 +41,7 @@ EXIT_CLEAN: Final = 0
|
|
|
41
41
|
EXIT_FOUND: Final = 1
|
|
42
42
|
EXIT_ERROR: Final = 2
|
|
43
43
|
_ALL: Final = 100 # percent
|
|
44
|
+
_PY: Final = ".py" # a module the index names (not a notebook)
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def _encoding(path: Path, data: bytes) -> str:
|
|
@@ -264,7 +265,11 @@ def _check_path(path: Path, outside: Outside, options: Options) -> CheckRun:
|
|
|
264
265
|
raw, checked, error = _read_checked(path, name, options.checks, outside)
|
|
265
266
|
if error:
|
|
266
267
|
return CheckRun(error=error)
|
|
267
|
-
return replace(
|
|
268
|
+
return replace(
|
|
269
|
+
_handled(path, (raw, name), checked.offences, options),
|
|
270
|
+
returned=checked.returned,
|
|
271
|
+
calls=checked.calls,
|
|
272
|
+
)
|
|
268
273
|
|
|
269
274
|
|
|
270
275
|
def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], options: Options) -> CheckRun:
|
|
@@ -350,8 +355,10 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
350
355
|
else:
|
|
351
356
|
check = partial(_check_path, options=options)
|
|
352
357
|
names: list[Path] = [options.input.name(path) for path in paths]
|
|
353
|
-
if
|
|
358
|
+
if options.mode in {Mode.COVERAGE, Mode.WRITE_BASELINE}:
|
|
354
359
|
return names, _checked_all(paths, check, options)[0]
|
|
360
|
+
if not options.infer_with:
|
|
361
|
+
return names, _called_again(paths, _checked_all(paths, check, options), check, options)[0]
|
|
355
362
|
session: hints.Session
|
|
356
363
|
# Imported only for `--infer-with`: it's slow to import, for every run's start.
|
|
357
364
|
with cast("type[hints.Session]", importlib.import_module("constricter.cli.hints").Session)(
|
|
@@ -362,7 +369,13 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
362
369
|
) as session:
|
|
363
370
|
runs: list[FileRun]
|
|
364
371
|
modules: project.Index
|
|
365
|
-
runs, modules =
|
|
372
|
+
runs, modules = _called_again(
|
|
373
|
+
paths,
|
|
374
|
+
_checked_all(paths, check, options, session),
|
|
375
|
+
check,
|
|
376
|
+
options,
|
|
377
|
+
session,
|
|
378
|
+
)
|
|
366
379
|
# The files the last round changed: only their hints can have changed.
|
|
367
380
|
again: list[int] = [
|
|
368
381
|
index
|
|
@@ -387,6 +400,67 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
387
400
|
return names, runs
|
|
388
401
|
|
|
389
402
|
|
|
403
|
+
def _called_again(
|
|
404
|
+
paths: Sequence[Path],
|
|
405
|
+
first: tuple[list[FileRun], project.Index],
|
|
406
|
+
check: Callable[[Path, Outside], FileRun],
|
|
407
|
+
options: Options,
|
|
408
|
+
session: "hints.Session | None" = None,
|
|
409
|
+
) -> tuple[list[FileRun], project.Index]:
|
|
410
|
+
"""Check again the files whose functions' unannotated parameters every call passes one type.
|
|
411
|
+
|
|
412
|
+
Knowing those types (see `callers`), as guesses; a file among several of one module name isn't
|
|
413
|
+
(which its callers call can't be told). Then the other files calling those functions, knowing
|
|
414
|
+
what they now return. With `--fix` a file's rounds are joined; otherwise the last, knowing more,
|
|
415
|
+
stands.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
What each file found, and the index, with the parameters' types.
|
|
419
|
+
|
|
420
|
+
"""
|
|
421
|
+
runs: list[FileRun]
|
|
422
|
+
modules: project.Index
|
|
423
|
+
runs, modules = first
|
|
424
|
+
found: dict[str, callers.Parameters] = callers.parameters(
|
|
425
|
+
(run.calls for run in runs if isinstance(run, CheckRun)),
|
|
426
|
+
modules,
|
|
427
|
+
)
|
|
428
|
+
names: list[str | None] = [project.module_name(path) if path.suffix == _PY else None for path in paths]
|
|
429
|
+
again: list[int]
|
|
430
|
+
if not (again := [at for at, name in enumerate(names) if name in found and names.count(name) == 1]):
|
|
431
|
+
return runs, modules
|
|
432
|
+
calling: list[int] = _calling(runs, found, again)
|
|
433
|
+
modules = callers.with_parameters(modules, found)
|
|
434
|
+
which: list[int]
|
|
435
|
+
for which in (again, calling):
|
|
436
|
+
redone: list[FileRun]
|
|
437
|
+
redone, modules = _checked_all([paths[at] for at in which], check, options, session, modules)
|
|
438
|
+
at: int
|
|
439
|
+
run: FileRun
|
|
440
|
+
for at, run in zip(which, redone, strict=True):
|
|
441
|
+
runs[at] = _merged(runs[at], run) if options.mode is Mode.FIX else run
|
|
442
|
+
return runs, modules
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _calling(
|
|
446
|
+
runs: Sequence[FileRun],
|
|
447
|
+
found: Mapping[str, callers.Parameters],
|
|
448
|
+
again: Sequence[int],
|
|
449
|
+
) -> list[int]:
|
|
450
|
+
"""Find the files (not among `again`) calling a function whose parameters `found` types.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Their indexes.
|
|
454
|
+
|
|
455
|
+
"""
|
|
456
|
+
typed: set[Callee] = {(module, function) for module, functions in found.items() for function in functions}
|
|
457
|
+
return [
|
|
458
|
+
at
|
|
459
|
+
for at, run in enumerate(runs)
|
|
460
|
+
if at not in again and isinstance(run, CheckRun) and typed & run.calls.calls.keys()
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
|
|
390
464
|
def _checked_all(
|
|
391
465
|
paths: Sequence[Path],
|
|
392
466
|
check: Callable[[Path, Outside], FileRun],
|
|
@@ -412,6 +486,8 @@ def _checked_all(
|
|
|
412
486
|
elif modules is None:
|
|
413
487
|
modules = project.Index({}, []) if coverage else project.index(paths)
|
|
414
488
|
collecting.indexed()
|
|
489
|
+
if not coverage:
|
|
490
|
+
modules = installed.with_installed(modules, installed.search_path())
|
|
415
491
|
return schedule.checked(
|
|
416
492
|
paths,
|
|
417
493
|
check,
|
|
@@ -59,6 +59,12 @@ _LINE_BREAK: Final = re.compile(r"\r\n|\r|\n") # the lines positions count, as
|
|
|
59
59
|
_UTF16: Final = "utf-16"
|
|
60
60
|
_UTF8: Final = "utf-8"
|
|
61
61
|
_METHOD_NOT_FOUND: Final = -32601
|
|
62
|
+
# A request the server dropped because a document changed under it (ty, as later files open): the
|
|
63
|
+
# client is to ask again.
|
|
64
|
+
_CONTENT_MODIFIED: Final = -32801
|
|
65
|
+
_ASK_AGAIN: Final = 5 # how many times a hint request is asked again after that, at most
|
|
66
|
+
_HINTS: Final = "textDocument/inlayHint"
|
|
67
|
+
_MODIFIED: Final[_Object] = {} # a hint request's answer, when it's to be asked again (by identity)
|
|
62
68
|
_ID: Final = "id"
|
|
63
69
|
_METHOD: Final = "method"
|
|
64
70
|
_ERROR: Final = "error"
|
|
@@ -325,14 +331,14 @@ def command(checker: str) -> list[str]:
|
|
|
325
331
|
|
|
326
332
|
"""
|
|
327
333
|
server: Server = SERVERS[checker]
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
334
|
+
found: str | None
|
|
335
|
+
if (found := protocol.executable(checker)) is None:
|
|
336
|
+
install: str = f"`pip install {checker}`"
|
|
331
337
|
message: str = (
|
|
332
|
-
f"--infer-with={checker} needs `{
|
|
338
|
+
f"--infer-with={checker} needs `{server.executable}`, which isn't installed ({install})"
|
|
333
339
|
)
|
|
334
340
|
raise HintError(message)
|
|
335
|
-
return [
|
|
341
|
+
return [found, *server.args]
|
|
336
342
|
|
|
337
343
|
|
|
338
344
|
class Connection:
|
|
@@ -450,32 +456,51 @@ class Connection:
|
|
|
450
456
|
text: str
|
|
451
457
|
for path, text in files.items():
|
|
452
458
|
self._opened(path.resolve().as_uri(), text)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
459
|
+
return _Asked({self._ask_hints(path, lines[path]): path for path in files}, lines)
|
|
460
|
+
|
|
461
|
+
def _ask_hints(self, path: Path, lines: Sequence[str]) -> int:
|
|
462
|
+
"""Ask for an open file's hints over the whole of it, without waiting for them.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
The request's number.
|
|
466
|
+
|
|
467
|
+
"""
|
|
468
|
+
return self._ask(
|
|
469
|
+
_HINTS,
|
|
470
|
+
{
|
|
471
|
+
"textDocument": {"uri": path.resolve().as_uri()},
|
|
472
|
+
"range": {"start": {"line": 0, "character": 0}, "end": {"line": len(lines), "character": 0}},
|
|
473
|
+
},
|
|
474
|
+
)
|
|
467
475
|
|
|
468
476
|
def answers(self, asked: "_Asked") -> _Found:
|
|
469
|
-
"""Wait for the answers to hint requests `ask` sent.
|
|
477
|
+
"""Wait for the answers to hint requests `ask` sent, asking again for those the server dropped.
|
|
470
478
|
|
|
471
479
|
Returns:
|
|
472
480
|
Each file's variable-type hints' texts, by where the name each types ends.
|
|
473
481
|
|
|
482
|
+
Raises:
|
|
483
|
+
HintError: The server dropped one `_ASK_AGAIN` times more (see `_CONTENT_MODIFIED`).
|
|
484
|
+
|
|
474
485
|
"""
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
486
|
+
found: _Found = {}
|
|
487
|
+
waiting: dict[int, Path] = asked.numbers
|
|
488
|
+
for _ in range(_ASK_AGAIN + 1):
|
|
489
|
+
answers: dict[int, _Json] = self._collect(set(waiting), _HINTS)
|
|
490
|
+
found.update(
|
|
491
|
+
(path, self._found(answers[number], asked.lines[path]))
|
|
492
|
+
for number, path in waiting.items()
|
|
493
|
+
if answers[number] is not _MODIFIED
|
|
494
|
+
)
|
|
495
|
+
waiting = {
|
|
496
|
+
self._ask_hints(path, asked.lines[path]): path
|
|
497
|
+
for number, path in waiting.items()
|
|
498
|
+
if answers[number] is _MODIFIED
|
|
499
|
+
}
|
|
500
|
+
if not waiting:
|
|
501
|
+
return found
|
|
502
|
+
error: str = f"{self.name} failed `{_HINTS}`: content modified, {_ASK_AGAIN + 1} times"
|
|
503
|
+
raise HintError(error)
|
|
479
504
|
|
|
480
505
|
def _opened(self, uri: str, text: str) -> None:
|
|
481
506
|
"""Send a file's text: opening it, or (open already) replacing its whole text, if it's changed."""
|
|
@@ -586,6 +611,8 @@ class Connection:
|
|
|
586
611
|
)
|
|
587
612
|
if _ERROR in answer:
|
|
588
613
|
failure: _Object = cast("_Object", answer[_ERROR])
|
|
614
|
+
if method == _HINTS and failure.get("code") == _CONTENT_MODIFIED:
|
|
615
|
+
return cast("int", answer[_ID]), _MODIFIED
|
|
589
616
|
error: str = f"{self.name} failed `{method}`: {failure.get('message')}"
|
|
590
617
|
raise HintError(error)
|
|
591
618
|
return cast("int", answer[_ID]), answer.get("result")
|
|
@@ -21,7 +21,7 @@ from constricter.cli.config import (
|
|
|
21
21
|
)
|
|
22
22
|
from constricter.cli.explain import explain
|
|
23
23
|
from constricter.cli.paths import STDIN, excluded
|
|
24
|
-
from constricter.cli.protocol import SERVERS
|
|
24
|
+
from constricter.cli.protocol import SERVERS, runs
|
|
25
25
|
from constricter.cli.report import Format, Result
|
|
26
26
|
from constricter.offences import (
|
|
27
27
|
LEVELS,
|
|
@@ -193,6 +193,16 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
193
193
|
default="strict",
|
|
194
194
|
help="which codes are errors rather than warnings (default: strict)",
|
|
195
195
|
)
|
|
196
|
+
_ = parser.add_argument(
|
|
197
|
+
"--max",
|
|
198
|
+
action="store_true",
|
|
199
|
+
help="the strictest check: suffocate everywhere, all scopes, the opt-in codes (command line only)",
|
|
200
|
+
)
|
|
201
|
+
_ = parser.add_argument(
|
|
202
|
+
"--max-fix",
|
|
203
|
+
action="store_true",
|
|
204
|
+
help="--max, --fix --unsafe-fixes, and every installed checker's hints (command line only)",
|
|
205
|
+
)
|
|
196
206
|
_ = parser.add_argument(
|
|
197
207
|
"--format",
|
|
198
208
|
type=Format,
|
|
@@ -492,7 +502,7 @@ class Options:
|
|
|
492
502
|
parser.set_defaults(**config_defaults(Path.cwd()))
|
|
493
503
|
except ValueError as error:
|
|
494
504
|
parser.error(str(error))
|
|
495
|
-
args: argparse.Namespace = parser.parse_args(argv)
|
|
505
|
+
args: argparse.Namespace = _maximal(parser.parse_args(argv))
|
|
496
506
|
code: str | None
|
|
497
507
|
if (code := cast("str | None", args.explain)) is not None:
|
|
498
508
|
_ = sys.stdout.write(explain(code))
|
|
@@ -538,6 +548,30 @@ class Options:
|
|
|
538
548
|
)
|
|
539
549
|
|
|
540
550
|
|
|
551
|
+
def _maximal(args: argparse.Namespace) -> argparse.Namespace:
|
|
552
|
+
"""Apply `--max-fix` (`--max`, every fix, every installed checker) and `--max`.
|
|
553
|
+
|
|
554
|
+
`--max`: `suffocate` for every path (over `per-path-levels`), all scopes, the opt-in codes.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
The arguments.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
if cast("bool", args.max_fix):
|
|
561
|
+
args.max = args.fix = args.unsafe_fixes = True
|
|
562
|
+
chosen: list[str] = cast("list[str]", args.infer_with)
|
|
563
|
+
args.infer_with = [
|
|
564
|
+
*chosen,
|
|
565
|
+
*(checker for checker in SERVERS if checker not in chosen and runs(checker)),
|
|
566
|
+
]
|
|
567
|
+
if cast("bool", args.max):
|
|
568
|
+
args.level = Level.SUFFOCATE.name.lower()
|
|
569
|
+
args.per_path_levels = {}
|
|
570
|
+
args.all_scopes = True
|
|
571
|
+
args.extend_select = [*cast("list[str]", args.extend_select), *sorted(OPT_IN)]
|
|
572
|
+
return args
|
|
573
|
+
|
|
574
|
+
|
|
541
575
|
def _filter(parser: argparse.ArgumentParser, args: argparse.Namespace, mode: Mode) -> Filter:
|
|
542
576
|
"""Build the filter the options set, reading the baseline (the default one, if it exists).
|
|
543
577
|
|
|
@@ -6,6 +6,10 @@ type is its label after its `:`; basedpyright is asked for variable types only.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess # asks a checker whether it runs
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
9
13
|
from typing import IO, Final, NamedTuple, TypeAlias, cast
|
|
10
14
|
|
|
11
15
|
Json: TypeAlias = dict[str, "Json"] | list["Json"] | str | int | float | bool | None
|
|
@@ -44,6 +48,59 @@ SERVERS: Final = {
|
|
|
44
48
|
# Parallel already: more servers only repeat its work (sqlalchemy's: 0.9s with one, 0.7s with four).
|
|
45
49
|
"ty": Server("ty", ("server",), 1),
|
|
46
50
|
}
|
|
51
|
+
_PROBE: Final = "--version" # quick for both; basedpyright-langserver's exits 1 all the same
|
|
52
|
+
_PROBE_TIMEOUT: Final = 10.0 # seconds it may take
|
|
53
|
+
_CANT_RUN: Final = frozenset({126, 127}) # the shell's "can't execute" and "not found": a broken shim's
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def executable(checker: str) -> str | None:
|
|
57
|
+
"""Find `checker`'s language server: on `PATH`, or beside this Python (an unactivated venv).
|
|
58
|
+
|
|
59
|
+
The first that runs (a version manager's shim can be found, and not run), else the first found.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Its path, or `None` if it isn't installed.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
name: str = SERVERS[checker].executable
|
|
66
|
+
found: list[str] = list(
|
|
67
|
+
dict.fromkeys(
|
|
68
|
+
path
|
|
69
|
+
for path in (shutil.which(name), shutil.which(name, path=str(Path(sys.executable).parent)))
|
|
70
|
+
if path is not None
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
return next((path for path in found if _runs(path)), found[0] if found else None)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def runs(checker: str) -> bool:
|
|
77
|
+
"""Check that `checker` is installed, and runs.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Whether it does.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
found: str | None = executable(checker)
|
|
84
|
+
return found is not None and _runs(found)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _runs(path: str) -> bool:
|
|
88
|
+
"""Check that an executable runs: its `--version` starts, whatever it answers.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Whether it does.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
done: subprocess.CompletedProcess[bytes] = subprocess.run(
|
|
96
|
+
[path, _PROBE],
|
|
97
|
+
capture_output=True,
|
|
98
|
+
timeout=_PROBE_TIMEOUT,
|
|
99
|
+
check=False,
|
|
100
|
+
)
|
|
101
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
102
|
+
return False
|
|
103
|
+
return done.returncode not in _CANT_RUN
|
|
47
104
|
|
|
48
105
|
|
|
49
106
|
class HintError(Exception):
|
|
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from typing import TypeAlias
|
|
6
6
|
|
|
7
7
|
from constricter.cli.report import Result
|
|
8
|
-
from constricter.fix.known import Returns
|
|
8
|
+
from constricter.fix.known import Observed, Returns
|
|
9
9
|
from constricter.offences import Offence
|
|
10
10
|
from constricter.rules.checker import Coverage
|
|
11
11
|
|
|
@@ -20,6 +20,7 @@ class CheckRun:
|
|
|
20
20
|
text: str = "" # --diff: the diff; --fix on standard input: the fixed source
|
|
21
21
|
error: str = ""
|
|
22
22
|
returned: Returns = field(default_factory=Returns) # what its unannotated functions return
|
|
23
|
+
calls: Observed = field(default_factory=Observed) # what it passes checked files' functions
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
@dataclass(frozen=True)
|
|
@@ -13,7 +13,7 @@ from typing import Final
|
|
|
13
13
|
|
|
14
14
|
from constricter.cli.runs import CoverageRun, FileRun
|
|
15
15
|
from constricter.cli.workers import Checking, Workers, check_share, first_done
|
|
16
|
-
from constricter.fix import order, project
|
|
16
|
+
from constricter.fix import callers, order, project, stubbed
|
|
17
17
|
from constricter.fix.known import Hints, Outside
|
|
18
18
|
|
|
19
19
|
CYCLE_ROUNDS: Final = 3 # how many times to check again files calling each other's functions
|
|
@@ -27,6 +27,7 @@ def outside(modules: project.Index, path: Path, hinted: Mapping[Path, tuple[Hint
|
|
|
27
27
|
|
|
28
28
|
"""
|
|
29
29
|
imported: project.Imported = project.imported(modules, path)
|
|
30
|
+
methods: stubbed.Methods = stubbed.methods(modules, path, imported.guarded)
|
|
30
31
|
return Outside(
|
|
31
32
|
imported.calls,
|
|
32
33
|
imported.classes,
|
|
@@ -35,6 +36,13 @@ def outside(modules: project.Index, path: Path, hinted: Mapping[Path, tuple[Hint
|
|
|
35
36
|
imported.returned,
|
|
36
37
|
imported.guarded,
|
|
37
38
|
imported.generics,
|
|
39
|
+
callers.callees(modules, path),
|
|
40
|
+
callers.own_parameters(modules, path),
|
|
41
|
+
{**stubbed.overloaded(modules, path), **methods.signatures},
|
|
42
|
+
stubbed.classes(modules, path),
|
|
43
|
+
methods.parameters,
|
|
44
|
+
methods.lineage,
|
|
45
|
+
methods.aliases,
|
|
38
46
|
)
|
|
39
47
|
|
|
40
48
|
|