python-constricter 0.4.1__cp314-cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp314-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp314-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp314-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp314-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp314-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp314-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp314-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp314-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp314-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp314-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp314-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp314-win_amd64.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,425 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Installed packages' declared types, for `--fix` to type calls into them as it does checked files'.
|
|
3
|
+
|
|
4
|
+
`with_installed` adds to the cross-file index the modules the checked files import from installed
|
|
5
|
+
packages that declare their types: a stub package (`numpy-stubs`), a `py.typed` package's stubs
|
|
6
|
+
(`.pyi`) or source, or a lone stub module. Each is found on the search path (`search_path`: the
|
|
7
|
+
running interpreter's, and the active virtual environment's) as the import system would, and read
|
|
8
|
+
as `modules.read` reads a checked file, but never checked or fixed; the modules they import from
|
|
9
|
+
their own package (re-exports) are read too, up to `LIMIT` modules in all. Each module read is
|
|
10
|
+
cached (`cached`) under the user's cache directory, by its file's path, size and modification time
|
|
11
|
+
and constricter's version and reading code: a package upgraded, or a changed constricter, reads it
|
|
12
|
+
again. Each version and reading code has its own part of the cache, swept as it's written to, a day
|
|
13
|
+
apart at most (`swept`): what's unread for a month goes, and what's over its cap.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
import pickle # the cache: written and read by constricter alone, in the user's own directory
|
|
20
|
+
import sys
|
|
21
|
+
import sysconfig
|
|
22
|
+
import tempfile
|
|
23
|
+
import time
|
|
24
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
25
|
+
from dataclasses import replace
|
|
26
|
+
from functools import lru_cache
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Final, cast
|
|
29
|
+
|
|
30
|
+
from constricter import __version__
|
|
31
|
+
from constricter.fix.index.modules import STUB, SUFFIX, Index, Memo, Module, read
|
|
32
|
+
|
|
33
|
+
LIMIT: Final = 2000 # the most installed modules one run reads
|
|
34
|
+
_HOPS: Final = 5 # re-exports followed to a type variable
|
|
35
|
+
_STUBS: Final = "-stubs"
|
|
36
|
+
_TYPED: Final = "py.typed"
|
|
37
|
+
_PACKAGE: Final = "__init__"
|
|
38
|
+
_UNDERSCORE: Final = "_"
|
|
39
|
+
_VIRTUAL_ENV: Final = "VIRTUAL_ENV"
|
|
40
|
+
_STDLIB: Final = sys.stdlib_module_names
|
|
41
|
+
_CACHE_HOME: Final = "XDG_CACHE_HOME"
|
|
42
|
+
_LOCAL_APP_DATA: Final = "LOCALAPPDATA" # Windows'
|
|
43
|
+
_CACHE: Final = "constricter"
|
|
44
|
+
_ENTRIES: Final = "*.pickle"
|
|
45
|
+
_SWEPT: Final = ".swept" # in a build's part of the cache: touched when that build last swept it
|
|
46
|
+
_DAY: Final = 86400.0 # the seconds between a build's sweeps
|
|
47
|
+
_KEPT: Final = 30 * _DAY # the seconds an entry is kept unread, and another build's part unswept
|
|
48
|
+
_CAP: Final = 256 << 20 # the bytes a build's entries may take: past it, the longest unread go
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def search_path() -> tuple[Path, ...]:
|
|
52
|
+
"""List the directories installed packages are found in: the interpreter's, then the active environment's.
|
|
53
|
+
|
|
54
|
+
The environment in `VIRTUAL_ENV`, where the checked code runs, even when constricter runs in
|
|
55
|
+
another (pre-commit's, pipx's).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Them, the standard library's own left out.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
stdlib: set[Path] = {Path(sysconfig.get_paths()[key]).resolve() for key in ("stdlib", "platstdlib")}
|
|
62
|
+
active: str | None = os.environ.get(_VIRTUAL_ENV)
|
|
63
|
+
extra: list[str] = (
|
|
64
|
+
[] if active is None else [str(p) for p in sorted(Path(active).glob("lib/python3*/site-packages"))]
|
|
65
|
+
)
|
|
66
|
+
extra += [] if active is None else [str(Path(active) / "Lib" / "site-packages")] # Windows
|
|
67
|
+
found: dict[Path, None] = {}
|
|
68
|
+
entry: str
|
|
69
|
+
for entry in (*sys.path, *extra):
|
|
70
|
+
path: Path = Path(entry or ".").resolve()
|
|
71
|
+
if path.is_dir() and path not in stdlib:
|
|
72
|
+
found[path] = None
|
|
73
|
+
return tuple(found)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def with_installed(catalog: Index, search: Sequence[Path]) -> Index:
|
|
77
|
+
"""Add the installed modules the checked files import to `catalog` (see the module docstring).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The index, with them.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
wanted: list[str] = _imported(catalog.modules.values()) # grows as it's walked
|
|
84
|
+
found: dict[str, Module] = {}
|
|
85
|
+
seen: set[str] = set(catalog.modules)
|
|
86
|
+
name: str
|
|
87
|
+
for name in wanted:
|
|
88
|
+
if len(found) >= LIMIT:
|
|
89
|
+
break
|
|
90
|
+
path: Path | None
|
|
91
|
+
module: Module | None
|
|
92
|
+
if (
|
|
93
|
+
name in seen
|
|
94
|
+
or (path := locate(name, tuple(search))) is None
|
|
95
|
+
or (module := cached(path, name)) is None
|
|
96
|
+
):
|
|
97
|
+
seen.add(name)
|
|
98
|
+
continue
|
|
99
|
+
seen.add(name)
|
|
100
|
+
found[name] = module
|
|
101
|
+
top: str = name.partition(".")[0].lstrip(_UNDERSCORE)
|
|
102
|
+
# Walked as it grows: the modules this one re-exports from, its package's private twin's
|
|
103
|
+
# too (`pytest`'s, from `_pytest`).
|
|
104
|
+
wanted.extend( # ruff: ignore[loop-iterator-mutation]
|
|
105
|
+
other for other in _imported([module]) if other.partition(".")[0].lstrip(_UNDERSCORE) == top
|
|
106
|
+
)
|
|
107
|
+
if not found:
|
|
108
|
+
return catalog
|
|
109
|
+
every: dict[str, Module] = {**catalog.modules, **found}
|
|
110
|
+
every.update((name, _generics(every, module)) for name, module in found.items())
|
|
111
|
+
# The checked files' repeated names kept; what was worked out of the modules there were, not.
|
|
112
|
+
return catalog._replace(modules=every, names=sorted(every), memo=Memo())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _generics(modules: Mapping[str, Module], module: Module) -> Module:
|
|
116
|
+
"""Drop the classes a subscripted base made look generic whose bases pass no type variable.
|
|
117
|
+
|
|
118
|
+
`class float64(floating[_64Bit])` binds every parameter it inherits: it's written bare.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The module, its `generics` corrected.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
bases: Mapping[str, frozenset[str]] = {} if module.declared is None else module.declared.bases
|
|
125
|
+
bound: frozenset[str] = frozenset(
|
|
126
|
+
name
|
|
127
|
+
for name, names in bases.items()
|
|
128
|
+
if name in module.generics and not any(_is_type_var(modules, module, found) for found in names)
|
|
129
|
+
)
|
|
130
|
+
return replace(module, generics=module.generics - bound) if bound else module
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _is_type_var(modules: Mapping[str, Module], module: Module, name: str, hops: int = _HOPS) -> bool:
|
|
134
|
+
"""Check whether `name` is a type variable in `module`: its own, or one it imports from an indexed module.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Whether it is (unknown: no).
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
if name in module.type_vars or (module.declared is not None and name in module.declared.variables):
|
|
141
|
+
return True
|
|
142
|
+
origin: tuple[str, str | None] | None = module.names.get(name) or module.guarded.get(name)
|
|
143
|
+
onward: Module | None = None if origin is None or origin[1] is None else modules.get(origin[0])
|
|
144
|
+
return (
|
|
145
|
+
hops > 0
|
|
146
|
+
and onward is not None
|
|
147
|
+
and onward is not module
|
|
148
|
+
and origin is not None
|
|
149
|
+
and _is_type_var(modules, onward, origin[1] or "", hops - 1)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def cache_directory() -> Path:
|
|
154
|
+
"""Find where installed modules' reads are cached: `$XDG_CACHE_HOME`, `%LOCALAPPDATA%`, or `~/.cache`.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Its `constricter` directory (not made yet).
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
base: str | None = os.environ.get(_CACHE_HOME) or os.environ.get(_LOCAL_APP_DATA)
|
|
161
|
+
return (Path(base) if base else Path.home() / ".cache") / _CACHE / "installed"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cached(path: Path, name: str) -> Module | None:
|
|
165
|
+
"""Read an installed module (see `modules.read`), from the cache if it's there and still current.
|
|
166
|
+
|
|
167
|
+
A cache that can't be read or written (a read-only home, a corrupt entry) is passed over: the
|
|
168
|
+
module is read from its file.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Its module, or `None` if it can't be read or parsed.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
stat: os.stat_result = path.stat()
|
|
176
|
+
except OSError:
|
|
177
|
+
return None
|
|
178
|
+
key: str = f"{name}\0{path.resolve()}\0{stat.st_size}\0{stat.st_mtime_ns}"
|
|
179
|
+
entry: Path = _own() / f"{hashlib.sha256(key.encode()).hexdigest()}.pickle"
|
|
180
|
+
loaded: Module | None
|
|
181
|
+
if (loaded := _load(entry)) is not None:
|
|
182
|
+
return loaded
|
|
183
|
+
module: Module | None
|
|
184
|
+
if (module := read(path, name)) is not None:
|
|
185
|
+
_store(entry, module)
|
|
186
|
+
return module
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@lru_cache(maxsize=1)
|
|
190
|
+
def _code() -> str:
|
|
191
|
+
"""Fingerprint the code that reads a module and shapes what's cached (`constricter.fix`'s).
|
|
192
|
+
|
|
193
|
+
A development build keeps its version as its modules' fields change: an entry pickled by another
|
|
194
|
+
unpickles with them shifted.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Its sources' digest.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
sources: list[Path] = sorted(Path(__file__).parents[1].rglob(f"*{SUFFIX}"))
|
|
201
|
+
return hashlib.sha256(b"\0".join(source.read_bytes() for source in sources)).hexdigest()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _own() -> Path:
|
|
205
|
+
"""Find this build's part of the cache: another version's or build's entries unpickle wrong.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Its directory, named by the version and `_code` (not made yet).
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
return cache_directory() / hashlib.sha256(f"{__version__}\0{_code()}".encode()).hexdigest()[:16]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _load(entry: Path) -> Module | None:
|
|
215
|
+
"""Read a cache entry, and mark one read after half its keep as read now.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Its module, or `None` if there's none, or it can't be read.
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
with contextlib.suppress(OSError, pickle.UnpicklingError, EOFError, AttributeError, TypeError):
|
|
222
|
+
module: Module | None = _module(cast("object", pickle.loads(entry.read_bytes())))
|
|
223
|
+
if _age(entry, time.time()) > _KEPT / 2:
|
|
224
|
+
os.utime(entry)
|
|
225
|
+
return module
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _age(path: Path, now: float) -> float:
|
|
230
|
+
"""Find how long ago `path` was written or marked read.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The seconds; none for one that can't be read, which is then kept.
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
return now - path.stat().st_mtime
|
|
238
|
+
except OSError:
|
|
239
|
+
return 0.0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _dropped(part: Path) -> None:
|
|
243
|
+
"""Remove a build's part of the cache: its entries, what a stopped write left, and its directory."""
|
|
244
|
+
with contextlib.suppress(OSError):
|
|
245
|
+
each: Path
|
|
246
|
+
for each in (*part.glob(_ENTRIES), *part.glob("*.tmp"), part / _SWEPT):
|
|
247
|
+
each.unlink(missing_ok=True)
|
|
248
|
+
part.rmdir()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@lru_cache(maxsize=4) # once a process for a cache, as it first writes there
|
|
252
|
+
def swept(own: Path) -> None:
|
|
253
|
+
"""Sweep the cache, a day or more after this build last did.
|
|
254
|
+
|
|
255
|
+
The entries no build's part holds (the cache's, before each had one) go, and another build's
|
|
256
|
+
part unswept for `_KEPT`: one still used sweeps, and marks its own. Of this build's entries,
|
|
257
|
+
those unread for `_KEPT`, then the longest unread until the rest fit `_CAP`.
|
|
258
|
+
"""
|
|
259
|
+
now: float = time.time()
|
|
260
|
+
if (own / _SWEPT).exists() and _age(own / _SWEPT, now) < _DAY:
|
|
261
|
+
return
|
|
262
|
+
with contextlib.suppress(OSError):
|
|
263
|
+
(own / _SWEPT).touch()
|
|
264
|
+
other: Path
|
|
265
|
+
for other in own.parent.iterdir():
|
|
266
|
+
if other.match(_ENTRIES):
|
|
267
|
+
other.unlink(missing_ok=True)
|
|
268
|
+
elif other != own and len(other.name) == len(own.name) and _age(other / _SWEPT, now) > _KEPT:
|
|
269
|
+
_dropped(other)
|
|
270
|
+
held: list[tuple[float, int, Path]] = sorted(
|
|
271
|
+
(stat.st_mtime, stat.st_size, entry) for entry in own.glob(_ENTRIES) for stat in (entry.stat(),)
|
|
272
|
+
)
|
|
273
|
+
size: int = sum(taken for _, taken, _ in held)
|
|
274
|
+
written: float
|
|
275
|
+
taken: int
|
|
276
|
+
entry: Path
|
|
277
|
+
for written, taken, entry in held:
|
|
278
|
+
if now - written <= _KEPT and size <= _CAP:
|
|
279
|
+
break
|
|
280
|
+
entry.unlink(missing_ok=True)
|
|
281
|
+
size -= taken
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _module(found: object) -> Module | None:
|
|
285
|
+
return found if isinstance(found, Module) else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _store(entry: Path, module: Module) -> None:
|
|
289
|
+
"""Write a module's read to its cache entry, whole or not at all (another run may be reading it)."""
|
|
290
|
+
with contextlib.suppress(OSError):
|
|
291
|
+
entry.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
handle: int
|
|
293
|
+
temporary: str
|
|
294
|
+
handle, temporary = tempfile.mkstemp(dir=entry.parent, suffix=".tmp")
|
|
295
|
+
os.close(handle)
|
|
296
|
+
_ = Path(temporary).write_bytes(pickle.dumps(module))
|
|
297
|
+
_ = Path(temporary).replace(entry)
|
|
298
|
+
swept(entry.parent)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _imported(found: Iterable[Module]) -> list[str]:
|
|
302
|
+
"""List the modules `found` import from outside the standard library, and those they may name.
|
|
303
|
+
|
|
304
|
+
`from pkg import util` may name the module `pkg.util` as well as something `pkg` defines; a call
|
|
305
|
+
through an imported module (`pkg.util.f()` after `import pkg.util`) names each module on its way.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Their names, in order, each once.
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
names: dict[str, None] = {}
|
|
312
|
+
module: Module
|
|
313
|
+
for module in found:
|
|
314
|
+
origin: tuple[str, str | None]
|
|
315
|
+
for origin in (*module.names.values(), *module.guarded.values()):
|
|
316
|
+
if origin[0].partition(".")[0] not in _STDLIB and origin[0] != module.name:
|
|
317
|
+
names[origin[0]] = None
|
|
318
|
+
if origin[1] is not None:
|
|
319
|
+
names[f"{origin[0]}.{origin[1]}"] = None
|
|
320
|
+
callee: str
|
|
321
|
+
for callee in module.called:
|
|
322
|
+
head: str
|
|
323
|
+
parts: list[str]
|
|
324
|
+
head, *parts = callee.split(".")
|
|
325
|
+
through: tuple[str, str | None] = module.names[head]
|
|
326
|
+
if through[1] is None and through[0].partition(".")[0] not in _STDLIB:
|
|
327
|
+
names.update(
|
|
328
|
+
dict.fromkeys(".".join([through[0], *parts[:end]]) for end in range(1, len(parts))),
|
|
329
|
+
)
|
|
330
|
+
return list(names)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@lru_cache(maxsize=4096)
|
|
334
|
+
def locate(name: str, search: tuple[Path, ...]) -> Path | None:
|
|
335
|
+
"""Find the file an installed module `name` declares its types in, as the import system would.
|
|
336
|
+
|
|
337
|
+
In the first directory of `search` that has its package: its stub package (`pkg-stubs`) first,
|
|
338
|
+
then the package itself if it's typed (`py.typed`), a stub (`.pyi`) before its source; or a lone
|
|
339
|
+
stub module (`mod.pyi`).
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
The file, or `None` if it isn't installed, or doesn't declare its types.
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
top: str
|
|
346
|
+
rest: list[str]
|
|
347
|
+
top, *rest = name.split(".")
|
|
348
|
+
directory: Path
|
|
349
|
+
for directory in search:
|
|
350
|
+
root: Path | None
|
|
351
|
+
stub: bool
|
|
352
|
+
here: bool
|
|
353
|
+
root, stub, here = _package(directory, top)
|
|
354
|
+
if root is not None:
|
|
355
|
+
return _module_file(root.joinpath(*rest))
|
|
356
|
+
if not rest and stub:
|
|
357
|
+
return directory / f"{top}{STUB}"
|
|
358
|
+
if here:
|
|
359
|
+
return None # installed here, untyped: an earlier directory shadows any later one
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def unseen(name: str) -> Module | None:
|
|
364
|
+
"""Read an installed module that declares no types, for its classes' bases alone.
|
|
365
|
+
|
|
366
|
+
Never part of the index: nothing it declares types a call, as no type checker reads it. What
|
|
367
|
+
its classes inherit from still says where a class under one ends (see `constricter.fix.index.beyond`).
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Its module, read from its source as `cached` reads one; `None` for the standard library's, or
|
|
371
|
+
one that isn't installed.
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
if not name or name.partition(".")[0] in _STDLIB:
|
|
375
|
+
return None
|
|
376
|
+
return _unseen(name, _searched(tuple(sys.path), os.environ.get(_VIRTUAL_ENV)))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@lru_cache(maxsize=4)
|
|
380
|
+
def _searched(_path: tuple[str, ...], _active: str | None) -> tuple[Path, ...]: # `search_path`, for both
|
|
381
|
+
return search_path()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@lru_cache(maxsize=1024) # asked for each class under one of its classes
|
|
385
|
+
def _unseen(name: str, search: tuple[Path, ...]) -> Module | None:
|
|
386
|
+
paths: Iterable[Path | None] = (
|
|
387
|
+
_module_file(directory.joinpath(*name.split("."))) for directory in search
|
|
388
|
+
)
|
|
389
|
+
path: Path | None = next((path for path in paths if path is not None), None)
|
|
390
|
+
return None if path is None else cached(path, name)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@lru_cache(maxsize=4096) # asked for each of a package's modules: the same answer for them all
|
|
394
|
+
def _package(directory: Path, top: str) -> tuple[Path | None, bool, bool]:
|
|
395
|
+
"""Find what `directory` has of the top-level package or module `top`.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Where its types are declared (its stub package, else the package itself if it's typed), or
|
|
399
|
+
`None`; whether it has a lone stub module for it; and whether it's installed there at all.
|
|
400
|
+
|
|
401
|
+
"""
|
|
402
|
+
roots: list[Path] = [directory / f"{top}{_STUBS}"]
|
|
403
|
+
if (directory / top / _TYPED).is_file():
|
|
404
|
+
roots.append(directory / top)
|
|
405
|
+
return (
|
|
406
|
+
next((root for root in roots if root.is_dir()), None),
|
|
407
|
+
(directory / f"{top}{STUB}").is_file(),
|
|
408
|
+
(directory / top).is_dir() or (directory / f"{top}{SUFFIX}").is_file(),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _module_file(base: Path) -> Path | None:
|
|
413
|
+
"""Find a module's file under a package's root: a package's `__init__`, or a module, stub first.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
It, or `None`.
|
|
417
|
+
|
|
418
|
+
"""
|
|
419
|
+
candidates: tuple[Path, ...] = (
|
|
420
|
+
base / f"{_PACKAGE}{STUB}",
|
|
421
|
+
base / f"{_PACKAGE}{SUFFIX}",
|
|
422
|
+
base.with_name(f"{base.name}{STUB}"),
|
|
423
|
+
base.with_name(f"{base.name}{SUFFIX}"),
|
|
424
|
+
)
|
|
425
|
+
return next((path for path in candidates if path.is_file()), None)
|
|
Binary file
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a name in a stub refers to, across the installed modules the index holds (`Resolver`).
|
|
3
|
+
|
|
4
|
+
An alias, a class or a type variable, where it's declared, a class's bases in order, and what a
|
|
5
|
+
call's name refers to through its module's imports; `stub_signatures.Reader` reads signatures with
|
|
6
|
+
it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import builtins
|
|
11
|
+
from collections.abc import Iterator, Mapping
|
|
12
|
+
from typing import Final, TypeAlias
|
|
13
|
+
|
|
14
|
+
from constricter.fix.core.known import Origin
|
|
15
|
+
from constricter.fix.index import project
|
|
16
|
+
from constricter.fix.index.modules import Module
|
|
17
|
+
from constricter.fix.index.stubs import classnames
|
|
18
|
+
from constricter.fix.index.stubs.atoms import (
|
|
19
|
+
BUILTINS_MODULE,
|
|
20
|
+
CLASS,
|
|
21
|
+
VAR,
|
|
22
|
+
Atom,
|
|
23
|
+
Scope,
|
|
24
|
+
atom_path,
|
|
25
|
+
parse_text,
|
|
26
|
+
)
|
|
27
|
+
from constricter.fix.index.stubs.declared import Alias, Class, Declarations, Variable
|
|
28
|
+
|
|
29
|
+
ARG: Final = "arg"
|
|
30
|
+
_BUILTINS: Final = frozenset(dir(builtins))
|
|
31
|
+
ALIAS_DEPTH: Final = 20 # aliases followed before giving up
|
|
32
|
+
_UNFOLLOWED: Final = "?" # a lineage's base that can't be followed
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Resolved: TypeAlias = Atom | tuple[Alias, Scope, Origin]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def call_origin(modules: Mapping[str, Module], target: Module, callee: str) -> Origin | None:
|
|
39
|
+
"""Find what a call's name refers to: `f`, `np.empty`, `pkg.util.f` through the module's imports.
|
|
40
|
+
|
|
41
|
+
A package's attribute, re-exported from anywhere (`numpy.empty`), not just what it defines.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Its origin, or `None`.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
head: str
|
|
48
|
+
parts: list[str]
|
|
49
|
+
head, *parts = callee.split(".")
|
|
50
|
+
origin: Origin | None = classnames.as_module(modules, target.names.get(head))
|
|
51
|
+
part: str
|
|
52
|
+
for part in parts:
|
|
53
|
+
if origin is None or origin[1] is not None:
|
|
54
|
+
return None
|
|
55
|
+
submodule: str = f"{origin[0]}.{part}"
|
|
56
|
+
origin = (submodule, None) if submodule in modules else (origin[0], part)
|
|
57
|
+
return origin
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Resolver:
|
|
61
|
+
"""Resolves a stub's names across `modules` (see the module docstring)."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, modules: Mapping[str, Module]) -> None:
|
|
64
|
+
"""Resolve in `modules`."""
|
|
65
|
+
self.modules: Mapping[str, Module] = modules
|
|
66
|
+
|
|
67
|
+
def resolve(self, expr: ast.Name | ast.Attribute, scope: Scope) -> Resolved | None:
|
|
68
|
+
"""Resolve a name an annotation uses: a type parameter, a class, an alias, a type variable.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
It as an atom (`arg`: a parameter an alias's argument binds), or an alias with the scope
|
|
72
|
+
its value is read in; or `None` if it's none of them.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
origin: Origin | None
|
|
76
|
+
if isinstance(expr, ast.Name) and expr.id in scope.params:
|
|
77
|
+
param: str | tuple[ast.expr, Scope] | None = scope.params[expr.id]
|
|
78
|
+
if isinstance(param, tuple):
|
|
79
|
+
return Atom(ARG, name=ast.unparse(param[0]), scope=param[1])
|
|
80
|
+
return Atom(VAR, name=expr.id, bound=param, scope=scope)
|
|
81
|
+
if (origin := self._origin(expr, scope.module)) is None or origin[1] is None:
|
|
82
|
+
return None
|
|
83
|
+
origin = project.canonical_origin(self.modules, origin)
|
|
84
|
+
defining: Module | None = self.modules.get(origin[0])
|
|
85
|
+
if defining is None or not defining.reads(origin[1]):
|
|
86
|
+
return Atom(CLASS, origin=origin)
|
|
87
|
+
return self._declared(defining, origin)
|
|
88
|
+
|
|
89
|
+
def resolved_base(self, base: str, module: Module) -> Resolved | None:
|
|
90
|
+
"""Resolve a base `module`'s class names, as written there (`Base`, `pkg.Base[T]`).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
What it refers to, or `None` for a base that isn't a name.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
parsed: ast.expr = parse_text(base)
|
|
97
|
+
parsed = parsed.value if isinstance(parsed, ast.Subscript) else parsed
|
|
98
|
+
return (
|
|
99
|
+
self.resolve(parsed, Scope(module, {}, module.name))
|
|
100
|
+
if isinstance(parsed, ast.Name | ast.Attribute)
|
|
101
|
+
else None
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def lineage(self, origin: Origin, hops: int) -> Iterator[str]:
|
|
105
|
+
"""Walk an installed class and its ancestors, depth first (see `Methods.lineage`).
|
|
106
|
+
|
|
107
|
+
Yields:
|
|
108
|
+
Where each is defined (`numpy.float64`), then `?` if a base can't be followed.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
yield f"{origin[0]}.{origin[1]}"
|
|
112
|
+
module: Module = self.modules[origin[0]]
|
|
113
|
+
klass: Class | None
|
|
114
|
+
# No class: a checked file's protocol, or one its module defines twice (attrs's `AttrsInstance_`).
|
|
115
|
+
if module.declared is None or (klass := module.declared.classes.get(origin[1] or "")) is None:
|
|
116
|
+
yield _UNFOLLOWED
|
|
117
|
+
return
|
|
118
|
+
base: str
|
|
119
|
+
for base in klass.bases:
|
|
120
|
+
resolved: Resolved | None = self.resolved_base(base, module)
|
|
121
|
+
if not isinstance(resolved, Atom) or resolved.kind != CLASS:
|
|
122
|
+
yield _UNFOLLOWED
|
|
123
|
+
elif resolved.module is None:
|
|
124
|
+
yield atom_path(resolved)
|
|
125
|
+
elif hops < ALIAS_DEPTH:
|
|
126
|
+
yield from self.lineage(resolved.origin, hops + 1)
|
|
127
|
+
else: # a cycle of bases
|
|
128
|
+
yield _UNFOLLOWED
|
|
129
|
+
|
|
130
|
+
def is_class(self, target: Module, name: str) -> bool:
|
|
131
|
+
"""Check whether a dotted name the module writes (`np.float64`) is an installed class or its alias.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Whether it is.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
origin: Origin | None = call_origin(self.modules, target, name)
|
|
138
|
+
resolved: Resolved | None = (
|
|
139
|
+
None
|
|
140
|
+
if origin is None or origin[1] is None
|
|
141
|
+
else self._declared_of(project.canonical_origin(self.modules, origin))
|
|
142
|
+
)
|
|
143
|
+
if resolved is not None and not isinstance(resolved, Atom): # an alias: of a class?
|
|
144
|
+
value: ast.expr = parse_text(resolved[0].value)
|
|
145
|
+
base: ast.expr = value.value if isinstance(value, ast.Subscript) else value
|
|
146
|
+
resolved = self.resolve(base, resolved[1]) if isinstance(base, ast.Name | ast.Attribute) else None
|
|
147
|
+
return isinstance(resolved, Atom) and resolved.kind == CLASS and resolved.module is not None
|
|
148
|
+
|
|
149
|
+
def _declared_of(self, origin: Origin) -> Resolved | None:
|
|
150
|
+
defining: Module | None = self.modules.get(origin[0])
|
|
151
|
+
return None if defining is None or not defining.reads(origin[1]) else self._declared(defining, origin)
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _declared(module: Module, origin: Origin) -> Resolved | None:
|
|
155
|
+
"""Read what an installed module defines under `origin`'s name: an alias, a type variable, a class.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
It, or `None` for anything else (a function, a variable).
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
name: str = origin[1] or ""
|
|
162
|
+
declared: Declarations | None = module.typing
|
|
163
|
+
own: Scope = Scope(module, {}, module.name)
|
|
164
|
+
if declared is not None and name in declared.aliases:
|
|
165
|
+
return declared.aliases[name], own, origin
|
|
166
|
+
variable: Variable | None
|
|
167
|
+
if (variable := None if declared is None else declared.variables.get(name)) is not None:
|
|
168
|
+
return Atom(VAR, name=name, bound=variable.bound, scope=own, constrained=variable.constrained)
|
|
169
|
+
if (
|
|
170
|
+
name in module.classes
|
|
171
|
+
or name in module.generics
|
|
172
|
+
or (declared is not None and name in declared.protocols)
|
|
173
|
+
):
|
|
174
|
+
return Atom(CLASS, origin=origin, module=module)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _origin(self, expr: ast.Name | ast.Attribute, module: Module) -> Origin | None:
|
|
178
|
+
"""Find what a (dotted) name refers to in `module`: an import, its own, or a builtin.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Its origin (a module's: `(name, None)`), or `None`.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(expr, ast.Name):
|
|
185
|
+
declared: Declarations | None = module.typing
|
|
186
|
+
return (
|
|
187
|
+
((module.name, expr.id) if declared is not None and expr.id in declared.aliases else None)
|
|
188
|
+
or module.names.get(expr.id)
|
|
189
|
+
or module.guarded.get(expr.id)
|
|
190
|
+
or ((BUILTINS_MODULE, expr.id) if expr.id in _BUILTINS else None)
|
|
191
|
+
)
|
|
192
|
+
if not isinstance(expr.value, ast.Name | ast.Attribute):
|
|
193
|
+
return None
|
|
194
|
+
base: Origin | None
|
|
195
|
+
if (base := self._origin(expr.value, module)) is None:
|
|
196
|
+
return None
|
|
197
|
+
if base[1] is None: # a module's attribute, or its submodule
|
|
198
|
+
submodule: str = f"{base[0]}.{expr.attr}"
|
|
199
|
+
return (submodule, None) if submodule in self.modules else (base[0], expr.attr)
|
|
200
|
+
return None
|
|
Binary file
|