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,189 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a module is checked with (`Settings`): its options, and what `--fix` knows of it.
|
|
3
|
+
|
|
4
|
+
Its own tables (`Tables`) joined with what other checked files say of it (`Outside`): functions'
|
|
5
|
+
returns, classes' members and lines of bases, the standard library's classes it takes from, what a
|
|
6
|
+
type checker sees otherwise (`facts`), and its parameters' types given from outside. A type the
|
|
7
|
+
module's own code writes with a type variable it imports is left out: `--fix` sees only its own.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
from typing import Final, cast
|
|
14
|
+
|
|
15
|
+
from constricter.fix.core import imports, inherited
|
|
16
|
+
from constricter.fix.core.known import (
|
|
17
|
+
Classes,
|
|
18
|
+
ClassSide,
|
|
19
|
+
Indirect,
|
|
20
|
+
Known,
|
|
21
|
+
LibraryNames,
|
|
22
|
+
Limits,
|
|
23
|
+
Outside,
|
|
24
|
+
Partial,
|
|
25
|
+
)
|
|
26
|
+
from constricter.fix.libraries import stdlib
|
|
27
|
+
from constricter.fix.values import callables, classvars, entered, fills, stored
|
|
28
|
+
from constricter.fix.values.doubts import facts, says_self
|
|
29
|
+
from constricter.offences import (
|
|
30
|
+
STARRED_SUBSCRIPTS,
|
|
31
|
+
Checks,
|
|
32
|
+
)
|
|
33
|
+
from constricter.rules import late, widened
|
|
34
|
+
from constricter.rules.annotations import free_of, free_of_all
|
|
35
|
+
from constricter.rules.calls import keyed, marked
|
|
36
|
+
from constricter.rules.declarations import (
|
|
37
|
+
awaited_returns,
|
|
38
|
+
casts,
|
|
39
|
+
class_attributes,
|
|
40
|
+
defined_type_vars,
|
|
41
|
+
factories,
|
|
42
|
+
self_returns,
|
|
43
|
+
)
|
|
44
|
+
from constricter.rules.decorators import passing
|
|
45
|
+
from constricter.rules.flow import Hierarchy
|
|
46
|
+
from constricter.rules.scoped import Seeded, Settings
|
|
47
|
+
from constricter.rules.syntax import (
|
|
48
|
+
owners,
|
|
49
|
+
python2_compatible,
|
|
50
|
+
)
|
|
51
|
+
from constricter.rules.tables import Tables
|
|
52
|
+
from constricter.rules.walked import of_type
|
|
53
|
+
|
|
54
|
+
TYPING_ALIAS: Final = (3, 10) # the first Python whose `typing` has `TypeAlias`
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def module_settings(
|
|
58
|
+
tree: ast.Module,
|
|
59
|
+
checks: Checks,
|
|
60
|
+
lines: Sequence[str],
|
|
61
|
+
own: Tables,
|
|
62
|
+
outside: Outside | None = None,
|
|
63
|
+
) -> Settings:
|
|
64
|
+
"""Work out what the module `tree` is checked with (see the module docstring).
|
|
65
|
+
|
|
66
|
+
`checks`: its options; `lines`: its source lines; `own`: its own tables; `outside`: what the CLI
|
|
67
|
+
knows of it from the other checked files, if it's one of them.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Its settings.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
stored.hand(tree, {} if outside is None else outside.emptied)
|
|
74
|
+
# The functions' declared returns: other checked files' (see `Outside`), then the module's own.
|
|
75
|
+
calls: dict[str, str] = {**({} if outside is None else outside.calls), **own.returns, **own.side_calls}
|
|
76
|
+
imported: Classes | None = None if outside is None else outside.classes
|
|
77
|
+
# The bases whose members are known whole: the standard library's, and other checked files' below.
|
|
78
|
+
bases: dict[str, str] = stdlib.bases(tree, stdlib.origins(tree))
|
|
79
|
+
beyond: Mapping[str, inherited.Beyond] = {} if outside is None else outside.beyond
|
|
80
|
+
library: dict[str, inherited.Library] = {
|
|
81
|
+
text: (stdlib.members(path), stdlib.lines(path))
|
|
82
|
+
for text, path in (*bases.items(), *((end, end) for end in inherited.several(beyond)))
|
|
83
|
+
if path
|
|
84
|
+
}
|
|
85
|
+
# The file's own types that mention a type variable it imports (`--fix` sees only its own).
|
|
86
|
+
free: frozenset[str] = frozenset() if outside is None else outside.type_vars
|
|
87
|
+
selfish: dict[str, frozenset[str]] = self_returns(tree)
|
|
88
|
+
return Settings(
|
|
89
|
+
checks._replace(type_comments=checks.type_comments or python2_compatible(tree)),
|
|
90
|
+
lines,
|
|
91
|
+
Known(
|
|
92
|
+
free_of(calls, free),
|
|
93
|
+
factories(tree) | (frozenset[str]() if outside is None else outside.values),
|
|
94
|
+
{**(imported.attributes if imported else {}), **free_of_all(own.classes, free)},
|
|
95
|
+
{**(imported.methods if imported else {}), **free_of_all(own.methods, free)},
|
|
96
|
+
Indirect(
|
|
97
|
+
{**({} if outside is None else outside.awaits), **free_of(awaited_returns(tree), free)},
|
|
98
|
+
_partial(own, free, outside),
|
|
99
|
+
{**({} if outside is None else outside.tuples), **free_of(own.tuples, free)},
|
|
100
|
+
{**({} if outside is None else outside.unions), **own.unions},
|
|
101
|
+
{**({} if outside is None else outside.takers), **fills.takers(tree)},
|
|
102
|
+
),
|
|
103
|
+
ClassSide(
|
|
104
|
+
free_of_all(own.order.flattened(class_attributes(tree)), free),
|
|
105
|
+
free_of_all(own.sides, free),
|
|
106
|
+
inherited.lineage(
|
|
107
|
+
tree,
|
|
108
|
+
selfish,
|
|
109
|
+
# A base whose line the index followed is in sight too, another package's included.
|
|
110
|
+
frozenset(bases).union(imported.methods if imported else (), beyond),
|
|
111
|
+
beyond,
|
|
112
|
+
library,
|
|
113
|
+
),
|
|
114
|
+
classvars.variables(tree, classvars.imported(tree), outside, checks.plain_bases),
|
|
115
|
+
classvars.own_plain(tree, classvars.imported(tree), outside, checks.plain_bases),
|
|
116
|
+
),
|
|
117
|
+
LibraryNames(
|
|
118
|
+
casts(tree),
|
|
119
|
+
stdlib.origins(tree),
|
|
120
|
+
# What annotations alone can name: its own imports for type checking, and other files' types'.
|
|
121
|
+
replace(
|
|
122
|
+
imports.plan(tree),
|
|
123
|
+
guarded={} if outside is None else {**outside.checking, **outside.guarded},
|
|
124
|
+
),
|
|
125
|
+
{} if outside is None else outside.overloaded,
|
|
126
|
+
frozenset() if outside is None else outside.installed_classes,
|
|
127
|
+
{} if outside is None else outside.installed_parameters,
|
|
128
|
+
{} if outside is None else outside.installed_lineage,
|
|
129
|
+
{} if outside is None else outside.installed_aliases,
|
|
130
|
+
checks.min_python is not None and checks.min_python >= STARRED_SUBSCRIPTS,
|
|
131
|
+
checks.min_python is None or checks.min_python >= TYPING_ALIAS,
|
|
132
|
+
),
|
|
133
|
+
Limits(checks.max_length, checks.vague, widened.marks(lines)),
|
|
134
|
+
vouched=passing(tree, defined_type_vars(tree)).union(
|
|
135
|
+
frozenset() if outside is None else outside.vouched,
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
Hierarchy.for_module(
|
|
139
|
+
tree,
|
|
140
|
+
{name: frozenset(wider) for name, wider in checks.narrower},
|
|
141
|
+
() if outside is None else outside.same,
|
|
142
|
+
),
|
|
143
|
+
owners(tree, says_self),
|
|
144
|
+
tuple(
|
|
145
|
+
sorted(
|
|
146
|
+
(node.lineno, node.col_offset)
|
|
147
|
+
for node in cast("list[ast.NamedExpr]", of_type(tree, ast.NamedExpr))
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
() if outside is None else outside.hints,
|
|
151
|
+
facts(
|
|
152
|
+
tree,
|
|
153
|
+
selfish,
|
|
154
|
+
stdlib.generics(stdlib.origins(tree)) | (outside.generics if outside else frozenset[str]()),
|
|
155
|
+
{
|
|
156
|
+
**({} if outside is None else outside.managers),
|
|
157
|
+
**{name: (yielded, frozenset[str]()) for name, yielded in entered.managers(tree).items()},
|
|
158
|
+
},
|
|
159
|
+
defined_type_vars(tree) | free,
|
|
160
|
+
)._replace(
|
|
161
|
+
untyped=frozenset() if outside is None else outside.untyped,
|
|
162
|
+
attributed=frozenset(
|
|
163
|
+
node.value.id
|
|
164
|
+
for node in cast("list[ast.Attribute]", of_type(tree, ast.Attribute))
|
|
165
|
+
if isinstance(node.value, ast.Name)
|
|
166
|
+
),
|
|
167
|
+
positional=callables.signatures(tree),
|
|
168
|
+
excused=late.excusing(lines),
|
|
169
|
+
),
|
|
170
|
+
Seeded(
|
|
171
|
+
keyed(tree, {} if outside is None else outside.parameters),
|
|
172
|
+
{} if outside is None else outside.fixtures,
|
|
173
|
+
marked=marked(tree),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _partial(own: Tables, free: frozenset[str], outside: Outside | None) -> Partial:
|
|
179
|
+
"""Gather the returns only an unpacking can use: other checked files', then the module's own.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Them, less the module's that mention a type variable it imports (`free`).
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
given: Partial = Partial() if outside is None else outside.partial
|
|
186
|
+
return Partial(
|
|
187
|
+
{**given.calls, **free_of(own.partial, free)},
|
|
188
|
+
{**given.methods, **free_of_all(own.partial_methods, free)},
|
|
189
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Walking the syntax the rules read: functions and methods, nested statements, binding targets."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import bisect
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
8
|
+
from typing import Final, TypeAlias, cast
|
|
9
|
+
|
|
10
|
+
from constricter.offences import at
|
|
11
|
+
from constricter.rules.walked import children as child_nodes
|
|
12
|
+
from constricter.rules.walked import classes, of_type
|
|
13
|
+
|
|
14
|
+
# A `# type:` comment, as a loop header writes one (LVA003).
|
|
15
|
+
_TYPE_COMMENT: Final = re.compile(rb"#\s*type:")
|
|
16
|
+
|
|
17
|
+
FunctionDef: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
|
|
18
|
+
FUNCTION_DEFS: tuple[type[ast.FunctionDef], type[ast.AsyncFunctionDef]] = (
|
|
19
|
+
ast.FunctionDef,
|
|
20
|
+
ast.AsyncFunctionDef,
|
|
21
|
+
)
|
|
22
|
+
# Statements whose nested statements may not all run (or not only once).
|
|
23
|
+
BRANCHING: Final = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ast.TryStar, ast.Match)
|
|
24
|
+
_LEAVING: Final = (ast.Return, ast.Raise, ast.Continue, ast.Break)
|
|
25
|
+
# The nodes with a scope of their own: what's inside one isn't the enclosing function's.
|
|
26
|
+
NESTED_SCOPES: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)
|
|
27
|
+
_END: Final = 1 << 62 # past any line: a module's span has no end
|
|
28
|
+
Start: TypeAlias = tuple[int, int] # where a node starts: its line and column
|
|
29
|
+
_FUTURE: Final = "__future__"
|
|
30
|
+
_SELF: Final = "self"
|
|
31
|
+
# `from __future__` features only code that also runs on Python 2 imports: its type comments count.
|
|
32
|
+
_PYTHON2_FUTURES: frozenset[str] = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"nested_scopes",
|
|
35
|
+
"generators",
|
|
36
|
+
"division",
|
|
37
|
+
"absolute_import",
|
|
38
|
+
"with_statement",
|
|
39
|
+
"print_function",
|
|
40
|
+
"unicode_literals",
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def python2_compatible(tree: ast.Module) -> bool:
|
|
46
|
+
"""Check for a `from __future__` import only Python 2 needs.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Whether it marks the module as written for Python 2.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
return any(
|
|
53
|
+
isinstance(stmt, ast.ImportFrom)
|
|
54
|
+
and stmt.module == _FUTURE
|
|
55
|
+
and any(alias.name in _PYTHON2_FUTURES for alias in stmt.names)
|
|
56
|
+
for stmt in tree.body
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def collect_functions(body: list[ast.stmt], into: list[FunctionDef]) -> None:
|
|
61
|
+
"""Collect functions in a module or class body, through compound statements and classes."""
|
|
62
|
+
stmt: ast.stmt
|
|
63
|
+
for stmt in body:
|
|
64
|
+
if isinstance(stmt, FUNCTION_DEFS):
|
|
65
|
+
into.append(stmt)
|
|
66
|
+
elif isinstance(stmt, ast.ClassDef):
|
|
67
|
+
collect_functions(stmt.body, into)
|
|
68
|
+
else:
|
|
69
|
+
collect_functions(child_statements(stmt), into)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def owners(tree: ast.Module, selfish: Callable[[FunctionDef], bool]) -> dict[int, str]:
|
|
73
|
+
"""Map each direct method of a class to that class's name, by the method's `id`.
|
|
74
|
+
|
|
75
|
+
For `--fix` to type a method's `self`. A method is a function directly in a class's body,
|
|
76
|
+
however deep through `if`/`try`/..., but not through a nested class's or function's own body.
|
|
77
|
+
A function defined in a method reads the method's `self` (see `_self_readers`): it's mapped
|
|
78
|
+
too, unless the method is one `selfish` says types its `self` otherwise (as `Self`).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Each such function, by `id()`, mapped to its class's name.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
found: dict[int, str] = {}
|
|
85
|
+
bindings: list[Start] = _self_bindings(tree)
|
|
86
|
+
node: ast.AST
|
|
87
|
+
methods: list[FunctionDef]
|
|
88
|
+
method: FunctionDef
|
|
89
|
+
for node in classes(tree):
|
|
90
|
+
methods = []
|
|
91
|
+
_direct_methods(node.body, methods)
|
|
92
|
+
for method in methods:
|
|
93
|
+
found[id(method)] = node.name
|
|
94
|
+
if (
|
|
95
|
+
_SELF in _first_parameter(method)
|
|
96
|
+
and not _binds_self(method, bindings)
|
|
97
|
+
and not selfish(method)
|
|
98
|
+
):
|
|
99
|
+
found.update((id(inner), node.name) for inner in _self_readers(method, bindings))
|
|
100
|
+
return found
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _self_bindings(tree: ast.Module) -> list[Start]:
|
|
104
|
+
"""Find where the module binds a name `self` other than as a parameter (few do, anywhere).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Where each such store, deletion or import starts, in source order.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
return sorted(
|
|
111
|
+
(node.lineno, node.col_offset)
|
|
112
|
+
for node in cast("list[ast.Name | ast.alias]", of_type(tree, ast.Name, ast.alias))
|
|
113
|
+
if (
|
|
114
|
+
(node.id == _SELF and not isinstance(node.ctx, ast.Load))
|
|
115
|
+
if isinstance(node, ast.Name)
|
|
116
|
+
else (node.asname or node.name) == _SELF
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _first_parameter(function: FunctionDef) -> list[str]:
|
|
122
|
+
"""Name a function's first positional parameter.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
It, alone; nothing for a function without one.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
return [arg.arg for arg in (*function.args.posonlyargs, *function.args.args)][:1]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _self_readers(function: FunctionDef, bindings: Sequence[Start]) -> Iterator[FunctionDef]:
|
|
132
|
+
"""Walk the functions defined in `function`, however deep, that read its `self`.
|
|
133
|
+
|
|
134
|
+
One taking no parameter `self` and binding none, in a function that does neither (but
|
|
135
|
+
`function` itself, whose `self` it is). `bindings`: the module's `_self_bindings`.
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
Each.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
nested: list[FunctionDef] = []
|
|
142
|
+
_direct_methods(function.body, nested)
|
|
143
|
+
inner: FunctionDef
|
|
144
|
+
for inner in nested:
|
|
145
|
+
args: ast.arguments = inner.args
|
|
146
|
+
params: list[ast.arg | None] = [
|
|
147
|
+
*args.posonlyargs,
|
|
148
|
+
*args.args,
|
|
149
|
+
*args.kwonlyargs,
|
|
150
|
+
args.vararg,
|
|
151
|
+
args.kwarg,
|
|
152
|
+
]
|
|
153
|
+
if not any(arg is not None and arg.arg == _SELF for arg in params) and not _binds_self(
|
|
154
|
+
inner,
|
|
155
|
+
bindings,
|
|
156
|
+
):
|
|
157
|
+
yield inner
|
|
158
|
+
yield from _self_readers(inner, bindings)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _binds_self(function: FunctionDef, bindings: Sequence[Start]) -> bool:
|
|
162
|
+
"""Check whether a function's own body binds `self` (an assignment, a loop, an import, ...).
|
|
163
|
+
|
|
164
|
+
`bindings`: where the module binds one at all (see `_self_bindings`): a function none is in
|
|
165
|
+
isn't walked.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Whether it does.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
return has_within(bindings, function) and any(
|
|
172
|
+
(isinstance(node, ast.Name) and node.id == _SELF and not isinstance(node.ctx, ast.Load))
|
|
173
|
+
or (isinstance(node, ast.alias) and (node.asname or node.name) == _SELF)
|
|
174
|
+
for node in own_nodes(function.body)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _direct_methods(body: list[ast.stmt], into: list[FunctionDef]) -> None:
|
|
179
|
+
"""Collect the functions directly in a class's body, through compound statements.
|
|
180
|
+
|
|
181
|
+
A nested class's own methods aren't included.
|
|
182
|
+
"""
|
|
183
|
+
stmt: ast.stmt
|
|
184
|
+
for stmt in body:
|
|
185
|
+
if isinstance(stmt, FUNCTION_DEFS):
|
|
186
|
+
into.append(stmt)
|
|
187
|
+
elif not isinstance(stmt, ast.ClassDef):
|
|
188
|
+
_direct_methods(child_statements(stmt), into)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def import_bindings(body: Iterable[ast.stmt]) -> Iterator[tuple[str, str, str]]:
|
|
192
|
+
"""Walk the absolute imports among `body`, and what each name they bind is.
|
|
193
|
+
|
|
194
|
+
`import os` binds `os` to `os` (and `import os.path` binds `os` too); `import os.path as p`
|
|
195
|
+
binds `p` to `os.path`; `from os import getpid as pid` binds `pid` to `os.getpid`. A relative
|
|
196
|
+
import names no module here.
|
|
197
|
+
|
|
198
|
+
Yields:
|
|
199
|
+
Each bound name, its dotted origin, and the module it comes from: an `import`'s top-level
|
|
200
|
+
package, a `from` import's module.
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
stmt: ast.stmt
|
|
204
|
+
module: str
|
|
205
|
+
alias: ast.alias
|
|
206
|
+
for stmt in body:
|
|
207
|
+
match stmt:
|
|
208
|
+
case ast.Import():
|
|
209
|
+
for alias in stmt.names:
|
|
210
|
+
top: str = alias.name.split(".", 1)[0]
|
|
211
|
+
yield alias.asname or top, alias.name if alias.asname else top, top
|
|
212
|
+
case ast.ImportFrom(module=str() as module, level=0):
|
|
213
|
+
for alias in stmt.names:
|
|
214
|
+
yield alias.asname or alias.name, f"{module}.{alias.name}", module
|
|
215
|
+
case _:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def within(starts: Sequence[Start], node: ast.AST, begin: Start | None = None) -> slice:
|
|
220
|
+
"""Find which of `starts` (in source order) are within `node`'s span of the source.
|
|
221
|
+
|
|
222
|
+
`begin`, if given, is where the span starts instead (a function's first decorator, say). A node
|
|
223
|
+
with no position (the module) spans everything.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Their slice.
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
return slice(*_bounds(starts, node, begin))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def has_within(starts: Sequence[Start], node: ast.AST) -> bool:
|
|
233
|
+
"""Check whether any of `starts` (in source order) is within `node`'s span (see `within`).
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Whether one is.
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
first: int
|
|
240
|
+
stop: int
|
|
241
|
+
first, stop = _bounds(starts, node, None)
|
|
242
|
+
return stop > first
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _bounds(starts: Sequence[Start], node: ast.AST, begin: Start | None) -> tuple[int, int]:
|
|
246
|
+
"""Find where `node`'s span (from `begin`, if given) starts and stops among `starts`.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
The first index within it, and the first past it.
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
first: Start = begin or (getattr(node, "lineno", 0), getattr(node, "col_offset", 0))
|
|
253
|
+
end: Start = (getattr(node, "end_lineno", _END) or _END, getattr(node, "end_col_offset", 0) or 0)
|
|
254
|
+
return bisect.bisect_left(starts, first), bisect.bisect_left(starts, end)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def own_nodes(found: Sequence[ast.AST], parents: dict[int, ast.AST] | None = None) -> Iterator[ast.AST]:
|
|
258
|
+
"""Walk `found`, depth first in source order, without entering a nested function, lambda or class.
|
|
259
|
+
|
|
260
|
+
`parents`, if given, records each node's parent (by `id()`) before the node is yielded.
|
|
261
|
+
|
|
262
|
+
Yields:
|
|
263
|
+
Each node, a nested scope's own too (but not what's inside it).
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
# A stack, not a recursion: a nested generator passes each node up through every level above it.
|
|
267
|
+
waiting: list[ast.AST | None] = [None, *reversed(found)] # `None` at its bottom ends it
|
|
268
|
+
node: ast.AST
|
|
269
|
+
for node in iter(waiting.pop, None):
|
|
270
|
+
yield node
|
|
271
|
+
if not isinstance(node, NESTED_SCOPES):
|
|
272
|
+
below: list[ast.AST] = child_nodes(node)
|
|
273
|
+
if parents is not None:
|
|
274
|
+
parents.update((id(child), node) for child in below)
|
|
275
|
+
waiting.extend(reversed(below))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def top_level(body: Sequence[ast.stmt]) -> Iterator[ast.stmt]:
|
|
279
|
+
"""Walk a module's top-level statements, those under its `if`s and `try`s too.
|
|
280
|
+
|
|
281
|
+
Yields:
|
|
282
|
+
Each.
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
stmt: ast.stmt
|
|
286
|
+
for stmt in body:
|
|
287
|
+
yield stmt
|
|
288
|
+
if isinstance(stmt, ast.If | ast.Try | ast.TryStar):
|
|
289
|
+
yield from top_level(child_statements(stmt))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def child_statements(stmt: ast.stmt) -> list[ast.stmt]:
|
|
293
|
+
"""Collect the statements nested directly in `stmt`.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Them, in source order.
|
|
297
|
+
|
|
298
|
+
"""
|
|
299
|
+
children: list[ast.stmt] = []
|
|
300
|
+
handler: ast.ExceptHandler
|
|
301
|
+
case: ast.match_case
|
|
302
|
+
match stmt:
|
|
303
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
304
|
+
children += stmt.body + stmt.orelse
|
|
305
|
+
case ast.With() | ast.AsyncWith():
|
|
306
|
+
children += stmt.body
|
|
307
|
+
case ast.Try() | ast.TryStar():
|
|
308
|
+
children += stmt.body
|
|
309
|
+
for handler in stmt.handlers:
|
|
310
|
+
children += handler.body
|
|
311
|
+
children += stmt.orelse + stmt.finalbody
|
|
312
|
+
case ast.Match():
|
|
313
|
+
for case in stmt.cases:
|
|
314
|
+
children += case.body
|
|
315
|
+
case _:
|
|
316
|
+
pass
|
|
317
|
+
return children
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def arm_ends(stmt: ast.stmt) -> frozenset[int]:
|
|
321
|
+
"""Find where each arm of a branching statement ends: a body, an `else`, a handler, a `match` case.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Each arm's last statement, by its `id()`; not one that leaves (`return`, `raise`, ...), past
|
|
325
|
+
which its arm doesn't go on.
|
|
326
|
+
|
|
327
|
+
"""
|
|
328
|
+
blocks: list[list[ast.stmt]]
|
|
329
|
+
match stmt:
|
|
330
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
331
|
+
blocks = [stmt.body, stmt.orelse]
|
|
332
|
+
case ast.Try() | ast.TryStar():
|
|
333
|
+
blocks = [stmt.body, *(handler.body for handler in stmt.handlers), stmt.orelse]
|
|
334
|
+
case _: # a `match`, the one other branching statement
|
|
335
|
+
blocks = [case.body for case in cast("ast.Match", stmt).cases]
|
|
336
|
+
return frozenset(id(block[-1]) for block in blocks if block and not isinstance(block[-1], _LEAVING))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def expressions(stmt: ast.stmt) -> Iterator[ast.AST]:
|
|
340
|
+
"""Walk the parts of `stmt` that aren't statements.
|
|
341
|
+
|
|
342
|
+
Yields:
|
|
343
|
+
Each, as where a `:=` can bind.
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
child: ast.AST
|
|
347
|
+
for child in child_nodes(stmt):
|
|
348
|
+
if isinstance(child, ast.match_case | ast.ExceptHandler):
|
|
349
|
+
yield from (part for part in child_nodes(child) if not isinstance(part, ast.stmt))
|
|
350
|
+
elif not isinstance(child, ast.stmt):
|
|
351
|
+
yield child
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def target_names(target: ast.expr) -> Iterator[ast.Name]:
|
|
355
|
+
"""Walk an assignment target.
|
|
356
|
+
|
|
357
|
+
Yields:
|
|
358
|
+
Each plain name it binds.
|
|
359
|
+
|
|
360
|
+
"""
|
|
361
|
+
elements: list[ast.expr]
|
|
362
|
+
element: ast.expr
|
|
363
|
+
value: ast.expr
|
|
364
|
+
match target:
|
|
365
|
+
case ast.Name():
|
|
366
|
+
yield target
|
|
367
|
+
case ast.Tuple(elts=elements) | ast.List(elts=elements):
|
|
368
|
+
for element in elements:
|
|
369
|
+
yield from target_names(element)
|
|
370
|
+
case ast.Starred(value=value):
|
|
371
|
+
yield from target_names(value)
|
|
372
|
+
case _:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def captures(pattern: ast.pattern, lines: Sequence[str]) -> Iterator[tuple[str, tuple[int, int]]]:
|
|
377
|
+
"""Walk a `case` pattern.
|
|
378
|
+
|
|
379
|
+
Yields:
|
|
380
|
+
Each name it captures, with where it's bound.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
node: ast.AST
|
|
384
|
+
name: str
|
|
385
|
+
for node in ast.walk(pattern):
|
|
386
|
+
match node:
|
|
387
|
+
case ast.MatchAs(name=str() as name) | ast.MatchStar(name=str() as name):
|
|
388
|
+
yield name, at(node)
|
|
389
|
+
case ast.MatchMapping(rest=str() as name):
|
|
390
|
+
yield name, _rest_at(node, name, lines)
|
|
391
|
+
case _:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
_REST: Final = re.compile(rb"\*\*\s*(\w+)\b")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _rest_at(node: ast.MatchMapping, name: str, lines: Sequence[str]) -> tuple[int, int]:
|
|
399
|
+
"""Find `**name` in a mapping pattern's source.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Its position (as `ast` gives it, a byte column), or else the pattern's start.
|
|
403
|
+
|
|
404
|
+
"""
|
|
405
|
+
target: bytes = name.encode()
|
|
406
|
+
number: int
|
|
407
|
+
encoded: bytes
|
|
408
|
+
start: int
|
|
409
|
+
found: re.Match[bytes]
|
|
410
|
+
for number in range(node.lineno, min(node.end_lineno or node.lineno, len(lines)) + 1):
|
|
411
|
+
encoded = lines[number - 1].encode()
|
|
412
|
+
start = node.col_offset if number == node.lineno else 0
|
|
413
|
+
for found in _REST.finditer(encoded, start):
|
|
414
|
+
if found.group(1) == target:
|
|
415
|
+
return number, found.start(1)
|
|
416
|
+
return at(node)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def comment_type(comment: str) -> str | None:
|
|
420
|
+
"""Read a loop's `# type:` comment as an annotation (`int, str` is a tuple's parts).
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
It, or `None` if it doesn't parse as one.
|
|
424
|
+
|
|
425
|
+
"""
|
|
426
|
+
text: str = comment.split("#", 1)[0].strip() # a trailing comment (a noqa one, say) isn't part of it
|
|
427
|
+
try:
|
|
428
|
+
parsed: ast.expr = ast.parse(text, mode="eval").body
|
|
429
|
+
except SyntaxError:
|
|
430
|
+
return None
|
|
431
|
+
if isinstance(parsed, ast.Tuple):
|
|
432
|
+
return f"tuple[{', '.join(ast.unparse(part) for part in parsed.elts)}]"
|
|
433
|
+
return ast.unparse(parsed)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def declared_return(func: FunctionDef) -> ast.expr | None:
|
|
437
|
+
"""Read the return type a function declares: its annotation, or its `# type:` signature comment's.
|
|
438
|
+
|
|
439
|
+
`def find(): # type: () -> List[str]` declares a `List[str]`, as a type checker reads it.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
It, or `None` for a function declaring none (or a comment that isn't a signature).
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
if func.returns is not None or not func.type_comment:
|
|
446
|
+
return func.returns
|
|
447
|
+
try:
|
|
448
|
+
return ast.parse(func.type_comment, mode="func_type").returns
|
|
449
|
+
except SyntaxError:
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def type_comment_span(lines: Sequence[str], stmt: ast.For | ast.AsyncFor) -> tuple[int, int] | None:
|
|
454
|
+
"""Find the columns (UTF-8 bytes) of a one-line loop header's `# type:` comment, to delete it.
|
|
455
|
+
|
|
456
|
+
From the end of the header's code (or, with a comment after it, `# type: int # noqa`, from the
|
|
457
|
+
`#`), up to that comment or the end of the line, so the other comment stays.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Them, or `None` if the header spans lines or the comment isn't found after the iterable.
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
if not lines or stmt.body[0].lineno == stmt.lineno or stmt.iter.end_lineno != stmt.lineno:
|
|
464
|
+
return None
|
|
465
|
+
raw: bytes = lines[stmt.lineno - 1].encode()
|
|
466
|
+
found: re.Match[bytes] | None
|
|
467
|
+
if (found := _TYPE_COMMENT.search(raw, stmt.iter.end_col_offset or 0)) is None:
|
|
468
|
+
return None
|
|
469
|
+
after: int
|
|
470
|
+
if (after := raw.find(b"#", found.end())) >= 0:
|
|
471
|
+
return found.start(), after
|
|
472
|
+
return len(raw[: found.start()].rstrip()), len(raw.rstrip(b"\r\n"))
|
|
Binary file
|