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,797 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Cross-module `--fix`: the functions and classes other checked files define.
|
|
3
|
+
|
|
4
|
+
`index` reads every file once (see `constricter.fix.index.modules`). `calls` then gives a file the
|
|
5
|
+
return type of each function it imports (`from m import f`, `import m as a` then `a.f()`), and
|
|
6
|
+
`crossed.imported` those and each imported class's attributes and methods, but only where every
|
|
7
|
+
name in a type means the same thing in the file as where it was written: otherwise the fix would name
|
|
8
|
+
something undefined, or something else. A name the file doesn't bind that way is imported where
|
|
9
|
+
the type is written from under `if TYPE_CHECKING:` (`Guarded`): that import can't make a cycle.
|
|
10
|
+
|
|
11
|
+
What a module's unannotated functions return is known only once it's checked: the CLI checks the
|
|
12
|
+
files in `order.plan`'s order, each after those whose unannotated functions it calls (`needs`), and
|
|
13
|
+
adds each module's (`with_returned`) to the index for the files after it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import builtins
|
|
18
|
+
import sys
|
|
19
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from functools import lru_cache
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
24
|
+
|
|
25
|
+
from constricter.empty import Empty
|
|
26
|
+
from constricter.fix.core.known import Classes, Guarded, Origin, Partial, Returns
|
|
27
|
+
from constricter.fix.index.modules import (
|
|
28
|
+
SUFFIX,
|
|
29
|
+
Index,
|
|
30
|
+
Module,
|
|
31
|
+
classes_under,
|
|
32
|
+
exported,
|
|
33
|
+
index,
|
|
34
|
+
indexed,
|
|
35
|
+
module_name,
|
|
36
|
+
read,
|
|
37
|
+
spliced,
|
|
38
|
+
takes,
|
|
39
|
+
written_under,
|
|
40
|
+
)
|
|
41
|
+
from constricter.fix.index.strings import quoted
|
|
42
|
+
from constricter.rules.annotations import roots
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Imported",
|
|
46
|
+
"Index",
|
|
47
|
+
"Module",
|
|
48
|
+
"index",
|
|
49
|
+
"indexed",
|
|
50
|
+
"module_name",
|
|
51
|
+
"read",
|
|
52
|
+
"spliced",
|
|
53
|
+
] # the index's, re-exported
|
|
54
|
+
|
|
55
|
+
BUILTINS: Final = frozenset(dir(builtins))
|
|
56
|
+
_STDLIB: Final = sys.stdlib_module_names
|
|
57
|
+
_BUILTINS_MODULE: Final = "builtins"
|
|
58
|
+
NO_TYPE: Final = "" # a class variable's type the file reading it can't write
|
|
59
|
+
HOPS: Final = 5 # how many re-exports (`from .util import f` in an `__init__`) to follow
|
|
60
|
+
_DOT: Final = "."
|
|
61
|
+
_UNDERSCORE: Final = "_"
|
|
62
|
+
_FUNCTION: Final = "function"
|
|
63
|
+
DECORATOR: Final = "decorator" # a function that gives back the one it decorates
|
|
64
|
+
CLASS: Final = "class"
|
|
65
|
+
ALIAS: Final = "alias" # a type alias (see `modules.Module.aliases`)
|
|
66
|
+
_TYPES: Final = frozenset({CLASS, ALIAS}) # the kinds a fix may write a name of that the file doesn't
|
|
67
|
+
_REEXPORTED: Final = "re-exported" # in the memo's key after a kind: what a package names of its submodules'
|
|
68
|
+
_TYPE_VAR: Final = "type variable"
|
|
69
|
+
_RETURNED: Final = "returned" # an unannotated function its `return`s type
|
|
70
|
+
UNANNOTATED: Final = "unannotated" # an unannotated function, typed or not
|
|
71
|
+
PARTIAL: Final = "partial" # a function whose declared return only an unpacking can use
|
|
72
|
+
OPEN: Final = "open" # a function with a parameter left unannotated (see `params.open_functions`)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Imported(NamedTuple):
|
|
76
|
+
"""What a file's imports from other checked files offer `--fix`, each keyed as the file spells it."""
|
|
77
|
+
|
|
78
|
+
calls: dict[str, str]
|
|
79
|
+
classes: Classes
|
|
80
|
+
returned: Returns = Returns()
|
|
81
|
+
guarded: Mapping[str, Guarded] = (
|
|
82
|
+
Empty()
|
|
83
|
+
) # names their types need imported to type check (pickled: a `dict`)
|
|
84
|
+
generics: frozenset[str] = frozenset() # its generic classes, as the file spells them
|
|
85
|
+
# Its plain classes' variables typed by their values, by class (see `constricter.fix.values.classvars`).
|
|
86
|
+
members: Mapping[str, Mapping[str, str]] = Empty()
|
|
87
|
+
partial: Partial = Partial() # the returns only an unpacking can use
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _origin(module: Module, name: str) -> Origin | None:
|
|
91
|
+
if name in module.names:
|
|
92
|
+
return module.names[name]
|
|
93
|
+
return (_BUILTINS_MODULE, name) if name in BUILTINS else None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def definition(
|
|
97
|
+
modules: Mapping[str, Module],
|
|
98
|
+
origin: Origin,
|
|
99
|
+
kind: str,
|
|
100
|
+
hops: int = HOPS,
|
|
101
|
+
) -> tuple[Module, str] | None:
|
|
102
|
+
"""Follow `origin` (through re-exports) to the module that defines it as a `kind` (function, class).
|
|
103
|
+
|
|
104
|
+
As `_canonical` does: through what a module imports for type checking alone too, as a package's
|
|
105
|
+
`__init__` does what it imports only when it's asked for (pydantic's `TypeAdapter`).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
That module and the name, or `None`.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
module: Module | None = modules.get(origin[0])
|
|
112
|
+
attribute: str | None = origin[1]
|
|
113
|
+
if module is None or attribute is None or not hops:
|
|
114
|
+
return None
|
|
115
|
+
if attribute in _kind(module, kind):
|
|
116
|
+
return module, attribute
|
|
117
|
+
onward: Origin | None = module.names.get(attribute) or module.guarded.get(attribute)
|
|
118
|
+
return definition(modules, onward, kind, hops - 1) if onward and onward[0] != module.name else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _kind(module: Module, kind: str) -> Iterable[str]:
|
|
122
|
+
"""List what `module` defines of a `kind`: functions (declared or typed by their `return`s), classes...
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Their names.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
found: _Defined | None = _KINDS.get(kind)
|
|
129
|
+
return module.type_vars if found is None else found(module)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_Defined: TypeAlias = Callable[[Module], Iterable[str]] # what a module defines of one kind
|
|
133
|
+
# What a module defines of each kind (see `_kind`): any other kind is its type variables.
|
|
134
|
+
_KINDS: Final[Mapping[str, _Defined]] = {
|
|
135
|
+
_FUNCTION: lambda module: module.returns,
|
|
136
|
+
CLASS: lambda module: module.classes,
|
|
137
|
+
ALIAS: lambda module: module.aliases,
|
|
138
|
+
_RETURNED: lambda module: module.returned.calls,
|
|
139
|
+
UNANNOTATED: lambda module: module.unannotated,
|
|
140
|
+
OPEN: lambda module: module.open,
|
|
141
|
+
PARTIAL: lambda module: module.partial,
|
|
142
|
+
DECORATOR: lambda module: module.passes,
|
|
143
|
+
"signatures": lambda module: module.overloads if module.declared is None else module.declared.signatures,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def type_vars(catalog: Index, path: Path) -> frozenset[str]:
|
|
148
|
+
"""Find the names the file at `path` imports that are type variables where they're defined.
|
|
149
|
+
|
|
150
|
+
Followed through re-exports, as calls are; for the checker to leave alone the file's own
|
|
151
|
+
functions whose declared return mentions one (`def f(x: T) -> T`, with `from ._typing import T`).
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Them, as the file binds them; none for a file `catalog` doesn't have.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
target: Module | None
|
|
158
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
|
|
159
|
+
return frozenset()
|
|
160
|
+
return frozenset(
|
|
161
|
+
local for local in {*target.names, *target.guarded} if is_type_var(catalog.modules, target, local)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def is_type_var(modules: Mapping[str, Module], module: Module, name: str) -> bool:
|
|
166
|
+
"""Check whether `name` is a type variable in `module`: its own, or one it imports from a checked file.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Whether it is.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
origin: Origin | None = module.names.get(name) or module.guarded.get(name)
|
|
173
|
+
return name in module.type_vars or (
|
|
174
|
+
origin is not None and origin[0] != module.name and definition(modules, origin, _TYPE_VAR) is not None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def calls(catalog: Index, path: Path, guarded: dict[str, Guarded] | None = None) -> dict[str, str]:
|
|
179
|
+
"""Return, for the file at `path`, the return type of each function it imports whose type it can name.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Each call's name as written (`helper`, `u.helper`, `pkg.util.helper`), mapped to its type; nothing
|
|
183
|
+
for a file `catalog` doesn't have (a notebook, standard input). Without `guarded` to record the
|
|
184
|
+
names its types need imported for type checking (see `Guarded`), only types needing none.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
return {key: annotation for key, annotation, _ in typed_calls(catalog, path, _FUNCTION, guarded)}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def returned(catalog: Index, path: Path, guarded: dict[str, Guarded] | None = None) -> Returns:
|
|
191
|
+
"""Return, for the file at `path`, what the unannotated functions it imports return, as `calls` does.
|
|
192
|
+
|
|
193
|
+
Only those of modules already checked (`Module.returned`), each with what it rests on if a guess.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Each call's type, and a guessed one's origins; nothing for a file `catalog` doesn't have.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
types: dict[str, str] = {}
|
|
200
|
+
guesses: dict[str, frozenset[str]] = {}
|
|
201
|
+
key: str
|
|
202
|
+
annotation: str
|
|
203
|
+
defined: tuple[Module, str]
|
|
204
|
+
for key, annotation, defined in typed_calls(catalog, path, _RETURNED, guarded):
|
|
205
|
+
types[key] = annotation
|
|
206
|
+
if defined[1] in defined[0].returned.guesses:
|
|
207
|
+
guesses[key] = defined[0].returned.guesses[defined[1]]
|
|
208
|
+
return Returns(types, guesses)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def typed_calls(
|
|
212
|
+
catalog: Index,
|
|
213
|
+
path: Path,
|
|
214
|
+
kind: str,
|
|
215
|
+
guarded: dict[str, Guarded] | None,
|
|
216
|
+
) -> Iterator[tuple[str, str, tuple[Module, str]]]:
|
|
217
|
+
"""Find the functions of a `kind` (declared or `returned`) the file at `path` imports, typed.
|
|
218
|
+
|
|
219
|
+
Yields:
|
|
220
|
+
Each call's name as written, its type, and the module and name that define it, for each whose
|
|
221
|
+
type the file can write (see `_portable_call`).
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
name: str = module_name(path)
|
|
225
|
+
modules: dict[str, Module] = catalog.modules
|
|
226
|
+
target: Module | None
|
|
227
|
+
if path.suffix != SUFFIX or (target := modules.get(name)) is None:
|
|
228
|
+
return
|
|
229
|
+
# Only what it calls, or (declaring its return) passes on (`partial(helper, 1)`): all that reads one.
|
|
230
|
+
wanted: frozenset[str] = target.called | target.passed if kind == _FUNCTION else target.called
|
|
231
|
+
key: str
|
|
232
|
+
origin: Origin
|
|
233
|
+
for key, origin in spellings(catalog, target, kind):
|
|
234
|
+
found: tuple[str, tuple[Module, str]] | None
|
|
235
|
+
if key in wanted and (found := _portable_call(modules, target, origin, kind, guarded)) is not None:
|
|
236
|
+
yield key, *found
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def spellings(catalog: Index, target: Module, kind: str) -> Iterator[tuple[str, Origin]]:
|
|
240
|
+
"""Find what `target` imports that other modules may define as a `kind`, as it spells each call.
|
|
241
|
+
|
|
242
|
+
Yields:
|
|
243
|
+
Each name as written (`helper`, `u.helper`, `pkg.util.helper`), and where it's from: a module's
|
|
244
|
+
function, or a declared one it imports in turn (`pd.array`), only where the file writes it.
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
local: str
|
|
248
|
+
origin: Origin
|
|
249
|
+
for local, origin in resolved(catalog, target.names):
|
|
250
|
+
if origin[1] is not None and origin[0] != target.name:
|
|
251
|
+
yield local, origin
|
|
252
|
+
elif origin[1] is None: # a module: `u.f()`, or `pkg.util.f()` after `import pkg.util`
|
|
253
|
+
if kind in _TYPES: # a fix may write one the file doesn't yet: every one
|
|
254
|
+
yield from classes_under(catalog, local, origin[0], kind)
|
|
255
|
+
yield from _named_under(catalog, local, origin[0], kind)
|
|
256
|
+
continue
|
|
257
|
+
key: str
|
|
258
|
+
sub: str
|
|
259
|
+
function: str
|
|
260
|
+
for key, sub, function in written_under(target, local): # of all it has, those the file calls
|
|
261
|
+
other: Module | None = catalog.modules.get(origin[0] + sub)
|
|
262
|
+
followed: bool = kind == _FUNCTION and other is not None and takes(other, function)
|
|
263
|
+
if other is not None and (followed or function in _kind(other, kind)):
|
|
264
|
+
yield key, (other.name, function)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _named_under(catalog: Index, local: str, package: str, kind: str) -> Iterator[tuple[str, Origin]]:
|
|
268
|
+
"""Spell the classes (or aliases) `package` imports from its submodules, as a file importing it may.
|
|
269
|
+
|
|
270
|
+
`local`: the name the file imports it as.
|
|
271
|
+
|
|
272
|
+
`pd.DataFrame`, by `from pandas.core.api import DataFrame` in `pandas/__init__.py`: the class
|
|
273
|
+
`pandas.core.frame` defines, which `classes_under` spells only by its own module.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Each spelling, and the module that defines it and its name there.
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
key: tuple[str, str] = (package, f"{kind} {_REEXPORTED}")
|
|
280
|
+
found: list[tuple[str, Origin]] | None
|
|
281
|
+
if (found := None if catalog.memo is None else catalog.memo.under.get(key)) is None:
|
|
282
|
+
top: Module | None = catalog.modules.get(package)
|
|
283
|
+
named: Iterable[tuple[str, Origin]] = () if top is None else exported(top).items()
|
|
284
|
+
found = []
|
|
285
|
+
name: str
|
|
286
|
+
origin: Origin
|
|
287
|
+
for name, origin in named:
|
|
288
|
+
defined: tuple[Module, str] | None
|
|
289
|
+
if not origin[0].startswith(package + _DOT):
|
|
290
|
+
continue
|
|
291
|
+
if defined := definition(catalog.modules, origin, kind):
|
|
292
|
+
found.append((f"{_DOT}{name}", (defined[0].name, defined[1])))
|
|
293
|
+
if catalog.memo is not None:
|
|
294
|
+
catalog.memo.under[key] = found
|
|
295
|
+
return ((f"{local}{suffix}", origin) for suffix, origin in found)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def resolved(catalog: Index, names: Mapping[str, Origin]) -> Iterator[tuple[str, Origin]]:
|
|
299
|
+
"""Take each name imported from a package that is its submodule (`from pkg import util`) as that module.
|
|
300
|
+
|
|
301
|
+
Unless the package itself defines the name (a function, class or type).
|
|
302
|
+
|
|
303
|
+
Yields:
|
|
304
|
+
Each name, and what it refers to.
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
local: str
|
|
308
|
+
origin: Origin
|
|
309
|
+
for local, origin in names.items():
|
|
310
|
+
package: Module | None
|
|
311
|
+
submodule: str = f"{origin[0]}.{origin[1]}" if origin[0] else origin[1] or ""
|
|
312
|
+
if (
|
|
313
|
+
origin[1] is not None
|
|
314
|
+
and submodule in catalog.modules
|
|
315
|
+
and not (
|
|
316
|
+
(package := catalog.modules.get(origin[0])) is not None
|
|
317
|
+
and any(origin[1] in _kind(package, kind) for kind in (_FUNCTION, UNANNOTATED, CLASS))
|
|
318
|
+
)
|
|
319
|
+
):
|
|
320
|
+
yield local, (submodule, None)
|
|
321
|
+
else:
|
|
322
|
+
yield local, origin
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _portable_call(
|
|
326
|
+
modules: Mapping[str, Module],
|
|
327
|
+
target: Module,
|
|
328
|
+
origin: Origin,
|
|
329
|
+
kind: str,
|
|
330
|
+
guarded: dict[str, Guarded] | None,
|
|
331
|
+
) -> tuple[str, tuple[Module, str]] | None:
|
|
332
|
+
"""Type a call to `origin`, a function of a `kind`, if `target` can write its type (see `_respelled`).
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Its type, as `target` writes it, and the module and name that define it; or `None`.
|
|
336
|
+
|
|
337
|
+
"""
|
|
338
|
+
defined: tuple[Module, str] | None
|
|
339
|
+
if (defined := definition(modules, origin, kind)) is None:
|
|
340
|
+
return None
|
|
341
|
+
declared: dict[str, Mapping[str, str]] = {_FUNCTION: defined[0].returns, PARTIAL: defined[0].partial}
|
|
342
|
+
annotation: str = declared.get(kind, defined[0].returned.calls)[defined[1]]
|
|
343
|
+
respelled: str | None = _respelled(modules, target, defined[0], annotation, guarded)
|
|
344
|
+
return None if respelled is None else (respelled, defined)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _respelled(
|
|
348
|
+
modules: Mapping[str, Module],
|
|
349
|
+
target: Module,
|
|
350
|
+
defined: Module,
|
|
351
|
+
annotation: str,
|
|
352
|
+
guarded: dict[str, Guarded] | None,
|
|
353
|
+
) -> str | None:
|
|
354
|
+
"""Write a type from module `defined` in `target`.
|
|
355
|
+
|
|
356
|
+
As it is, if every name in it means the same in both; else, with `guarded` to record them in,
|
|
357
|
+
each other name as `target` imports what it refers to (under any name), or by an import to add
|
|
358
|
+
under `if TYPE_CHECKING:` (see `Guarded`), if nothing else in `target` has that name. Never one
|
|
359
|
+
naming a type variable (see `is_type_var`), an alias its module assigns twice (a variable, to a
|
|
360
|
+
type checker), a builtin `target` rebinds, what `defined` doesn't import at its top level (nor
|
|
361
|
+
define), a checked file's generic class without its arguments, or
|
|
362
|
+
in a string left inside it (an `Annotated`'s metadata) what means anything else in `target`.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
The type, or `None`.
|
|
366
|
+
|
|
367
|
+
"""
|
|
368
|
+
names: frozenset[str] = roots(annotation)
|
|
369
|
+
if _refused(modules, target, defined, annotation):
|
|
370
|
+
return None
|
|
371
|
+
if all(_same(target, defined, root) for root in names):
|
|
372
|
+
return annotation
|
|
373
|
+
if guarded is None:
|
|
374
|
+
return None
|
|
375
|
+
renamed: dict[str, str] = {}
|
|
376
|
+
found: dict[str, Guarded] = {}
|
|
377
|
+
root: str
|
|
378
|
+
for root in {root for root in names if not _same(target, defined, root)}:
|
|
379
|
+
origin: Origin | None = _where(defined, root)
|
|
380
|
+
origin = None if origin is None else _public(modules, origin)
|
|
381
|
+
name: str | None
|
|
382
|
+
if (
|
|
383
|
+
origin is None
|
|
384
|
+
or origin[0] == _BUILTINS_MODULE
|
|
385
|
+
or (name := _named(modules, target, origin, root, {**guarded, **found})) is None
|
|
386
|
+
):
|
|
387
|
+
return None
|
|
388
|
+
renamed[root] = name
|
|
389
|
+
needed: Guarded | None
|
|
390
|
+
if (needed := _import_for(target, origin, name, {**found, **guarded})) is not None:
|
|
391
|
+
found[name] = needed
|
|
392
|
+
guarded.update(found)
|
|
393
|
+
return _renamed(annotation, renamed)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def spelled_in(
|
|
397
|
+
catalog: Index,
|
|
398
|
+
target: Module,
|
|
399
|
+
defined: Module,
|
|
400
|
+
annotation: str,
|
|
401
|
+
guarded: dict[str, Guarded],
|
|
402
|
+
) -> str | None:
|
|
403
|
+
"""Write a type from module `defined` in `target` (see `_respelled`).
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
The type, or `None`.
|
|
407
|
+
|
|
408
|
+
"""
|
|
409
|
+
return _respelled(catalog.modules, target, defined, annotation, guarded)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _refused(modules: Mapping[str, Module], target: Module, defined: Module, annotation: str) -> bool:
|
|
413
|
+
"""Check whether a type from module `defined` is one never written in `target` (see `_respelled`).
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Whether it is.
|
|
417
|
+
|
|
418
|
+
"""
|
|
419
|
+
return (
|
|
420
|
+
any(
|
|
421
|
+
is_type_var(modules, defined, root) or _is_rebound(modules, defined, root)
|
|
422
|
+
for root in roots(annotation)
|
|
423
|
+
)
|
|
424
|
+
or _bare(modules, defined, annotation)
|
|
425
|
+
# A name in a string left inside it stays as it is: no import is added for it, and it isn't renamed.
|
|
426
|
+
or any(not _same(target, defined, root) for root in quoted(annotation))
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _import_for(target: Module, origin: Origin, name: str, known: Mapping[str, Guarded]) -> Guarded | None:
|
|
431
|
+
"""Find the import for type checking a type's `name`, as `_named` spells `origin`, takes in `target`.
|
|
432
|
+
|
|
433
|
+
`known`: those other types' names take already.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
It: the file's own (no statement), or one to add; `None` for a name an import it runs binds,
|
|
437
|
+
or one written through a module it imports (`m.Row`).
|
|
438
|
+
|
|
439
|
+
"""
|
|
440
|
+
if name in target.guarded:
|
|
441
|
+
return Guarded(target.guarded[name], None)
|
|
442
|
+
if _DOT in name or name in target.names:
|
|
443
|
+
return None
|
|
444
|
+
return known.get(name) or Guarded(origin, _statement(origin, name))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _is_rebound(modules: Mapping[str, Module], defined: Module, name: str) -> bool:
|
|
448
|
+
"""Check whether `name`, in module `defined`, is one its own module assigns twice (see `Module.rebound`).
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Whether it is: `defined`'s own, or what it imports from an indexed module.
|
|
452
|
+
|
|
453
|
+
"""
|
|
454
|
+
origin: Origin | None = _where(defined, name)
|
|
455
|
+
found: Origin | None = None if origin is None else _canonical(modules, origin)
|
|
456
|
+
return name in defined.rebound or (
|
|
457
|
+
found is not None and found[0] in modules and found[1] in modules[found[0]].rebound
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _bare(modules: Mapping[str, Module], defined: Module, annotation: str) -> bool:
|
|
462
|
+
"""Check whether a type from module `defined` names a checked file's generic class unsubscripted.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Whether it does: the class's arguments are missing.
|
|
466
|
+
|
|
467
|
+
"""
|
|
468
|
+
return any(is_generic(modules, _where(defined, bare)) for bare in _unsubscripted(annotation))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def is_generic(modules: Mapping[str, Module], origin: Origin | None) -> bool:
|
|
472
|
+
"""Check whether `origin` is (through re-exports) an indexed module's generic class.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Whether it is.
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
found: Origin | None = None if origin is None else _canonical(modules, origin)
|
|
479
|
+
return found is not None and found[0] in modules and found[1] in modules[found[0]].generics
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@lru_cache(maxsize=4096)
|
|
483
|
+
def _unsubscripted(annotation: str) -> tuple[str, ...]:
|
|
484
|
+
"""Name the names an annotation writes without type arguments (`Box` in `list[Box]`, not `list`).
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Them, in order.
|
|
488
|
+
|
|
489
|
+
"""
|
|
490
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
491
|
+
subscripted: set[int] = {id(node.value) for node in ast.walk(tree) if isinstance(node, ast.Subscript)}
|
|
492
|
+
return tuple(
|
|
493
|
+
node.id for node in ast.walk(tree) if isinstance(node, ast.Name) and id(node) not in subscripted
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _where(module: Module, name: str) -> Origin | None:
|
|
498
|
+
"""Find what `name` refers to in `module`: at run time, for type checking alone, or a builtin.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Its origin, or `None`.
|
|
502
|
+
|
|
503
|
+
"""
|
|
504
|
+
if name in module.names:
|
|
505
|
+
return module.names[name]
|
|
506
|
+
return module.guarded.get(name) or module.returned.names.get(name) or _origin(module, name)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@dataclass
|
|
510
|
+
class _Memo:
|
|
511
|
+
"""What `_canonical` and `_public` found, for the index they were last asked about.
|
|
512
|
+
|
|
513
|
+
Each file's `imported` asks them about the same origins again (every member of every class it
|
|
514
|
+
imports): with installed packages indexed too, that was most of a run's time.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
modules: Mapping[str, Module] | None = None
|
|
518
|
+
canonical: dict[Origin, Origin] = field(default_factory=dict[Origin, Origin])
|
|
519
|
+
public: dict[Origin, Origin] = field(default_factory=dict[Origin, Origin])
|
|
520
|
+
|
|
521
|
+
def of(self, modules: Mapping[str, Module]) -> "_Memo":
|
|
522
|
+
"""Keep what's found for `modules` alone (an index changes as files are checked).
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
This memo, emptied if it was another index's.
|
|
526
|
+
|
|
527
|
+
"""
|
|
528
|
+
if modules is not self.modules:
|
|
529
|
+
self.modules = modules
|
|
530
|
+
self.canonical = {}
|
|
531
|
+
self.public = {}
|
|
532
|
+
return self
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
_MEMO: Final = _Memo()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def canonical_origin(modules: Mapping[str, Module], origin: Origin) -> Origin:
|
|
539
|
+
"""Follow `origin` through indexed modules' re-exports to where it's defined (see `_canonical`).
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
That origin.
|
|
543
|
+
|
|
544
|
+
"""
|
|
545
|
+
return _canonical(modules, origin)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def public_origin(modules: Mapping[str, Module], origin: Origin) -> Origin:
|
|
549
|
+
"""Find where an installed package's public module re-exports `origin` (see `_public`).
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
That origin, or `origin` itself.
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
return _public(modules, origin)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _canonical(modules: Mapping[str, Module], origin: Origin) -> Origin:
|
|
559
|
+
"""Follow `origin` through checked modules' re-exports to where it's defined.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
That origin, or `origin` itself when it isn't a re-export of a checked module.
|
|
563
|
+
|
|
564
|
+
"""
|
|
565
|
+
found: dict[Origin, Origin] = _MEMO.of(modules).canonical
|
|
566
|
+
if origin not in found:
|
|
567
|
+
found[origin] = _followed(modules, origin, HOPS)
|
|
568
|
+
return found[origin]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _followed(modules: Mapping[str, Module], origin: Origin, hops: int) -> Origin:
|
|
572
|
+
"""Follow `origin` through up to `hops` re-exports (see `_canonical`).
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Where it leads.
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
module: Module | None = modules.get(origin[0])
|
|
579
|
+
if module is None or origin[1] is None or not hops:
|
|
580
|
+
return origin
|
|
581
|
+
onward: Origin | None = (
|
|
582
|
+
module.names.get(origin[1]) or module.guarded.get(origin[1]) or _starred(modules, module, origin[1])
|
|
583
|
+
)
|
|
584
|
+
return _followed(modules, onward, hops - 1) if onward and onward[0] != module.name else origin
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _starred(modules: Mapping[str, Module], module: Module, name: str) -> Origin | None:
|
|
588
|
+
"""Find which module `module` takes `name` from by importing everything from it (`from .main import *`).
|
|
589
|
+
|
|
590
|
+
The last that binds it, as the imports run; a name with a leading underscore no such import takes.
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
The name there, or `None`.
|
|
594
|
+
|
|
595
|
+
"""
|
|
596
|
+
star: str
|
|
597
|
+
for star in reversed(module.stars):
|
|
598
|
+
other: Module | None = modules.get(star)
|
|
599
|
+
if other is not None and not name.startswith("_") and name in exported(other):
|
|
600
|
+
return star, name
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _named(
|
|
605
|
+
modules: Mapping[str, Module],
|
|
606
|
+
target: Module,
|
|
607
|
+
origin: Origin,
|
|
608
|
+
name: str,
|
|
609
|
+
guarded: Mapping[str, Guarded],
|
|
610
|
+
) -> str | None:
|
|
611
|
+
"""Name `origin` in `target`: as an import it has names it (preferring `name`), else `name` if free.
|
|
612
|
+
|
|
613
|
+
An import of the thing itself, else of a module that has it (`core_schema.CoreSchema`, as a type
|
|
614
|
+
checker's hint is written; `pytest.MonkeyPatch`, by the public module `origin` names). A new
|
|
615
|
+
import only from a module certain to resolve: a checked file's, an installed package's public
|
|
616
|
+
one (not `numpy._typing`), or the standard library's (a third-party one the type's file imports
|
|
617
|
+
may not be installed where the type checker runs).
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
The name, or `None` if `target` imports nothing for it and binds `name` to something else, or
|
|
621
|
+
what it's from may not resolve.
|
|
622
|
+
|
|
623
|
+
"""
|
|
624
|
+
wanted: Origin = _canonical(modules, origin)
|
|
625
|
+
known: dict[str, Origin] = {**target.guarded, **{n: g.origin for n, g in guarded.items()}, **target.names}
|
|
626
|
+
matches: list[str] = sorted(
|
|
627
|
+
(local for local, where in known.items() if _canonical(modules, where) == wanted),
|
|
628
|
+
key=lambda local: local != name,
|
|
629
|
+
)
|
|
630
|
+
if matches:
|
|
631
|
+
return matches[0]
|
|
632
|
+
module: Module | None = modules.get(origin[0])
|
|
633
|
+
# The module `origin` names has it when it runs, not for type checking alone.
|
|
634
|
+
runs: bool = module is not None and origin[1] in module.names
|
|
635
|
+
through: str | None = _through(modules, target, wanted) or (
|
|
636
|
+
_through(modules, target, origin) if runs else None
|
|
637
|
+
)
|
|
638
|
+
if through is not None:
|
|
639
|
+
return through
|
|
640
|
+
public: bool = module is not None and not (module.installed and _private(origin[0]))
|
|
641
|
+
resolves: bool = public or origin[0].partition(".")[0] in _STDLIB
|
|
642
|
+
return None if name in known or name in BUILTINS or not resolves else name
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _through(modules: Mapping[str, Module], target: Module, wanted: Origin) -> str | None:
|
|
646
|
+
"""Name `wanted` through the module defining it, if `target` imports that module to run.
|
|
647
|
+
|
|
648
|
+
`m.Row`, after `import pkg.m as m` or `from pkg import m`. Not a module's own name, which
|
|
649
|
+
importing its package needn't bind; nor through a name `target` binds as a value somewhere (a
|
|
650
|
+
local `m`), which isn't the module there.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
The dotted name, or `None` if `target` imports no such module.
|
|
654
|
+
|
|
655
|
+
"""
|
|
656
|
+
if wanted[1] is None or f"{wanted[0]}.{wanted[1]}" in modules:
|
|
657
|
+
return None
|
|
658
|
+
local: str
|
|
659
|
+
where: Origin
|
|
660
|
+
for local, where in target.names.items():
|
|
661
|
+
if local not in target.shadowed and wanted[0] == (
|
|
662
|
+
where[0] if where[1] is None else f"{where[0]}.{where[1]}"
|
|
663
|
+
):
|
|
664
|
+
return f"{local}.{wanted[1]}"
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _public(modules: Mapping[str, Module], origin: Origin) -> Origin:
|
|
669
|
+
"""Find where an installed package's public module re-exports what `origin` names from a private one.
|
|
670
|
+
|
|
671
|
+
`numpy._core.multiarray`'s `ndarray` as `numpy`'s: its shortest public module binding the name to
|
|
672
|
+
the same thing.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
That origin; `origin` itself if it isn't in an installed package's private module, or no
|
|
676
|
+
public one re-exports it.
|
|
677
|
+
|
|
678
|
+
"""
|
|
679
|
+
module: Module | None = modules.get(origin[0])
|
|
680
|
+
if module is None or not module.installed or origin[1] is None or not _private(origin[0]):
|
|
681
|
+
return origin
|
|
682
|
+
found: dict[Origin, Origin] = _MEMO.of(modules).public
|
|
683
|
+
if origin not in found:
|
|
684
|
+
found[origin] = _reexported(modules, origin)
|
|
685
|
+
return found[origin]
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _reexported(modules: Mapping[str, Module], origin: Origin) -> Origin:
|
|
689
|
+
"""Find the shortest public module of an installed package re-exporting `origin` (see `_public`).
|
|
690
|
+
|
|
691
|
+
The package's own, or the one its private twin is for (`pytest`, for `_pytest`).
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Its origin, or `origin` itself if there's none.
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
wanted: Origin = _canonical(modules, origin)
|
|
698
|
+
top: str = origin[0].partition(_DOT)[0].lstrip(_UNDERSCORE)
|
|
699
|
+
found: list[str] = sorted(
|
|
700
|
+
(
|
|
701
|
+
name
|
|
702
|
+
for name, other in modules.items()
|
|
703
|
+
if other.installed
|
|
704
|
+
and name.partition(_DOT)[0].lstrip(_UNDERSCORE) == top
|
|
705
|
+
and not _private(name)
|
|
706
|
+
and origin[1] in other.names
|
|
707
|
+
and (
|
|
708
|
+
other.declared is None
|
|
709
|
+
or other.declared.exports is None
|
|
710
|
+
or origin[1] in other.declared.exports
|
|
711
|
+
)
|
|
712
|
+
and _canonical(modules, other.names[origin[1]]) == wanted
|
|
713
|
+
),
|
|
714
|
+
key=lambda name: (name.count("."), name),
|
|
715
|
+
)
|
|
716
|
+
return (found[0], origin[1]) if found else origin
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _private(module: str) -> bool:
|
|
720
|
+
"""Check whether a module's path has a private part (`numpy._typing`).
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Whether it has.
|
|
724
|
+
|
|
725
|
+
"""
|
|
726
|
+
return any(part.startswith("_") for part in module.split("."))
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _statement(origin: Origin, name: str) -> str:
|
|
730
|
+
"""Write the import that binds `name` to `origin`.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
It: `from m import T`, `from m import T as U`, `import m`, `import m as n`.
|
|
734
|
+
|
|
735
|
+
"""
|
|
736
|
+
module: str
|
|
737
|
+
attribute: str | None
|
|
738
|
+
module, attribute = origin
|
|
739
|
+
if attribute is None:
|
|
740
|
+
return f"import {module}" + ("" if module == name else f" as {name}")
|
|
741
|
+
return f"from {module} import {attribute}" + ("" if attribute == name else f" as {name}")
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _renamed(annotation: str, names: Mapping[str, str]) -> str:
|
|
745
|
+
"""Rename names in an annotation.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
The annotation.
|
|
749
|
+
|
|
750
|
+
"""
|
|
751
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
752
|
+
node: ast.AST
|
|
753
|
+
for node in ast.walk(tree): # a fresh tree: renamed in place
|
|
754
|
+
if isinstance(node, ast.Name):
|
|
755
|
+
node.id = names.get(node.id, node.id)
|
|
756
|
+
return ast.unparse(tree)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _same(target: Module, defined: Module, name: str) -> bool:
|
|
760
|
+
"""Compare what `name` refers to in both modules.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Whether it's something, and the same thing.
|
|
764
|
+
|
|
765
|
+
"""
|
|
766
|
+
origin: Origin | None = _origin(target, name)
|
|
767
|
+
return origin is not None and origin == _origin(defined, name)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def portable(
|
|
771
|
+
modules: Mapping[str, Module],
|
|
772
|
+
where: tuple[Module, tuple[Module, str]],
|
|
773
|
+
key: str,
|
|
774
|
+
types: Mapping[str, str],
|
|
775
|
+
guarded: dict[str, Guarded],
|
|
776
|
+
) -> dict[str, str]:
|
|
777
|
+
"""Keep the types a class's members have that the file (`where[0]`) can write (see `_respelled`).
|
|
778
|
+
|
|
779
|
+
One that is the class itself (`where[1]`) is written as the file spells it (`key`).
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Each kept member's type.
|
|
783
|
+
|
|
784
|
+
"""
|
|
785
|
+
target: Module
|
|
786
|
+
defined: tuple[Module, str]
|
|
787
|
+
target, defined = where
|
|
788
|
+
kept: dict[str, str] = {}
|
|
789
|
+
member: str
|
|
790
|
+
annotation: str
|
|
791
|
+
for member, annotation in types.items():
|
|
792
|
+
respelled: str | None
|
|
793
|
+
if annotation == defined[1]:
|
|
794
|
+
kept[member] = key
|
|
795
|
+
elif (respelled := _respelled(modules, target, defined[0], annotation, guarded)) is not None:
|
|
796
|
+
kept[member] = respelled
|
|
797
|
+
return kept
|