python-constricter 0.4.1__cp313-cp313-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__.cp313-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp313-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp313-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp313-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp313-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp313-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp313-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp313-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp313-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp313-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp313-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp313-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp313-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp313-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp313-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp313-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp313-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp313-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp313-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp313-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp313-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp313-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp313-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp313-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp313-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp313-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp313-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp313-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp313-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp313-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__.cp313-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp313-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp313-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp313-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp313-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp313-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp313-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp313-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp313-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp313-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp313-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp313-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp313-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp313-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp313-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp313-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp313-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp313-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp313-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp313-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp313-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp313-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp313-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp313-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__.cp313-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp313-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp313-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp313-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp313-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp313-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp313-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp313-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp313-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp313-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp313-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp313-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp313-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp313-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp313-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp313-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp313-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp313-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp313-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp313-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp313-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp313-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp313-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp313-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp313-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,580 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Fixes that add an import: how a module can name a library type, and where an import goes."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import bisect
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
9
|
+
from itertools import accumulate
|
|
10
|
+
from typing import Final, TypeAlias, cast
|
|
11
|
+
from weakref import WeakKeyDictionary
|
|
12
|
+
|
|
13
|
+
from constricter.fix.core.known import Checking, Guarded, ImportPlan, Origin, Returns
|
|
14
|
+
from constricter.rules.annotations import dotted, roots
|
|
15
|
+
from constricter.rules.syntax import FunctionDef, Start, import_bindings
|
|
16
|
+
from constricter.rules.walked import classes, of_type
|
|
17
|
+
|
|
18
|
+
_TYPE_CHECKING: Final = "TYPE_CHECKING"
|
|
19
|
+
_QUOTES: Final = ("'", '"') # what starts a string in an annotation: a name in one isn't a class's
|
|
20
|
+
_STDLIB: Final = sys.stdlib_module_names
|
|
21
|
+
_FUTURE: Final = "__future__"
|
|
22
|
+
_IMPORTORSKIP: Final = "pytest.importorskip" # a module it names may be missing
|
|
23
|
+
_MODNAME: Final = "modname" # its parameter
|
|
24
|
+
_DOT: Final = "."
|
|
25
|
+
_ANNOTATIONS: Final = "annotations" # `from __future__ import annotations` postpones them all
|
|
26
|
+
# Each module's names, for as long as its tree lives: the index reads them, then the check.
|
|
27
|
+
_Taken: TypeAlias = tuple[frozenset[str], frozenset[str]]
|
|
28
|
+
_TAKEN: Final[WeakKeyDictionary[ast.Module, _Taken]] = WeakKeyDictionary()
|
|
29
|
+
_Rebound: TypeAlias = dict[str, list[Start]]
|
|
30
|
+
_REBOUND: Final[WeakKeyDictionary[ast.Module, _Rebound]] = WeakKeyDictionary()
|
|
31
|
+
_Import: TypeAlias = ast.Import | ast.ImportFrom
|
|
32
|
+
_Inner: TypeAlias = tuple[_Import, ...]
|
|
33
|
+
_INNER: Final[WeakKeyDictionary[ast.Module, _Inner]] = WeakKeyDictionary()
|
|
34
|
+
# The nodes that bind a name: all `_taken` needs look at.
|
|
35
|
+
_BINDERS: Final = (
|
|
36
|
+
ast.Name,
|
|
37
|
+
ast.arg,
|
|
38
|
+
ast.FunctionDef,
|
|
39
|
+
ast.AsyncFunctionDef,
|
|
40
|
+
ast.ClassDef,
|
|
41
|
+
ast.alias,
|
|
42
|
+
ast.ExceptHandler,
|
|
43
|
+
ast.MatchAs,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plan(tree: ast.Module) -> ImportPlan:
|
|
48
|
+
"""Read what `tree` already imports, every name it binds, and where its imports end.
|
|
49
|
+
|
|
50
|
+
Only imports that run count as binding a name to use (not those under `if TYPE_CHECKING:`: a
|
|
51
|
+
module's own annotations are evaluated when it's imported); every name bound anywhere in the
|
|
52
|
+
file is taken, so an added import never shadows one (nor a builtin).
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A fresh plan for the module's fixes.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
taken: frozenset[str]
|
|
59
|
+
values: frozenset[str]
|
|
60
|
+
taken, values = taken_names(tree)
|
|
61
|
+
bound: dict[str, str] = _bound(tree)
|
|
62
|
+
defined: dict[str, int] = _defined(tree)
|
|
63
|
+
return ImportPlan(
|
|
64
|
+
bound,
|
|
65
|
+
taken,
|
|
66
|
+
_after(tree),
|
|
67
|
+
defined,
|
|
68
|
+
checking=Checking(
|
|
69
|
+
_block(tree),
|
|
70
|
+
_lazy(tree, bound),
|
|
71
|
+
{
|
|
72
|
+
name: origin
|
|
73
|
+
for stmt in tree.body
|
|
74
|
+
if isinstance(stmt, ast.If) and _is_checking(stmt.test)
|
|
75
|
+
for name, origin, _ in import_bindings(stmt.body)
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
postponed=_postponed(tree),
|
|
79
|
+
values=values,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def nested_classes(tree: ast.Module) -> dict[str, str]:
|
|
84
|
+
"""Name the classes defined in another class's body: bare, names of that body alone.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Each, mapped to the class that names it anywhere (`_Names`: `Group`, for `Group._Names`): one
|
|
88
|
+
of the module's own body, the only class defining that name; else to nothing.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
top: frozenset[int] = frozenset(id(stmt) for stmt in tree.body)
|
|
92
|
+
found: dict[str, str] = {}
|
|
93
|
+
node: ast.ClassDef
|
|
94
|
+
for node in classes(tree):
|
|
95
|
+
stmt: ast.stmt
|
|
96
|
+
for stmt in node.body:
|
|
97
|
+
if isinstance(stmt, ast.ClassDef):
|
|
98
|
+
found[stmt.name] = node.name if id(node) in top and stmt.name not in found else ""
|
|
99
|
+
return found
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def through(annotation: str, planned: ImportPlan, nested: Mapping[str, str]) -> str:
|
|
103
|
+
"""Spell `annotation` for a function: a class of a class's body through that class (`Group._Names`).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
It, each such class the module binds nowhere else named through its class, where one names it.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
name: str
|
|
110
|
+
for name in roots(annotation) & nested.keys() - planned.bound.keys() - planned.defined.keys():
|
|
111
|
+
if nested[name] and not any(quote in annotation for quote in _QUOTES):
|
|
112
|
+
annotation = re.sub(rf"(?<![\w.]){re.escape(name)}\b", f"{nested[name]}.{name}", annotation)
|
|
113
|
+
return annotation
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _private(statement: str) -> bool:
|
|
117
|
+
"""Check whether an import to add takes a name its module keeps to itself (`from m import _T`).
|
|
118
|
+
|
|
119
|
+
Not the standard library's, whose stubs name such classes in what they declare
|
|
120
|
+
(`unittest.case._AssertRaisesContext`).
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Whether it does: one underscore before it, not a dunder's two.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
module: str
|
|
127
|
+
name: str
|
|
128
|
+
module, _, name = statement.removeprefix("from ").rpartition(" import ")
|
|
129
|
+
return name.startswith("_") and not name.startswith("__") and module.partition(".")[0] not in _STDLIB
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def unwritable(annotation: str, planned: ImportPlan, nested: Mapping[str, str]) -> bool:
|
|
133
|
+
"""Check whether the module can't write `annotation`, whatever it imports.
|
|
134
|
+
|
|
135
|
+
It names a class of `nested` (see `nested_classes`) the module binds nowhere else, which no
|
|
136
|
+
function names bare; or an import it needs would take another module's private name.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Whether it can't.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
if roots(annotation) & nested.keys() - planned.bound.keys() - planned.defined.keys():
|
|
143
|
+
return True
|
|
144
|
+
return any(
|
|
145
|
+
_private(statement)
|
|
146
|
+
for statement in (*guarded_imports(annotation, planned), *imports_of(annotation, planned))
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def added_origin(statement: str) -> Origin:
|
|
151
|
+
"""Read what an import `ImportPlan.spell` added binds: `from m import T`, or `import m`.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Its origin: `("m", "T")`, or `("m", None)`.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
module: str
|
|
158
|
+
name: str
|
|
159
|
+
module, _, name = statement.removeprefix("from ").removeprefix("import ").partition(" import ")
|
|
160
|
+
return module, name or None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def added_dotted(statement: str) -> str:
|
|
164
|
+
"""Spell what an added import binds, dotted: `m.T` for `from m import T`, `m` for `import m`.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
It.
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
module: str
|
|
171
|
+
name: str | None
|
|
172
|
+
module, name = added_origin(statement)
|
|
173
|
+
return f"{module}.{name}" if name else module
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def present(found: ImportPlan, guarded: Mapping[str, Guarded]) -> frozenset[str]:
|
|
177
|
+
"""Name those of `guarded` the module imports already as their statements would.
|
|
178
|
+
|
|
179
|
+
Under `if TYPE_CHECKING:`, or to run: a fix of its own may have added either.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Each name whose import to add is one the module has.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
return frozenset(
|
|
186
|
+
name
|
|
187
|
+
for name, each in guarded.items()
|
|
188
|
+
if each.statement is not None
|
|
189
|
+
and added_dotted(each.statement) in {found.checking.bound.get(name), found.bound.get(name)}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _is_checking(test: ast.expr) -> bool:
|
|
194
|
+
"""Check whether an `if`'s test is `TYPE_CHECKING` (or `typing.TYPE_CHECKING`).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Whether it is.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
return (isinstance(test, ast.Name) and test.id == _TYPE_CHECKING) or (
|
|
201
|
+
isinstance(test, ast.Attribute) and test.attr == _TYPE_CHECKING
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _block(tree: ast.Module) -> tuple[int, int]:
|
|
206
|
+
"""Find the body of the module's first top-level `if TYPE_CHECKING:` with no `else`.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Its first and last line (from 1), or zeros.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
return next(
|
|
213
|
+
(
|
|
214
|
+
(stmt.body[0].lineno, stmt.end_lineno or stmt.lineno)
|
|
215
|
+
for stmt in tree.body
|
|
216
|
+
if isinstance(stmt, ast.If) and _is_checking(stmt.test) and not stmt.orelse
|
|
217
|
+
),
|
|
218
|
+
(0, 0),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def checking(tree: ast.Module) -> frozenset[str]:
|
|
223
|
+
"""Name what the module imports under a top-level `if` on a flag, which may not run.
|
|
224
|
+
|
|
225
|
+
`if TYPE_CHECKING:`, and any other name tested alone (`if MYPY_CHECK_RUNNING:`): what it imports
|
|
226
|
+
may be unbound when the module runs.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
The names they bind.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
return frozenset(
|
|
233
|
+
alias.asname or alias.name.split(".", 1)[0]
|
|
234
|
+
for stmt in tree.body
|
|
235
|
+
if isinstance(stmt, ast.If) and isinstance(stmt.test, ast.Name | ast.Attribute)
|
|
236
|
+
for node in stmt.body
|
|
237
|
+
if isinstance(node, ast.Import | ast.ImportFrom)
|
|
238
|
+
for alias in node.names
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _postponed(tree: ast.Module) -> bool:
|
|
243
|
+
"""Check whether the module has `from __future__ import annotations`.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Whether it has.
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
return any(
|
|
250
|
+
isinstance(stmt, ast.ImportFrom)
|
|
251
|
+
and stmt.module == _FUTURE
|
|
252
|
+
and any(alias.name == _ANNOTATIONS for alias in stmt.names)
|
|
253
|
+
for stmt in tree.body
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _bound(tree: ast.Module) -> dict[str, str]:
|
|
258
|
+
"""Map the names the module's own imports bind (at its top, or under a top-level `if`/`try`).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Each name, mapped to what it is: `os` for `import os` (`import os.path` binds `os`),
|
|
262
|
+
`os.path` for `import os.path as p`, `io.BytesIO` for `from io import BytesIO`.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
return {name: origin for name, origin, _ in import_bindings(_running(tree.body))}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _defined(tree: ast.Module) -> dict[str, int]:
|
|
269
|
+
"""Map each name the module binds at its top level (or under a top-level `if`/`try`) to its line.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Each name, and the line it's first bound on.
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
found: dict[str, int] = {}
|
|
276
|
+
stmt: ast.stmt
|
|
277
|
+
for stmt in _running(tree.body):
|
|
278
|
+
name: str
|
|
279
|
+
for name in _names(stmt):
|
|
280
|
+
_ = found.setdefault(name, stmt.lineno)
|
|
281
|
+
return found
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _names(stmt: ast.stmt) -> list[str]:
|
|
285
|
+
"""Name what one top-level statement binds, when it's plainly a name (not an unpacking's parts).
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
The names.
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
name: str
|
|
292
|
+
targets: list[ast.expr]
|
|
293
|
+
match stmt:
|
|
294
|
+
case ast.Import() | ast.ImportFrom():
|
|
295
|
+
return [alias.asname or alias.name.split(".", 1)[0] for alias in stmt.names]
|
|
296
|
+
case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.ClassDef(name=name):
|
|
297
|
+
return [name]
|
|
298
|
+
case ast.AnnAssign(target=ast.Name(id=name)):
|
|
299
|
+
return [name]
|
|
300
|
+
case ast.Assign(targets=targets):
|
|
301
|
+
return [target.id for target in targets if isinstance(target, ast.Name)]
|
|
302
|
+
case _:
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _running(body: list[ast.stmt]) -> Iterator[ast.stmt]:
|
|
307
|
+
"""Walk the module's top-level statements, into `if` and `try` blocks but not `if TYPE_CHECKING:`.
|
|
308
|
+
|
|
309
|
+
Yields:
|
|
310
|
+
Each statement.
|
|
311
|
+
|
|
312
|
+
"""
|
|
313
|
+
stmt: ast.stmt
|
|
314
|
+
test: ast.expr
|
|
315
|
+
for stmt in body:
|
|
316
|
+
yield stmt
|
|
317
|
+
match stmt:
|
|
318
|
+
case ast.If(test=test) if not _is_checking(test):
|
|
319
|
+
yield from _running(stmt.body + stmt.orelse)
|
|
320
|
+
case ast.Try() | ast.TryStar():
|
|
321
|
+
yield from _running(stmt.body + [s for h in stmt.handlers for s in h.body] + stmt.orelse)
|
|
322
|
+
case _:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def inner_imports(tree: ast.Module) -> Sequence[ast.Import | ast.ImportFrom]:
|
|
327
|
+
"""Find the imports inside the module's functions: what one binds, it binds there alone.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Them, in source order.
|
|
331
|
+
|
|
332
|
+
"""
|
|
333
|
+
found: _Inner | None
|
|
334
|
+
if (found := _INNER.get(tree)) is None:
|
|
335
|
+
found = _INNER[tree] = tuple(_read_inner_imports(tree))
|
|
336
|
+
return found
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _read_inner_imports(tree: ast.Module) -> Iterator[ast.Import | ast.ImportFrom]:
|
|
340
|
+
statements: list[_Import] = cast("list[_Import]", of_type(tree, ast.Import, ast.ImportFrom))
|
|
341
|
+
functions: list[FunctionDef] = cast(
|
|
342
|
+
"list[FunctionDef]",
|
|
343
|
+
of_type(tree, ast.FunctionDef, ast.AsyncFunctionDef),
|
|
344
|
+
)
|
|
345
|
+
spans: list[tuple[Start, Start]] = sorted(
|
|
346
|
+
((node.lineno, node.col_offset), (node.end_lineno or node.lineno, node.end_col_offset or 0))
|
|
347
|
+
for node in functions
|
|
348
|
+
)
|
|
349
|
+
starts: list[Start] = [start for start, _ in spans]
|
|
350
|
+
# Where the last of the functions starting no later than each one ends.
|
|
351
|
+
ends: list[Start] = list(accumulate((end for _, end in spans), max))
|
|
352
|
+
stmt: _Import
|
|
353
|
+
for stmt in sorted(statements, key=lambda node: (node.lineno, node.col_offset)):
|
|
354
|
+
start: Start = (stmt.lineno, stmt.col_offset)
|
|
355
|
+
before: int = bisect.bisect_left(starts, start)
|
|
356
|
+
if before and start < ends[before - 1]:
|
|
357
|
+
yield stmt
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _lazy(tree: ast.Module, bound: dict[str, str]) -> frozenset[str]:
|
|
361
|
+
"""Name the top-level packages only the module's functions import (see `ImportPlan.lazy`).
|
|
362
|
+
|
|
363
|
+
And those it imports by `pytest.importorskip("mod")`, anywhere: they may be missing when it runs.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Them.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
running: set[str] = {origin.partition(".")[0] for origin in bound.values()}
|
|
370
|
+
return (
|
|
371
|
+
frozenset(
|
|
372
|
+
module.partition(".")[0]
|
|
373
|
+
for stmt in inner_imports(tree)
|
|
374
|
+
for module in (
|
|
375
|
+
[alias.name for alias in stmt.names]
|
|
376
|
+
if isinstance(stmt, ast.Import)
|
|
377
|
+
else [stmt.module or ""] * (not stmt.level)
|
|
378
|
+
)
|
|
379
|
+
).union(_skipped(tree, bound))
|
|
380
|
+
- running
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _skipped(tree: ast.Module, bound: Mapping[str, str]) -> Iterator[str]:
|
|
385
|
+
"""Name the top-level packages `pytest.importorskip("mod")` imports, however the module names it.
|
|
386
|
+
|
|
387
|
+
Yields:
|
|
388
|
+
Each, once a call.
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
node: ast.Call
|
|
392
|
+
for node in cast("list[ast.Call]", of_type(tree, ast.Call)):
|
|
393
|
+
callee: str | None = dotted(node.func)
|
|
394
|
+
head: str
|
|
395
|
+
rest: str
|
|
396
|
+
head, _, rest = (callee or "").partition(_DOT)
|
|
397
|
+
named: list[ast.expr] = [*node.args[:1], *(k.value for k in node.keywords if k.arg == _MODNAME)]
|
|
398
|
+
if (
|
|
399
|
+
head in bound
|
|
400
|
+
and (f"{bound[head]}{_DOT}{rest}" if rest else bound[head]) == _IMPORTORSKIP
|
|
401
|
+
and len(named) == 1
|
|
402
|
+
and isinstance(named[0], ast.Constant)
|
|
403
|
+
and isinstance(named[0].value, str)
|
|
404
|
+
):
|
|
405
|
+
yield named[0].value.partition(_DOT)[0]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def taken_names(tree: ast.Module) -> tuple[frozenset[str], frozenset[str]]:
|
|
409
|
+
"""Find every name bound anywhere in the module: its own, a function's, a class's, a parameter's.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Them all; and those bound by anything but an import or a class statement (an assignment, a
|
|
413
|
+
parameter, a `def`), which name a value, not a class or module, somewhere.
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
found: tuple[frozenset[str], frozenset[str]] | None
|
|
417
|
+
if (found := _TAKEN.get(tree)) is None:
|
|
418
|
+
found = _TAKEN[tree] = _read_taken_names(tree)
|
|
419
|
+
return found
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _read_taken_names(tree: ast.Module) -> tuple[frozenset[str], frozenset[str]]:
|
|
423
|
+
names: set[str] = set()
|
|
424
|
+
values: set[str] = set()
|
|
425
|
+
node: ast.AST
|
|
426
|
+
name: str
|
|
427
|
+
asname: str | None
|
|
428
|
+
for node in of_type(tree, *_BINDERS):
|
|
429
|
+
match node:
|
|
430
|
+
case ast.ClassDef(name=name):
|
|
431
|
+
names.add(name)
|
|
432
|
+
case ast.alias(name=name, asname=asname):
|
|
433
|
+
names.add(asname or name.split(".", 1)[0])
|
|
434
|
+
case (
|
|
435
|
+
ast.Name(id=name, ctx=ast.Store())
|
|
436
|
+
| ast.arg(arg=name)
|
|
437
|
+
| ast.FunctionDef(name=name)
|
|
438
|
+
| ast.AsyncFunctionDef(name=name)
|
|
439
|
+
):
|
|
440
|
+
names.add(name)
|
|
441
|
+
values.add(name)
|
|
442
|
+
case ast.ExceptHandler(name=str() as name) | ast.MatchAs(name=str() as name):
|
|
443
|
+
names.add(name)
|
|
444
|
+
values.add(name)
|
|
445
|
+
case _:
|
|
446
|
+
pass
|
|
447
|
+
return frozenset(names), frozenset(values)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def rebound_names(tree: ast.Module) -> dict[str, list[Start]]:
|
|
451
|
+
"""Find the names the module binds more than once, anywhere in it, and where.
|
|
452
|
+
|
|
453
|
+
One bound once (by its import, say) means the same thing throughout: no function shadows it.
|
|
454
|
+
One bound again is shadowed only by a function one of its bindings is in.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Each such name, and where each of its bindings starts, in source order.
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
found: dict[str, list[Start]] | None
|
|
461
|
+
if (found := _REBOUND.get(tree)) is None:
|
|
462
|
+
found = _REBOUND[tree] = _read_rebound_names(tree)
|
|
463
|
+
return found
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _read_rebound_names(tree: ast.Module) -> dict[str, list[Start]]:
|
|
467
|
+
found: dict[str, list[Start]] = {}
|
|
468
|
+
node: ast.AST
|
|
469
|
+
name: str
|
|
470
|
+
asname: str | None
|
|
471
|
+
for node in of_type(tree, *_BINDERS):
|
|
472
|
+
match node:
|
|
473
|
+
case ast.alias(name=name, asname=asname):
|
|
474
|
+
found.setdefault(asname or name.split(".", 1)[0], []).append((node.lineno, node.col_offset))
|
|
475
|
+
case (
|
|
476
|
+
ast.Name(id=name, ctx=ast.Store() | ast.Del())
|
|
477
|
+
| ast.arg(arg=name)
|
|
478
|
+
| ast.FunctionDef(name=name)
|
|
479
|
+
| ast.AsyncFunctionDef(name=name)
|
|
480
|
+
| ast.ClassDef(name=name)
|
|
481
|
+
):
|
|
482
|
+
found.setdefault(name, []).append((node.lineno, node.col_offset))
|
|
483
|
+
case ast.ExceptHandler(name=str() as name) | ast.MatchAs(name=str() as name):
|
|
484
|
+
found.setdefault(name, []).append((node.lineno, node.col_offset))
|
|
485
|
+
case _:
|
|
486
|
+
pass
|
|
487
|
+
return {name: sorted(starts) for name, starts in found.items() if len(starts) > 1}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _after(tree: ast.Module) -> int:
|
|
491
|
+
"""Find the line an added import goes after: the end of the imports the module starts with.
|
|
492
|
+
|
|
493
|
+
After its docstring and `from __future__` imports, before anything else (and so before an
|
|
494
|
+
`if TYPE_CHECKING:` block); the top of the file (0) if it starts with neither.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
That line (from 1), or 0.
|
|
498
|
+
|
|
499
|
+
"""
|
|
500
|
+
after: int = 0
|
|
501
|
+
stmt: ast.stmt
|
|
502
|
+
index: int
|
|
503
|
+
for index, stmt in enumerate(tree.body):
|
|
504
|
+
is_docstring: bool = (
|
|
505
|
+
index == 0
|
|
506
|
+
and isinstance(stmt, ast.Expr)
|
|
507
|
+
and isinstance(stmt.value, ast.Constant)
|
|
508
|
+
and isinstance(stmt.value.value, str)
|
|
509
|
+
)
|
|
510
|
+
if not (is_docstring or isinstance(stmt, ast.Import | ast.ImportFrom)):
|
|
511
|
+
break
|
|
512
|
+
after = stmt.end_lineno or stmt.lineno
|
|
513
|
+
return after
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def exported_names(
|
|
517
|
+
returns: Returns,
|
|
518
|
+
guarded: Mapping[str, Guarded],
|
|
519
|
+
added: Mapping[str, str],
|
|
520
|
+
) -> dict[str, Origin]:
|
|
521
|
+
"""Find what each name `returns`' types use refers to, where the module doesn't import it to run.
|
|
522
|
+
|
|
523
|
+
Imported for type checking alone (`guarded`), or by an import its fixes add (`added`): a library
|
|
524
|
+
type it doesn't import yet (`types.ModuleType`) is named for its importers too, or they'd type
|
|
525
|
+
its calls only on a second pass, once the import is in the source.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Each such name's origin.
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
return {
|
|
532
|
+
name: guarded[name].origin if name in guarded else added_origin(added[name])
|
|
533
|
+
for annotation in (
|
|
534
|
+
*returns.calls.values(),
|
|
535
|
+
*(a for m in returns.methods.values() for a in m.values()),
|
|
536
|
+
)
|
|
537
|
+
for name in roots(annotation)
|
|
538
|
+
if name in guarded or name in added
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def bound_within(func: FunctionDef) -> frozenset[str]:
|
|
543
|
+
"""Name what a function's own body imports or defines as a class, at any depth.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Each name: one nothing outside the function can spell.
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
found: set[str] = set()
|
|
550
|
+
node: ast.AST
|
|
551
|
+
for node in ast.walk(func):
|
|
552
|
+
if isinstance(node, ast.ClassDef):
|
|
553
|
+
found.add(node.name)
|
|
554
|
+
elif isinstance(node, ast.Import | ast.ImportFrom):
|
|
555
|
+
found.update((alias.asname or alias.name).split(".")[0] for alias in node.names)
|
|
556
|
+
return frozenset(found)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def imports_of(annotation: str, planned: ImportPlan) -> tuple[str, ...]:
|
|
560
|
+
"""Find the imports `annotation` needs: those `planned` added for a name it's written with.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Their statements, sorted.
|
|
564
|
+
|
|
565
|
+
"""
|
|
566
|
+
# `annotation` is always `ast.unparse`'s own output (or a name `plan` spelled), so it parses.
|
|
567
|
+
return tuple(sorted({planned.added[root] for root in roots(annotation) if root in planned.added}))
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def guarded_imports(annotation: str, planned: ImportPlan) -> tuple[str, ...]:
|
|
571
|
+
"""Find the imports under `if TYPE_CHECKING:` `annotation` needs (see `Guarded`).
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Their statements, sorted.
|
|
575
|
+
|
|
576
|
+
"""
|
|
577
|
+
statements: Iterator[str | None] = (
|
|
578
|
+
planned.guarded[root].statement for root in roots(annotation) if root in planned.guarded
|
|
579
|
+
)
|
|
580
|
+
return tuple(sorted({statement for statement in statements if statement is not None}))
|
|
Binary file
|