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,741 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What each checked file offers other files' `--fix`, read once: the cross-file index (`index`).
|
|
3
|
+
|
|
4
|
+
`read` takes a file's module name, its top-level functions' declared return types (as
|
|
5
|
+
`declarations.returns` picks them), its classes' attributes and methods' returns (as `classes` and
|
|
6
|
+
`method_returns` do), its generic classes and type variables, and what each top-level name refers
|
|
7
|
+
to; `constricter.fix.index.project` looks things up in it for each file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import bisect
|
|
12
|
+
import itertools
|
|
13
|
+
import re
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
16
|
+
from dataclasses import dataclass, field, replace
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
20
|
+
|
|
21
|
+
from constricter.empty import Empty
|
|
22
|
+
from constricter.fix.core.imports import taken_names
|
|
23
|
+
from constricter.fix.core.known import Origin, Passed, Returns, Takers, pickled, unpickled
|
|
24
|
+
from constricter.fix.core.signatures import AWAIT
|
|
25
|
+
from constricter.fix.index.origins import guarded_origins, origins_of, skipped, starred
|
|
26
|
+
from constricter.fix.index.params import Param, open_functions
|
|
27
|
+
from constricter.fix.index.stubs.declared import Declarations, Signature, declarations, kinds, overloads
|
|
28
|
+
from constricter.fix.values import classvars, entered, fills
|
|
29
|
+
from constricter.fix.values.bodies import yields_itself
|
|
30
|
+
from constricter.fix.values.returned import held_back, unannotated
|
|
31
|
+
from constricter.fix.values.stored import untouched
|
|
32
|
+
from constricter.overrides import override
|
|
33
|
+
from constricter.rules import parsed
|
|
34
|
+
from constricter.rules.annotations import dotted, is_composite, node_name
|
|
35
|
+
from constricter.rules.declarations import awaited_returns, defined_type_vars, generic_classes
|
|
36
|
+
from constricter.rules.decorators import Held, Pass, is_fixture, passing
|
|
37
|
+
from constricter.rules.keys import key, optional
|
|
38
|
+
from constricter.rules.syntax import child_statements, top_level
|
|
39
|
+
from constricter.rules.tables import Tables, module_tables
|
|
40
|
+
from constricter.rules.walked import classes as walked_classes
|
|
41
|
+
from constricter.rules.walked import of_type
|
|
42
|
+
|
|
43
|
+
_PACKAGE: Final = "__init__"
|
|
44
|
+
_DOT: Final = "."
|
|
45
|
+
_TYPE_ALIAS: Final = "TypeAlias" # the annotation, and the node class of `type X = ...` (Python 3.12+)
|
|
46
|
+
# What a type checker decides an `if` by, taking one arm alone: `sys.version_info`, `TYPE_CHECKING`.
|
|
47
|
+
_DECIDED: Final = frozenset({"version_info", "platform", "TYPE_CHECKING"})
|
|
48
|
+
SUFFIX: Final = ".py"
|
|
49
|
+
CONFTEST: Final = "conftest" # the module pytest reads a directory's fixtures from
|
|
50
|
+
_UNNAMED: Final = frozenset({"__call__", "__enter__"}) # members a statement takes without naming them
|
|
51
|
+
_GET: Final = "get"
|
|
52
|
+
_CLASS: Final = "class" # `project.CLASS`: the kind of name `classes_under` spells, with an alias
|
|
53
|
+
_ANNOTATION: Final = 200 # the longest string read for the names a type in it is written with
|
|
54
|
+
_DOTTED: Final = re.compile(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+")
|
|
55
|
+
STUB: Final = ".pyi"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
59
|
+
class Module: # pylint: disable=too-many-instance-attributes
|
|
60
|
+
"""What one file offers and uses: its name, functions' return types, names' origins, and classes'.
|
|
61
|
+
|
|
62
|
+
`classes` and `methods`: each class's attributes and its methods' returns (see `Classes`), those
|
|
63
|
+
it takes from the file's other classes too (see `Lineage`).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
returns: dict[str, str]
|
|
68
|
+
names: dict[str, Origin]
|
|
69
|
+
classes: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
70
|
+
methods: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
71
|
+
type_vars: frozenset[str] = frozenset() # its module-level type variables
|
|
72
|
+
# What it imports under a top-level `if` or `try` (`if TYPE_CHECKING:`), for `type_vars` alone.
|
|
73
|
+
guarded: Mapping[str, Origin] = field(default_factory=dict[str, Origin])
|
|
74
|
+
# The modules it imports everything from, a top-level `if` or `try`'s too (see `origins.starred`).
|
|
75
|
+
stars: tuple[str, ...] = ()
|
|
76
|
+
unannotated: frozenset[str] = frozenset() # its functions a `return` could type (`returned`)
|
|
77
|
+
# Those a `return` could type but for their decorators, with them spelled: the index may vouch
|
|
78
|
+
# for them (see `decorated.passed`), and they join `unannotated`.
|
|
79
|
+
decorated: Mapping[str, tuple[str, ...]] = field(default_factory=dict[str, tuple[str, ...]])
|
|
80
|
+
called: frozenset[str] = frozenset() # what it calls through its top-level names (`f`, `u.f`)
|
|
81
|
+
passed: frozenset[str] = frozenset() # what it passes as an argument through them (`np.float64`)
|
|
82
|
+
method_calls: frozenset[str] = frozenset() # the methods it calls on anything (`astype` in `a.astype(x)`)
|
|
83
|
+
attributes: frozenset[str] = frozenset() # every attribute it reads or calls, of anything (`x` in `a.x`)
|
|
84
|
+
returned: Returns = field(default_factory=Returns) # what they return, once it's checked
|
|
85
|
+
generics: frozenset[str] = frozenset() # its generic classes, which a type mustn't write bare
|
|
86
|
+
installed: bool = False # an installed package's, read for its types alone (see `installed`)
|
|
87
|
+
# Its plain top-level functions with a parameter left unannotated (see `open_functions`), and
|
|
88
|
+
# what every call passes each such parameter, once they're all seen (see `callers`). Plain
|
|
89
|
+
# `dict`s: the CLI's worker processes send modules back, pickled.
|
|
90
|
+
open: Mapping[str, tuple[Param, ...]] = field(default_factory=dict[str, tuple[Param, ...]])
|
|
91
|
+
# See `Seeds`
|
|
92
|
+
parameters: Mapping[str, Mapping[str, Passed]] = field(
|
|
93
|
+
default_factory=dict[str, Mapping[str, Passed]],
|
|
94
|
+
)
|
|
95
|
+
declared: Declarations | None = None # an installed module's, for its overloads (see `declared`)
|
|
96
|
+
# Its functions declaring a return under decorators other modules may vouch for, each with those
|
|
97
|
+
# Decorators as written, and its own decorators that give a function back (see `decorators`).
|
|
98
|
+
held: Mapping[str, Held] = field(default_factory=dict[str, Held])
|
|
99
|
+
passes: Mapping[str, Pass] = field(default_factory=dict[str, Pass])
|
|
100
|
+
vouched: frozenset[str] = frozenset() # those of `held` they vouched for, now among `returns`
|
|
101
|
+
# Its type aliases, and whether each takes type arguments (see `_aliases`)
|
|
102
|
+
aliases: Mapping[str, bool] = field(default_factory=dict[str, bool])
|
|
103
|
+
# The names its top level assigns twice or more (see `_assigned`): variables, to a type checker.
|
|
104
|
+
rebound: frozenset[str] = frozenset()
|
|
105
|
+
# Its top-level names something in it binds as a value too (a local `m`, under `import pkg.m as m`).
|
|
106
|
+
shadowed: frozenset[str] = frozenset()
|
|
107
|
+
# Its classes' bases as written, their variables typed by their values, and which of them are
|
|
108
|
+
# Plain, once the index settles it (see `constricter.fix.values.classvars`, `plain.settled`).
|
|
109
|
+
bases: Mapping[str, tuple[str, ...]] = field(default_factory=dict[str, tuple[str, ...]])
|
|
110
|
+
# The names each of its classes' bodies binds
|
|
111
|
+
bound: Mapping[str, frozenset[str]] = field(default_factory=dict[str, frozenset[str]])
|
|
112
|
+
members: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
113
|
+
# Its classes' variables bound to a bare name, with that name (`classvars.named`): typed as the
|
|
114
|
+
# Class it names, among `members`, where the index finds one (`plain.settled`).
|
|
115
|
+
named: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
116
|
+
plain: frozenset[str] = frozenset()
|
|
117
|
+
# Its classes' classmethods' and staticmethods' declared returns (see `declarations.class_methods`);
|
|
118
|
+
# Those held back as `held` are, and those of them the index vouched for, now among `sides`.
|
|
119
|
+
sides: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
120
|
+
held_sides: Mapping[str, Mapping[str, Held]] = field(default_factory=dict[str, Mapping[str, Held]])
|
|
121
|
+
vouched_sides: frozenset[tuple[str, str]] = frozenset()
|
|
122
|
+
# Its functions' and classes' methods' declared returns that are tuples with a vague part, which
|
|
123
|
+
# Only an unpacking can use (see `known.Partial`): the methods as `methods` has them.
|
|
124
|
+
partial: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
125
|
+
partial_methods: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
126
|
+
# Its named tuples' fields (see `targets.named_tuples`)
|
|
127
|
+
tuples: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
128
|
+
# Its type aliases of a union (see `targets.aliased_unions`)
|
|
129
|
+
unions: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
130
|
+
# Its top-level pytest fixtures, and whether each is a generator (its value is what it yields).
|
|
131
|
+
fixtures: Mapping[str, bool] = field(default_factory=dict[str, bool])
|
|
132
|
+
# A checked file's functions defined with `@overload`, each with its overloads' signatures.
|
|
133
|
+
overloads: Mapping[str, tuple[Signature, ...]] = field(default_factory=dict[str, tuple[Signature, ...]])
|
|
134
|
+
folder: str = "" # a checked file's directory: where pytest looks for the `conftest.py`s above it
|
|
135
|
+
# Its top-level names bound by assignment to a value: not a type alias, nor a type variable.
|
|
136
|
+
assigned: frozenset[str] = frozenset()
|
|
137
|
+
# Its classes' methods a `return` could type (as `unannotated`), by name, whatever the class.
|
|
138
|
+
loose: frozenset[str] = frozenset()
|
|
139
|
+
# Its classes' methods declared to return a bare `Self` (see `declarations.self_returns`).
|
|
140
|
+
selfish: Mapping[str, frozenset[str]] = field(default_factory=dict[str, frozenset[str]])
|
|
141
|
+
# What awaiting each of its `async def`s' calls gives (`awaited_returns`)
|
|
142
|
+
awaits: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
143
|
+
# The dotted names a checked file writes (`u.helper`, `pkg.m.Row`, in a string too), by their
|
|
144
|
+
# First name: of all a module it imports has, what it can mean (see `written_under`).
|
|
145
|
+
written: Mapping[str, tuple[str, ...]] = field(default_factory=dict[str, tuple[str, ...]])
|
|
146
|
+
# Its top-level functions `@contextmanager` makes managers: what each declares it yields, `""`
|
|
147
|
+
# For one declaring nothing, which its `yield`s may type (see `constricter.fix.values.entered`).
|
|
148
|
+
managers: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
149
|
+
# Its top-level `async def`s declaring no return, which their `return`s may type (see `returned`).
|
|
150
|
+
unawaited: frozenset[str] = frozenset()
|
|
151
|
+
# Its functions that declare a parameter a builtin container (see `constricter.fix.values.fills.takers`).
|
|
152
|
+
takers: Mapping[str, Takers] = field(default_factory=dict[str, Takers])
|
|
153
|
+
# A checked file's type aliases, type variables and protocols, read as a stub's (see `declared.kinds`).
|
|
154
|
+
kinds: Declarations | None = None
|
|
155
|
+
# The attributes its classes bind to an empty container and do no more with (see `stored.untouched`).
|
|
156
|
+
emptied: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
|
|
157
|
+
|
|
158
|
+
@override
|
|
159
|
+
def __getstate__(self) -> dict[str, object]:
|
|
160
|
+
"""Pickle it by its fields (see `pickled`).
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Them.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
return pickled(self)
|
|
167
|
+
|
|
168
|
+
def __setstate__(self, state: dict[str, object]) -> None:
|
|
169
|
+
"""Unpickle it (see `unpickled`)."""
|
|
170
|
+
unpickled(self, state)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def typing(self) -> Declarations | None:
|
|
174
|
+
"""What its names stand for in a signature: an installed module's declarations, or its `kinds`."""
|
|
175
|
+
return self.declared or self.kinds
|
|
176
|
+
|
|
177
|
+
def reads(self, name: str | None) -> bool:
|
|
178
|
+
"""Check whether a signature's name is read by what the module declares it: an alias, a type variable.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Whether it is: any of an installed module's; a checked file's alias, type variable or protocol.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
found: Declarations | None = self.kinds
|
|
185
|
+
return self.installed or (
|
|
186
|
+
found is not None
|
|
187
|
+
and any(name in each for each in (found.aliases, found.variables, found.protocols))
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# The classes a module names by its own imports: each one's spelling, its module and its name
|
|
192
|
+
# there; and the generic ones it spells through a module.
|
|
193
|
+
_Spelled: TypeAlias = tuple[list[tuple[str, str, str]], list[str]]
|
|
194
|
+
_ByClass: TypeAlias = dict[str, frozenset[str]] # each class's methods of a kind, by its name
|
|
195
|
+
_Under: TypeAlias = list[tuple[str, Origin]] # classes' spellings after a package's name, and what each is
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class Memo:
|
|
200
|
+
"""What's worked out once of an index's classes, for every file that asks.
|
|
201
|
+
|
|
202
|
+
Which classes its modules define, what they're named by and their bases don't change as the
|
|
203
|
+
files are checked (what their functions return does): what follows from those alone is kept
|
|
204
|
+
here, by the index that has it and each one made from it with the same modules.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
# A package's and its submodules' classes or aliases: each one's spelling after the package's
|
|
208
|
+
# Name, and what it is, by the package and the kind.
|
|
209
|
+
under: dict[tuple[str, str], _Under] = field(default_factory=dict)
|
|
210
|
+
# What a module names (see `_Spelled`), by its name and folder.
|
|
211
|
+
spelled: dict[tuple[str, str], _Spelled] = field(default_factory=dict)
|
|
212
|
+
# A class's line of bases (see `own_types.lineages`), by its path.
|
|
213
|
+
lines: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
214
|
+
# The members a class declares a type for (attributes, methods' returns), by its module and name.
|
|
215
|
+
members: dict[tuple[str, str], frozenset[str]] = field(default_factory=dict)
|
|
216
|
+
# The methods a module's classes overload, by class (see `own_methods`), by its name and folder.
|
|
217
|
+
overloaded: dict[tuple[str, str], "_ByClass"] = field(default_factory=dict)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Index(NamedTuple):
|
|
221
|
+
"""Every checked file's module, and their names sorted for a module/submodule lookup."""
|
|
222
|
+
|
|
223
|
+
modules: dict[str, Module]
|
|
224
|
+
names: list[str] # modules, sorted by name
|
|
225
|
+
repeated: frozenset[str] = frozenset() # the names more than one file has: `modules` has the last
|
|
226
|
+
# Each checked `conftest.py` outside any package, by its directory: several share the one name.
|
|
227
|
+
conftests: Mapping[str, Module] = Empty()
|
|
228
|
+
memo: Memo | None = None # `None`: nothing is kept (an index made by hand)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@lru_cache(maxsize=65536) # asked of each checked file a dozen times: its folders are looked at once
|
|
232
|
+
def module_name(path: Path) -> str:
|
|
233
|
+
"""Name `path`'s module: its package folders (those with an `__init__.py`), then it.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
The dotted module name.
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
packages: list[Path] = list(
|
|
240
|
+
itertools.takewhile(
|
|
241
|
+
lambda folder: (folder / f"{_PACKAGE}{SUFFIX}").is_file(),
|
|
242
|
+
path.resolve().parents,
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
return ".".join(
|
|
246
|
+
[*(folder.name for folder in reversed(packages)), *([] if path.stem == _PACKAGE else [path.stem])],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def index(paths: Sequence[Path]) -> Index:
|
|
251
|
+
"""Read each `.py` file in `paths` (one that can't be read or parsed is left out).
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Each module's name, mapped to what it offers and uses.
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
return indexed(read(path) for path in paths)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def indexed(found: Iterable[Module | None]) -> Index:
|
|
261
|
+
"""Index modules already read (`read`'s, in any process): `None`s, for files it couldn't, left out.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Each module's name, mapped to what it offers and uses.
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
counts: Counter[str] = Counter()
|
|
268
|
+
modules: dict[str, Module] = {}
|
|
269
|
+
conftests: dict[str, Module] = {}
|
|
270
|
+
module: Module | None
|
|
271
|
+
for module in found:
|
|
272
|
+
if module is not None:
|
|
273
|
+
counts[module.name] += 1
|
|
274
|
+
modules[module.name] = module
|
|
275
|
+
if module.name == CONFTEST and module.folder:
|
|
276
|
+
conftests[module.folder] = module
|
|
277
|
+
repeated: frozenset[str] = frozenset(name for name, count in counts.items() if count > 1)
|
|
278
|
+
return Index(modules, sorted(modules), repeated, conftests, Memo())
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def spliced(catalog: Index, found: Iterable[Module | None]) -> Index:
|
|
282
|
+
"""Put modules read again (`read`'s, of files `--fix` changed) in place of the ones `catalog` has.
|
|
283
|
+
|
|
284
|
+
What the index learned of each from the others (`returned`, `parameters`) is kept; a name several
|
|
285
|
+
files have keeps the module `catalog` has, but in `conftests`. What's kept worked out goes: a
|
|
286
|
+
class's members may now declare their types.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
The index, with them.
|
|
290
|
+
|
|
291
|
+
"""
|
|
292
|
+
modules: dict[str, Module] = dict(catalog.modules)
|
|
293
|
+
conftests: dict[str, Module] = dict(catalog.conftests)
|
|
294
|
+
again: Module
|
|
295
|
+
for again in (each for each in found if each is not None):
|
|
296
|
+
local: bool = again.name == CONFTEST and again.folder in conftests
|
|
297
|
+
before: Module | None
|
|
298
|
+
if (before := conftests[again.folder] if local else modules.get(again.name)) is None:
|
|
299
|
+
continue
|
|
300
|
+
module: Module = replace(again, returned=before.returned, parameters=before.parameters)
|
|
301
|
+
if local:
|
|
302
|
+
conftests[module.folder] = module
|
|
303
|
+
if modules.get(module.name) is before if local else module.name not in catalog.repeated:
|
|
304
|
+
modules[module.name] = module
|
|
305
|
+
return catalog._replace(modules=modules, conftests=conftests, memo=Memo())
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def read(path: Path, name: str | None = None) -> Module | None:
|
|
309
|
+
"""Read what one `.py` file offers and uses.
|
|
310
|
+
|
|
311
|
+
With `name`, an installed package's module (see `installed`): a stub (`.pyi`) too, named that,
|
|
312
|
+
and not kept for a check.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Its module, or `None` if it isn't a `.py` file, or can't be read or parsed.
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
if path.suffix not in ({SUFFIX} if name is None else {SUFFIX, STUB}) or not path.is_file():
|
|
319
|
+
return None
|
|
320
|
+
source: str | None
|
|
321
|
+
if (source := _source(path)) is None:
|
|
322
|
+
return None
|
|
323
|
+
try:
|
|
324
|
+
tree: ast.Module = parsed.parse(source, str(path))
|
|
325
|
+
except (SyntaxError, ValueError): # a null byte is a ValueError
|
|
326
|
+
return None
|
|
327
|
+
own: Tables = module_tables(tree)
|
|
328
|
+
assigned: Counter[str] = _assigned(tree.body)
|
|
329
|
+
rebound: frozenset[str] = frozenset(bound for bound, count in assigned.items() if count > 1)
|
|
330
|
+
if name is None:
|
|
331
|
+
parsed.keep(source, (tree, own)) # for the check to take, rather than parse it and read it again
|
|
332
|
+
named: str = name or module_name(path)
|
|
333
|
+
names: dict[str, Origin] = origins_of(tree, named, is_package=path.stem == _PACKAGE)
|
|
334
|
+
names.update(skipped(tree, names)) # `pa = pytest.importorskip("pyarrow")`, as `import pyarrow as pa`
|
|
335
|
+
return Module(
|
|
336
|
+
name=named,
|
|
337
|
+
returns=own.returns,
|
|
338
|
+
names=names,
|
|
339
|
+
classes=own.classes,
|
|
340
|
+
methods=own.order.flattened(own.methods),
|
|
341
|
+
type_vars=defined_type_vars(tree),
|
|
342
|
+
guarded=guarded_origins(tree, named, is_package=path.stem == _PACKAGE),
|
|
343
|
+
stars=starred(tree, named, is_package=path.stem == _PACKAGE),
|
|
344
|
+
unannotated=unannotated(tree.body, passing(tree, defined_type_vars(tree))),
|
|
345
|
+
decorated=held_back(tree.body),
|
|
346
|
+
called=_called(tree, names),
|
|
347
|
+
generics=generic_classes(tree),
|
|
348
|
+
loose=frozenset[str]().union(*(_loose(node.body) for node in walked_classes(tree))),
|
|
349
|
+
selfish=dict(own.order.selfish),
|
|
350
|
+
awaits=awaited_returns(tree),
|
|
351
|
+
written={} if name is not None else _written(tree),
|
|
352
|
+
passed=frozenset() if name is not None else _passed(tree, names),
|
|
353
|
+
method_calls=frozenset() if name is not None else _method_calls(tree),
|
|
354
|
+
attributes=frozenset() if name is not None else _attributes(tree),
|
|
355
|
+
installed=name is not None,
|
|
356
|
+
open=open_functions(tree),
|
|
357
|
+
declared=None if name is None else declarations(tree),
|
|
358
|
+
held=own.held,
|
|
359
|
+
passes=own.passes,
|
|
360
|
+
aliases={alias: generic for alias, generic in _aliases(tree).items() if alias not in rebound},
|
|
361
|
+
rebound=rebound,
|
|
362
|
+
assigned=frozenset(assigned).difference(_aliases(tree).keys(), defined_type_vars(tree)),
|
|
363
|
+
shadowed=frozenset(names) & taken_names(tree)[1] if name is None else frozenset(),
|
|
364
|
+
bases={} if name is not None else classvars.bases(tree),
|
|
365
|
+
bound=dict(own.order.bound),
|
|
366
|
+
members={} if name is not None else classvars.members(tree),
|
|
367
|
+
named={} if name is not None else classvars.named(tree),
|
|
368
|
+
sides=own.sides,
|
|
369
|
+
held_sides=own.held_sides,
|
|
370
|
+
partial=own.partial,
|
|
371
|
+
partial_methods=own.order.flattened(own.partial_methods),
|
|
372
|
+
tuples=own.tuples,
|
|
373
|
+
unions=own.unions,
|
|
374
|
+
fixtures=_fixtures(tree),
|
|
375
|
+
overloads={} if name is not None else overloads(tree),
|
|
376
|
+
folder="" if name is not None else str(path.resolve().parent),
|
|
377
|
+
managers={} if name is not None else _managers(tree),
|
|
378
|
+
takers={} if name is not None else fills.takers(tree),
|
|
379
|
+
emptied={} if name is not None else untouched(tree),
|
|
380
|
+
kinds=None if name is not None else kinds(tree, _aliases(tree).keys() - rebound),
|
|
381
|
+
unawaited=frozenset(
|
|
382
|
+
stmt.name
|
|
383
|
+
for stmt in tree.body
|
|
384
|
+
if name is None and isinstance(stmt, ast.AsyncFunctionDef) and stmt.returns is None
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _loose(body: Sequence[ast.stmt]) -> frozenset[str]:
|
|
390
|
+
"""Name a class's methods whose `return`s could type their calls, as the files calling them name them.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Each one's name, an `async def`'s after `AWAIT` (see `_attributes`).
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
return unannotated(body) | {
|
|
397
|
+
f"{AWAIT}{stmt.name}"
|
|
398
|
+
for stmt in body
|
|
399
|
+
if isinstance(stmt, ast.AsyncFunctionDef) and stmt.returns is None
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _managers(tree: ast.Module) -> dict[str, str]:
|
|
404
|
+
"""Find a module's top-level functions `@contextmanager` makes context managers.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Each one's name, and what it declares it yields (`""`: nothing, see `entered.undeclared`).
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
declared: dict[str, str] = entered.managers(tree)
|
|
411
|
+
return {
|
|
412
|
+
**dict.fromkeys(entered.undeclared(tree), ""),
|
|
413
|
+
**{name: yielded for name, yielded in declared.items() if _DOT not in name},
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _written(tree: ast.Module) -> dict[str, tuple[str, ...]]:
|
|
418
|
+
"""Find the dotted names a module writes: each attribute chain on a name, and those its strings hold.
|
|
419
|
+
|
|
420
|
+
A string's (an annotation in quotes, a `cast`'s target) and a type comment's count: a type may
|
|
421
|
+
be written in either.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Them, sorted, by their first name; each chain's own beginnings too (`pkg.m`, of `pkg.m.Row`).
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
found: set[str] = set()
|
|
428
|
+
node: ast.AST
|
|
429
|
+
for node in of_type(tree, ast.Attribute):
|
|
430
|
+
text: str | None
|
|
431
|
+
if (text := dotted(cast("ast.Attribute", node))) is not None:
|
|
432
|
+
found.add(text)
|
|
433
|
+
texts: list[str] = [
|
|
434
|
+
node.value
|
|
435
|
+
for node in cast("list[ast.Constant]", of_type(tree, ast.Constant))
|
|
436
|
+
if isinstance(node.value, str) and len(node.value) <= _ANNOTATION
|
|
437
|
+
]
|
|
438
|
+
texts.extend(
|
|
439
|
+
cast("str | None", getattr(node, "type_comment", None)) or ""
|
|
440
|
+
for kind in (ast.FunctionDef, ast.AsyncFunctionDef, ast.Assign, ast.For, ast.With, ast.arg)
|
|
441
|
+
for node in of_type(tree, kind)
|
|
442
|
+
)
|
|
443
|
+
chain: str
|
|
444
|
+
for chain in cast("list[str]", _DOTTED.findall("\n".join(texts))):
|
|
445
|
+
parts: list[str] = chain.split(".")
|
|
446
|
+
found.update(".".join(parts[:count]) for count in range(2, len(parts) + 1))
|
|
447
|
+
grouped: dict[str, list[str]] = {}
|
|
448
|
+
for chain in sorted(found):
|
|
449
|
+
grouped.setdefault(chain.partition(".")[0], []).append(chain)
|
|
450
|
+
return {root: tuple(chains) for root, chains in grouped.items()}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def exported(module: Module) -> Mapping[str, Origin]:
|
|
454
|
+
"""Name what a module imports from others, which a file may import from it in turn.
|
|
455
|
+
|
|
456
|
+
What it imports for type checking alone too: a package's `__init__` that imports its names only
|
|
457
|
+
when they're asked for (pydantic's) has them there.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Each, by its name there.
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
return {**module.guarded, **module.names} if module.guarded else module.names
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def takes(module: Module, name: str) -> bool:
|
|
467
|
+
"""Check whether `module` binds `name` by an import from another module: one to follow there.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Whether it does.
|
|
471
|
+
|
|
472
|
+
"""
|
|
473
|
+
return module.names.get(name, (module.name, None))[0] != module.name
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def written_under(target: Module, local: str) -> Iterator[tuple[str, str, str]]:
|
|
477
|
+
"""Find the names `target` writes under `local`, a module it imports (`u.helper`, `pkg.util.helper`).
|
|
478
|
+
|
|
479
|
+
Yields:
|
|
480
|
+
Each one, the submodule it's written through (`""`, or `.util`), and its last name.
|
|
481
|
+
|
|
482
|
+
"""
|
|
483
|
+
prefix: str = f"{local}."
|
|
484
|
+
chain: str
|
|
485
|
+
for chain in target.written.get(local.partition(".")[0], ()):
|
|
486
|
+
if chain.startswith(prefix):
|
|
487
|
+
sub: str
|
|
488
|
+
name: str
|
|
489
|
+
sub, _, name = chain.removeprefix(local).rpartition(".")
|
|
490
|
+
yield chain, sub, name
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def submodules(catalog: Index, prefix: str) -> Iterator[Module]:
|
|
494
|
+
"""Find the module named `prefix`, and every module dotted under it (`pkg.util` under `pkg`).
|
|
495
|
+
|
|
496
|
+
A module's identifier characters all sort after `.`, so the names in `[prefix, prefix + "/")` are
|
|
497
|
+
exactly `prefix` itself and those starting with `prefix + "."` (`/` is the character after `.`).
|
|
498
|
+
|
|
499
|
+
Yields:
|
|
500
|
+
Each such module, by name.
|
|
501
|
+
|
|
502
|
+
"""
|
|
503
|
+
start: int = bisect.bisect_left(catalog.names, prefix)
|
|
504
|
+
stop: int = bisect.bisect_left(catalog.names, f"{prefix}/")
|
|
505
|
+
name: str
|
|
506
|
+
for name in catalog.names[start:stop]:
|
|
507
|
+
yield catalog.modules[name]
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def classes_under(catalog: Index, local: str, package: str, kind: str) -> Iterator[tuple[str, Origin]]:
|
|
511
|
+
"""Spell the classes (or, by `kind`, the aliases) of `package` and its submodules as a file may name them.
|
|
512
|
+
|
|
513
|
+
One importing it as `local`: every one, written in the file or not, since a type written for it
|
|
514
|
+
may name any (`m.Row`, by `import m`).
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Each spelling (`m.Row`, `pkg.m.Row`), and the module and name it is.
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
found: _Under | None
|
|
521
|
+
if (found := None if catalog.memo is None else catalog.memo.under.get((package, kind))) is None:
|
|
522
|
+
found = [
|
|
523
|
+
(f"{other.name.removeprefix(package)}.{name}", (other.name, name))
|
|
524
|
+
for other in submodules(catalog, package)
|
|
525
|
+
for name in (other.classes if kind == _CLASS else other.aliases)
|
|
526
|
+
]
|
|
527
|
+
if catalog.memo is not None:
|
|
528
|
+
catalog.memo.under[package, kind] = found
|
|
529
|
+
return ((f"{local}{suffix}", origin) for suffix, origin in found)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _fixtures(tree: ast.Module) -> dict[str, bool]:
|
|
533
|
+
"""Find the module's top-level pytest fixtures.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Each one's name, and whether it's a generator.
|
|
537
|
+
|
|
538
|
+
"""
|
|
539
|
+
return {
|
|
540
|
+
stmt.name: yields_itself(stmt)
|
|
541
|
+
for stmt in tree.body
|
|
542
|
+
if isinstance(stmt, ast.FunctionDef) and is_fixture(stmt)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _aliases(tree: ast.Module) -> dict[str, bool]:
|
|
547
|
+
"""Find a module's type aliases, at its top level or under an `if` or `try` there.
|
|
548
|
+
|
|
549
|
+
A name annotated `TypeAlias`, bound to a subscript or a union (`Json = dict[str, "Json"] | str`),
|
|
550
|
+
or a `type` statement's.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Each one's name, and whether it takes type arguments: its value names one of the module's type
|
|
554
|
+
variables, or it has type parameters of its own.
|
|
555
|
+
|
|
556
|
+
"""
|
|
557
|
+
type_vars: frozenset[str] = defined_type_vars(tree)
|
|
558
|
+
found: dict[str, bool] = {}
|
|
559
|
+
stmt: ast.stmt
|
|
560
|
+
name: str
|
|
561
|
+
value: ast.expr
|
|
562
|
+
annotation: ast.expr
|
|
563
|
+
for stmt in top_level(tree.body):
|
|
564
|
+
match stmt:
|
|
565
|
+
case ast.AnnAssign(
|
|
566
|
+
target=ast.Name(id=name),
|
|
567
|
+
annotation=annotation,
|
|
568
|
+
value=ast.expr() as value,
|
|
569
|
+
) if node_name(annotation) == _TYPE_ALIAS:
|
|
570
|
+
found[name] = _names_any(value, type_vars)
|
|
571
|
+
case ast.Assign(targets=[ast.Name(id=name)], value=value) if is_composite(value):
|
|
572
|
+
found[name] = _names_any(value, type_vars)
|
|
573
|
+
case _ if type(stmt).__name__ == _TYPE_ALIAS:
|
|
574
|
+
named: ast.Name = cast("ast.Name", getattr(stmt, "name", None))
|
|
575
|
+
found[named.id] = bool(cast("object", getattr(stmt, "type_params", ())))
|
|
576
|
+
case _:
|
|
577
|
+
pass
|
|
578
|
+
return found
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _assigned(body: Sequence[ast.stmt]) -> Counter[str]:
|
|
582
|
+
"""Count how many times a run of a module's top level may assign each name, as a type checker reads it.
|
|
583
|
+
|
|
584
|
+
Under its `if`s and `try`s too: both arms of an `if`, but one of an `if` a checker decides
|
|
585
|
+
(`sys.version_info >= (3, 10)`, `TYPE_CHECKING`), and never the arm a constant rules out
|
|
586
|
+
(`elif False:`). A name assigned twice is a variable to it, not an alias (`if MYPY: X = A` /
|
|
587
|
+
`else: X = B`).
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Each name's count.
|
|
591
|
+
|
|
592
|
+
"""
|
|
593
|
+
counts: Counter[str] = Counter()
|
|
594
|
+
stmt: ast.stmt
|
|
595
|
+
name: str
|
|
596
|
+
test: ast.expr
|
|
597
|
+
constant: bool
|
|
598
|
+
for stmt in body:
|
|
599
|
+
match stmt:
|
|
600
|
+
case (
|
|
601
|
+
ast.Assign(targets=[ast.Name(id=name)])
|
|
602
|
+
| ast.AnnAssign(target=ast.Name(id=name), value=ast.expr())
|
|
603
|
+
):
|
|
604
|
+
counts[name] += 1
|
|
605
|
+
case ast.If(test=ast.Constant(value=bool() as constant)):
|
|
606
|
+
counts += _assigned(stmt.body if constant else stmt.orelse)
|
|
607
|
+
case ast.If(test=test):
|
|
608
|
+
arms: tuple[Counter[str], Counter[str]] = (_assigned(stmt.body), _assigned(stmt.orelse))
|
|
609
|
+
counts += (arms[0] | arms[1]) if _names_any(test, _DECIDED) else (arms[0] + arms[1])
|
|
610
|
+
case ast.Try() | ast.TryStar():
|
|
611
|
+
counts += _assigned(child_statements(stmt))
|
|
612
|
+
case _:
|
|
613
|
+
pass
|
|
614
|
+
return counts
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _names_any(value: ast.expr, names: frozenset[str]) -> bool:
|
|
618
|
+
return any(node_name(node) in names for node in ast.walk(value))
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _called(tree: ast.Module, names: Mapping[str, Origin]) -> frozenset[str]:
|
|
622
|
+
"""Find what the module calls through its top-level names: `f()`, `u.f()`, `pkg.util.f()`.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Each callee, as written.
|
|
626
|
+
|
|
627
|
+
"""
|
|
628
|
+
callees: Iterator[str | None] = (
|
|
629
|
+
dotted(node.func) for node in cast("list[ast.Call]", of_type(tree, ast.Call))
|
|
630
|
+
)
|
|
631
|
+
return frozenset(callee for callee in callees if callee is not None and callee.partition(".")[0] in names)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _passed(tree: ast.Module, names: Mapping[str, Origin]) -> frozenset[str]:
|
|
635
|
+
"""Find what the module passes as an argument through its top-level names: `np.float64`, `Row`.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Each, as written.
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
arguments: Iterator[str | None] = (
|
|
642
|
+
dotted(argument)
|
|
643
|
+
for node in cast("list[ast.Call]", of_type(tree, ast.Call))
|
|
644
|
+
for argument in (*node.args, *(keyword.value for keyword in node.keywords))
|
|
645
|
+
)
|
|
646
|
+
return frozenset(name for name in arguments if name is not None and name.partition(".")[0] in names)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _method_calls(tree: ast.Module) -> frozenset[str]:
|
|
650
|
+
"""Name the methods the module calls on anything: `astype` in `a.astype(x)`, and a `with`'s `__enter__`.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Them.
|
|
654
|
+
|
|
655
|
+
"""
|
|
656
|
+
entering: bool = any(
|
|
657
|
+
item.optional_vars is not None
|
|
658
|
+
for node in cast("list[ast.With]", of_type(tree, ast.With))
|
|
659
|
+
for item in node.items
|
|
660
|
+
)
|
|
661
|
+
return frozenset(
|
|
662
|
+
node.func.attr
|
|
663
|
+
for node in cast("list[ast.Call]", of_type(tree, ast.Call))
|
|
664
|
+
if isinstance(node.func, ast.Attribute)
|
|
665
|
+
) | (frozenset({entered.ENTER}) if entering else frozenset[str]())
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _attributes(tree: ast.Module) -> frozenset[str]:
|
|
669
|
+
"""Name the attributes the module takes of anything: `x` in `a.x`, `astype` in `a.astype(x)`.
|
|
670
|
+
|
|
671
|
+
And those its statements take without naming them: a call's `__call__`, a `with`'s `__enter__`;
|
|
672
|
+
and the literal keys it reads of anything (`d["x"]`, `d.get("x")`), as a `TypedDict`'s are held
|
|
673
|
+
(see `keys.key`, `keys.optional`), and the methods it awaits a call of, as an `async def`'s are held.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
Them: all of another file's class's members it can use.
|
|
677
|
+
|
|
678
|
+
"""
|
|
679
|
+
named: frozenset[str] = frozenset(
|
|
680
|
+
node.attr for node in cast("list[ast.Attribute]", of_type(tree, ast.Attribute))
|
|
681
|
+
)
|
|
682
|
+
indexes: list[ast.expr] = [
|
|
683
|
+
node.slice for node in cast("list[ast.Subscript]", of_type(tree, ast.Subscript))
|
|
684
|
+
]
|
|
685
|
+
indexes.extend(
|
|
686
|
+
node.args[0]
|
|
687
|
+
for node in cast("list[ast.Call]", of_type(tree, ast.Call))
|
|
688
|
+
if node.args and isinstance(node.func, ast.Attribute) and node.func.attr == _GET
|
|
689
|
+
)
|
|
690
|
+
awaited: frozenset[str] = frozenset(
|
|
691
|
+
AWAIT + node.value.func.attr
|
|
692
|
+
for node in cast("list[ast.Await]", of_type(tree, ast.Await))
|
|
693
|
+
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute)
|
|
694
|
+
)
|
|
695
|
+
keys: frozenset[str] = frozenset(
|
|
696
|
+
spelled(index.value)
|
|
697
|
+
for index in indexes
|
|
698
|
+
if isinstance(index, ast.Constant) and isinstance(index.value, str)
|
|
699
|
+
for spelled in (key, optional)
|
|
700
|
+
)
|
|
701
|
+
return named | keys | awaited | _quoted(tree) | _UNNAMED
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _quoted(tree: ast.Module) -> frozenset[str]:
|
|
705
|
+
"""Name the attributes the module's quoted annotations take: `Study` in `study: "optuna.Study"`.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Them.
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
functions: list[ast.FunctionDef | ast.AsyncFunctionDef] = cast(
|
|
712
|
+
"list[ast.FunctionDef | ast.AsyncFunctionDef]",
|
|
713
|
+
of_type(tree, ast.FunctionDef, ast.AsyncFunctionDef),
|
|
714
|
+
)
|
|
715
|
+
annotations: list[ast.expr | None] = [
|
|
716
|
+
*(node.annotation for node in cast("list[ast.arg]", of_type(tree, ast.arg))),
|
|
717
|
+
*(node.annotation for node in cast("list[ast.AnnAssign]", of_type(tree, ast.AnnAssign))),
|
|
718
|
+
*(node.returns for node in functions),
|
|
719
|
+
]
|
|
720
|
+
return frozenset(
|
|
721
|
+
part
|
|
722
|
+
for annotation in annotations
|
|
723
|
+
if annotation is not None
|
|
724
|
+
for node in ast.walk(annotation)
|
|
725
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str)
|
|
726
|
+
for dotted in cast("list[str]", _DOTTED.findall(node.value))
|
|
727
|
+
for part in dotted.split(".")[1:]
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _source(path: Path) -> str | None:
|
|
732
|
+
"""Read a module's text.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
It, or `None` if it can't be read, or decoded (`SyntaxError`: an unknown encoding).
|
|
736
|
+
|
|
737
|
+
"""
|
|
738
|
+
try:
|
|
739
|
+
return parsed.text(path.read_bytes())
|
|
740
|
+
except (OSError, SyntaxError, ValueError): # UnicodeDecodeError is a ValueError
|
|
741
|
+
return None
|