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
|
Binary file
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--infer-with`: what a hint may name that the file doesn't bind where its annotations run.
|
|
3
|
+
|
|
4
|
+
A hint is text, and a name in it may not be a type: basedpyright shows a value that is a module by
|
|
5
|
+
the module's name, and a generic class without its arguments when it doesn't know them. So a name
|
|
6
|
+
is taken only for a class or a type alias the index of checked files and installed packages
|
|
7
|
+
defines: `own`, for what the file imports under `if TYPE_CHECKING:`; `vetted`, for what a hint's
|
|
8
|
+
edits would import (the standard library's are `hinted`'s to judge, by its tables).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
17
|
+
|
|
18
|
+
from constricter.empty import Empty
|
|
19
|
+
from constricter.fix.core.known import Guarded, Hints, Offered, Origin
|
|
20
|
+
from constricter.fix.index import project
|
|
21
|
+
from constricter.fix.index.modules import SUFFIX
|
|
22
|
+
from constricter.fix.index.origins import absolute
|
|
23
|
+
from constricter.fix.libraries import stdlib
|
|
24
|
+
|
|
25
|
+
_PACKAGE: Final = "__init__"
|
|
26
|
+
_STDLIB: Final = sys.stdlib_module_names
|
|
27
|
+
_Defined: TypeAlias = tuple[str, str] # where the index defines a class: its module, and its name there
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Own(NamedTuple):
|
|
31
|
+
"""The classes and aliases a file imports for type checking alone (`guarded`), and the generic ones."""
|
|
32
|
+
|
|
33
|
+
# A plain `dict`, not a `MappingProxyType`: the CLI's worker processes are sent it, pickled.
|
|
34
|
+
guarded: Mapping[str, Guarded] = Empty()
|
|
35
|
+
generics: frozenset[str] = frozenset()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def own(catalog: project.Index, path: Path) -> Own:
|
|
39
|
+
"""Find the classes the file at `path` imports under a top-level `if` (`if TYPE_CHECKING:`) alone.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Those the index or the standard library's tables define; none for a file `catalog` doesn't have.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
target: project.Module | None
|
|
46
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(project.module_name(path))) is None:
|
|
47
|
+
return Own()
|
|
48
|
+
found: dict[str, Guarded] = {}
|
|
49
|
+
generics: set[str] = set()
|
|
50
|
+
name: str
|
|
51
|
+
origin: Origin
|
|
52
|
+
for name, origin in target.guarded.items():
|
|
53
|
+
generic: bool | None = _class(catalog, origin)
|
|
54
|
+
if generic is not None and name not in target.names:
|
|
55
|
+
found[name] = Guarded(origin, None)
|
|
56
|
+
if generic:
|
|
57
|
+
generics.add(name)
|
|
58
|
+
return Own(found, frozenset(generics))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def vetted(catalog: project.Index, path: Path, hints: tuple[Hints, ...]) -> tuple[Hints, ...]:
|
|
62
|
+
"""Drop the edits of the file at `path`'s hints that import what isn't a class, or a generic one bare.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The hints, without them: such a hint is judged by what it shows alone.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
if not any(found.offered for found in hints):
|
|
69
|
+
return hints
|
|
70
|
+
module: tuple[str, bool] = (project.module_name(path), path.stem == _PACKAGE)
|
|
71
|
+
target: project.Module | None = catalog.modules.get(module[0])
|
|
72
|
+
checking: Mapping[str, Origin] = Empty() if target is None else target.guarded
|
|
73
|
+
found: dict[str, _Defined | None] = {name: _defined(catalog, origin) for name, origin in checking.items()}
|
|
74
|
+
guarded: dict[str, _Defined] = {name: defined for name, defined in found.items() if defined is not None}
|
|
75
|
+
return tuple(
|
|
76
|
+
_shared(
|
|
77
|
+
catalog,
|
|
78
|
+
module,
|
|
79
|
+
found._replace(
|
|
80
|
+
offered={
|
|
81
|
+
where: offered
|
|
82
|
+
for where, offered in found.offered.items()
|
|
83
|
+
if _sound(catalog, module, offered, found.types.get(where, ""))
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
guarded,
|
|
87
|
+
)
|
|
88
|
+
for found in hints
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _shared(
|
|
93
|
+
catalog: project.Index,
|
|
94
|
+
module: tuple[str, bool],
|
|
95
|
+
found: Hints,
|
|
96
|
+
guarded: Mapping[str, _Defined],
|
|
97
|
+
) -> Hints:
|
|
98
|
+
"""Import one class by one statement in all of a file's hints' edits, the first's, as it's written once.
|
|
99
|
+
|
|
100
|
+
A checker's edits may import a class by any module that re-exports it (`pint`'s `Unit`, then
|
|
101
|
+
`pint.registry`'s): the first made the second's an import of a name taken, and its fix waited
|
|
102
|
+
for a second pass, which reads the name as the file's. A hint whose edits import another class
|
|
103
|
+
by a name the file imports for type checking (`guarded`, each where it's defined) is
|
|
104
|
+
dropped: judged by what it shows, it would be taken for that one. Only a class the index
|
|
105
|
+
defines: it doesn't follow the standard library's re-exports (`typing`'s `Callable`).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The hints so.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
first: dict[str, tuple[_Defined, str]] = {} # each name's class, and its statement
|
|
112
|
+
types: dict[tuple[int, int], str] = dict(found.types)
|
|
113
|
+
offered: dict[tuple[int, int], Offered] = {}
|
|
114
|
+
where: tuple[int, int]
|
|
115
|
+
each: Offered
|
|
116
|
+
for where, each in sorted(found.offered.items()):
|
|
117
|
+
statements: list[str] = []
|
|
118
|
+
statement: str
|
|
119
|
+
for statement in each.imports:
|
|
120
|
+
name: str
|
|
121
|
+
origin: Origin
|
|
122
|
+
name, origin = _bound(module, statement)
|
|
123
|
+
defined: _Defined | None
|
|
124
|
+
if (defined := _defined(catalog, origin)) is None:
|
|
125
|
+
statements.append(statement)
|
|
126
|
+
continue
|
|
127
|
+
if name in guarded and guarded[name] != defined:
|
|
128
|
+
break
|
|
129
|
+
was: tuple[_Defined, str] = first.setdefault(name, (defined, statement))
|
|
130
|
+
statements.append(was[1] if was[0] == defined else statement)
|
|
131
|
+
else:
|
|
132
|
+
offered[where] = each._replace(imports=tuple(statements))
|
|
133
|
+
continue
|
|
134
|
+
_ = types.pop(where, None)
|
|
135
|
+
return found._replace(types=types, offered=offered)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def imported(path: Path, hints: tuple[Hints, ...]) -> dict[str, Guarded]:
|
|
139
|
+
"""Find what the file at `path`'s vetted hints' edits import for type checking alone.
|
|
140
|
+
|
|
141
|
+
For `linked.same`: a class an edit imports (`DataFrame`, from `pandas.core.frame`) is a spelling
|
|
142
|
+
of it beside the file's own (`pd.DataFrame`) in the pass that writes the import, as in the next.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Each name one statement binds in every edit, with it.
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
module: tuple[str, bool] = (project.module_name(path), path.stem == _PACKAGE)
|
|
149
|
+
found: dict[str, Guarded] = {}
|
|
150
|
+
clashing: set[str] = set()
|
|
151
|
+
statement: str
|
|
152
|
+
for statement in {
|
|
153
|
+
statement for each in hints for offered in each.offered.values() for statement in offered.imports
|
|
154
|
+
}:
|
|
155
|
+
name: str
|
|
156
|
+
origin: Origin
|
|
157
|
+
name, origin = _bound(module, statement)
|
|
158
|
+
if name in found:
|
|
159
|
+
clashing.add(name)
|
|
160
|
+
found[name] = Guarded(origin, statement)
|
|
161
|
+
return {name: guarded for name, guarded in found.items() if name not in clashing}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _bound(module: tuple[str, bool], statement: str) -> tuple[str, Origin]:
|
|
165
|
+
"""Read what a `from` import of a hint's edits binds, from the hinted file's `module`.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The name it binds, and what that is.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
node: ast.ImportFrom = cast("ast.ImportFrom", ast.parse(statement).body[0])
|
|
172
|
+
alias: ast.alias = node.names[0]
|
|
173
|
+
return alias.asname or alias.name, (
|
|
174
|
+
absolute(module[0], node.module, node.level, is_package=module[1]),
|
|
175
|
+
alias.name,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _defined(catalog: project.Index, origin: Origin) -> _Defined | None:
|
|
180
|
+
"""Find where the index defines a class or a type alias, through re-exports.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Its module and its name there; `None` where it doesn't.
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
kind: str
|
|
187
|
+
for kind in (project.CLASS, project.ALIAS):
|
|
188
|
+
found: tuple[project.Module, str] | None
|
|
189
|
+
if (found := project.definition(catalog.modules, origin, kind)) is not None:
|
|
190
|
+
return found[0].name, found[1]
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _sound(catalog: project.Index, module: tuple[str, bool], offered: Offered, shown: str) -> bool:
|
|
195
|
+
"""Check that each import of a hint's edits names a class, and no generic one written bare.
|
|
196
|
+
|
|
197
|
+
`module`: the hinted file's module name, and whether it's a package's `__init__`; `shown`: what
|
|
198
|
+
the hint shows, which may write a class bare that its edit's spelling doesn't.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Whether they do; an `import m`, which names no class, never does, nor an import from an
|
|
202
|
+
installed package's private module (`numpy._core`), which its next release may move.
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
statement: str
|
|
206
|
+
for statement in offered.imports:
|
|
207
|
+
try:
|
|
208
|
+
node: ast.stmt = ast.parse(statement).body[0]
|
|
209
|
+
except SyntaxError: # from a file no import can name (`v3.0.0.c.py`)
|
|
210
|
+
return False
|
|
211
|
+
if not isinstance(node, ast.ImportFrom):
|
|
212
|
+
return False
|
|
213
|
+
alias: ast.alias = node.names[0]
|
|
214
|
+
origin: Origin = (absolute(module[0], node.module, node.level, is_package=module[1]), alias.name)
|
|
215
|
+
if origin[0].partition(".")[0] in _STDLIB:
|
|
216
|
+
continue
|
|
217
|
+
lone: re.Pattern[str] = re.compile(rf"(?<![\w.]){re.escape(alias.asname or alias.name)}(?![\w\[])")
|
|
218
|
+
generic: bool | None = _class(catalog, origin)
|
|
219
|
+
bare: bool = bool(generic and (lone.search(offered.text) or lone.search(shown)))
|
|
220
|
+
if generic is None or bare or _private(catalog, origin[0]):
|
|
221
|
+
return False
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _private(catalog: project.Index, name: str) -> bool:
|
|
226
|
+
"""Check whether module `name` is an installed package's private one.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Whether it is.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
module: project.Module | None = catalog.modules.get(name)
|
|
233
|
+
return module is not None and module.installed and any(part.startswith("_") for part in name.split("."))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _class(catalog: project.Index, origin: Origin) -> bool | None:
|
|
237
|
+
"""Find whether `origin` is a class or a type alias: an indexed module's (through re-exports).
|
|
238
|
+
|
|
239
|
+
Or a class of the standard library's.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Whether it's generic, missing its arguments written bare; `None` if it's neither, to anyone's
|
|
243
|
+
knowledge.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
defined: tuple[project.Module, str] | None
|
|
247
|
+
if (defined := project.definition(catalog.modules, origin, project.CLASS)) is not None:
|
|
248
|
+
return defined[1] in defined[0].generics
|
|
249
|
+
if (defined := project.definition(catalog.modules, origin, project.ALIAS)) is not None:
|
|
250
|
+
return defined[0].aliases[defined[1]]
|
|
251
|
+
path: str = f"{origin[0]}.{origin[1]}"
|
|
252
|
+
return stdlib.needs_arguments(path) if stdlib.defines_class(path) else None
|
|
Binary file
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The order the CLI checks files in: each after those whose unannotated functions it calls.
|
|
3
|
+
|
|
4
|
+
So their `return`s are known when it's checked (see `project.returned`); files calling each other's
|
|
5
|
+
functions (a cycle in the graph `linked.needs` gives) form one component.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
11
|
+
|
|
12
|
+
from constricter.fix.index import awaits, fixtures, linked, loose, managed, project
|
|
13
|
+
from constricter.fix.index.project import Index
|
|
14
|
+
|
|
15
|
+
_SUFFIX: Final = ".py"
|
|
16
|
+
_Visit: TypeAlias = tuple[str, Iterator[str]] # a module in the graph, and its callees still to visit
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Plan(NamedTuple):
|
|
20
|
+
"""The order to check files in (see `plan`), and the module each one's `returned` goes to."""
|
|
21
|
+
|
|
22
|
+
components: list[list[int]] # files importing each other, by their index in the paths; callees first
|
|
23
|
+
after: list[frozenset[int]] # the components each one's files import unannotated functions from
|
|
24
|
+
names: dict[int, str] # each file's module name, if it's the only file with it
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def plan(catalog: Index, paths: Sequence[Path]) -> Plan:
|
|
28
|
+
"""Order the files in `paths` for checking: each after the files whose unannotated functions it calls.
|
|
29
|
+
|
|
30
|
+
Those it imports and calls (through re-exports, or an imported module), so their `return`s are known
|
|
31
|
+
when it's checked (see `returned`); files calling each other's (a cycle) are one component. A
|
|
32
|
+
file `catalog` doesn't have, or one of several with the same module name, is one on its own.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The components, callees first, and what each comes after; and each file's module.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
names: list[str | None] = [
|
|
39
|
+
project.module_name(path) if path.suffix == _SUFFIX else None for path in paths
|
|
40
|
+
]
|
|
41
|
+
counts: dict[str | None, int] = {}
|
|
42
|
+
name: str | None
|
|
43
|
+
for name in names:
|
|
44
|
+
counts[name] = counts.get(name, 0) + 1
|
|
45
|
+
unique: dict[str, int] = {
|
|
46
|
+
name: at
|
|
47
|
+
for at, name in enumerate(names)
|
|
48
|
+
if name is not None and counts[name] == 1 and name in catalog.modules
|
|
49
|
+
}
|
|
50
|
+
graph: dict[str, list[str]] = {
|
|
51
|
+
name: sorted(
|
|
52
|
+
(
|
|
53
|
+
linked.needs(catalog, catalog.modules[name])
|
|
54
|
+
| loose.needs(catalog, catalog.modules[name])
|
|
55
|
+
| managed.needs(catalog, catalog.modules[name])
|
|
56
|
+
| awaits.needs(catalog, catalog.modules[name])
|
|
57
|
+
| fixtures.needed(catalog, name)
|
|
58
|
+
)
|
|
59
|
+
& unique.keys(),
|
|
60
|
+
)
|
|
61
|
+
for name in sorted(unique)
|
|
62
|
+
}
|
|
63
|
+
components: list[list[int]] = []
|
|
64
|
+
after: list[frozenset[int]] = []
|
|
65
|
+
component: dict[str, int] = {}
|
|
66
|
+
members: frozenset[str]
|
|
67
|
+
for members in _Components(graph): # callees' components first
|
|
68
|
+
member: str
|
|
69
|
+
for member in members:
|
|
70
|
+
component[member] = len(components)
|
|
71
|
+
after.append(
|
|
72
|
+
frozenset(
|
|
73
|
+
component[callee] for member in members for callee in graph[member] if callee not in members
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
components.append(sorted(unique[member] for member in members))
|
|
77
|
+
at: int
|
|
78
|
+
for at in sorted(set(range(len(paths))) - set(unique.values())):
|
|
79
|
+
components.append([at])
|
|
80
|
+
after.append(frozenset())
|
|
81
|
+
return Plan(components, after, {at: name for name, at in unique.items()})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _Components:
|
|
85
|
+
"""A graph's strongly connected components (Tarjan's, with a stack of its own: chains can be long)."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, graph: Mapping[str, Sequence[str]]) -> None:
|
|
88
|
+
"""Start on `graph`: each node's successors."""
|
|
89
|
+
self.graph: Mapping[str, Sequence[str]] = graph
|
|
90
|
+
self.order: dict[str, int] = {} # each node visited, and when
|
|
91
|
+
self.low: dict[str, int] = {} # the earliest node on the stack each node reaches
|
|
92
|
+
self.stack: list[str] = []
|
|
93
|
+
self.on_stack: set[str] = set()
|
|
94
|
+
|
|
95
|
+
def __iter__(self) -> Iterator[frozenset[str]]:
|
|
96
|
+
"""Find the components.
|
|
97
|
+
|
|
98
|
+
Yields:
|
|
99
|
+
Each component, after every component it reaches.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
start: str
|
|
103
|
+
for start in self.graph:
|
|
104
|
+
yield from self.reached(start)
|
|
105
|
+
|
|
106
|
+
def reached(self, start: str) -> Iterator[frozenset[str]]:
|
|
107
|
+
"""Visit what `start` reaches that isn't visited yet (nothing, if it's been visited itself).
|
|
108
|
+
|
|
109
|
+
Yields:
|
|
110
|
+
Each component it completes.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
if start in self.order:
|
|
114
|
+
return
|
|
115
|
+
# Each entry: a node, and its successors still to visit; `None` at the bottom ends it.
|
|
116
|
+
waiting: list[_Visit | None] = [None, self._visit(start)]
|
|
117
|
+
entry: _Visit
|
|
118
|
+
for entry in iter(waiting.pop, None):
|
|
119
|
+
node: str = entry[0]
|
|
120
|
+
successor: str | None
|
|
121
|
+
if (successor := next(entry[1], None)) is not None:
|
|
122
|
+
waiting.append(entry)
|
|
123
|
+
if successor not in self.order:
|
|
124
|
+
waiting.append(self._visit(successor))
|
|
125
|
+
elif successor in self.on_stack:
|
|
126
|
+
self.low[node] = min(self.low[node], self.order[successor])
|
|
127
|
+
continue
|
|
128
|
+
parent: _Visit | None
|
|
129
|
+
if (parent := waiting[-1]) is not None:
|
|
130
|
+
self.low[parent[0]] = min(self.low[parent[0]], self.low[node])
|
|
131
|
+
if self.low[node] == self.order[node]:
|
|
132
|
+
yield self._popped(node)
|
|
133
|
+
|
|
134
|
+
def _visit(self, node: str) -> _Visit:
|
|
135
|
+
"""Put `node` on the stack.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
It, and its successors to visit.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
self.order[node] = self.low[node] = len(self.order)
|
|
142
|
+
self.stack.append(node)
|
|
143
|
+
self.on_stack.add(node)
|
|
144
|
+
return node, iter(self.graph[node])
|
|
145
|
+
|
|
146
|
+
def _popped(self, node: str) -> frozenset[str]:
|
|
147
|
+
"""Take `node`'s component off the stack: `node` and what's above it.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The component.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
members: list[str] = [*iter(self.stack.pop, node), node]
|
|
154
|
+
self.on_stack.difference_update(members)
|
|
155
|
+
return frozenset(members)
|
|
Binary file
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Where each of a checked module's top-level names comes from (`Origin`), for `modules.read`.
|
|
3
|
+
|
|
4
|
+
What it imports, under a top-level `if` or `try` too, and what it binds itself.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from collections.abc import Iterator, Mapping
|
|
9
|
+
from typing import Final, cast
|
|
10
|
+
|
|
11
|
+
from constricter.fix.core.known import Origin
|
|
12
|
+
|
|
13
|
+
_PYTEST: Final = "pytest"
|
|
14
|
+
_IMPORTORSKIP: Final = "importorskip"
|
|
15
|
+
_MODNAME: Final = "modname" # its parameter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def absolute(name: str, module: str | None, level: int, *, is_package: bool) -> str:
|
|
19
|
+
"""Resolve `from <.level><module> import ...` in module `name`.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The absolute module name.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
if not level:
|
|
26
|
+
return module or ""
|
|
27
|
+
package: list[str] = name.split(".") if is_package else name.split(".")[:-1]
|
|
28
|
+
base: list[str] = package[: len(package) - (level - 1)] if level > 1 else package
|
|
29
|
+
return ".".join([*base, *([module] if module else [])])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def origins_of(tree: ast.Module, name: str, *, is_package: bool) -> dict[str, Origin]:
|
|
33
|
+
"""Map module `name`'s top-level names (the last binding wins).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
What each refers to.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
names: dict[str, Origin] = {}
|
|
40
|
+
stmt: ast.stmt
|
|
41
|
+
for stmt in tree.body:
|
|
42
|
+
match stmt:
|
|
43
|
+
case ast.Import() | ast.ImportFrom():
|
|
44
|
+
names.update(_imported(stmt, name, is_package=is_package))
|
|
45
|
+
case _:
|
|
46
|
+
names.update((bound, (name, bound)) for bound in _bound(stmt))
|
|
47
|
+
return names
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _imported(stmt: ast.Import | ast.ImportFrom, name: str, *, is_package: bool) -> dict[str, Origin]:
|
|
51
|
+
"""Map the names one import in module `name` binds.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
What each refers to.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
names: dict[str, Origin] = {}
|
|
58
|
+
alias: ast.alias
|
|
59
|
+
if isinstance(stmt, ast.Import):
|
|
60
|
+
for alias in stmt.names:
|
|
61
|
+
if alias.asname:
|
|
62
|
+
names[alias.asname] = (alias.name, None)
|
|
63
|
+
else: # `import a.b` binds `a`
|
|
64
|
+
names[alias.name.split(".")[0]] = (alias.name.split(".")[0], None)
|
|
65
|
+
return names
|
|
66
|
+
for alias in stmt.names:
|
|
67
|
+
names[alias.asname or alias.name] = (
|
|
68
|
+
absolute(name, stmt.module, stmt.level, is_package=is_package),
|
|
69
|
+
alias.name,
|
|
70
|
+
)
|
|
71
|
+
return names
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def guarded_origins(tree: ast.Module, name: str, *, is_package: bool) -> dict[str, Origin]:
|
|
75
|
+
"""Map the names module `name` imports under a top-level `if` or `try` (`if TYPE_CHECKING:`).
|
|
76
|
+
|
|
77
|
+
Not what it binds at run time (`origins_of`): a type variable imported only for the checker is a type
|
|
78
|
+
variable all the same. Not a name two of them bind to different things (one in each branch of an
|
|
79
|
+
`if`): which it is, another file can't say.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
What each refers to.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
found: dict[str, Origin] = {}
|
|
86
|
+
either: set[str] = set()
|
|
87
|
+
node: ast.AST
|
|
88
|
+
bound: str
|
|
89
|
+
origin: Origin
|
|
90
|
+
for node in (
|
|
91
|
+
n for stmt in tree.body if isinstance(stmt, ast.If | ast.Try | ast.TryStar) for n in ast.walk(stmt)
|
|
92
|
+
):
|
|
93
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
94
|
+
for bound, origin in _imported(node, name, is_package=is_package).items():
|
|
95
|
+
if found.setdefault(bound, origin) != origin:
|
|
96
|
+
either.add(bound)
|
|
97
|
+
return {bound: origin for bound, origin in found.items() if bound not in either}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def skipped(tree: ast.Module, names: Mapping[str, Origin]) -> dict[str, Origin]:
|
|
101
|
+
"""Map each name the module binds to `pytest.importorskip("mod")` to that module, as an import binds it.
|
|
102
|
+
|
|
103
|
+
Wherever it's bound, in a function too: the name is the module (`import mod as name`) when every
|
|
104
|
+
binding of it in the module is such a call for that one module. `names`: the module's top-level
|
|
105
|
+
names (`origins_of`), which say how it imports `pytest`.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Each such name, mapped to the module's origin.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
found: dict[str, set[str]] = {}
|
|
112
|
+
calls: set[int] = set()
|
|
113
|
+
node: ast.AST
|
|
114
|
+
for node in ast.walk(tree):
|
|
115
|
+
target: str
|
|
116
|
+
stored: ast.Name
|
|
117
|
+
call: ast.Call
|
|
118
|
+
module: str | None
|
|
119
|
+
match node:
|
|
120
|
+
case ast.Assign(targets=[ast.Name(id=target) as stored], value=ast.Call() as call) if (
|
|
121
|
+
module := _skipped_module(call, names)
|
|
122
|
+
) is not None:
|
|
123
|
+
found.setdefault(target, set()).add(module)
|
|
124
|
+
calls.add(id(stored))
|
|
125
|
+
case _:
|
|
126
|
+
pass
|
|
127
|
+
if not found:
|
|
128
|
+
return {}
|
|
129
|
+
others: set[str] = set(_bindings(tree, calls))
|
|
130
|
+
stmt: ast.Import | ast.ImportFrom
|
|
131
|
+
for stmt in (each for each in ast.walk(tree) if isinstance(each, ast.Import | ast.ImportFrom)):
|
|
132
|
+
alias: ast.alias
|
|
133
|
+
for alias in stmt.names:
|
|
134
|
+
bound: str = alias.asname or alias.name.partition(".")[0]
|
|
135
|
+
# `import mod` (or `import mod.sub as name`) binds the module it skips for: no other binding.
|
|
136
|
+
same: bool = isinstance(stmt, ast.Import) and found.get(bound) == {
|
|
137
|
+
alias.name if alias.asname else bound,
|
|
138
|
+
}
|
|
139
|
+
if not same:
|
|
140
|
+
others.add(bound)
|
|
141
|
+
return {
|
|
142
|
+
target: (next(iter(modules)), None)
|
|
143
|
+
for target, modules in found.items()
|
|
144
|
+
if len(modules) == 1 and target not in others
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _bindings(tree: ast.Module, skipping: set[int]) -> Iterator[str]:
|
|
149
|
+
"""Walk every name the module binds anywhere but by the `importorskip` calls (`skipping`, their targets).
|
|
150
|
+
|
|
151
|
+
Yields:
|
|
152
|
+
Each name: a store, a parameter, a definition or an `except`'s.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
node: ast.AST
|
|
156
|
+
for node in ast.walk(tree):
|
|
157
|
+
match node:
|
|
158
|
+
case ast.Name(ctx=ast.Store() | ast.Del()) if id(node) not in skipping:
|
|
159
|
+
yield node.id
|
|
160
|
+
case ast.arg():
|
|
161
|
+
yield node.arg
|
|
162
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
163
|
+
yield node.name
|
|
164
|
+
case ast.ExceptHandler(name=str()) | ast.MatchAs(name=str()) | ast.MatchStar(name=str()):
|
|
165
|
+
yield cast("str", node.name)
|
|
166
|
+
case _:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _skipped_module(call: ast.Call, names: Mapping[str, Origin]) -> str | None:
|
|
171
|
+
"""Find the module `pytest.importorskip(...)` imports: its first argument, a literal.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Its name, or `None` for any other call, or a name that isn't a literal.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
func: ast.expr = call.func
|
|
178
|
+
owner: str
|
|
179
|
+
attr: str
|
|
180
|
+
bound: str
|
|
181
|
+
origin: Origin | None
|
|
182
|
+
match func:
|
|
183
|
+
case ast.Attribute(value=ast.Name(id=owner), attr=attr) if attr == _IMPORTORSKIP:
|
|
184
|
+
origin = None if names.get(owner) != (_PYTEST, None) else (_PYTEST, _IMPORTORSKIP)
|
|
185
|
+
case ast.Name(id=bound):
|
|
186
|
+
origin = names.get(bound)
|
|
187
|
+
case _:
|
|
188
|
+
origin = None
|
|
189
|
+
if origin != (_PYTEST, _IMPORTORSKIP):
|
|
190
|
+
return None
|
|
191
|
+
named: list[ast.expr] = [*call.args[:1], *(k.value for k in call.keywords if k.arg == _MODNAME)]
|
|
192
|
+
module: str
|
|
193
|
+
match named:
|
|
194
|
+
case [ast.Constant(value=str() as module)] if all(part.isidentifier() for part in module.split(".")):
|
|
195
|
+
return module
|
|
196
|
+
case _:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def starred(tree: ast.Module, name: str, *, is_package: bool) -> tuple[str, ...]:
|
|
201
|
+
"""List the modules module `name` imports everything from (`from .main import *`), in order.
|
|
202
|
+
|
|
203
|
+
At its top level, or under a top-level `if` or `try` (pydantic's `__init__`, for type checking).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Their absolute names.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
nodes: Iterator[ast.AST] = (
|
|
210
|
+
node
|
|
211
|
+
for stmt in tree.body
|
|
212
|
+
for node in (ast.walk(stmt) if isinstance(stmt, ast.If | ast.Try | ast.TryStar) else (stmt,))
|
|
213
|
+
)
|
|
214
|
+
return tuple(
|
|
215
|
+
absolute(name, node.module, node.level, is_package=is_package)
|
|
216
|
+
for node in nodes
|
|
217
|
+
if isinstance(node, ast.ImportFrom) and [alias.name for alias in node.names] == ["*"]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _bound(stmt: ast.stmt) -> Iterator[str]:
|
|
222
|
+
"""Walk a top-level statement other than an import.
|
|
223
|
+
|
|
224
|
+
Yields:
|
|
225
|
+
Each name it binds.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
node: ast.AST
|
|
229
|
+
match stmt:
|
|
230
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
231
|
+
yield stmt.name
|
|
232
|
+
case ast.Assign() | ast.AnnAssign() | ast.AugAssign():
|
|
233
|
+
targets: list[ast.expr] = stmt.targets if isinstance(stmt, ast.Assign) else [stmt.target]
|
|
234
|
+
for node in (n for target in targets for n in ast.walk(target)):
|
|
235
|
+
if isinstance(node, ast.Name):
|
|
236
|
+
yield node.id
|
|
237
|
+
case _:
|
|
238
|
+
pass
|
|
Binary file
|