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,460 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a module passes checked files' functions whose parameters aren't all annotated (`observed`).
|
|
3
|
+
|
|
4
|
+
Each call to one is recorded with what `--fix` knows each argument's type to be where it's made: in a
|
|
5
|
+
function, by that function's scope; elsewhere (a module or class body, a lambda), by the value alone.
|
|
6
|
+
Only a type made of builtins alone counts (`int`, `list[str]`, not a union): another's spelling may
|
|
7
|
+
mean nothing in the callee's module, and a union is what a check narrows. A guess counts, with what
|
|
8
|
+
it rests on: `--unsafe-fixes` writes it, and the next run would take it as certain. A function
|
|
9
|
+
used any way but called (a callback, a stored reference) escapes: its callers can't all be known.
|
|
10
|
+
A name the calling function binds itself isn't the module's function, whatever it's called.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import builtins
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
from dataclasses import replace
|
|
17
|
+
from typing import Final, cast
|
|
18
|
+
|
|
19
|
+
from constricter.fix.core.known import (
|
|
20
|
+
PARAM,
|
|
21
|
+
Call,
|
|
22
|
+
Callee,
|
|
23
|
+
ImportPlan,
|
|
24
|
+
Inference,
|
|
25
|
+
Known,
|
|
26
|
+
LibraryNames,
|
|
27
|
+
Observed,
|
|
28
|
+
Passed,
|
|
29
|
+
Seeds,
|
|
30
|
+
)
|
|
31
|
+
from constricter.fix.index import callers, fixtures
|
|
32
|
+
from constricter.fix.libraries import stdlib
|
|
33
|
+
from constricter.fix.values.guesses import guessing
|
|
34
|
+
from constricter.fix.values.inference import inference, scalar
|
|
35
|
+
from constricter.rules.annotations import dotted
|
|
36
|
+
from constricter.rules.decorators import FIXTURES, is_fixture
|
|
37
|
+
from constricter.rules.decorators import spelled as decorated_as
|
|
38
|
+
from constricter.rules.flow import members
|
|
39
|
+
from constricter.rules.scope import Scope, guesses_in
|
|
40
|
+
from constricter.rules.scoped import Seeded, Settings
|
|
41
|
+
from constricter.rules.syntax import FunctionDef, Start, has_within, own_nodes
|
|
42
|
+
from constricter.rules.walked import classes, of_type
|
|
43
|
+
|
|
44
|
+
_BUILTINS: Final = frozenset(dir(builtins))
|
|
45
|
+
_TEST: Final = "test" # how pytest's test functions' names start
|
|
46
|
+
_NONE: Final = "None" # says nothing of what the parameter's other callers pass, nor what it's for
|
|
47
|
+
_DEFINED: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) # what binds a name, inside
|
|
48
|
+
_REQUEST: Final = "request" # the parameter pytest gives a fixture its request by
|
|
49
|
+
_PARAMS: Final = "params" # `pytest.fixture`'s keyword: the values `request.param` takes in turn
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def observed(
|
|
53
|
+
tree: ast.Module,
|
|
54
|
+
scopes: Sequence[Scope],
|
|
55
|
+
known: Known,
|
|
56
|
+
callees: Mapping[str, Callee],
|
|
57
|
+
) -> Observed:
|
|
58
|
+
"""Find what the module passes, and does with, the functions in `callees` (as it spells them).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Each one's calls, and those that escape.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
named: list[tuple[ast.AST, str]]
|
|
65
|
+
if not (named := _naming(tree, callees) if callees else []):
|
|
66
|
+
return Observed()
|
|
67
|
+
owner: dict[int, Scope] # each such node's innermost function scope, by `id()`
|
|
68
|
+
bound: dict[int, frozenset[str]] # each of those functions' own names, by its scope's `id()`
|
|
69
|
+
owner, bound = _owning(scopes, [node for node, _ in named])
|
|
70
|
+
called: set[int] = {id(node.func) for node in cast("list[ast.Call]", of_type(tree, ast.Call))}
|
|
71
|
+
calls: dict[Callee, list[Call]] = {}
|
|
72
|
+
escaped: set[Callee] = set()
|
|
73
|
+
node: ast.AST
|
|
74
|
+
spelled: str
|
|
75
|
+
for node, spelled in named:
|
|
76
|
+
where: Scope | None = owner.get(id(node))
|
|
77
|
+
if where is not None and spelled.partition(".")[0] in bound[id(where)]:
|
|
78
|
+
continue
|
|
79
|
+
if isinstance(node, ast.Call):
|
|
80
|
+
calls.setdefault(callees[spelled], []).append(_call(node, where, known))
|
|
81
|
+
elif id(node) not in called and isinstance(cast("ast.Name", node).ctx, ast.Load):
|
|
82
|
+
escaped.add(callees[spelled])
|
|
83
|
+
return Observed({callee: tuple(found) for callee, found in calls.items()}, frozenset(escaped))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _owning(
|
|
87
|
+
scopes: Sequence[Scope],
|
|
88
|
+
named: Sequence[ast.AST],
|
|
89
|
+
) -> tuple[dict[int, Scope], dict[int, frozenset[str]]]:
|
|
90
|
+
"""Find the innermost function scope of each node in `named`, and what each such function binds.
|
|
91
|
+
|
|
92
|
+
Only the functions one of them is in are walked: most have none.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Each node's scope, by the node's `id()` (none for one outside any function's own body); and
|
|
96
|
+
each of those scopes' function's own names, by the scope's `id()`.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
wanted: set[int] = {id(node) for node in named}
|
|
100
|
+
starts: list[Start] = sorted(
|
|
101
|
+
(cast("ast.expr", node).lineno, cast("ast.expr", node).col_offset) for node in named
|
|
102
|
+
)
|
|
103
|
+
owner: dict[int, Scope] = {}
|
|
104
|
+
bound: dict[int, frozenset[str]] = {}
|
|
105
|
+
scope: Scope
|
|
106
|
+
for scope in scopes:
|
|
107
|
+
if scope.kind.function is not None and has_within(starts, scope.kind.function):
|
|
108
|
+
nodes: list[ast.AST] = list(own_nodes(scope.kind.function.body))
|
|
109
|
+
owner.update((id(node), scope) for node in nodes if id(node) in wanted)
|
|
110
|
+
bound[id(scope)] = _bound(scope.kind.function, nodes)
|
|
111
|
+
return owner, bound
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _naming(tree: ast.Module, callees: Mapping[str, Callee]) -> list[tuple[ast.AST, str]]:
|
|
115
|
+
"""Find the module's calls, names and attributes that spell one of `callees`.
|
|
116
|
+
|
|
117
|
+
From its one shared walk, each looked at only if its last name is one of theirs.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Each such node, with what it spells (a call's callee), calls first, each kind in the walk's order.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
last: frozenset[str] = frozenset(spelled.rpartition(".")[2] for spelled in callees)
|
|
124
|
+
found: list[tuple[ast.AST, str]] = []
|
|
125
|
+
node: ast.AST
|
|
126
|
+
for node in of_type(tree, ast.Call, ast.Name, ast.Attribute):
|
|
127
|
+
target: ast.AST = node.func if isinstance(node, ast.Call) else node
|
|
128
|
+
name: str = (
|
|
129
|
+
target.id
|
|
130
|
+
if isinstance(target, ast.Name)
|
|
131
|
+
else target.attr
|
|
132
|
+
if isinstance(target, ast.Attribute)
|
|
133
|
+
else ""
|
|
134
|
+
)
|
|
135
|
+
spelled: str | None = dotted(cast("ast.expr", target)) if name in last else None
|
|
136
|
+
if spelled is not None and spelled in callees:
|
|
137
|
+
found.append((node, spelled))
|
|
138
|
+
return found
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _bound(
|
|
142
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
143
|
+
nodes: Sequence[ast.AST],
|
|
144
|
+
*,
|
|
145
|
+
imports: bool = True,
|
|
146
|
+
) -> frozenset[str]:
|
|
147
|
+
"""Name what a function binds itself: its parameters, and what it assigns or (`imports`) imports.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Them.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
args: ast.arguments = function.args
|
|
154
|
+
params: list[ast.arg | None] = [*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg]
|
|
155
|
+
return frozenset(
|
|
156
|
+
[
|
|
157
|
+
*(arg.arg for arg in params if arg is not None),
|
|
158
|
+
*(node.id for node in nodes if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load)),
|
|
159
|
+
*(
|
|
160
|
+
(alias.asname or alias.name).partition(".")[0]
|
|
161
|
+
for node in nodes
|
|
162
|
+
if imports and isinstance(node, ast.Import | ast.ImportFrom)
|
|
163
|
+
for alias in node.names
|
|
164
|
+
),
|
|
165
|
+
*(node.name for node in nodes if isinstance(node, _DEFINED)),
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _call(call: ast.Call, where: Scope | None, known: Known) -> Call:
|
|
171
|
+
"""Read one call's arguments' types (see the module docstring).
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Them; a call unpacking `*args` or `**kwargs` is only marked so.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
if any(isinstance(arg, ast.Starred) for arg in call.args) or any(k.arg is None for k in call.keywords):
|
|
178
|
+
return Call(unpacked=True)
|
|
179
|
+
return Call(
|
|
180
|
+
tuple(_type(arg, where, known) for arg in call.args),
|
|
181
|
+
tuple((keyword.arg or "", _type(keyword.value, where, known)) for keyword in call.keywords),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _type(value: ast.expr, where: Scope | None, known: Known) -> Passed | None:
|
|
186
|
+
"""Type one argument, with builtins alone (see the module docstring).
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Its type, and what it rests on if it's a guess; or `None`.
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
typed: Inference | None
|
|
193
|
+
guess: tuple[bool, frozenset[str]]
|
|
194
|
+
if where is None:
|
|
195
|
+
typed = inference(value, known, {})
|
|
196
|
+
guess = guessing(value, known, frozenset(), {}, {})
|
|
197
|
+
else:
|
|
198
|
+
typed = inference(value, where.settings.known, where.inferred.types)
|
|
199
|
+
guess = guesses_in(where, [value])
|
|
200
|
+
if typed is None or not plain(typed.annotation, known):
|
|
201
|
+
return None
|
|
202
|
+
return typed.annotation, guess[1] if guess[0] else frozenset()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def plain(annotation: str, known: Known) -> bool:
|
|
206
|
+
"""Check an annotation is made of builtins alone, none of them rebound, and isn't a union.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Whether it is.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
213
|
+
return (
|
|
214
|
+
annotation != _NONE
|
|
215
|
+
and len(members(annotation) or ()) == 1
|
|
216
|
+
and all(
|
|
217
|
+
isinstance(node, ast.Name) and node.id in _BUILTINS and known.is_builtin(node.id)
|
|
218
|
+
for node in ast.walk(tree)
|
|
219
|
+
if isinstance(node, ast.Name | ast.Attribute)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def keyed(tree: ast.Module, typed: Seeds) -> dict[int, Mapping[str, Passed]]:
|
|
225
|
+
"""Key what every call passes the module's top-level functions' parameters by each function's `id()`.
|
|
226
|
+
|
|
227
|
+
A nested function or method of the same name isn't the one its callers call.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Each function's parameters' types.
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
return {
|
|
234
|
+
id(stmt): typed[stmt.name]
|
|
235
|
+
for stmt in tree.body
|
|
236
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef) and stmt.name in typed
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def seed_parameters(scope: Scope, func: FunctionDef, named: Sequence[ast.arg]) -> None:
|
|
241
|
+
"""Type the unannotated parameters every call passes one type (see `fix.index.callers`), as guesses.
|
|
242
|
+
|
|
243
|
+
Not one the function binds again itself, whose type a guess from its callers wouldn't be after.
|
|
244
|
+
"""
|
|
245
|
+
seeded: Seeded = scope.settings.parameters or Seeded()
|
|
246
|
+
typed: Mapping[str, Passed] = seeded.callers.get(id(func), {})
|
|
247
|
+
injected: Mapping[str, Passed] = _injected(seeded, func, scope.settings.known)
|
|
248
|
+
varied: Passed | None = _varied(func)
|
|
249
|
+
if not (typed or injected or varied):
|
|
250
|
+
return
|
|
251
|
+
rebound: set[str] = {
|
|
252
|
+
node.id
|
|
253
|
+
for node in own_nodes(func.body)
|
|
254
|
+
if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load)
|
|
255
|
+
}
|
|
256
|
+
arg: ast.arg
|
|
257
|
+
for arg in named:
|
|
258
|
+
given: Passed | None = typed.get(arg.arg)
|
|
259
|
+
if varied is not None and arg.arg == _REQUEST and arg.arg not in rebound:
|
|
260
|
+
scope.inferred.learn(f"{arg.arg}{PARAM}", *varied)
|
|
261
|
+
if arg.annotation is not None or arg.arg in rebound:
|
|
262
|
+
continue
|
|
263
|
+
if arg.arg in injected:
|
|
264
|
+
scope.inferred.learn(arg.arg, *injected[arg.arg])
|
|
265
|
+
elif given is not None and plain(given[0], scope.settings.known):
|
|
266
|
+
scope.inferred.learn(arg.arg, given[0], frozenset({callers.KIND}) | given[1])
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _injected(seeded: Seeded, func: FunctionDef, known: Known) -> Mapping[str, Passed]:
|
|
270
|
+
"""Type the parameters pytest gives a test or a fixture: its fixtures' values, and `parametrize`'s.
|
|
271
|
+
|
|
272
|
+
A fixture the module's tests can take (see `fix.index.fixtures`), by its name, or else one of
|
|
273
|
+
pytest's own (`tmp_path`, a `Path`: see `fixtures.STDLIB_OWN`); and, before them, a name
|
|
274
|
+
`@pytest.mark.parametrize` gives literals of one type, on the function or (`Seeded.marked`) its
|
|
275
|
+
class. Guesses (`fixture`).
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Each such parameter's type and what it rests on; nothing for any other function.
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
if not (func.name.startswith(_TEST) or is_fixture(func)):
|
|
282
|
+
return {}
|
|
283
|
+
classed: Mapping[str, Passed] = seeded.marked.get(id(func), {})
|
|
284
|
+
return {**_own(func, known), **seeded.fixtures, **classed, **_parametrized(func.decorator_list)}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _own(func: FunctionDef, known: Known) -> dict[str, Passed]:
|
|
288
|
+
"""Type the parameters of `func` that name pytest's own fixtures (see `fixtures.STDLIB_OWN`).
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Each one's type, as the module can write it, a guess resting on `fixture`.
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
plan: ImportPlan | None = known.names.plan
|
|
295
|
+
args: ast.arguments = func.args
|
|
296
|
+
named: set[str] = {arg.arg for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs)}
|
|
297
|
+
found: dict[str, Passed] = {}
|
|
298
|
+
name: str
|
|
299
|
+
for name in sorted(fixtures.STDLIB_OWN.keys() & named):
|
|
300
|
+
spelled: str | None
|
|
301
|
+
if (spelled := None if plan is None else plan.spell(fixtures.STDLIB_OWN[name])) is not None:
|
|
302
|
+
found[name] = (spelled, frozenset({fixtures.KIND}))
|
|
303
|
+
return found
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def marked(tree: ast.Module) -> dict[int, Mapping[str, Passed]]:
|
|
307
|
+
"""Type the names `@pytest.mark.parametrize` on a class gives each of its methods (see `_parametrized`).
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Each such method's, by its `id()`: those directly in a class decorated so.
|
|
311
|
+
|
|
312
|
+
"""
|
|
313
|
+
found: dict[int, Mapping[str, Passed]] = {}
|
|
314
|
+
node: ast.ClassDef
|
|
315
|
+
for node in classes(tree):
|
|
316
|
+
typed: dict[str, Passed]
|
|
317
|
+
if typed := _parametrized(node.decorator_list):
|
|
318
|
+
found.update(
|
|
319
|
+
(id(stmt), typed)
|
|
320
|
+
for stmt in node.body
|
|
321
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
322
|
+
)
|
|
323
|
+
return found
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _varied(func: FunctionDef) -> Passed | None:
|
|
327
|
+
"""Type what a fixture's `request.param` gives: its decorator's `params`, literals of one type.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
The type, a guess resting on `fixture` (a test's indirect `parametrize` may give another); or
|
|
331
|
+
`None` for any other function, or `params` not written out so.
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
params: list[ast.expr] = [
|
|
335
|
+
keyword.value
|
|
336
|
+
for decorator in func.decorator_list
|
|
337
|
+
if isinstance(decorator, ast.Call) and decorated_as(decorator) in FIXTURES
|
|
338
|
+
for keyword in decorator.keywords
|
|
339
|
+
if keyword.arg == _PARAMS
|
|
340
|
+
]
|
|
341
|
+
cases: list[ast.expr] = list(params[0].elts) if params and isinstance(params[0], ast.List) else []
|
|
342
|
+
types: set[str | None] = {scalar(case) for case in cases}
|
|
343
|
+
only: str | None = types.pop() if len(types) == 1 else None
|
|
344
|
+
return None if only is None or only == _NONE else (only, frozenset({fixtures.KIND}))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _parametrized(decorators: Sequence[ast.expr]) -> dict[str, Passed]:
|
|
348
|
+
"""Type the names `@pytest.mark.parametrize` decorators give a function literals of one type each.
|
|
349
|
+
|
|
350
|
+
`"n"` with `[1, 2]`, or `"n, s"` (or `("n", "s")`) with `[(1, "a"), (2, "b")]`.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Each one's type, a guess resting on `fixture`.
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
found: dict[str, Passed] = {}
|
|
357
|
+
decorator: ast.expr
|
|
358
|
+
for decorator in decorators:
|
|
359
|
+
names: list[str]
|
|
360
|
+
rows: list[list[ast.expr]]
|
|
361
|
+
names, rows = _cases(decorator)
|
|
362
|
+
at: int
|
|
363
|
+
name: str
|
|
364
|
+
for at, name in enumerate(names):
|
|
365
|
+
types: set[str | None] = {scalar(row[at]) if len(row) == len(names) else None for row in rows}
|
|
366
|
+
only: str | None = types.pop() if len(types) == 1 else None
|
|
367
|
+
if only is not None and only != _NONE:
|
|
368
|
+
found[name] = (only, frozenset({fixtures.KIND}))
|
|
369
|
+
return found
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _cases(decorator: ast.expr) -> tuple[list[str], list[list[ast.expr]]]:
|
|
373
|
+
"""Read a `parametrize` decorator: the names it gives values, and each case's values.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Them; no names for any other decorator, or one whose names or cases aren't written out.
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
names: ast.expr
|
|
380
|
+
cases: list[ast.expr]
|
|
381
|
+
parts: list[ast.expr]
|
|
382
|
+
text: str
|
|
383
|
+
match decorator:
|
|
384
|
+
case ast.Call(func=ast.Attribute(attr="parametrize"), args=[names, ast.List(elts=cases), *_]):
|
|
385
|
+
pass
|
|
386
|
+
case _:
|
|
387
|
+
return [], []
|
|
388
|
+
found: list[str]
|
|
389
|
+
match names:
|
|
390
|
+
case ast.Constant(value=str() as text):
|
|
391
|
+
found = [part.strip() for part in text.split(",")]
|
|
392
|
+
case ast.Tuple(elts=parts) | ast.List(elts=parts) if all(
|
|
393
|
+
isinstance(part, ast.Constant) and isinstance(part.value, str) for part in parts
|
|
394
|
+
):
|
|
395
|
+
found = [str(cast("ast.Constant", part).value) for part in parts]
|
|
396
|
+
case _:
|
|
397
|
+
return [], []
|
|
398
|
+
if len(found) == 1:
|
|
399
|
+
return found, [[case] for case in cases]
|
|
400
|
+
return found, [list(case.elts) if isinstance(case, ast.Tuple | ast.List) else [] for case in cases]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def unshadowed(settings: Settings, function: FunctionDef) -> Settings:
|
|
404
|
+
"""Fit the standard-library names a function is checked knowing to what it binds itself.
|
|
405
|
+
|
|
406
|
+
A parameter, local, import or nested definition named `getpid` isn't `os.getpid`, whatever the
|
|
407
|
+
module imports; a name its own imports alone bind is what they import (`import os`, in its
|
|
408
|
+
body). The functions inside it are checked knowing the same.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
The settings, with those names; the same settings if it binds none of them.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
names: LibraryNames = settings.known.names
|
|
415
|
+
rebound: Mapping[str, Sequence[Start]] | None = settings.facts.rebound
|
|
416
|
+
lazy: Sequence[Start] | None = settings.facts.lazy
|
|
417
|
+
# Most functions bind none: no name the module imports, and no import. They aren't walked.
|
|
418
|
+
if (
|
|
419
|
+
rebound is not None
|
|
420
|
+
and lazy is not None
|
|
421
|
+
and not has_within(lazy, function)
|
|
422
|
+
and not any(has_within(rebound[name], function) for name in names.stdlib.keys() & rebound.keys())
|
|
423
|
+
):
|
|
424
|
+
return settings
|
|
425
|
+
nodes: list[ast.AST] = list(own_nodes(function.body))
|
|
426
|
+
bound: frozenset[str] = _bound(function, nodes)
|
|
427
|
+
own: dict[str, str] = _own_imports(nodes, _bound(function, nodes, imports=False))
|
|
428
|
+
if not (names.stdlib.keys() & bound or own):
|
|
429
|
+
return settings
|
|
430
|
+
stdlib_names: dict[str, str] = {
|
|
431
|
+
name: origin for name, origin in names.stdlib.items() if name not in bound
|
|
432
|
+
} | own
|
|
433
|
+
return replace(settings, known=replace(settings.known, names=names._replace(stdlib=stdlib_names)))
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _own_imports(nodes: Sequence[ast.AST], assigned: frozenset[str]) -> dict[str, str]:
|
|
437
|
+
"""Map the names a function's own imports bind to what they are in the standard library.
|
|
438
|
+
|
|
439
|
+
Not a name it binds another way too (`assigned`), nor one two imports bind to different things.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Each name, mapped to its dotted origin (see `stdlib.origins`).
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
origins: dict[str, set[str | None]] = {}
|
|
446
|
+
node: ast.AST
|
|
447
|
+
for node in nodes:
|
|
448
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
449
|
+
known: dict[str, str] = stdlib.imported([node])
|
|
450
|
+
alias: ast.alias
|
|
451
|
+
for alias in node.names:
|
|
452
|
+
name: str = (alias.asname or alias.name).partition(".")[0]
|
|
453
|
+
origins.setdefault(name, set()).add(known.get(name))
|
|
454
|
+
return {
|
|
455
|
+
name: origin
|
|
456
|
+
for name, found in origins.items()
|
|
457
|
+
if name not in assigned and len(found) == 1
|
|
458
|
+
for origin in found
|
|
459
|
+
if origin is not None
|
|
460
|
+
}
|
|
Binary file
|