python-constricter 0.3.2__tar.gz → 0.3.3__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.3.2 → python_constricter-0.3.3}/.gitignore +1 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/PKG-INFO +5 -3
- {python_constricter-0.3.2 → python_constricter-0.3.3}/README.md +4 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/__init__.py +1 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/command.py +9 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/config.py +2 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/options.py +28 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/schedule.py +24 -4
- python_constricter-0.3.3/constricter/cli/traced.py +289 -0
- python_constricter-0.3.3/constricter/fix/core/asked.py +56 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/core/fixes.py +12 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/core/imports.py +98 -10
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/core/inherited.py +20 -7
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/core/known.py +99 -11
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/core/signatures.py +7 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/atoms.py +4 -1
- python_constricter-0.3.3/constricter/fix/index/beyond.py +90 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/callers.py +1 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/declared.py +16 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/decorated.py +1 -1
- python_constricter-0.3.3/constricter/fix/index/fixtures.py +176 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/installed.py +35 -14
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/modules.py +43 -5
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/order.py +5 -2
- python_constricter-0.3.3/constricter/fix/index/own_overloads.py +57 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/plain.py +10 -7
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/project.py +104 -46
- python_constricter-0.3.3/constricter/fix/index/read.py +70 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/stubbed.py +39 -72
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/libraries/library.py +116 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/libraries/opened.py +33 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/libraries/overloads.py +126 -11
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/libraries/stdlib.py +216 -13
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/attributes.json +23 -0
- python_constricter-0.3.3/constricter/fix/tables/awaited.json +69 -0
- python_constricter-0.3.3/constricter/fix/tables/awaited_overloads.json +14 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/classes.json +2 -1
- python_constricter-0.3.3/constricter/fix/tables/elements.json +156 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/functions.json +0 -1
- python_constricter-0.3.3/constricter/fix/tables/generic_attributes.json +252 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/method_overloads.json +133 -94
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/method_signatures.json +179 -20
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/methods.json +34 -10
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/overloads.json +52 -28
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/scalars.json +3 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/source.json +4 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/subscriptable.json +1 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/type_parameters.json +2 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/variables.json +21 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/classvars.py +100 -27
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/decided.py +52 -9
- python_constricter-0.3.3/constricter/fix/values/displays.py +87 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/doubts.py +48 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/entered.py +22 -1
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/fills.py +56 -25
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/guesses.py +34 -7
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/hinted.py +94 -7
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/inference.py +104 -99
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/members.py +57 -9
- python_constricter-0.3.3/constricter/fix/values/operated.py +199 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/returned.py +184 -18
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/returns.py +1 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/shapes.py +136 -21
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/targets.py +9 -3
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/offences.py +3 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/annotations.py +22 -3
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/binding.py +11 -14
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/calls.py +171 -25
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/checker.py +139 -22
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/decorators.py +12 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/late.py +2 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/parsed.py +42 -3
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/rebinding.py +4 -2
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/recorded.py +42 -3
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/scope.py +51 -15
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/tables.py +62 -6
- python_constricter-0.3.3/constricter/trace.py +385 -0
- python_constricter-0.3.3/stdlib_tables/elements.py +77 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/generate.py +165 -13
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/overloads.py +100 -16
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/reading.py +42 -11
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/templates.py +6 -1
- python_constricter-0.3.2/constricter/fix/tables/generic_attributes.json +0 -55
- {python_constricter-0.3.2 → python_constricter-0.3.3}/LICENSE.md +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/__main__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/baseline.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/collecting.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/edits.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/explain.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/guard.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/hints.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/paths.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/protocol.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/report.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/runs.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/cli/workers.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/core/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/classnames.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/offers.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/sides.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/index/tuples.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/libraries/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/aliases.json +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/bases.json +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/returns.json +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/tables/scalar_members.json +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/aliased.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/called.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/fix/values/narrowed.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/jsonc.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/noqa.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/notebook.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/plugins/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/plugins/flake8.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/plugins/pylint.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/py.typed +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/flow.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/narrowing.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/quoted.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/redundant.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/syntax.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/constricter/rules/walked.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/hatch_build.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/pyproject.toml +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/__init__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/__main__.py +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/partial.json +0 -0
- {python_constricter-0.3.2 → python_constricter-0.3.3}/stdlib_tables/stubs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-constricter
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Import-Name: constricter
|
|
5
5
|
Summary: Lint rules: every local variable is typed where it's first bound. A flake8 plugin, a pylint plugin and a standalone CLI.
|
|
6
6
|
Author: Ivy Duggan
|
|
@@ -187,6 +187,7 @@ Options:
|
|
|
187
187
|
| type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
|
|
188
188
|
| fix | `--fix` (`--unsafe-fixes` for guesses), `--diff` to preview | - | - | - |
|
|
189
189
|
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`, `pyrefly`, several: inferred types, as guesses) | `infer-with` | - | - |
|
|
190
|
+
| infer from | `--infer-from FILE` (the types `python -m constricter.trace -m pytest` recorded from a run, as guesses) | `infer-from` | - | - |
|
|
190
191
|
| show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
|
|
191
192
|
| fix levels | `--fix-select`, `--fix-ignore`, `--unsafe-fix-select` (mechanisms: [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#fix-levels)) | `fix-select`, `fix-ignore`, `unsafe-fix-select` | - | - |
|
|
192
193
|
| fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
|
|
@@ -246,7 +247,8 @@ method call of a local whose type is known, and values computed from those; a lo
|
|
|
246
247
|
unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
|
|
247
248
|
`--show-fixes` lists each fix and how its value decided it. `--infer-with basedpyright` (or `ty`, or
|
|
248
249
|
`pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
|
|
249
|
-
what `--fix` can't type itself, as guesses
|
|
250
|
+
what `--fix` can't type itself, as guesses; `--infer-from FILE` takes the types a run bound each
|
|
251
|
+
name to, recorded by `python -m constricter.trace -m pytest`, the same way. The full list is in
|
|
250
252
|
[docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
|
|
251
253
|
|
|
252
254
|
### Installing and running
|
|
@@ -272,7 +274,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
272
274
|
|
|
273
275
|
```yaml
|
|
274
276
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
275
|
-
rev: v0.3.
|
|
277
|
+
rev: v0.3.3
|
|
276
278
|
hooks:
|
|
277
279
|
- id: constricter
|
|
278
280
|
```
|
|
@@ -160,6 +160,7 @@ Options:
|
|
|
160
160
|
| type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
|
|
161
161
|
| fix | `--fix` (`--unsafe-fixes` for guesses), `--diff` to preview | - | - | - |
|
|
162
162
|
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`, `pyrefly`, several: inferred types, as guesses) | `infer-with` | - | - |
|
|
163
|
+
| infer from | `--infer-from FILE` (the types `python -m constricter.trace -m pytest` recorded from a run, as guesses) | `infer-from` | - | - |
|
|
163
164
|
| show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
|
|
164
165
|
| fix levels | `--fix-select`, `--fix-ignore`, `--unsafe-fix-select` (mechanisms: [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#fix-levels)) | `fix-select`, `fix-ignore`, `unsafe-fix-select` | - | - |
|
|
165
166
|
| fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
|
|
@@ -219,7 +220,8 @@ method call of a local whose type is known, and values computed from those; a lo
|
|
|
219
220
|
unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
|
|
220
221
|
`--show-fixes` lists each fix and how its value decided it. `--infer-with basedpyright` (or `ty`, or
|
|
221
222
|
`pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
|
|
222
|
-
what `--fix` can't type itself, as guesses
|
|
223
|
+
what `--fix` can't type itself, as guesses; `--infer-from FILE` takes the types a run bound each
|
|
224
|
+
name to, recorded by `python -m constricter.trace -m pytest`, the same way. The full list is in
|
|
223
225
|
[docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
|
|
224
226
|
|
|
225
227
|
### Installing and running
|
|
@@ -245,7 +247,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
245
247
|
|
|
246
248
|
```yaml
|
|
247
249
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
248
|
-
rev: v0.3.
|
|
250
|
+
rev: v0.3.3
|
|
249
251
|
hooks:
|
|
250
252
|
- id: constricter
|
|
251
253
|
```
|
|
@@ -16,7 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
|
|
17
17
|
|
|
18
18
|
from constricter import notebook
|
|
19
|
-
from constricter.cli import baseline, collecting, schedule
|
|
19
|
+
from constricter.cli import baseline, collecting, schedule, traced
|
|
20
20
|
from constricter.cli.options import Mode, Options, Output
|
|
21
21
|
from constricter.cli.paths import STDIN, python_files
|
|
22
22
|
from constricter.cli.protocol import HintError
|
|
@@ -33,6 +33,7 @@ from constricter.offences import (
|
|
|
33
33
|
Edit,
|
|
34
34
|
Offence,
|
|
35
35
|
)
|
|
36
|
+
from constricter.rules import parsed
|
|
36
37
|
from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
|
|
37
38
|
|
|
38
39
|
if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
@@ -297,13 +298,17 @@ def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], option
|
|
|
297
298
|
left: list[Result] = [r for r in results if r.offence not in fixing]
|
|
298
299
|
if path == STDIN: # the fixed source goes to stdout
|
|
299
300
|
return CheckRun(left, baselined, len(fixing), _fixed(raw, name, fixing).text)
|
|
301
|
+
fixed: int
|
|
300
302
|
try:
|
|
301
|
-
|
|
303
|
+
fixed = fix_file(path, fixing)
|
|
302
304
|
except UnicodeEncodeError as failure: # the file is left as it was, its offences unfixed
|
|
303
305
|
# Its canonical name: PyPy reports `latin1` where CPython says `latin-1`.
|
|
304
306
|
encoding: str = codecs.lookup(failure.encoding).name
|
|
305
307
|
message: str = f"an annotation can't be written in its encoding, {encoding}; left as it was"
|
|
306
308
|
return CheckRun(results, baselined, error=f"{name}: error: {message}")
|
|
309
|
+
if fixed:
|
|
310
|
+
_ = parsed.take(raw) # the tree of the text it had: no check will ask for it again
|
|
311
|
+
return CheckRun(left, baselined, fixed)
|
|
307
312
|
|
|
308
313
|
|
|
309
314
|
def _baseline_path(path: Path, outside: Outside, options: Options) -> BaselineRun:
|
|
@@ -505,6 +510,8 @@ def _checked_all(
|
|
|
505
510
|
|
|
506
511
|
"""
|
|
507
512
|
hinted: dict[Path, tuple[Hints, ...]] = {} if session is None else session.hints(_texts(paths))
|
|
513
|
+
if options.input.trace is not None:
|
|
514
|
+
hinted = traced.merged(hinted, traced.load(options.input.trace), paths)
|
|
508
515
|
coverage: bool = options.mode is Mode.COVERAGE # needs nothing from the other files
|
|
509
516
|
stack: contextlib.ExitStack
|
|
510
517
|
with contextlib.ExitStack() as stack:
|
|
@@ -223,7 +223,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
|
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
def _baseline(value: _Toml, root: Path) -> str | None:
|
|
226
|
-
"""Read a baseline path, relative to `root` (the pyproject.toml that names it).
|
|
226
|
+
"""Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
|
|
227
227
|
|
|
228
228
|
Returns:
|
|
229
229
|
The resolved path, or `None` if `value` isn't a non-empty string.
|
|
@@ -276,6 +276,7 @@ def config_defaults(start: Path) -> dict[str, Default]:
|
|
|
276
276
|
readers: dict[str, Callable[[_Toml], Default | None]] = {
|
|
277
277
|
**_READERS,
|
|
278
278
|
"baseline": partial(_baseline, root=path.parent),
|
|
279
|
+
"infer-from": partial(_baseline, root=path.parent),
|
|
279
280
|
}
|
|
280
281
|
defaults: dict[str, Default] = {}
|
|
281
282
|
key: str
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Final, cast
|
|
12
12
|
|
|
13
13
|
from constricter import __version__
|
|
14
|
-
from constricter.cli import baseline
|
|
14
|
+
from constricter.cli import baseline, traced
|
|
15
15
|
from constricter.cli.config import (
|
|
16
16
|
DEFAULT_BASELINE,
|
|
17
17
|
config_defaults,
|
|
@@ -353,6 +353,12 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
353
353
|
metavar="GB",
|
|
354
354
|
help="with --infer-with: the most memory each checker's servers use together (default: 8)",
|
|
355
355
|
)
|
|
356
|
+
_ = parser.add_argument(
|
|
357
|
+
"--infer-from",
|
|
358
|
+
type=Path,
|
|
359
|
+
metavar="FILE",
|
|
360
|
+
help="type what --fix can't with the types `python -m constricter.trace` recorded, as guesses",
|
|
361
|
+
)
|
|
356
362
|
_ = parser.add_argument(
|
|
357
363
|
"--diff",
|
|
358
364
|
action="store_true",
|
|
@@ -440,6 +446,7 @@ class Input:
|
|
|
440
446
|
paths: list[Path]
|
|
441
447
|
exclude: list[str]
|
|
442
448
|
stdin_name: Path = STDIN
|
|
449
|
+
trace: Path | None = None # the trace whose types `--fix` guesses with (`--infer-from`)
|
|
443
450
|
|
|
444
451
|
def name(self, path: Path) -> Path:
|
|
445
452
|
"""Name `path` for reports.
|
|
@@ -566,7 +573,12 @@ class Options:
|
|
|
566
573
|
parser.error("`-` (standard input) must be the only path")
|
|
567
574
|
mode: Mode = _mode(parser, args)
|
|
568
575
|
return cls(
|
|
569
|
-
input=Input(
|
|
576
|
+
input=Input(
|
|
577
|
+
paths,
|
|
578
|
+
cast("list[str]", args.exclude),
|
|
579
|
+
cast("Path", args.stdin_filename),
|
|
580
|
+
_trace(parser, cast("Path | None", args.infer_from)),
|
|
581
|
+
),
|
|
570
582
|
checks=Checks(
|
|
571
583
|
type_comments=cast("bool", args.type_comments),
|
|
572
584
|
all_scopes=cast("bool", args.all_scopes),
|
|
@@ -605,6 +617,20 @@ class Options:
|
|
|
605
617
|
)
|
|
606
618
|
|
|
607
619
|
|
|
620
|
+
def _trace(parser: argparse.ArgumentParser, path: Path | None) -> Path | None:
|
|
621
|
+
"""Check that `--infer-from`'s file is a trace, read here once for the run.
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
The path.
|
|
625
|
+
|
|
626
|
+
"""
|
|
627
|
+
try:
|
|
628
|
+
_ = None if path is None else traced.load(path)
|
|
629
|
+
except ValueError as error:
|
|
630
|
+
parser.error(str(error))
|
|
631
|
+
return path
|
|
632
|
+
|
|
633
|
+
|
|
608
634
|
def _maximal(args: argparse.Namespace) -> argparse.Namespace:
|
|
609
635
|
"""Apply `--max-fix` (`--max`, every fix, every installed checker) and `--max`.
|
|
610
636
|
|
|
@@ -13,8 +13,20 @@ 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.core.known import Guarded, Hints, Outside
|
|
17
|
-
from constricter.fix.index import
|
|
16
|
+
from constricter.fix.core.known import Guarded, Hints, Outside, Passed
|
|
17
|
+
from constricter.fix.index import (
|
|
18
|
+
beyond,
|
|
19
|
+
callers,
|
|
20
|
+
decorated,
|
|
21
|
+
fixtures,
|
|
22
|
+
offers,
|
|
23
|
+
order,
|
|
24
|
+
plain,
|
|
25
|
+
project,
|
|
26
|
+
sides,
|
|
27
|
+
stubbed,
|
|
28
|
+
tuples,
|
|
29
|
+
)
|
|
18
30
|
|
|
19
31
|
CYCLE_ROUNDS: Final = 3 # how many times to check again files calling each other's functions
|
|
20
32
|
|
|
@@ -26,7 +38,13 @@ def outside(modules: project.Index, path: Path, hinted: Mapping[Path, tuple[Hint
|
|
|
26
38
|
It.
|
|
27
39
|
|
|
28
40
|
"""
|
|
29
|
-
|
|
41
|
+
seeded: dict[str, Guarded] = {}
|
|
42
|
+
seeds: dict[str, Passed] = fixtures.visible(modules, path, seeded)
|
|
43
|
+
imported: project.Imported = project.imported(
|
|
44
|
+
modules,
|
|
45
|
+
path,
|
|
46
|
+
(seeded, fixtures.attributes(seeds)),
|
|
47
|
+
)
|
|
30
48
|
side_calls: dict[str, str]
|
|
31
49
|
guarded: dict[str, Guarded]
|
|
32
50
|
side_calls, guarded = sides.calls(modules, path, imported.guarded)
|
|
@@ -44,7 +62,7 @@ def outside(modules: project.Index, path: Path, hinted: Mapping[Path, tuple[Hint
|
|
|
44
62
|
imported.generics | own.generics,
|
|
45
63
|
callers.callees(modules, path),
|
|
46
64
|
callers.own_parameters(modules, path),
|
|
47
|
-
{**stubbed.overloaded(modules, path), **methods.signatures},
|
|
65
|
+
{**stubbed.overloaded(modules, path, guarded), **methods.signatures},
|
|
48
66
|
stubbed.classes(modules, path),
|
|
49
67
|
methods.parameters,
|
|
50
68
|
methods.lineage,
|
|
@@ -55,6 +73,8 @@ def outside(modules: project.Index, path: Path, hinted: Mapping[Path, tuple[Hint
|
|
|
55
73
|
project.same(modules, path, guarded),
|
|
56
74
|
imported.partial,
|
|
57
75
|
tuples.fields(modules, path, guarded),
|
|
76
|
+
seeds,
|
|
77
|
+
beyond.library_bases(modules, path),
|
|
58
78
|
)
|
|
59
79
|
|
|
60
80
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--infer-from`: the types a traced run saw (`python -m constricter.trace`), as a type checker's hints are.
|
|
3
|
+
|
|
4
|
+
A trace holds, for each file by its SHA-256, the types each function's locals held when it returned
|
|
5
|
+
(see `constricter.trace`). `hints` turns a file's into `Hints`, which `--fix` judges as it does a
|
|
6
|
+
checker's (`constricter.fix.values.hinted`): each a guess, of kind `traced`. Only for a local no
|
|
7
|
+
annotation types: one assigned a value rooted at a parameter its function leaves unannotated
|
|
8
|
+
(`row = rows[0].load()`; not `self` or `cls`). Anything else a type checker may
|
|
9
|
+
type wider than the run saw (an `X | None` that was never `None`, a base class, a `TypedDict`),
|
|
10
|
+
which the narrower annotation would make an error. And only one its function binds once, outside any
|
|
11
|
+
loop: the trace says what it held last, not where. Up to three types seen are their union, `None`
|
|
12
|
+
last.
|
|
13
|
+
|
|
14
|
+
A class is shown bare, with the import that names it for type checking alone (`Offered`), unless
|
|
15
|
+
the file defines it: `constricter.fix.index.offers` then takes only a class the index knows. One of
|
|
16
|
+
a private module of the standard library has no hint.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from collections import Counter
|
|
25
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
26
|
+
from functools import cache
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
29
|
+
|
|
30
|
+
from constricter import trace
|
|
31
|
+
from constricter.fix.core.known import Hints, Offered
|
|
32
|
+
from constricter.fix.values.hinted import TRACED
|
|
33
|
+
|
|
34
|
+
_LABEL: Final = "a traced run"
|
|
35
|
+
_SUFFIX: Final = ".py"
|
|
36
|
+
_MEMBERS: Final = 3 # how many types seen make a union
|
|
37
|
+
_CLASS: Final = re.compile(rf"([\w.]+){trace.SEPARATOR}(\w+)")
|
|
38
|
+
_STDLIB: Final = sys.stdlib_module_names
|
|
39
|
+
_TYPED: Final = frozenset({"self", "cls"}) # the parameters a class types, unannotated
|
|
40
|
+
_SCOPES: Final = (
|
|
41
|
+
ast.FunctionDef,
|
|
42
|
+
ast.AsyncFunctionDef,
|
|
43
|
+
ast.ClassDef,
|
|
44
|
+
ast.Lambda,
|
|
45
|
+
ast.ListComp,
|
|
46
|
+
ast.SetComp,
|
|
47
|
+
ast.DictComp,
|
|
48
|
+
ast.GeneratorExp,
|
|
49
|
+
)
|
|
50
|
+
_Function: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
|
|
51
|
+
# Each function's locals' spellings, by name, by the function's first line.
|
|
52
|
+
_Functions: TypeAlias = Mapping[int, Mapping[str, tuple[str, ...]]]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Trace(NamedTuple):
|
|
56
|
+
"""A trace file: each file's functions by its SHA-256, and the file each module named in it is."""
|
|
57
|
+
|
|
58
|
+
files: Mapping[str, _Functions]
|
|
59
|
+
modules: Mapping[str, str]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cache
|
|
63
|
+
def load(path: Path) -> Trace:
|
|
64
|
+
"""Read the trace file at `path`, once.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
It.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: It can't be read, or isn't a trace this version reads.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
return _read(_table(cast("object", json.loads(path.read_text(encoding="utf-8")))))
|
|
75
|
+
except (OSError, ValueError) as error:
|
|
76
|
+
message: str = f"{path}: {error}"
|
|
77
|
+
raise ValueError(message) from error
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read(found: Mapping[str, object]) -> Trace:
|
|
81
|
+
"""Read a trace file's contents.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The trace, without what isn't as the recorder writes it.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: It isn't a trace this version reads.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
if found.get("version") != trace.VERSION:
|
|
91
|
+
message: str = f"not a trace `python -m constricter.trace` version {trace.VERSION} wrote"
|
|
92
|
+
raise ValueError(message)
|
|
93
|
+
return Trace(
|
|
94
|
+
{
|
|
95
|
+
digest: {
|
|
96
|
+
int(line): {
|
|
97
|
+
name: tuple(cast("list[str]", types))
|
|
98
|
+
for name, types in _table(held).items()
|
|
99
|
+
if isinstance(types, list)
|
|
100
|
+
}
|
|
101
|
+
for line, held in _table(_table(entry).get("functions")).items()
|
|
102
|
+
if line.isdecimal()
|
|
103
|
+
}
|
|
104
|
+
for digest, entry in _table(found.get("files")).items()
|
|
105
|
+
},
|
|
106
|
+
{name: digest for name, digest in _table(found.get("modules")).items() if isinstance(digest, str)},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _table(value: object) -> dict[str, object]:
|
|
111
|
+
return cast("dict[str, object]", value) if isinstance(value, dict) else {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def merged(
|
|
115
|
+
hinted: Mapping[Path, tuple[Hints, ...]],
|
|
116
|
+
found: Trace,
|
|
117
|
+
paths: Iterable[Path],
|
|
118
|
+
) -> dict[Path, tuple[Hints, ...]]:
|
|
119
|
+
"""Add the trace's hints for each of `paths` after the type checkers' (`hinted`).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Each file's hints.
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
every: dict[Path, tuple[Hints, ...]] = dict(hinted)
|
|
126
|
+
path: Path
|
|
127
|
+
for path in paths:
|
|
128
|
+
try:
|
|
129
|
+
traced: Hints | None = hints(found, path.read_bytes()) if path.suffix == _SUFFIX else None
|
|
130
|
+
except OSError: # checking it reports that
|
|
131
|
+
continue
|
|
132
|
+
if traced is not None:
|
|
133
|
+
every[path] = (*every.get(path, ()), traced)
|
|
134
|
+
return every
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def hints(found: Trace, source: bytes) -> Hints | None:
|
|
138
|
+
"""Make the hints a trace has for a file's `source`.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Them; `None` for a file the trace doesn't have as it is, or has nothing for.
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
digest: str = hashlib.sha256(source).hexdigest()
|
|
145
|
+
functions: _Functions = found.files.get(digest, {})
|
|
146
|
+
try:
|
|
147
|
+
tree: ast.Module | None = ast.parse(source) if functions else None
|
|
148
|
+
except (SyntaxError, ValueError):
|
|
149
|
+
tree = None
|
|
150
|
+
types: dict[tuple[int, int], str] = {}
|
|
151
|
+
offered: dict[tuple[int, int], Offered] = {}
|
|
152
|
+
node: ast.AST
|
|
153
|
+
for node in ast.walk(tree) if tree is not None else ():
|
|
154
|
+
if not isinstance(node, _Function):
|
|
155
|
+
continue
|
|
156
|
+
held: Mapping[str, tuple[str, ...]] = functions.get(
|
|
157
|
+
min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]),
|
|
158
|
+
{},
|
|
159
|
+
)
|
|
160
|
+
target: ast.Name
|
|
161
|
+
for target in _untyped(node) if held else ():
|
|
162
|
+
typed: Offered | None
|
|
163
|
+
if (typed := _typed(held.get(target.id, ()), found.modules, digest)) is not None:
|
|
164
|
+
where: tuple[int, int] = (target.lineno, target.end_col_offset or 0)
|
|
165
|
+
types[where] = typed.text
|
|
166
|
+
if typed.imports:
|
|
167
|
+
offered[where] = typed
|
|
168
|
+
return Hints(_LABEL, types, offered, TRACED) if types else None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _typed(spellings: Sequence[str], modules: Mapping[str, str], digest: str) -> Offered | None:
|
|
172
|
+
"""Join a local's `spellings` into the annotation they make, with the imports its classes need.
|
|
173
|
+
|
|
174
|
+
`modules`, `digest`: see `Trace.modules`, and the file's own SHA-256, whose classes need none.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
It, or `None`: nothing seen, something that couldn't be spelled, more than `_MEMBERS` types,
|
|
178
|
+
two classes of one name, or one of a private module of the standard library.
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
if not spellings or trace.UNKNOWN in spellings or len(spellings) > _MEMBERS:
|
|
182
|
+
return None
|
|
183
|
+
origins: dict[str, str] = {}
|
|
184
|
+
spelling: str
|
|
185
|
+
for spelling in spellings:
|
|
186
|
+
module: str
|
|
187
|
+
name: str
|
|
188
|
+
for module, name in cast("list[tuple[str, str]]", _CLASS.findall(spelling)):
|
|
189
|
+
private: bool = module.partition(".")[0] in _STDLIB and any(
|
|
190
|
+
part.startswith("_") for part in module.split(".")
|
|
191
|
+
)
|
|
192
|
+
if private or origins.setdefault(name, module) != module:
|
|
193
|
+
return None
|
|
194
|
+
return Offered(
|
|
195
|
+
" | ".join(
|
|
196
|
+
_CLASS.sub(r"\2", spelling)
|
|
197
|
+
for spelling in sorted(spellings, key=lambda text: (text == trace.NONE, text))
|
|
198
|
+
),
|
|
199
|
+
tuple(
|
|
200
|
+
f"from {module} import {name}"
|
|
201
|
+
for name, module in sorted(origins.items())
|
|
202
|
+
if modules.get(module) != digest
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _untyped(function: _Function) -> list[ast.Name]:
|
|
208
|
+
"""Find the locals `function` binds once, outside any loop, to a value no annotation types.
|
|
209
|
+
|
|
210
|
+
One rooted at a parameter it leaves unannotated (`rows[0].load()`, `make(n)`; not `self` or
|
|
211
|
+
`cls`, which its class types): a type checker has no type for it either, so none to set against
|
|
212
|
+
what the run saw. Not one rooted at such a local, which a checker types by its new annotation.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Each such name's binding.
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
args: ast.arguments = function.args
|
|
219
|
+
params: list[ast.arg] = [
|
|
220
|
+
arg
|
|
221
|
+
for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg)
|
|
222
|
+
if arg is not None
|
|
223
|
+
]
|
|
224
|
+
others: set[str] = {arg.arg for arg in params}
|
|
225
|
+
rooted: set[str | None] = {arg.arg for arg in params if arg.annotation is None and arg.arg not in _TYPED}
|
|
226
|
+
stores: list[ast.Name] = []
|
|
227
|
+
looped: set[int] = set() # the nodes a loop runs again
|
|
228
|
+
node: ast.AST
|
|
229
|
+
for node in _own(function):
|
|
230
|
+
if isinstance(node, ast.For | ast.AsyncFor | ast.While):
|
|
231
|
+
looped.update(id(inside) for inside in _own(node))
|
|
232
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
233
|
+
stores.append(node)
|
|
234
|
+
elif isinstance(node, ast.Global | ast.Nonlocal):
|
|
235
|
+
others.update(node.names)
|
|
236
|
+
elif isinstance(node, ast.alias):
|
|
237
|
+
others.add((node.asname or node.name).partition(".")[0])
|
|
238
|
+
elif isinstance(node, ast.ExceptHandler | ast.MatchAs | ast.MatchStar) and node.name is not None:
|
|
239
|
+
others.add(node.name)
|
|
240
|
+
elif isinstance(node, ast.MatchMapping) and node.rest is not None:
|
|
241
|
+
others.add(node.rest)
|
|
242
|
+
counts: Counter[str] = Counter(store.id for store in stores)
|
|
243
|
+
others.update(store.id for store in stores if id(store) in looped or counts[store.id] > 1)
|
|
244
|
+
return [
|
|
245
|
+
target for target, value in _assigned(function) if target.id not in others and _root(value) in rooted
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _assigned(function: _Function) -> Iterator[tuple[ast.Name, ast.expr]]:
|
|
250
|
+
"""Find the plain assignments of one name in a function's own scope, in order.
|
|
251
|
+
|
|
252
|
+
Yields:
|
|
253
|
+
Each one's name and value.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
node: ast.AST
|
|
257
|
+
for node in _own(function):
|
|
258
|
+
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
259
|
+
target: ast.expr = node.targets[0]
|
|
260
|
+
if isinstance(target, ast.Name):
|
|
261
|
+
yield target, node.value
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _root(value: ast.expr) -> str | None:
|
|
265
|
+
"""Find the name a value is taken from: `rows` in `rows[0].load()`, `make` in `make(n)`.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
It, or `None` for a value that's computed some other way.
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
if isinstance(value, ast.Attribute | ast.Subscript | ast.Await):
|
|
272
|
+
return _root(value.value)
|
|
273
|
+
if isinstance(value, ast.Call):
|
|
274
|
+
return _root(value.func)
|
|
275
|
+
return value.id if isinstance(value, ast.Name) else None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _own(node: ast.AST) -> Iterator[ast.AST]:
|
|
279
|
+
"""Walk what a function's own scope holds: not what a function, class or comprehension inside it does.
|
|
280
|
+
|
|
281
|
+
Yields:
|
|
282
|
+
Each node under `node`, a nested scope's own node included.
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
child: ast.AST
|
|
286
|
+
for child in ast.iter_child_nodes(node):
|
|
287
|
+
yield child
|
|
288
|
+
if not isinstance(child, _SCOPES):
|
|
289
|
+
yield from _own(child)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a value was inferred to be, kept until its scope types another name.
|
|
3
|
+
|
|
4
|
+
A statement's value is asked for its type, then for whether that's a guess, then as a part of the
|
|
5
|
+
next value (a `with` statement's manager, three times): `asked` answers again what `keep` was told,
|
|
6
|
+
for the same value (by `id`), the same scope's types (`Typed`) unchanged since, and the same number
|
|
7
|
+
of imports the module's plan has added or guarded (an import `--fix` adds can resolve a name the
|
|
8
|
+
next time).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Final, TypeAlias
|
|
14
|
+
|
|
15
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known, Typed
|
|
16
|
+
|
|
17
|
+
# What was asked: the value and the scope's types (by `id`), how often those had changed, and how
|
|
18
|
+
# many imports the module's plan had added or guarded.
|
|
19
|
+
_Asked: TypeAlias = tuple[int, int, int, int]
|
|
20
|
+
# Its answer, with what was asked kept alive beside it: an `id` is only theirs while they live.
|
|
21
|
+
_Answered: TypeAlias = tuple[ast.expr, Known, Typed, Inference | None]
|
|
22
|
+
_ANSWERS: Final[dict[_Asked, _Answered]] = {}
|
|
23
|
+
_KEPT: Final = 2048 # then all are dropped: a few functions' worth
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _key(value: ast.expr, known: Known, declared: Typed) -> _Asked:
|
|
27
|
+
plan: ImportPlan | None = known.names.plan
|
|
28
|
+
return (
|
|
29
|
+
id(value),
|
|
30
|
+
id(declared),
|
|
31
|
+
declared.version,
|
|
32
|
+
0 if plan is None else len(plan.added) + len(plan.guarded),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def asked(value: ast.expr, known: Known, declared: Mapping[str, str]) -> tuple[Inference | None] | None:
|
|
37
|
+
"""Find what `value` was inferred to be, if it was asked before with nothing changed since.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The inference (or `None`, for no type), in a tuple; `None` if it's yet to be worked out.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
held: _Answered | None = (
|
|
44
|
+
_ANSWERS.get(_key(value, known, declared)) if isinstance(declared, Typed) else None
|
|
45
|
+
)
|
|
46
|
+
if held is None or held[0] is not value or held[1] is not known or held[2] is not declared:
|
|
47
|
+
return None
|
|
48
|
+
return (held[3],)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def keep(value: ast.expr, known: Known, declared: Mapping[str, str], found: Inference | None) -> None:
|
|
52
|
+
"""Keep what `value` was inferred to be, for a scope's own types (any other mapping may change unseen)."""
|
|
53
|
+
if isinstance(declared, Typed):
|
|
54
|
+
if len(_ANSWERS) >= _KEPT:
|
|
55
|
+
_ANSWERS.clear()
|
|
56
|
+
_ANSWERS[_key(value, known, declared)] = (value, known, declared, found)
|
|
@@ -10,6 +10,9 @@ from typing import Final, NamedTuple, TypeAlias
|
|
|
10
10
|
from constricter.offences import Edit, Fix, Offence
|
|
11
11
|
|
|
12
12
|
_HEADER: Final = 2 # a file's shebang and coding lines come first, if it has them
|
|
13
|
+
# The last lines `_missing` read, stripped each way (see there): the lines, their count, and the set.
|
|
14
|
+
_Present: TypeAlias = tuple[Sequence[str], int, frozenset[str]]
|
|
15
|
+
_PRESENT: Final[dict[str | None, _Present]] = {}
|
|
13
16
|
_HEADER_LINE: Final = re.compile(r"#!|#.*coding[:=]")
|
|
14
17
|
# Lines to insert, and the number of lines before them.
|
|
15
18
|
_Run: TypeAlias = tuple[int, list[str]]
|
|
@@ -155,8 +158,15 @@ def _missing(lines: Sequence[str], statements: Sequence[str], stripped: str | No
|
|
|
155
158
|
"""
|
|
156
159
|
if not statements: # most fixes add none: the file's lines needn't be read for them
|
|
157
160
|
return []
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
# A file's fixes are asked one at a time, of the same lines: stripped once for them all.
|
|
162
|
+
held: _Present | None = _PRESENT.get(stripped)
|
|
163
|
+
if held is None or held[0] is not lines or held[1] != len(lines):
|
|
164
|
+
held = _PRESENT[stripped] = (
|
|
165
|
+
lines,
|
|
166
|
+
len(lines),
|
|
167
|
+
frozenset(line.rstrip(stripped) if stripped else line.strip() for line in lines),
|
|
168
|
+
)
|
|
169
|
+
return [statement for statement in statements if statement not in held[2]]
|
|
160
170
|
|
|
161
171
|
|
|
162
172
|
def _import_line(lines: Sequence[str], after: int) -> int:
|