python-constricter 0.4.1__cp311-cp311-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp311-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-win32.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp311-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-win32.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp311-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-win32.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The rules: every local variable is typed where it's first bound (see README)."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import Final, NamedTuple, cast
|
|
8
|
+
|
|
9
|
+
from constricter.fix.core import imports
|
|
10
|
+
from constricter.fix.core.known import (
|
|
11
|
+
Guarded,
|
|
12
|
+
ImportPlan,
|
|
13
|
+
Observed,
|
|
14
|
+
Outside,
|
|
15
|
+
Passed,
|
|
16
|
+
Returned,
|
|
17
|
+
Returns,
|
|
18
|
+
)
|
|
19
|
+
from constricter.fix.values import returned, stored
|
|
20
|
+
from constricter.jsonc import as_text
|
|
21
|
+
from constricter.offences import (
|
|
22
|
+
DEFAULT_CHECKS,
|
|
23
|
+
UNANNOTATED,
|
|
24
|
+
UNANNOTATED_MEMBER,
|
|
25
|
+
UNTYPED_TARGET,
|
|
26
|
+
Checks,
|
|
27
|
+
Offence,
|
|
28
|
+
)
|
|
29
|
+
from constricter.rules import late, parsed, recorded
|
|
30
|
+
from constricter.rules.calls import keyed, observed
|
|
31
|
+
from constricter.rules.flow import Finding
|
|
32
|
+
from constricter.rules.narrowing import flow_offences, module_flow
|
|
33
|
+
from constricter.rules.redundant import redundant
|
|
34
|
+
from constricter.rules.scope import Scope
|
|
35
|
+
from constricter.rules.scoped import Seeded, Settings
|
|
36
|
+
from constricter.rules.scoping import body_scopes, bound_once, finish, function_scope, module_scopes
|
|
37
|
+
from constricter.rules.settings import module_settings
|
|
38
|
+
from constricter.rules.syntax import (
|
|
39
|
+
FUNCTION_DEFS,
|
|
40
|
+
FunctionDef,
|
|
41
|
+
child_statements,
|
|
42
|
+
)
|
|
43
|
+
from constricter.rules.tables import Tables, module_tables
|
|
44
|
+
|
|
45
|
+
_KEPT: Final = "_constricter_kept" # the attribute a tree holds its last check in
|
|
46
|
+
_ROUNDS: Final = 5 # how many times to re-check what calls an unannotated function, at most
|
|
47
|
+
_TYPE_CHECKING: Final = "TYPE_CHECKING"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_source(
|
|
51
|
+
source: str | bytes,
|
|
52
|
+
filename: str = "<unknown>",
|
|
53
|
+
checks: Checks = DEFAULT_CHECKS,
|
|
54
|
+
*,
|
|
55
|
+
outside: Outside | None = None,
|
|
56
|
+
) -> list[Offence]:
|
|
57
|
+
"""Return the offences in `source`, sorted. Raises `SyntaxError`.
|
|
58
|
+
|
|
59
|
+
`outside` adds what's known of it from other files and a type checker, for `--fix` (see `Outside`).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Every offence; `# noqa` comments are the caller's to apply.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
return checked_source(source, filename, checks, outside=outside).offences
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def checked_source(
|
|
69
|
+
source: str | bytes,
|
|
70
|
+
filename: str = "<unknown>",
|
|
71
|
+
checks: Checks = DEFAULT_CHECKS,
|
|
72
|
+
*,
|
|
73
|
+
outside: Outside | None = None,
|
|
74
|
+
) -> "Checked":
|
|
75
|
+
"""Check `source`, as `check_source` does.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Its offences, and what its functions return (see `Checked`).
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
tree: ast.Module
|
|
82
|
+
own: Tables | None
|
|
83
|
+
tree, own = _parse(source, filename)
|
|
84
|
+
found: Checked = checked_tree(tree, checks, lines=as_text(source).splitlines(), outside=outside, own=own)
|
|
85
|
+
if own is not None and isinstance(source, str):
|
|
86
|
+
# Kept again: a file is checked again once its callers type its parameters, or its cycle's
|
|
87
|
+
# files change what it imports. One `--fix` changed since is no longer this text.
|
|
88
|
+
parsed.keep(source, (tree, own))
|
|
89
|
+
return found
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse(source: str | bytes, filename: str) -> tuple[ast.Module, Tables | None]:
|
|
93
|
+
"""Parse `source` (see `parsed.parse`), or take the tree the index kept for it (`parsed.take`).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The module, and the tables the index read from it if it was kept. Raises `SyntaxError`.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
kept: parsed.Kept | None = parsed.take(source) if isinstance(source, str) else None
|
|
100
|
+
return kept or (parsed.parse(source, filename), None)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def check_tree(
|
|
104
|
+
tree: ast.Module,
|
|
105
|
+
checks: Checks = DEFAULT_CHECKS,
|
|
106
|
+
*,
|
|
107
|
+
lines: Sequence[str] = (),
|
|
108
|
+
outside: Outside | None = None,
|
|
109
|
+
own: Tables | None = None,
|
|
110
|
+
) -> list[Offence]:
|
|
111
|
+
"""Return the offences in a parsed module, sorted.
|
|
112
|
+
|
|
113
|
+
`# type:` comments are seen only if it was parsed with `type_comments=True`; they count for `=`
|
|
114
|
+
and `with` too in a module written to run on Python 2. With its source `lines`, a `**rest`
|
|
115
|
+
capture is reported at its name rather than at its pattern's start. `outside` adds what's known
|
|
116
|
+
of it from other files and a type checker, for `--fix` (see `Outside`). `own`: the module's own
|
|
117
|
+
tables, if already read from this tree (see `parsed.keep`).
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Every offence, in source order.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
return checked_tree(tree, checks, lines=lines, outside=outside, own=own).offences
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Checked(NamedTuple):
|
|
127
|
+
"""A module's offences, what its unannotated functions return, and what it passes others' functions.
|
|
128
|
+
|
|
129
|
+
What they return is for the files importing them; what it passes, for `fix.index.callers`.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
offences: list[Offence]
|
|
133
|
+
returned: Returns
|
|
134
|
+
calls: Observed = Observed()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def checked_tree(
|
|
138
|
+
tree: ast.Module,
|
|
139
|
+
checks: Checks = DEFAULT_CHECKS,
|
|
140
|
+
*,
|
|
141
|
+
lines: Sequence[str] = (),
|
|
142
|
+
outside: Outside | None = None,
|
|
143
|
+
own: Tables | None = None,
|
|
144
|
+
) -> Checked:
|
|
145
|
+
"""Check a parsed module, as `check_tree` does.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Its offences, in source order, and what its functions return (see `returned.exported`).
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
own = own or module_tables(tree)
|
|
152
|
+
read: ImportPlan = imports.plan(tree)
|
|
153
|
+
outside = None if outside is None else outside.usable(read.taken, imports.present(read, outside.guarded))
|
|
154
|
+
# A second check's scopes it didn't check again are its first's, finished then.
|
|
155
|
+
finished: frozenset[int] = _kept_scopes(tree)
|
|
156
|
+
done: _Round = _resumed(tree, checks, outside) or _started(tree, checks, lines, own, outside)
|
|
157
|
+
# Kept with the tree, for as long as it is: a second check may only know its parameters anew.
|
|
158
|
+
vars(tree)[_KEPT] = _Kept(outside, checks, done)
|
|
159
|
+
settings: Settings = done.settings
|
|
160
|
+
scopes: list[Scope] = done.scopes()
|
|
161
|
+
# A finding's kind is the code that reports it (LVA008, LVA009, LVA010).
|
|
162
|
+
finish(tree, [scope for scope in scopes if id(scope) not in finished])
|
|
163
|
+
finals: list[Offence] = [o for scope in scopes for o in late.finals(scope)] if checks.final else []
|
|
164
|
+
reported: list[Offence] = [o for scope in scopes for o in scope.reported()]
|
|
165
|
+
exported: Returns = returned.exported(done.found, settings.known.class_side.lineage)
|
|
166
|
+
plan: ImportPlan = settings.known.names.plan or imports.plan(tree)
|
|
167
|
+
# With the imports its fixes add under `if TYPE_CHECKING:`, which `outside.guarded` doesn't hold.
|
|
168
|
+
guarded: dict[str, Guarded] = {
|
|
169
|
+
**{name: each for name, each in plan.guarded.items() if each.statement is not None},
|
|
170
|
+
**({} if outside is None else outside.guarded),
|
|
171
|
+
}
|
|
172
|
+
return Checked(
|
|
173
|
+
sorted(
|
|
174
|
+
[
|
|
175
|
+
*reported,
|
|
176
|
+
*redundant(tree, settings.checks.fixes),
|
|
177
|
+
*flow_offences(module_flow(tree, scopes), settings.checks.fixes),
|
|
178
|
+
*finals,
|
|
179
|
+
],
|
|
180
|
+
),
|
|
181
|
+
exported._replace(names=imports.exported_names(exported, guarded, plan.added)),
|
|
182
|
+
observed(tree, scopes, settings.known, {} if outside is None else outside.callees),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _kept_scopes(tree: ast.Module) -> frozenset[int]:
|
|
187
|
+
"""Identify the scopes of a module's last check, kept on its tree (see `_Kept`).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Their `id`s; none before its first.
|
|
191
|
+
|
|
192
|
+
"""
|
|
193
|
+
kept: _Kept | None = cast("_Kept | None", vars(tree).get(_KEPT))
|
|
194
|
+
return frozenset() if kept is None else frozenset(id(scope) for scope in kept.done.scopes())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _started(
|
|
198
|
+
tree: ast.Module,
|
|
199
|
+
checks: Checks,
|
|
200
|
+
lines: Sequence[str],
|
|
201
|
+
own: Tables,
|
|
202
|
+
outside: Outside | None,
|
|
203
|
+
) -> "_Round":
|
|
204
|
+
"""Check a module from the start (see `_settled`).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The check.
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
first: Settings = module_settings(tree, checks, lines, own, outside)
|
|
211
|
+
imported: Returns = Returns() if outside is None else outside.returned
|
|
212
|
+
return _settled(tree, first, imported, bound_once(tree, first))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class _Round(NamedTuple):
|
|
216
|
+
"""A module's check as its rounds leave it: what checking it again starts from (see `_resumed`).
|
|
217
|
+
|
|
218
|
+
The settings with what its functions return, each function with its latest scope, the module's
|
|
219
|
+
and classes' bodies' scopes, the table its functions' `return`s are recorded in, and what they
|
|
220
|
+
return.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
settings: Settings
|
|
224
|
+
functions: list[tuple[Scope, FunctionDef]]
|
|
225
|
+
bodies: list[Scope]
|
|
226
|
+
table: returned.Table
|
|
227
|
+
found: Returned
|
|
228
|
+
|
|
229
|
+
def scopes(self) -> list[Scope]:
|
|
230
|
+
"""List every scope: the functions', then the bodies'.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Them.
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
return [scope for scope, _ in self.functions] + self.bodies
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _Kept(NamedTuple):
|
|
240
|
+
"""A module's last check, kept on its tree: what it knew from outside, its checks, and how it ended."""
|
|
241
|
+
|
|
242
|
+
outside: Outside | None
|
|
243
|
+
checks: Checks
|
|
244
|
+
done: _Round
|
|
245
|
+
|
|
246
|
+
def __deepcopy__(self, _memo: dict[int, object]) -> None:
|
|
247
|
+
"""Keep nothing in a copied tree: its nodes are new, so its check starts over.
|
|
248
|
+
|
|
249
|
+
Any node's deep copy can reach the tree: `ast.Load()` is one object every tree shares, and
|
|
250
|
+
pyflakes gives it a parent.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _resumed(tree: ast.Module, checks: Checks, outside: Outside | None) -> _Round | None:
|
|
255
|
+
"""Check a module again from where its last check ended, where only its parameters are typed anew.
|
|
256
|
+
|
|
257
|
+
A file whose functions' parameters every call types (`Outside.parameters`) is checked a second
|
|
258
|
+
time knowing them: with the same tree, checks and everything else from outside, only those
|
|
259
|
+
functions are checked again, and what their new types reach, as any round's are (`_rounds`).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
The check; or `None` where it must start over: no check kept, anything else changed, LVA012
|
|
263
|
+
asked for (its fix rewrites a scope's own), or a name of the module's top level typed anew.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
kept: _Kept | None = cast("_Kept | None", vars(tree).get(_KEPT))
|
|
267
|
+
if kept is None or outside is None or kept.outside is None or checks.final or kept.checks != checks:
|
|
268
|
+
return None
|
|
269
|
+
if replace(outside, parameters={}) != replace(kept.outside, parameters={}):
|
|
270
|
+
return None
|
|
271
|
+
seeded: Seeded = kept.done.settings.parameters or Seeded()
|
|
272
|
+
passed: Mapping[int, Mapping[str, Passed]] = keyed(tree, outside.parameters)
|
|
273
|
+
again: set[int] = {
|
|
274
|
+
id(scope)
|
|
275
|
+
for scope, func in kept.done.functions
|
|
276
|
+
if seeded.callers.get(id(func)) != passed.get(id(func))
|
|
277
|
+
}
|
|
278
|
+
settings: Settings = replace(kept.done.settings, parameters=seeded._replace(callers=passed))
|
|
279
|
+
done: _Round = _rounds(tree, kept.done._replace(settings=settings), outside.returned, again)
|
|
280
|
+
return done if bound_once(tree, done.settings) == seeded.module else None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _settled(
|
|
284
|
+
tree: ast.Module,
|
|
285
|
+
first: Settings,
|
|
286
|
+
imported: Returns,
|
|
287
|
+
names: dict[str, Passed],
|
|
288
|
+
rounds: int = _ROUNDS,
|
|
289
|
+
) -> _Round:
|
|
290
|
+
"""Check the module's scopes, its functions reading its top level's `names` as typed (`bound_once`).
|
|
291
|
+
|
|
292
|
+
A name the module binds to what an unannotated function returns is typed only once that
|
|
293
|
+
function is checked: the scopes are then checked once more, knowing it, until no name's type
|
|
294
|
+
is new (`rounds` times at most).
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
The settings with what its functions return, the scopes checked with them, and what its own
|
|
298
|
+
return.
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
table: returned.Table = returned.Table(tree, imported, first.known.vouched)
|
|
302
|
+
seeded: Seeded = first.parameters or Seeded()
|
|
303
|
+
# The table, filled in as the functions are checked in call order, is what they all read.
|
|
304
|
+
settings: Settings = replace(
|
|
305
|
+
first,
|
|
306
|
+
known=replace(first.known, returned=table.returned),
|
|
307
|
+
parameters=seeded._replace(module=names),
|
|
308
|
+
)
|
|
309
|
+
checked: _Round = _returned(tree, settings, module_scopes(tree, settings, table), table, imported)
|
|
310
|
+
latest: dict[str, Passed] = bound_once(tree, checked.settings)
|
|
311
|
+
return checked if latest == names or rounds <= 1 else _settled(tree, first, imported, latest, rounds - 1)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Coverage(NamedTuple):
|
|
315
|
+
"""How many of a module's typeable first bindings are typed, of how many.
|
|
316
|
+
|
|
317
|
+
`widened`: those a widening annotated (see `widened`), counted apart: not among the typed.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
typed: int
|
|
321
|
+
total: int
|
|
322
|
+
widened: int = 0
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def percent(self) -> float:
|
|
326
|
+
"""The typed share, as a percentage (100 when there's nothing to type)."""
|
|
327
|
+
return 100 * self.typed / self.total if self.total else 100.0
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# The codes that mean a binding has no type at all (LVA003's type comment is a type).
|
|
331
|
+
_UNTYPED: Final = frozenset({UNANNOTATED, UNTYPED_TARGET, UNANNOTATED_MEMBER})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def annotation_coverage(source: str | bytes, checks: Checks = DEFAULT_CHECKS) -> Coverage:
|
|
335
|
+
"""Count the first bindings in `source` the rules cover, and how many are typed.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
The counts; `# noqa` comments don't make a binding typed. Raises `SyntaxError`.
|
|
339
|
+
|
|
340
|
+
"""
|
|
341
|
+
tree: ast.Module = _parse(source, "<unknown>")[0]
|
|
342
|
+
settings: Settings = module_settings(tree, checks, as_text(source).splitlines(), module_tables(tree))
|
|
343
|
+
scopes: list[Scope] = module_scopes(tree, settings)
|
|
344
|
+
total: int = sum(len(scope.bound()) for scope in scopes)
|
|
345
|
+
untyped: int = sum(o.code in _UNTYPED for scope in scopes for o in scope.reported())
|
|
346
|
+
wider: int = sum(len(scope.assignments.widened.intersection(scope.bound())) for scope in scopes)
|
|
347
|
+
return Coverage(total - untyped - wider, total, wider)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _returned(
|
|
351
|
+
tree: ast.Module,
|
|
352
|
+
settings: Settings,
|
|
353
|
+
scopes: list[Scope],
|
|
354
|
+
table: returned.Table,
|
|
355
|
+
imported: Returns,
|
|
356
|
+
) -> _Round:
|
|
357
|
+
"""Check again what the first pass, in call order, couldn't type the first time.
|
|
358
|
+
|
|
359
|
+
That pass typed each function knowing its callees' returns (see `module_scopes`); what's left is a
|
|
360
|
+
function checked before a callee of its was typed (a cycle, `table.stale`), or with a late-typed
|
|
361
|
+
name it didn't know from the start: `_rounds` checks those again.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The check (see `_Round`), its settings with what the functions it imports return too.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
found: Returned = returned.returned(
|
|
368
|
+
tree,
|
|
369
|
+
table.recorded,
|
|
370
|
+
table.assigned,
|
|
371
|
+
table.used,
|
|
372
|
+
settings.known.vouched,
|
|
373
|
+
)
|
|
374
|
+
settings = replace(settings, known=replace(settings.known, returned=returned.joined(imported, found)))
|
|
375
|
+
functions: list[tuple[Scope, FunctionDef]] = [
|
|
376
|
+
(scope, scope.kind.function) for scope in scopes if scope.kind.function is not None
|
|
377
|
+
]
|
|
378
|
+
# The first pass knew no attribute's type: they're read once every method is checked.
|
|
379
|
+
again: set[int] = {
|
|
380
|
+
id(scope)
|
|
381
|
+
for scope, func in functions
|
|
382
|
+
if table.stale(func)
|
|
383
|
+
or scope.inferred.late.keys() - scope.inferred.seeded.keys()
|
|
384
|
+
or stored.reads_own(tree, func, settings.owners, found)
|
|
385
|
+
}
|
|
386
|
+
bodies: list[Scope] = [scope for scope in scopes if scope.kind.function is None]
|
|
387
|
+
return _rounds(tree, _Round(settings, functions, bodies, table, found), imported, again)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _rounds(tree: ast.Module, start: _Round, imported: Returns, again: set[int]) -> _Round:
|
|
391
|
+
"""Check the functions whose scopes are in `again` once more, and then what their new types reach.
|
|
392
|
+
|
|
393
|
+
Each round checks them again, reads what the unannotated functions return, and checks again any
|
|
394
|
+
function calling one whose type is new; repeated until nothing changes, so `--fix` finds in one
|
|
395
|
+
run what it would over several. The bodies are checked again if the rounds typed what they use.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
The check as the rounds leave it.
|
|
399
|
+
|
|
400
|
+
"""
|
|
401
|
+
settings: Settings = start.settings
|
|
402
|
+
functions: list[tuple[Scope, FunctionDef]] = start.functions
|
|
403
|
+
table: returned.Table = start.table
|
|
404
|
+
found: Returned = start.found
|
|
405
|
+
changed: bool = False
|
|
406
|
+
retyped: set[str] = set() # the attributes the rounds typed anew
|
|
407
|
+
_round: int
|
|
408
|
+
for _round in range(_ROUNDS):
|
|
409
|
+
if not again:
|
|
410
|
+
break
|
|
411
|
+
functions = _checked_again(tree, functions, again, settings, table)
|
|
412
|
+
latest: Returned = returned.returned(
|
|
413
|
+
tree,
|
|
414
|
+
table.recorded,
|
|
415
|
+
table.assigned,
|
|
416
|
+
table.used,
|
|
417
|
+
settings.known.vouched,
|
|
418
|
+
)
|
|
419
|
+
typed: bool = latest != found and returned.called(tree, tree, latest) # even if only a body calls one
|
|
420
|
+
# Attributes typed anew: what reads one, of any value, may be typed now.
|
|
421
|
+
newly: set[str] = returned.retyped(found, latest)
|
|
422
|
+
# Kept even when nothing here calls it: other files import what the module's functions return.
|
|
423
|
+
if latest != found:
|
|
424
|
+
found = latest
|
|
425
|
+
settings = replace(
|
|
426
|
+
settings,
|
|
427
|
+
known=replace(settings.known, returned=returned.joined(imported, found)),
|
|
428
|
+
)
|
|
429
|
+
changed = changed or typed
|
|
430
|
+
retyped |= newly
|
|
431
|
+
again = {
|
|
432
|
+
id(scope)
|
|
433
|
+
for scope, func in functions
|
|
434
|
+
if (typed and returned.called(tree, func, found))
|
|
435
|
+
or stored.reads(tree, func, newly, anywhere=True)
|
|
436
|
+
or scope.inferred.late.keys() - scope.inferred.seeded.keys()
|
|
437
|
+
}
|
|
438
|
+
bodies: list[Scope] = _bodies(
|
|
439
|
+
tree,
|
|
440
|
+
settings,
|
|
441
|
+
start.bodies,
|
|
442
|
+
(retyped if changed or retyped else None, found),
|
|
443
|
+
)
|
|
444
|
+
return _Round(settings, functions, bodies, table, found)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _checked_again(
|
|
448
|
+
tree: ast.Module,
|
|
449
|
+
functions: list[tuple[Scope, FunctionDef]],
|
|
450
|
+
again: set[int],
|
|
451
|
+
settings: Settings,
|
|
452
|
+
table: returned.Table,
|
|
453
|
+
) -> list[tuple[Scope, FunctionDef]]:
|
|
454
|
+
"""Check the functions whose scopes are in `again` once more, recording their `return`s and assignments.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Every function, with its latest scope.
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
fresh: list[tuple[Scope, FunctionDef]] = [
|
|
461
|
+
(function_scope(func, [], settings, scope.inferred.late), func)
|
|
462
|
+
for scope, func in functions
|
|
463
|
+
if id(scope) in again
|
|
464
|
+
]
|
|
465
|
+
renewed: dict[int, Scope] = {id(func): scope for scope, func in fresh}
|
|
466
|
+
finish(tree, [scope for scope, _ in fresh])
|
|
467
|
+
table.recorded.update((id(func), recorded.returns(scope, func, tree)) for scope, func in fresh)
|
|
468
|
+
table.assigned.update((id(func), recorded.assigned(scope)) for scope, func in fresh)
|
|
469
|
+
table.used.update((id(func), recorded.used(scope, func, tree)) for scope, func in fresh)
|
|
470
|
+
return [(renewed.get(id(func), scope), func) for scope, func in functions]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _bodies(
|
|
474
|
+
tree: ast.Module,
|
|
475
|
+
settings: Settings,
|
|
476
|
+
bodies: list[Scope],
|
|
477
|
+
news: tuple[set[str] | None, Returned],
|
|
478
|
+
) -> list[Scope]:
|
|
479
|
+
"""Check the module and class bodies again, if the rounds typed anything they use.
|
|
480
|
+
|
|
481
|
+
They were checked after every function, knowing the first pass's types: only what the rounds
|
|
482
|
+
typed since is news to them (`news`: the attributes they typed, `None` if nothing at all; and
|
|
483
|
+
what the module's own functions return).
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Their scopes, checked again or as they were.
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
retyped: set[str] | None
|
|
490
|
+
own: Returned
|
|
491
|
+
retyped, own = news
|
|
492
|
+
if (
|
|
493
|
+
retyped is not None
|
|
494
|
+
and bodies
|
|
495
|
+
and any(
|
|
496
|
+
returned.called(tree, stmt, own) or stored.reads(tree, stmt, retyped, anywhere=True)
|
|
497
|
+
for stmt in _body_statements(tree.body)
|
|
498
|
+
)
|
|
499
|
+
):
|
|
500
|
+
return body_scopes(tree, settings)
|
|
501
|
+
return bodies
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _body_statements(body: Sequence[ast.stmt]) -> Iterator[ast.stmt]:
|
|
505
|
+
"""Walk a module's and its classes' own statements, not their functions'.
|
|
506
|
+
|
|
507
|
+
Yields:
|
|
508
|
+
Each statement (a function's definition too, but not what's inside it).
|
|
509
|
+
|
|
510
|
+
"""
|
|
511
|
+
stmt: ast.stmt
|
|
512
|
+
for stmt in body:
|
|
513
|
+
yield stmt
|
|
514
|
+
if isinstance(stmt, ast.ClassDef):
|
|
515
|
+
yield from _body_statements(stmt.body)
|
|
516
|
+
elif not isinstance(stmt, FUNCTION_DEFS):
|
|
517
|
+
yield from _body_statements(child_statements(stmt))
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def value_flow(
|
|
521
|
+
source: str | bytes,
|
|
522
|
+
filename: str = "<unknown>",
|
|
523
|
+
checks: Checks = DEFAULT_CHECKS,
|
|
524
|
+
) -> list[Finding]:
|
|
525
|
+
"""Return the value-flow findings in `source`, sorted (see `constricter.flow`). Raises `SyntaxError`.
|
|
526
|
+
|
|
527
|
+
`check_tree` reports each as the code its kind names (LVA008, LVA009, LVA010).
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Every finding, in source order.
|
|
531
|
+
|
|
532
|
+
"""
|
|
533
|
+
tree: ast.Module = _parse(source, filename)[0]
|
|
534
|
+
# Its lines place a `**rest` capture at its name, as `check_source` does.
|
|
535
|
+
settings: Settings = module_settings(tree, checks, as_text(source).splitlines(), module_tables(tree))
|
|
536
|
+
return module_flow(tree, module_scopes(tree, settings))
|
|
Binary file
|