python-constricter 0.4.1__cp311-cp311-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__.cp311-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-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__.cp311-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-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__.cp311-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-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,479 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--fix` for calls to unannotated functions: their own `return` statements decide their type.
|
|
3
|
+
|
|
4
|
+
A function (or method) counts when it's plain (a `def` directly in the module or a class body, not
|
|
5
|
+
decorated or redefined), declares no return type, every `return` it has gives a value whose type
|
|
6
|
+
`--fix` is sure of, all the same, and it can't fall off its end (which returns `None`). A generator
|
|
7
|
+
counts by its `yield`s instead, all of one type `T`: a `Generator[T, None, None]` (see
|
|
8
|
+
`constricter.rules.recorded`). An `async def`'s `return`s type what awaiting its call gives, under
|
|
9
|
+
`AWAIT` before its name; a function `@contextmanager` makes a manager, what `with` gives of it,
|
|
10
|
+
under `entered.ENTERED`. A module function's type is certain; a method's is a guess, since a
|
|
11
|
+
subclass may override it. Each value is typed as the checker sees it there, with the function's own
|
|
12
|
+
locals.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import operator
|
|
17
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
20
|
+
|
|
21
|
+
from constricter.fix.core.imports import bound_within
|
|
22
|
+
from constricter.fix.core.inherited import Lineage
|
|
23
|
+
from constricter.fix.core.known import Returned, Returns
|
|
24
|
+
from constricter.fix.core.signatures import AWAIT
|
|
25
|
+
from constricter.fix.values import entered
|
|
26
|
+
from constricter.fix.values.bodies import is_generator, terminates
|
|
27
|
+
from constricter.fix.values.inference import RETURNED
|
|
28
|
+
from constricter.fix.values.stored import (
|
|
29
|
+
FUNCTIONS,
|
|
30
|
+
Assigned,
|
|
31
|
+
Recorded,
|
|
32
|
+
Used,
|
|
33
|
+
attribute_types,
|
|
34
|
+
class_names,
|
|
35
|
+
owners,
|
|
36
|
+
self_methods,
|
|
37
|
+
subclasses,
|
|
38
|
+
superclasses,
|
|
39
|
+
)
|
|
40
|
+
from constricter.rules.annotations import roots
|
|
41
|
+
from constricter.rules.decorators import FIXTURES, spelled
|
|
42
|
+
from constricter.rules.flow import members
|
|
43
|
+
from constricter.rules.syntax import FunctionDef, Start, within
|
|
44
|
+
from constricter.rules.walked import classes, of_type
|
|
45
|
+
|
|
46
|
+
_NONE: Final = "None"
|
|
47
|
+
_UNVOUCHED: Final[frozenset[str]] = frozenset() # no decorator vouched for: a module read alone
|
|
48
|
+
# What a call calls: a name (`f()`), or an attribute (`x.m()`), the other `None`.
|
|
49
|
+
_Callee: TypeAlias = tuple[str | None, str | None]
|
|
50
|
+
_Call: TypeAlias = tuple[tuple[int, int], _Callee] # where a call starts (line, column), and what it calls
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def returned(
|
|
54
|
+
tree: ast.Module,
|
|
55
|
+
recorded: Mapping[int, Sequence[Recorded]],
|
|
56
|
+
assigned: Mapping[int, Sequence[Assigned]],
|
|
57
|
+
used: Mapping[int, Sequence[Used]],
|
|
58
|
+
vouched: frozenset[str] = _UNVOUCHED,
|
|
59
|
+
) -> Returned:
|
|
60
|
+
"""Read what the module's unannotated functions and methods return, and what its classes assign.
|
|
61
|
+
|
|
62
|
+
`recorded` holds each checked function's `return` statements, `assigned` its `self.x = value`
|
|
63
|
+
assignments and `used` its reads of `self`'s empty containers, by the function's `id()`;
|
|
64
|
+
`vouched` spells the decorators that give a function back (see `constricter.rules.decorators`).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Their return types: the module's functions', and each class's methods'; and each class's
|
|
68
|
+
unannotated instance attributes' types (see `attribute_types`).
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
calls: dict[str, str] = {}
|
|
72
|
+
methods: dict[str, dict[str, str]] = {}
|
|
73
|
+
guesses: dict[str, frozenset[str]] = {}
|
|
74
|
+
name: str
|
|
75
|
+
annotation: str
|
|
76
|
+
origins: frozenset[str]
|
|
77
|
+
for name, annotation, origins in _typed(tree, tree.body, recorded, vouched):
|
|
78
|
+
calls[name] = annotation
|
|
79
|
+
if origins:
|
|
80
|
+
guesses[name] = origins
|
|
81
|
+
attributes: dict[str, dict[str, str]] = {}
|
|
82
|
+
node: ast.ClassDef
|
|
83
|
+
for node in classes(tree):
|
|
84
|
+
for name, annotation, origins in _typed(tree, node.body, recorded, vouched):
|
|
85
|
+
methods.setdefault(node.name, {})[name] = annotation
|
|
86
|
+
if origins:
|
|
87
|
+
guesses[f"{node.name}.{name}"] = origins
|
|
88
|
+
if class_names(tree)[node.name] == 1: # its attributes are looked up by its name
|
|
89
|
+
for name, annotation, origins in attribute_types(tree, node, (assigned, used)):
|
|
90
|
+
attributes.setdefault(node.name, {})[name] = annotation
|
|
91
|
+
guesses[f"{node.name}.{name}"] = origins
|
|
92
|
+
return Returned(calls, methods, guesses, attributes)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def joined(imported: Returns, own: Returned) -> Returned:
|
|
96
|
+
"""Add what the functions a module imports return to what its own return (its own first).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Both.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
if not imported.calls and not imported.methods:
|
|
103
|
+
return own
|
|
104
|
+
return own._replace(
|
|
105
|
+
calls={**imported.calls, **own.calls},
|
|
106
|
+
methods={**imported.methods, **own.methods},
|
|
107
|
+
guesses={**imported.guesses, **own.guesses},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def exported(own: Returned, lineage: Lineage) -> Returns:
|
|
112
|
+
"""Pick out what the module's own functions and its classes' methods return, for the files importing them.
|
|
113
|
+
|
|
114
|
+
A class has the methods it takes from the module's other classes too (see `Lineage`), but one
|
|
115
|
+
whose type names that base, which may be the class itself (`return self`).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Their types, and their guesses' origins (a method's as `C.m`).
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
methods: dict[str, dict[str, str]] = {owner: dict(typed) for owner, typed in own.methods.items() if typed}
|
|
122
|
+
guesses: dict[str, frozenset[str]] = {
|
|
123
|
+
name: own.guesses[name]
|
|
124
|
+
for name in (*own.calls, *(f"{owner}.{name}" for owner, typed in methods.items() for name in typed))
|
|
125
|
+
if name in own.guesses
|
|
126
|
+
}
|
|
127
|
+
owner: str
|
|
128
|
+
bases: tuple[str, ...]
|
|
129
|
+
for owner, bases in lineage.order.items():
|
|
130
|
+
base: str
|
|
131
|
+
name: str
|
|
132
|
+
annotation: str
|
|
133
|
+
for base in bases:
|
|
134
|
+
for name, annotation in own.methods.get(base, {}).items():
|
|
135
|
+
if lineage.definer(owner, name) == base and base not in roots(annotation):
|
|
136
|
+
methods.setdefault(owner, {})[name] = annotation
|
|
137
|
+
if f"{base}.{name}" in own.guesses:
|
|
138
|
+
guesses[f"{owner}.{name}"] = own.guesses[f"{base}.{name}"]
|
|
139
|
+
return Returns(dict(own.calls), guesses, methods=methods)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def unannotated(body: Sequence[ast.stmt], vouched: frozenset[str] = _UNVOUCHED) -> frozenset[str]:
|
|
143
|
+
"""Name the functions in `body` whose `return`s could type their calls (see `_return_type`).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Each plain function's name that has no declared return, nor a decorator but those `vouched`
|
|
147
|
+
spells (see `constricter.rules.decorators`).
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
return frozenset(
|
|
151
|
+
func.name
|
|
152
|
+
for func in _plain(body)
|
|
153
|
+
if isinstance(func, ast.FunctionDef) and not _decorated(func, vouched) and func.returns is None
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def held_back(body: Sequence[ast.stmt]) -> dict[str, tuple[str, ...]]:
|
|
158
|
+
"""Name the functions in `body` whose `return`s could type their calls, but for their decorators.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Each plain function's name that has no declared return, with its decorators as spelled: another
|
|
162
|
+
module may vouch for them (see `constricter.fix.index.decorated`). Not one with a decorator
|
|
163
|
+
that can't be spelled.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
found: dict[str, tuple[str, ...]] = {}
|
|
167
|
+
func: FunctionDef
|
|
168
|
+
for func in _plain(body):
|
|
169
|
+
if isinstance(func, ast.FunctionDef) and func.returns is None and _decorated(func, _UNVOUCHED):
|
|
170
|
+
spellings: list[str | None] = [spelled(each) for each in func.decorator_list]
|
|
171
|
+
if None not in spellings:
|
|
172
|
+
found[func.name] = tuple(text for text in spellings if text is not None)
|
|
173
|
+
return found
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def called(module: ast.Module, node: ast.AST, found: Returned) -> bool:
|
|
177
|
+
"""Check whether `node` (the module, a function, or a statement in it) calls any function `found` types.
|
|
178
|
+
|
|
179
|
+
By name, or as a method. A function's decorators aren't its own calls: they're before it.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Whether it does: otherwise checking it again would change nothing.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
methods: set[str] = {_called(name) for methods in found.methods.values() for name in methods}
|
|
186
|
+
calls: set[str] = {_called(name) for name in found.calls}
|
|
187
|
+
return any(name in calls or attr in methods for name, attr in callees_in(module, node))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _called(key: str) -> str: # a table's entry's function's name (see `_key`)
|
|
191
|
+
return key.removeprefix(entered.ENTERED).removeprefix(AWAIT)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def retyped(before: Returned, after: Returned) -> set[str]:
|
|
195
|
+
"""Name the attributes, of any class, that `after` types and `before` didn't, or typed otherwise.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Their names.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
return {
|
|
202
|
+
attr
|
|
203
|
+
for owner, attributes in after.attributes.items()
|
|
204
|
+
for attr, annotation in attributes.items()
|
|
205
|
+
if before.attributes.get(owner, {}).get(attr) != annotation
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def callees_in(module: ast.Module, node: ast.AST) -> Sequence[_Callee]:
|
|
210
|
+
"""Find what `node` (the module, a function, or a statement in it) calls, by its span of the source.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Each call's callee: a name (`f()`) or an attribute (`x.m()`).
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
starts: list[Start]
|
|
217
|
+
callees: list[_Callee]
|
|
218
|
+
starts, callees = _calls(module)
|
|
219
|
+
return callees[within(starts, node)]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Slot(NamedTuple):
|
|
223
|
+
"""Where a function's return type goes in the table: its class (`None`: the module's), and its name."""
|
|
224
|
+
|
|
225
|
+
owner: str | None
|
|
226
|
+
name: str
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@lru_cache(maxsize=4)
|
|
230
|
+
def slots(module: ast.Module) -> dict[int, Slot]:
|
|
231
|
+
"""Find the functions whose `return`s can type their calls: plain, directly in the module or a class.
|
|
232
|
+
|
|
233
|
+
As `_typed` finds them (see `_plain`).
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Each one's slot, by the function's `id()`.
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
return {
|
|
240
|
+
id(func): Slot(owner, _key(func))
|
|
241
|
+
for owner, body in ((None, module.body), *((node.name, node.body) for node in classes(module)))
|
|
242
|
+
for func in _plain(body)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _key(func: FunctionDef) -> str:
|
|
247
|
+
"""Name a function's entry in the tables: its name, after what says how its call is read.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
It: after `AWAIT` for an `async def`, `entered.ENTERED` for a manager (see the module docstring).
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
if isinstance(func, ast.AsyncFunctionDef):
|
|
254
|
+
return f"{AWAIT}{func.name}"
|
|
255
|
+
return f"{entered.ENTERED}{func.name}" if entered.is_manager(func) else func.name
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _decorated(func: FunctionDef, vouched: frozenset[str]) -> bool:
|
|
259
|
+
"""Check whether a function has a decorator that may change what calling it gives.
|
|
260
|
+
|
|
261
|
+
Not a pytest fixture's: the fixture's value is what the function returns (or yields). Nor one
|
|
262
|
+
`vouched` spells, which gives the function back.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Whether it has.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
return any(spelled(each) not in FIXTURES and spelled(each) not in vouched for each in func.decorator_list)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _plain(body: Sequence[ast.stmt]) -> list[FunctionDef]:
|
|
272
|
+
"""Find the plain functions directly in `body`: a `def` or `async def` no other there shares a name with.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Them, in source order.
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
counts: dict[str, int] = {}
|
|
279
|
+
stmt: ast.stmt
|
|
280
|
+
for stmt in body:
|
|
281
|
+
if isinstance(stmt, FUNCTIONS):
|
|
282
|
+
counts[stmt.name] = counts.get(stmt.name, 0) + 1
|
|
283
|
+
return [stmt for stmt in body if isinstance(stmt, FUNCTIONS) and counts[stmt.name] == 1]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class _Tables(NamedTuple):
|
|
287
|
+
"""The live tables a `Table` fills in, as `Returned` holds them."""
|
|
288
|
+
|
|
289
|
+
calls: dict[str, str]
|
|
290
|
+
methods: dict[str, dict[str, str]]
|
|
291
|
+
guesses: dict[str, frozenset[str]]
|
|
292
|
+
attributes: dict[str, dict[str, str]]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class Table:
|
|
296
|
+
"""What the module's unannotated functions return, filled in as each is checked, in call order.
|
|
297
|
+
|
|
298
|
+
`returned` is the live table, for the check to read as it goes. Each function's stamp is how
|
|
299
|
+
many entries the table had when it was checked: one checked before a callee of its was typed
|
|
300
|
+
(in a cycle) is `stale`, and checked again. A class's attributes are typed once all its methods
|
|
301
|
+
are checked: what reads them later knows them the first time.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
module: ast.Module,
|
|
307
|
+
imported: Returns | None = None,
|
|
308
|
+
vouched: frozenset[str] = _UNVOUCHED,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Start a table for `module`, with what the functions it imports from other files return.
|
|
311
|
+
|
|
312
|
+
`vouched` spells the decorators that give a function back (see `constricter.rules.decorators`).
|
|
313
|
+
"""
|
|
314
|
+
imported = imported or Returns()
|
|
315
|
+
self.module: ast.Module = module
|
|
316
|
+
self.vouched: frozenset[str] = vouched
|
|
317
|
+
self.recorded: dict[int, list[Recorded]] = {}
|
|
318
|
+
self.assigned: dict[int, list[Assigned]] = {} # each checked function's `self.x = value`s
|
|
319
|
+
self.used: dict[int, list[Used]] = {} # each checked function's reads of `self`'s empty containers
|
|
320
|
+
methods: dict[str, dict[str, str]] = {owner: dict(typed) for owner, typed in imported.methods.items()}
|
|
321
|
+
self.tables: _Tables = _Tables(dict(imported.calls), methods, dict(imported.guesses), {})
|
|
322
|
+
self.returned: Returned = Returned(*self.tables)
|
|
323
|
+
self.entries: list[tuple[Slot, str]] = []
|
|
324
|
+
self.stamps: dict[int, int] = {}
|
|
325
|
+
|
|
326
|
+
def checked(
|
|
327
|
+
self,
|
|
328
|
+
func: FunctionDef,
|
|
329
|
+
returns: list[Recorded],
|
|
330
|
+
stored: tuple[list[Assigned], list[Used]],
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Record a checked function's `return`s, and what it stores in `self`'s attributes.
|
|
333
|
+
|
|
334
|
+
`stored`: its `self.x = value`s, and its reads of `self`'s empty containers. Its return type
|
|
335
|
+
is recorded too, if its `return`s decide it.
|
|
336
|
+
"""
|
|
337
|
+
self.stamps[id(func)] = len(self.entries)
|
|
338
|
+
self.recorded[id(func)] = returns
|
|
339
|
+
self.assigned[id(func)], self.used[id(func)] = stored
|
|
340
|
+
self._completed(func)
|
|
341
|
+
slot: Slot | None = slots(self.module).get(id(func))
|
|
342
|
+
found: tuple[str, frozenset[str]] | None
|
|
343
|
+
if slot is None or (found := _return_type(self.module, func, returns, self.vouched)) is None:
|
|
344
|
+
return
|
|
345
|
+
annotation: str
|
|
346
|
+
origins: frozenset[str]
|
|
347
|
+
annotation, origins = found
|
|
348
|
+
if slot.owner is None:
|
|
349
|
+
self.tables.calls[slot.name] = annotation
|
|
350
|
+
else:
|
|
351
|
+
self.tables.methods.setdefault(slot.owner, {})[slot.name] = annotation
|
|
352
|
+
if origins:
|
|
353
|
+
self.tables.guesses[slot.name if slot.owner is None else f"{slot.owner}.{slot.name}"] = origins
|
|
354
|
+
self.entries.append((slot, annotation))
|
|
355
|
+
|
|
356
|
+
def _completed(self, func: FunctionDef) -> None:
|
|
357
|
+
"""Type the attributes of `func`'s class (and those above it) whose methods are all checked now."""
|
|
358
|
+
own: ast.ClassDef | None = owners(self.module).get(id(func))
|
|
359
|
+
above: list[ast.ClassDef] = [] if own is None else superclasses(self.module, own)
|
|
360
|
+
node: ast.ClassDef
|
|
361
|
+
for node in above if own is None else (own, *above):
|
|
362
|
+
# With the classes under it: their methods fill its empty containers too.
|
|
363
|
+
if class_names(self.module)[node.name] != 1 or any(
|
|
364
|
+
id(method) not in self.assigned
|
|
365
|
+
for each in (node, *subclasses(self.module, node))
|
|
366
|
+
for method in self_methods(each)
|
|
367
|
+
):
|
|
368
|
+
continue
|
|
369
|
+
name: str
|
|
370
|
+
annotation: str
|
|
371
|
+
origins: frozenset[str]
|
|
372
|
+
for name, annotation, origins in attribute_types(self.module, node, (self.assigned, self.used)):
|
|
373
|
+
self.tables.attributes.setdefault(node.name, {})[name] = annotation
|
|
374
|
+
self.tables.guesses[f"{node.name}.{name}"] = origins
|
|
375
|
+
|
|
376
|
+
def stale(self, func: FunctionDef) -> bool:
|
|
377
|
+
"""Check whether `func` calls a function whose type the table gained after `func` was checked.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Whether it does: checking it again, knowing that, could type more.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
since: slice = slice(self.stamps.get(id(func), 0), None)
|
|
384
|
+
newer: list[tuple[Slot, str]] = self.entries[since]
|
|
385
|
+
calls: dict[str, str] = {}
|
|
386
|
+
methods: dict[str, dict[str, str]] = {}
|
|
387
|
+
slot: Slot
|
|
388
|
+
annotation: str
|
|
389
|
+
for slot, annotation in newer:
|
|
390
|
+
if slot.owner is None:
|
|
391
|
+
calls[slot.name] = annotation
|
|
392
|
+
else:
|
|
393
|
+
methods.setdefault(slot.owner, {})[slot.name] = annotation
|
|
394
|
+
return bool(newer) and called(self.module, func, Returned(calls, methods))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@lru_cache(maxsize=4) # asked of the same module's functions, each round
|
|
398
|
+
def _calls(module: ast.Module) -> tuple[list[tuple[int, int]], list[_Callee]]:
|
|
399
|
+
"""Find every call in the module, in source order, from its one shared walk (`nodes`).
|
|
400
|
+
|
|
401
|
+
A function's (or statement's) calls are then those within its span of the source: no walk of
|
|
402
|
+
its own, which the rounds of `checker._returned` asked of every function again and again. A
|
|
403
|
+
`with` statement that binds a target calls its manager's `__enter__`.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Where each call starts (its line and column), and what it calls: a name (`f()`) or an
|
|
407
|
+
attribute (`x.m()`), each `None` if not.
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
found: list[_Call] = [(start, (None, entered.ENTER)) for start in entered.targets(module)]
|
|
411
|
+
node: ast.AST
|
|
412
|
+
name: str
|
|
413
|
+
attr: str
|
|
414
|
+
for node in of_type(module, ast.Call):
|
|
415
|
+
match node:
|
|
416
|
+
case ast.Call(func=ast.Name(id=name)):
|
|
417
|
+
found.append(((node.lineno, node.col_offset), (name, None)))
|
|
418
|
+
case ast.Call(func=ast.Attribute(attr=attr)):
|
|
419
|
+
found.append(((node.lineno, node.col_offset), (None, attr)))
|
|
420
|
+
case _:
|
|
421
|
+
pass
|
|
422
|
+
found.sort(key=operator.itemgetter(0))
|
|
423
|
+
return [start for start, _ in found], [callee for _, callee in found]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _typed(
|
|
427
|
+
module: ast.Module,
|
|
428
|
+
body: Sequence[ast.stmt],
|
|
429
|
+
recorded: Mapping[int, Sequence[Recorded]],
|
|
430
|
+
vouched: frozenset[str],
|
|
431
|
+
) -> Iterator[tuple[str, str, frozenset[str]]]:
|
|
432
|
+
"""Find the plain unannotated functions directly in `body` whose `return`s decide their type.
|
|
433
|
+
|
|
434
|
+
Yields:
|
|
435
|
+
Each one's name, return type, and what that rests on if it's a guess.
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
func: FunctionDef
|
|
439
|
+
for func in _plain(body):
|
|
440
|
+
found: tuple[str, frozenset[str]] | None
|
|
441
|
+
if (found := _return_type(module, func, recorded.get(id(func), ()), vouched)) is not None:
|
|
442
|
+
yield _key(func), *found
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _return_type(
|
|
446
|
+
module: ast.Module,
|
|
447
|
+
func: FunctionDef,
|
|
448
|
+
returns: Sequence[Recorded],
|
|
449
|
+
vouched: frozenset[str],
|
|
450
|
+
) -> tuple[str, frozenset[str]] | None:
|
|
451
|
+
"""Type a plain function from its recorded `return`s.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
The one type they all give, and what it rests on if any is a guess; or `None` if it's
|
|
455
|
+
decorated or annotated (by a `# type:` comment too), can fall off its end (but for a
|
|
456
|
+
generator, recorded as its type; not an `async` one), or has a `return` without a value or
|
|
457
|
+
of an unknown or different type.
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
manager: bool = entered.is_manager(func)
|
|
461
|
+
declared: bool = func.returns is not None or bool(func.type_comment)
|
|
462
|
+
# A generator's type is its `yield`s' (not an `async` one's); any other must end by a `return`.
|
|
463
|
+
decided: bool = (
|
|
464
|
+
isinstance(func, ast.FunctionDef)
|
|
465
|
+
if is_generator(module, func)
|
|
466
|
+
else terminates(func.body) and not manager
|
|
467
|
+
)
|
|
468
|
+
if (_decorated(func, vouched) and not manager) or declared or not returns or not decided:
|
|
469
|
+
return None
|
|
470
|
+
types: set[str | None] = {None if found is None else found.annotation for found, _ in returns}
|
|
471
|
+
found: str | None
|
|
472
|
+
if (found := next(iter(types)) if len(types) == 1 else None) is None:
|
|
473
|
+
return None
|
|
474
|
+
if roots(found) & bound_within(func): # a class it imports or defines itself: no caller can name it
|
|
475
|
+
return None
|
|
476
|
+
# An `X | None` is a guess: a type checker takes the unannotated function's call for anything,
|
|
477
|
+
# and what its callers do with it unchecked for `None` is an error only once it's declared.
|
|
478
|
+
doubted: frozenset[str] = frozenset({RETURNED}) if _NONE in (members(found) or ()) else frozenset()
|
|
479
|
+
return found, doubted.union(*(origins for _, origins in returns))
|
|
Binary file
|