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,91 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A checked file's classes' methods defined with `@overload`, for the calls their arguments decide.
|
|
3
|
+
|
|
4
|
+
On a receiver typed as the class, as the calling file spells it: read and written as its functions'
|
|
5
|
+
overloads are (see `constricter.fix.index.own_overloads`), each under `Class.method`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Final
|
|
10
|
+
|
|
11
|
+
from constricter.fix.core.known import Guarded
|
|
12
|
+
from constricter.fix.core.signatures import ReadSignature
|
|
13
|
+
from constricter.fix.index import crossed, own_overloads
|
|
14
|
+
from constricter.fix.index.modules import SUFFIX, Index, Module, module_name
|
|
15
|
+
from constricter.fix.index.stubs import stubbed
|
|
16
|
+
from constricter.rules.annotations import roots
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from constricter.fix.index.stubs.declared import Signature
|
|
20
|
+
|
|
21
|
+
_SELF: Final = "Self"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def overloaded(
|
|
25
|
+
catalog: Index,
|
|
26
|
+
path: Path,
|
|
27
|
+
guarded: dict[str, Guarded],
|
|
28
|
+
) -> dict[str, tuple[ReadSignature, ...]]:
|
|
29
|
+
"""Find the overloaded methods the file at `path` calls, of the checked files' classes it names.
|
|
30
|
+
|
|
31
|
+
`guarded` records the names their returns need imported for type checking.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Each one's signatures, by the class as the file writes it and the method (`Frame.get`); none
|
|
35
|
+
for a file `catalog` doesn't have.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
target: Module | None
|
|
39
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
|
|
40
|
+
return {}
|
|
41
|
+
found: dict[str, tuple[ReadSignature, ...]] = {}
|
|
42
|
+
key: str
|
|
43
|
+
defined: tuple[Module, str]
|
|
44
|
+
named: dict[str, tuple[Module, str]] = {
|
|
45
|
+
**dict(crossed.spelled_classes(catalog, target, (target.names, target.attributes), set())),
|
|
46
|
+
**{name: (target, name) for name in target.bases}, # its own
|
|
47
|
+
}
|
|
48
|
+
for key, defined in named.items():
|
|
49
|
+
name: str
|
|
50
|
+
# Those it calls of the class's overloaded ones: few, of the thousands of classes it can name.
|
|
51
|
+
for name in sorted(_methods(catalog, defined[0]).get(defined[1], frozenset()) & target.method_calls):
|
|
52
|
+
entry: str = f"{defined[1]}.{name}"
|
|
53
|
+
written: tuple[Signature, ...] = defined[0].overloads.get(entry, ())
|
|
54
|
+
# One returning `Self` is its receiver's class: not the type the signature writes; one
|
|
55
|
+
# declaring its `self` is for some receivers alone.
|
|
56
|
+
if not written or any(
|
|
57
|
+
each.self_type is not None or _SELF in roots(each.returns or _SELF) for each in written
|
|
58
|
+
):
|
|
59
|
+
continue
|
|
60
|
+
found[f"{key}.{name}"] = own_overloads.spelled(
|
|
61
|
+
catalog,
|
|
62
|
+
target,
|
|
63
|
+
(defined[0], entry),
|
|
64
|
+
stubbed.read_signatures(catalog.modules, defined[0], entry),
|
|
65
|
+
guarded,
|
|
66
|
+
)
|
|
67
|
+
return {entry: read for entry, read in found.items() if read}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _methods(catalog: Index, module: Module) -> dict[str, frozenset[str]]:
|
|
71
|
+
"""Group the methods a checked module's classes define with `@overload` by class, once for the index.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Each class's, by its name (`Frame`, of `Frame.get`); none for an installed module.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
key: tuple[str, str] = (module.name, module.folder)
|
|
78
|
+
found: dict[str, frozenset[str]] | None
|
|
79
|
+
if (found := None if catalog.memo is None else catalog.memo.overloaded.get(key)) is None:
|
|
80
|
+
grouped: dict[str, set[str]] = {}
|
|
81
|
+
entry: str
|
|
82
|
+
for entry in () if module.installed else module.overloads:
|
|
83
|
+
owner: str
|
|
84
|
+
method: str
|
|
85
|
+
owner, _, method = entry.rpartition(".")
|
|
86
|
+
if owner:
|
|
87
|
+
grouped.setdefault(owner, set()).add(method)
|
|
88
|
+
found = {owner: frozenset(methods) for owner, methods in grouped.items()}
|
|
89
|
+
if catalog.memo is not None:
|
|
90
|
+
catalog.memo.overloaded[key] = found
|
|
91
|
+
return found
|
|
Binary file
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A checked file's functions defined with `@overload`, for the calls their arguments decide.
|
|
3
|
+
|
|
4
|
+
Their signatures are read as an installed package's are (see `constricter.fix.index.stubs.stubbed`), but
|
|
5
|
+
for what each returns: a type the calling file writes as it would any other checked file's.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
|
|
10
|
+
from constricter.fix.core.known import SPELLED, Guarded
|
|
11
|
+
from constricter.fix.core.signatures import ReadSignature
|
|
12
|
+
from constricter.fix.index import project
|
|
13
|
+
from constricter.fix.index.modules import Index, Module
|
|
14
|
+
from constricter.fix.index.stubs.declared import Signature
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def written(module: Module, name: str) -> tuple[Signature, ...]:
|
|
18
|
+
"""Find a function's signatures as written: an installed module's, or a checked file's overloads.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Them.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
return module.overloads[name] if module.declared is None else module.declared.signatures[name]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def spelled(
|
|
28
|
+
catalog: Index,
|
|
29
|
+
target: Module,
|
|
30
|
+
defined: tuple[Module, str],
|
|
31
|
+
read: tuple[ReadSignature, ...],
|
|
32
|
+
guarded: dict[str, Guarded] | None,
|
|
33
|
+
) -> tuple[ReadSignature, ...]:
|
|
34
|
+
"""Write a checked file's overloads' returns as `target` can (see `project.spelled_in`).
|
|
35
|
+
|
|
36
|
+
Each is the type itself then (`SPELLED`), not a template: no overload of a checked file's binds
|
|
37
|
+
a type variable.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The signatures; none if a return isn't declared, or `target` can't write it (a type
|
|
41
|
+
variable, a generic class without its arguments, a name it binds otherwise).
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
found: list[ReadSignature] = []
|
|
45
|
+
names: dict[str, Guarded] = dict(guarded or {})
|
|
46
|
+
signature: Signature
|
|
47
|
+
each: ReadSignature
|
|
48
|
+
for signature, each in zip(defined[0].overloads[defined[1]], read, strict=True):
|
|
49
|
+
text: str | None = (
|
|
50
|
+
None
|
|
51
|
+
if signature.returns is None
|
|
52
|
+
else project.spelled_in(catalog, target, defined[0], _unquoted(signature.returns), names)
|
|
53
|
+
)
|
|
54
|
+
if text is None:
|
|
55
|
+
return ()
|
|
56
|
+
found.append(each._replace(returns=f"{SPELLED}{text}"))
|
|
57
|
+
if guarded is not None:
|
|
58
|
+
guarded.update(names)
|
|
59
|
+
return tuple(found)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _unquoted(returns: str) -> str:
|
|
63
|
+
"""Read a return written in quotes (`-> "Frame"`) as the type it names.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Its text; any other return as it is.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
node: ast.expr = ast.parse(returns, mode="eval").body
|
|
70
|
+
return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str) else returns
|
|
Binary file
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A checked file's classes in its overloads' parameters, and where each class a calling file names is from.
|
|
3
|
+
|
|
4
|
+
A parameter typed as nothing but checked files' classes (`frame: DataFrame`), or as an iterable of
|
|
5
|
+
them (`objs: Iterable[DataFrame] | Mapping[K, DataFrame]`), takes an argument by its class's
|
|
6
|
+
lineage: its own path and its bases', through the checked files, as an installed class's is
|
|
7
|
+
matched (see `constricter.fix.index.stubs.stubbed`). `concat([df, df])` then picks the overload taking
|
|
8
|
+
`DataFrame`s, and not the one taking `Series`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import MappingProxyType
|
|
15
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
16
|
+
|
|
17
|
+
from constricter.fix.core.known import Guarded, Origin
|
|
18
|
+
from constricter.fix.core.signatures import OWN, OWN_ELEMENTS, Accepts
|
|
19
|
+
from constricter.fix.index import plain, project
|
|
20
|
+
from constricter.fix.index.modules import SUFFIX, Index, Memo, Module, classes_under, exported, module_name
|
|
21
|
+
from constricter.fix.index.stubs.atoms import BUILTINS_MODULE, CLASS, Atom, Scope
|
|
22
|
+
from constricter.fix.libraries import stdlib
|
|
23
|
+
from constricter.fix.values import classvars
|
|
24
|
+
from constricter.overrides import override
|
|
25
|
+
|
|
26
|
+
_NONE_ATOM: Final = "none"
|
|
27
|
+
_LITERAL_ATOM: Final = "literal"
|
|
28
|
+
_UNFOLLOWED: Final = "?" # a lineage's base that can't be followed
|
|
29
|
+
_DEPTH: Final = 8 # how many classes a line of bases is followed through
|
|
30
|
+
# Generic classes of one argument whose instance a builtin sequence or set argument is, by its elements.
|
|
31
|
+
_ITERABLES: Final = frozenset(
|
|
32
|
+
{"Iterable", "Iterator", "Collection", "Sequence", "MutableSequence", "list", "List"}
|
|
33
|
+
| {"set", "Set", "AbstractSet", "frozenset", "FrozenSet"},
|
|
34
|
+
)
|
|
35
|
+
# Those a builtin sequence or set isn't: they neither take one nor say what its elements are.
|
|
36
|
+
_MAPPINGS: Final = frozenset({"Mapping", "MutableMapping", "dict", "Dict"})
|
|
37
|
+
_Atoms: TypeAlias = Callable[[ast.expr, Scope], Iterator[Atom]]
|
|
38
|
+
_Lines: TypeAlias = Mapping[str, tuple[str, ...]] # each class's line of bases, by how a file names it
|
|
39
|
+
_NO_LINES: Final[_Lines] = MappingProxyType({})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def accept(modules: Mapping[str, Module], atoms: Sequence[Atom], read: _Atoms, found: Accepts) -> None:
|
|
43
|
+
"""Add which checked files' classes a parameter takes to `found`, where it takes nothing else.
|
|
44
|
+
|
|
45
|
+
`atoms`: its annotation's members; `read`: what reads a type argument's. `OWN`: the classes
|
|
46
|
+
(each `module.Class`) an argument must be one of, for a parameter whose every member is one,
|
|
47
|
+
`None` or a builtin class; `OWN_ELEMENTS`: those a builtin sequence's or set's elements must
|
|
48
|
+
be, for one whose every member is an iterable of such classes, a mapping or `None`. Neither
|
|
49
|
+
where a member could take anything else.
|
|
50
|
+
"""
|
|
51
|
+
direct: list[str] | None
|
|
52
|
+
if direct := _classes(modules, atoms):
|
|
53
|
+
found[OWN] = direct
|
|
54
|
+
elements: list[str] = []
|
|
55
|
+
atom: Atom
|
|
56
|
+
for atom in atoms:
|
|
57
|
+
name: str = atom.origin[1] or ""
|
|
58
|
+
if atom.kind == _NONE_ATOM or (atom.kind == CLASS and name in _MAPPINGS):
|
|
59
|
+
continue
|
|
60
|
+
inner: list[str] | None = (
|
|
61
|
+
_classes(modules, list(read(atom.args[0], atom.scope)))
|
|
62
|
+
if atom.kind == CLASS and name in _ITERABLES and len(atom.args) == 1 and atom.scope is not None
|
|
63
|
+
else None
|
|
64
|
+
)
|
|
65
|
+
if not inner:
|
|
66
|
+
return
|
|
67
|
+
elements.extend(inner)
|
|
68
|
+
if elements:
|
|
69
|
+
found[OWN_ELEMENTS] = sorted(set(elements))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _classes(modules: Mapping[str, Module], atoms: Sequence[Atom]) -> list[str] | None:
|
|
73
|
+
"""Name the checked files' classes a union's members are, where each is one, `None` or a builtin class.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Their paths, sorted; `None` where a member is anything else (an alias left unread, a type
|
|
77
|
+
variable, another package's class).
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
found: set[str] = set()
|
|
81
|
+
atom: Atom
|
|
82
|
+
for atom in atoms:
|
|
83
|
+
module: Module | None = modules.get(atom.origin[0])
|
|
84
|
+
if (
|
|
85
|
+
atom.kind == CLASS
|
|
86
|
+
and module is not None
|
|
87
|
+
and not module.installed
|
|
88
|
+
and atom.origin[1] in module.bases
|
|
89
|
+
):
|
|
90
|
+
found.add(f"{atom.origin[0]}.{atom.origin[1]}")
|
|
91
|
+
elif atom.kind not in {_NONE_ATOM, _LITERAL_ATOM} and not (
|
|
92
|
+
atom.kind == CLASS and atom.origin[0] == BUILTINS_MODULE and not atom.args
|
|
93
|
+
):
|
|
94
|
+
return None
|
|
95
|
+
return sorted(found)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def lineages(
|
|
99
|
+
catalog: Index,
|
|
100
|
+
path: Path,
|
|
101
|
+
guarded: Mapping[str, Guarded],
|
|
102
|
+
installed: _Lines = _NO_LINES,
|
|
103
|
+
) -> "Lineages":
|
|
104
|
+
"""Find where each checked file's class the file at `path` names comes from, by how it names it.
|
|
105
|
+
|
|
106
|
+
Its own classes, those it imports (for type checking too, `guarded`), and those of the modules
|
|
107
|
+
it imports; and `installed`, installed packages' classes, which go first.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Each one's path and its bases', through the checked files, `?` last where a base can't be
|
|
111
|
+
followed (another package's, a computed one); none for a file `catalog` doesn't have.
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
target: Module | None
|
|
115
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
|
|
116
|
+
return Lineages(catalog, Named({}, {}, frozenset()), installed)
|
|
117
|
+
own: dict[str, tuple[str, ...]] = {
|
|
118
|
+
name: _lined(catalog, f"{target.name}.{name}") for name in target.bases
|
|
119
|
+
}
|
|
120
|
+
# Those it names for type checking alone, then by its own imports, which the index keeps.
|
|
121
|
+
checking: dict[str, Origin] = {
|
|
122
|
+
name: each.origin for name, each in guarded.items() if name not in target.names
|
|
123
|
+
}
|
|
124
|
+
under: dict[str, str] = {}
|
|
125
|
+
names: Mapping[str, Origin]
|
|
126
|
+
for names in (checking, target.names):
|
|
127
|
+
local: str
|
|
128
|
+
origin: Origin
|
|
129
|
+
for local, origin in project.resolved(catalog, names):
|
|
130
|
+
defined: tuple[Module, str] | None
|
|
131
|
+
if origin[1] is None:
|
|
132
|
+
under[local] = origin[0]
|
|
133
|
+
continue
|
|
134
|
+
defined = None if origin[0] == target.name else project.definition(catalog.modules, origin, CLASS)
|
|
135
|
+
if defined is not None and not defined[0].installed:
|
|
136
|
+
own[local] = _lined(catalog, f"{defined[0].name}.{defined[1]}")
|
|
137
|
+
return Lineages(catalog, Named(own, under, target.attributes), installed)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Named(NamedTuple):
|
|
141
|
+
"""The classes a file names itself, by its own or imported name, and where it can name more.
|
|
142
|
+
|
|
143
|
+
`under`: each module it imports, by how it names it, a class of whose (or of whose submodules')
|
|
144
|
+
it may name by that (`pkg.m.Row`); `attributes`: the attributes it takes of anything, which a
|
|
145
|
+
class its package re-exports must be one of (`pkg.Row`).
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
own: dict[str, tuple[str, ...]]
|
|
149
|
+
under: dict[str, str]
|
|
150
|
+
attributes: frozenset[str]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Lineages(Mapping[str, tuple[str, ...]]):
|
|
154
|
+
"""Each class a file names, by how it names it, and its line of bases (see `lineages`).
|
|
155
|
+
|
|
156
|
+
Those it names itself or imports by name are worked out at once; one of a module it imports
|
|
157
|
+
(`test.support.Foo`, any of a package's) only when it's asked for: a file may name any of
|
|
158
|
+
thousands, and names a few. Two are equal where the file names the same, the index aside.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, catalog: Index, named: Named, installed: Mapping[str, tuple[str, ...]]) -> None:
|
|
162
|
+
"""Keep what `lineages` found, `installed` first, and the index the rest are found in."""
|
|
163
|
+
self.catalog: Index = catalog
|
|
164
|
+
self.named: Named = named
|
|
165
|
+
self.installed: Mapping[str, tuple[str, ...]] = installed
|
|
166
|
+
self.asked: dict[str, tuple[str, ...] | None] = {}
|
|
167
|
+
|
|
168
|
+
@override
|
|
169
|
+
def __getitem__(self, key: str) -> tuple[str, ...]:
|
|
170
|
+
"""Find a class's line of bases, as `lineages` would have.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
It.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
KeyError: the file names no such class.
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
found: tuple[str, ...] | None = self.installed.get(key) or self.named.own.get(key)
|
|
180
|
+
if found is None:
|
|
181
|
+
if key not in self.asked:
|
|
182
|
+
self.asked[key] = self._under(key)
|
|
183
|
+
found = self.asked[key]
|
|
184
|
+
if found is None:
|
|
185
|
+
raise KeyError(key)
|
|
186
|
+
return found
|
|
187
|
+
|
|
188
|
+
def _under(self, key: str) -> tuple[str, ...] | None:
|
|
189
|
+
"""Find a class `key` names through a module the file imports, a checked file's.
|
|
190
|
+
|
|
191
|
+
`local.Row` for one the module defines, `local.m.Row` for one a submodule does, and
|
|
192
|
+
`local.Row` for one it imports from another of its package's, a name the file takes.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Its line of bases, or `None`.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
parts: list[str] = key.split(".")
|
|
199
|
+
cut: int
|
|
200
|
+
for cut in range(len(parts) - 1, 0, -1):
|
|
201
|
+
package: str | None
|
|
202
|
+
if (package := self.named.under.get(".".join(parts[:cut]))) is None:
|
|
203
|
+
continue
|
|
204
|
+
rest: list[str] = parts[cut:]
|
|
205
|
+
modules: Mapping[str, Module] = self.catalog.modules
|
|
206
|
+
defined: tuple[Module, str] | None = project.definition(
|
|
207
|
+
modules,
|
|
208
|
+
(".".join([package, *rest[:-1]]), rest[-1]),
|
|
209
|
+
CLASS,
|
|
210
|
+
1,
|
|
211
|
+
)
|
|
212
|
+
top: Module | None = modules.get(package)
|
|
213
|
+
where: Origin | None = None if top is None or len(rest) != 1 else exported(top).get(rest[0])
|
|
214
|
+
if defined is None and where is not None and where[1] is not None and where[0] != package:
|
|
215
|
+
defined = (
|
|
216
|
+
project.definition(modules, where, CLASS) if rest[0] in self.named.attributes else None
|
|
217
|
+
)
|
|
218
|
+
if defined is None or defined[0].installed:
|
|
219
|
+
return None
|
|
220
|
+
return _lined(self.catalog, f"{defined[0].name}.{defined[1]}")
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
@override
|
|
224
|
+
def __iter__(self) -> Iterator[str]:
|
|
225
|
+
"""Name every class the file can, its own and installed ones first: each one asked for.
|
|
226
|
+
|
|
227
|
+
Yields:
|
|
228
|
+
Each one's name, as the file names it.
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
yield from self.installed
|
|
232
|
+
yield from (name for name in self.named.own if name not in self.installed)
|
|
233
|
+
local: str
|
|
234
|
+
package: str
|
|
235
|
+
for local, package in self.named.under.items():
|
|
236
|
+
top: Module | None = self.catalog.modules.get(package)
|
|
237
|
+
spelled: list[str] = [key for key, _ in classes_under(self.catalog, local, package, CLASS)]
|
|
238
|
+
spelled += [
|
|
239
|
+
f"{local}.{name}"
|
|
240
|
+
for name in (() if top is None else exported(top))
|
|
241
|
+
if name in self.named.attributes
|
|
242
|
+
]
|
|
243
|
+
key: str
|
|
244
|
+
for key in spelled:
|
|
245
|
+
if key not in self.installed and key not in self.named.own and key in self:
|
|
246
|
+
yield key
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
def __len__(self) -> int:
|
|
250
|
+
"""Count the classes the file can name (see `__iter__`).
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
How many.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
return sum(1 for _ in self)
|
|
257
|
+
|
|
258
|
+
@override
|
|
259
|
+
def __eq__(self, other: object) -> bool:
|
|
260
|
+
"""Compare two by what decides them: the classes named at once and the modules the rest are under.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Whether they're equal.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
if isinstance(other, Lineages):
|
|
267
|
+
return (self.named, dict(self.installed)) == (other.named, dict(other.installed))
|
|
268
|
+
return super().__eq__(other)
|
|
269
|
+
|
|
270
|
+
@override
|
|
271
|
+
def __hash__(self) -> int:
|
|
272
|
+
"""Hash it by part of what `__eq__` compares: the modules the file names classes under.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
The hash.
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
return hash((tuple(self.named.under.items()), self.named.attributes))
|
|
279
|
+
|
|
280
|
+
@override
|
|
281
|
+
def __reduce__(self) -> tuple[type[dict[str, tuple[str, ...]]], tuple[dict[str, tuple[str, ...]]]]:
|
|
282
|
+
"""Pickle what's worked out so far, as a `dict`: the index stays behind.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
How.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
asked: dict[str, tuple[str, ...]] = {
|
|
289
|
+
key: line for key, line in self.asked.items() if line is not None
|
|
290
|
+
}
|
|
291
|
+
return dict, ({**asked, **self.named.own, **self.installed},)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _lined(catalog: Index, defined: str) -> tuple[str, ...]:
|
|
295
|
+
"""Follow a checked file's class up its bases (see `_line`), once for the index.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Its path and its bases', each once.
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
memo: Memo | None = catalog.memo
|
|
302
|
+
line: tuple[str, ...] | None
|
|
303
|
+
if (line := None if memo is None else memo.lines.get(defined)) is None:
|
|
304
|
+
line = tuple(dict.fromkeys(_line(catalog.modules, defined, _DEPTH)))
|
|
305
|
+
if memo is not None:
|
|
306
|
+
memo.lines[defined] = line
|
|
307
|
+
return line
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _line(modules: Mapping[str, Module], defined: str, depth: int) -> Iterator[str]:
|
|
311
|
+
"""Follow a checked file's class (`defined`: its dotted path) up its bases.
|
|
312
|
+
|
|
313
|
+
Yields:
|
|
314
|
+
Its path, then its bases' in turn; `?` for a base that isn't a checked file's class or the
|
|
315
|
+
standard library's, or one too deep.
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
module: Module | None = modules.get(defined.rpartition(".")[0])
|
|
319
|
+
name: str = defined.rpartition(".")[2]
|
|
320
|
+
if module is None or module.installed or name not in module.bases:
|
|
321
|
+
if stdlib.held_whole(defined) is None and defined.rpartition(".")[0] != BUILTINS_MODULE:
|
|
322
|
+
yield _UNFOLLOWED
|
|
323
|
+
return
|
|
324
|
+
yield defined
|
|
325
|
+
base: str
|
|
326
|
+
for base in module.bases[name]:
|
|
327
|
+
if base == classvars.SPECIAL or not depth:
|
|
328
|
+
yield _UNFOLLOWED
|
|
329
|
+
else:
|
|
330
|
+
yield from _line(modules, plain.resolved(modules, module, base), depth - 1)
|
|
Binary file
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A module's plain top-level functions with a parameter left unannotated, and their parameters.
|
|
3
|
+
|
|
4
|
+
What `constricter.fix.index.callers` matches each call's arguments to (`Param`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from typing import Final, TypeAlias
|
|
9
|
+
|
|
10
|
+
# A function's parameter: its name, kind (`p` positional only, `e` either, `k` keyword only), and
|
|
11
|
+
# whether it has a default and an annotation.
|
|
12
|
+
Param: TypeAlias = tuple[str, str, bool, bool]
|
|
13
|
+
_POSITIONAL: Final = "p"
|
|
14
|
+
_EITHER: Final = "e"
|
|
15
|
+
_KEYWORD: Final = "k"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def open_functions(tree: ast.Module) -> dict[str, tuple[Param, ...]]:
|
|
19
|
+
"""Find a module's plain top-level functions with a parameter left unannotated.
|
|
20
|
+
|
|
21
|
+
Plain: not decorated (a decorator may change how it's called), without `*args` or `**kwargs`
|
|
22
|
+
(whose arguments can't be matched), and defined once.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Each one's parameters, by its name.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
counts: dict[str, int] = {}
|
|
29
|
+
stmt: ast.stmt
|
|
30
|
+
for stmt in tree.body:
|
|
31
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
32
|
+
counts[stmt.name] = counts.get(stmt.name, 0) + 1
|
|
33
|
+
return {
|
|
34
|
+
stmt.name: _params(stmt.args)
|
|
35
|
+
for stmt in tree.body
|
|
36
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
37
|
+
and counts[stmt.name] == 1
|
|
38
|
+
and _plain(stmt)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _plain(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
43
|
+
"""Check a function is undecorated, without `*args` or `**kwargs`, and has an unannotated parameter.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Whether it is.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
args: ast.arguments = function.args
|
|
50
|
+
return (
|
|
51
|
+
not function.decorator_list
|
|
52
|
+
and args.vararg is None
|
|
53
|
+
and args.kwarg is None
|
|
54
|
+
and any(arg.annotation is None for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _params(args: ast.arguments) -> tuple[Param, ...]:
|
|
59
|
+
"""Read a function's parameters (see `Param`).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Them, in order.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
positional: list[ast.arg] = [*args.posonlyargs, *args.args]
|
|
66
|
+
first_default: int = len(positional) - len(args.defaults)
|
|
67
|
+
return (
|
|
68
|
+
*(
|
|
69
|
+
(
|
|
70
|
+
arg.arg,
|
|
71
|
+
_POSITIONAL if at < len(args.posonlyargs) else _EITHER,
|
|
72
|
+
at >= first_default,
|
|
73
|
+
arg.annotation is not None,
|
|
74
|
+
)
|
|
75
|
+
for at, arg in enumerate(positional)
|
|
76
|
+
),
|
|
77
|
+
*(
|
|
78
|
+
(arg.arg, _KEYWORD, default is not None, arg.annotation is not None)
|
|
79
|
+
for arg, default in zip(args.kwonlyargs, args.kw_defaults, strict=True)
|
|
80
|
+
),
|
|
81
|
+
)
|
|
Binary file
|