python-constricter 0.4.1__cp314-cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp314-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp314-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp314-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp314-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp314-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp314-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp314-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp314-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp314-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp314-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp314-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp314-win_amd64.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
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--fix` for a module's type alias: `Json = dict[str, "Json"]`, declared `Json: TypeAlias`.
|
|
3
|
+
|
|
4
|
+
Only a value that can be nothing but a type made of others: a subscript of what `typing` or
|
|
5
|
+
`collections.abc` define (`Union[A, B]`, `Callable[..., R]`), of a builtin generic
|
|
6
|
+
(`dict[str, int]`) or of a generic class the module names (its own, another checked file's, the
|
|
7
|
+
standard library's); or a union of those, of builtin classes, of classes the module knows and of
|
|
8
|
+
`None`; or a copy of a name the module declares an alias (`Rows = Table`). Not a bare class's alias
|
|
9
|
+
(`Alias = Class`), which declared one loses the class's type parameters; nor a name the module
|
|
10
|
+
binds again, which is a variable to a type checker.
|
|
11
|
+
|
|
12
|
+
`TypeAlias` is named as the module's imports can, else imported from `typing`, which has it from
|
|
13
|
+
Python 3.10: certain where every Python the module runs on has it there (`min-python`), or the
|
|
14
|
+
module imports the name already; a guess where `min-python` isn't known, and no fix where it says
|
|
15
|
+
some Python lacks it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import builtins
|
|
20
|
+
from typing import Final, TypeAlias, cast
|
|
21
|
+
|
|
22
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
23
|
+
from constricter.fix.libraries import stdlib
|
|
24
|
+
from constricter.fix.values import hinted
|
|
25
|
+
from constricter.fix.values.doubts import Facts
|
|
26
|
+
from constricter.rules.annotations import dotted, node_name
|
|
27
|
+
|
|
28
|
+
KIND: Final = "alias" # the fix kind
|
|
29
|
+
_SINCE: Final = (3, 10) # the first Python whose `typing` has `TypeAlias`
|
|
30
|
+
# Where every name that takes a subscript makes a type of it.
|
|
31
|
+
_FORMS: Final = frozenset({"typing", "typing_extensions", "collections.abc"})
|
|
32
|
+
_GENERICS: Final = frozenset({"dict", "frozenset", "list", "set", "tuple", "type"})
|
|
33
|
+
_CLASSES: Final = frozenset(
|
|
34
|
+
name for name in dir(builtins) if isinstance(cast("object", getattr(builtins, name)), type)
|
|
35
|
+
)
|
|
36
|
+
# Imports that bind `TypeAlias`, or a module that has it, on every Python.
|
|
37
|
+
_ALWAYS: Final = frozenset({"typing.TypeAlias", "typing_extensions.TypeAlias", "typing_extensions"})
|
|
38
|
+
_REASON: Final = "a type alias, written as a type made of others"
|
|
39
|
+
# What makes a type of a call's result, by where it's from.
|
|
40
|
+
_FACTORIES: Final = frozenset(
|
|
41
|
+
{
|
|
42
|
+
*(
|
|
43
|
+
f"{module}.{name}"
|
|
44
|
+
for module in ("typing", "typing_extensions")
|
|
45
|
+
for name in (
|
|
46
|
+
"NamedTuple",
|
|
47
|
+
"NewType",
|
|
48
|
+
"ParamSpec",
|
|
49
|
+
"TypeAliasType",
|
|
50
|
+
"TypeVar",
|
|
51
|
+
"TypeVarTuple",
|
|
52
|
+
"TypedDict",
|
|
53
|
+
)
|
|
54
|
+
),
|
|
55
|
+
*(f"enum.{name}" for name in ("Enum", "Flag", "IntEnum", "IntFlag", "StrEnum")),
|
|
56
|
+
"collections.namedtuple",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
# An inference, whether it's a guess, and what the guess rests on: what `Scope.valued` gives.
|
|
60
|
+
_Valued: TypeAlias = tuple[Inference, bool, frozenset[str]]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def factory(value: ast.expr, known: Known) -> bool:
|
|
64
|
+
"""Check whether `value` makes a type, not a value: a `TypeVar`, a `NewType`, a functional `NamedTuple`.
|
|
65
|
+
|
|
66
|
+
What it's bound to can't be annotated: a type checker would take the name for a variable. However
|
|
67
|
+
the factory is imported (`TypeVar`, `typing.TypeVar`, `t.TypeVar`), as long as it's `typing`'s,
|
|
68
|
+
`typing_extensions`', `enum`'s or `collections.namedtuple`.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Whether it does.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
plan: ImportPlan | None = known.names.plan
|
|
75
|
+
return (
|
|
76
|
+
isinstance(value, ast.Call)
|
|
77
|
+
and plan is not None
|
|
78
|
+
and stdlib.resolved(value.func, plan.bound) in _FACTORIES
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def declares(annotation: ast.expr) -> bool:
|
|
83
|
+
"""Check whether an annotation declares its name a type alias: `TypeAlias`, however it's spelled.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Whether it does.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
return node_name(annotation) == hinted.ALIAS
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def declared(
|
|
93
|
+
target: ast.Name,
|
|
94
|
+
value: ast.expr,
|
|
95
|
+
known: Known,
|
|
96
|
+
facts: Facts,
|
|
97
|
+
scope: tuple[frozenset[str], tuple[int, int] | None],
|
|
98
|
+
) -> _Valued | None:
|
|
99
|
+
"""Declare the type alias a module body's `target = value` binds (see the module docstring).
|
|
100
|
+
|
|
101
|
+
`scope`: the names the module body has declared aliases so far, and the oldest Python the
|
|
102
|
+
module runs on (`min-python`).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
`TypeAlias` as the module names it, whether that's a guess, and what the guess rests on; or
|
|
106
|
+
`None` for anything else, or a module that can't name it there.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
plan: ImportPlan | None = known.names.plan
|
|
110
|
+
if plan is None or facts.rebound is None or target.id in facts.rebound:
|
|
111
|
+
return None
|
|
112
|
+
aliases: frozenset[str]
|
|
113
|
+
min_python: tuple[int, int] | None
|
|
114
|
+
aliases, min_python = scope
|
|
115
|
+
older: bool = min_python is not None and min_python < _SINCE
|
|
116
|
+
if older and not _ALWAYS.intersection(plan.bound.values()):
|
|
117
|
+
return None
|
|
118
|
+
copied: bool = isinstance(value, ast.Name) and value.id in aliases
|
|
119
|
+
named: str | None = (
|
|
120
|
+
hinted.type_alias(known, target.lineno)
|
|
121
|
+
if copied or composite(value, known, plan, facts.generics)
|
|
122
|
+
else None
|
|
123
|
+
)
|
|
124
|
+
if named is None:
|
|
125
|
+
return None
|
|
126
|
+
always: bool = plan.bound.get(named.partition(".")[0]) in _ALWAYS
|
|
127
|
+
if older and not always:
|
|
128
|
+
return None
|
|
129
|
+
certain: bool = min_python is not None or always
|
|
130
|
+
return (
|
|
131
|
+
Inference(named, _REASON, frozenset({KIND})),
|
|
132
|
+
not certain,
|
|
133
|
+
frozenset() if certain else frozenset({KIND}),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def composite(value: ast.expr, known: Known, plan: ImportPlan, generics: frozenset[str]) -> bool:
|
|
138
|
+
"""Check whether `value` can only be a type made of others: a subscript of a generic, or a union.
|
|
139
|
+
|
|
140
|
+
`generics`: the generic classes the module names (see `Facts.generics`).
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Whether it is.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
head: ast.expr
|
|
147
|
+
left: ast.expr
|
|
148
|
+
right: ast.expr
|
|
149
|
+
match value:
|
|
150
|
+
case ast.Subscript(value=ast.Name() | ast.Attribute() as head):
|
|
151
|
+
return _generic(head, known, plan, generics)
|
|
152
|
+
case ast.BinOp(op=ast.BitOr(), left=left, right=right):
|
|
153
|
+
return all(_member(side, known, plan, generics) for side in (left, right))
|
|
154
|
+
case _:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _generic(
|
|
159
|
+
head: ast.Name | ast.Attribute,
|
|
160
|
+
known: Known,
|
|
161
|
+
plan: ImportPlan,
|
|
162
|
+
generics: frozenset[str],
|
|
163
|
+
) -> bool:
|
|
164
|
+
"""Check whether a subscript's `head` is what only a type's is: a special form, or a generic class.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Whether it is, by what the module's imports say it is; never a name it binds as a value.
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
spelled: str | None = dotted(head)
|
|
171
|
+
if spelled is None or spelled.partition(".")[0] in plan.values:
|
|
172
|
+
return False
|
|
173
|
+
if spelled in _GENERICS:
|
|
174
|
+
return known.is_builtin(spelled)
|
|
175
|
+
origin: str = stdlib.resolved(head, plan.bound) or ""
|
|
176
|
+
return spelled in generics or origin.rpartition(".")[0] in _FORMS
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _member(node: ast.expr, known: Known, plan: ImportPlan, generics: frozenset[str]) -> bool:
|
|
180
|
+
"""Check whether a union's member is a type: `None`, a class, or a type made of others.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Whether it is: a builtin class, or one the module defines or imports from a checked file.
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
name: str
|
|
187
|
+
match node:
|
|
188
|
+
case ast.Constant(value=None):
|
|
189
|
+
return True
|
|
190
|
+
case ast.Name(id=name):
|
|
191
|
+
return (name in _CLASSES and known.is_builtin(name)) or (
|
|
192
|
+
name in known.classes and name not in plan.values
|
|
193
|
+
)
|
|
194
|
+
case _:
|
|
195
|
+
return composite(node, known, plan, generics)
|
|
Binary file
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a function's body says of its calls: whether it's a generator, and whether it can fall off its end."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from constricter.rules.syntax import FunctionDef, Start, has_within, own_nodes
|
|
10
|
+
from constricter.rules.walked import of_type
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_generator(module: ast.Module, func: FunctionDef) -> bool:
|
|
14
|
+
"""Check whether `func`, in `module`, is a generator: a `yield` in its own body (not a nested function's).
|
|
15
|
+
|
|
16
|
+
Most functions have no `yield` anywhere in them (see `_yields`): only one with some is walked.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Whether it is.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
return has_within(_yields(module), func) and yields_itself(func)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# By the function alone, not its module too: a cache holding modules keeps checked trees alive, and
|
|
26
|
+
# the garbage collector then looks through them again and again (the standard library's check took
|
|
27
|
+
# 10% longer).
|
|
28
|
+
@lru_cache(maxsize=4096) # asked of the same functions once per round
|
|
29
|
+
def yields_itself(func: FunctionDef) -> bool:
|
|
30
|
+
"""Check whether a `yield` is in `func`'s own body, not a nested function's.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Whether one is.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
return any(isinstance(node, ast.Yield | ast.YieldFrom) for node in own_nodes(func.body))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=4) # asked of the same module's functions, each round
|
|
40
|
+
def _yields(module: ast.Module) -> list[Start]:
|
|
41
|
+
"""Find where each of the module's `yield`s starts.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Them, in source order.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
return sorted(
|
|
48
|
+
(node.lineno, node.col_offset)
|
|
49
|
+
for node in cast("list[ast.expr]", of_type(module, ast.Yield, ast.YieldFrom))
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def terminates(body: Sequence[ast.stmt]) -> bool:
|
|
54
|
+
"""Check that running `body` can't reach its end: it always returns or raises first.
|
|
55
|
+
|
|
56
|
+
Its last statement returns or raises, or is an `if` or `try` whose every way through does (a
|
|
57
|
+
`try`'s body with its `else`, and each handler), or a `with` whose body does.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Whether it can't.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
if not body:
|
|
64
|
+
return False
|
|
65
|
+
last: ast.stmt = body[-1]
|
|
66
|
+
match last:
|
|
67
|
+
case ast.Return() | ast.Raise():
|
|
68
|
+
return True
|
|
69
|
+
case ast.If():
|
|
70
|
+
return terminates(last.body) and terminates(last.orelse)
|
|
71
|
+
case ast.With() | ast.AsyncWith():
|
|
72
|
+
return terminates(last.body)
|
|
73
|
+
case ast.Try() | ast.TryStar():
|
|
74
|
+
return terminates(last.orelse or last.body) and all(terminates(h.body) for h in last.handlers)
|
|
75
|
+
case _:
|
|
76
|
+
return False
|
|
Binary file
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--fix` for a function, a bound method or a lambda bound to a name: `dump = json.dumps`, `grow = it.grow`.
|
|
3
|
+
|
|
4
|
+
A `Callable[..., R]`, `R` what a call of it gives whatever it's passed: a function declaring its
|
|
5
|
+
return (the module's, another checked file's, a builtin's or the standard library's with one), or
|
|
6
|
+
a method one does on a receiver whose type is known. Its parameters are left open (`...`): a
|
|
7
|
+
`Callable[[A], R]` would refuse the keywords and defaults a call through the name may use; they're
|
|
8
|
+
listed where the function has neither, and no call through the name passes one (`_taken`). Not a
|
|
9
|
+
class (a `type[C]`), a callee typed only by its `return`s, nor a name the function reads an
|
|
10
|
+
attribute of (`run.cache_clear()`), which a `Callable` doesn't have. A lambda is one of what its
|
|
11
|
+
body gives, where that rests on none of its parameters or its function's names: `Callable[[], R]`
|
|
12
|
+
if it takes none; one resting on its parameters, by what every call of it in its function passes
|
|
13
|
+
them (`called`). A module's or a class body's name is one too: a plain class's, as a guess.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import builtins
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
20
|
+
from itertools import islice
|
|
21
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
22
|
+
|
|
23
|
+
from constricter.empty import Empty
|
|
24
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
25
|
+
from constricter.fix.libraries import stdlib
|
|
26
|
+
from constricter.fix.values.fills import Uses
|
|
27
|
+
from constricter.fix.values.guesses import fixed_by_callee
|
|
28
|
+
from constricter.fix.values.inference import inference
|
|
29
|
+
from constricter.rules.annotations import dotted, roots
|
|
30
|
+
from constricter.rules.declarations import defined_type_vars
|
|
31
|
+
from constricter.rules.syntax import FunctionDef
|
|
32
|
+
from constricter.rules.walked import classes, walk
|
|
33
|
+
|
|
34
|
+
KIND: Final = "callable" # the fix kind of a function or method bound to a name
|
|
35
|
+
_CALLERS: Final = "callers" # and of what every call passes a parameter (see `constricter.fix.index.callers`)
|
|
36
|
+
_CALLABLE: Final = "collections.abc.Callable"
|
|
37
|
+
_OPEN: Final = "..." # a `Callable`'s parameters, left open
|
|
38
|
+
_SELF: Final = "Self"
|
|
39
|
+
_Infer: TypeAlias = Callable[[ast.expr], Inference | None]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Site(NamedTuple):
|
|
43
|
+
"""Where a name is bound to a callable.
|
|
44
|
+
|
|
45
|
+
`function`: the function it's bound in (`None`: a module's or a class's body); `owner`: a class
|
|
46
|
+
body's class; `attributed`: the names the module reads an attribute of, anywhere; `positional`:
|
|
47
|
+
its functions' parameters' types, where a `Callable` can list them (see `signatures`).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
function: FunctionDef | None
|
|
51
|
+
owner: str | None = None
|
|
52
|
+
attributed: frozenset[str] = frozenset()
|
|
53
|
+
positional: Mapping[str, tuple[str, ...]] = Empty()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def signatures(tree: ast.Module) -> dict[str, tuple[str, ...]]:
|
|
57
|
+
"""Read what each of the module's functions takes, where a `Callable` can say it: `[int, str]`.
|
|
58
|
+
|
|
59
|
+
A plain `def` at its top level, or (as `Class.method`, less its first parameter) directly in a
|
|
60
|
+
class, defined once and undecorated, every parameter positional, annotated and without a
|
|
61
|
+
default, none naming a type variable or `Self`.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Each one's parameters' types, in order.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
unusable: frozenset[str] = defined_type_vars(tree) | {_SELF}
|
|
68
|
+
found: dict[str, tuple[str, ...]] = _signatures(tree.body, "", unusable)
|
|
69
|
+
owners: Counter[str] = Counter(node.name for node in classes(tree))
|
|
70
|
+
node: ast.ClassDef
|
|
71
|
+
for node in classes(tree):
|
|
72
|
+
if owners[node.name] == 1:
|
|
73
|
+
found.update(_signatures(node.body, f"{node.name}.", unusable))
|
|
74
|
+
return found
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _signatures(
|
|
78
|
+
body: Sequence[ast.stmt],
|
|
79
|
+
prefix: str,
|
|
80
|
+
unusable: frozenset[str],
|
|
81
|
+
) -> dict[str, tuple[str, ...]]:
|
|
82
|
+
"""Read what the functions directly in `body` take (see `signatures`); `prefix`: a class's, its methods'.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Each one's parameters' types, by its name after `prefix`.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
counts: Counter[str] = Counter(
|
|
89
|
+
stmt.name for stmt in body if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
90
|
+
)
|
|
91
|
+
found: dict[str, tuple[str, ...]] = {}
|
|
92
|
+
stmt: ast.stmt
|
|
93
|
+
for stmt in body:
|
|
94
|
+
if not isinstance(stmt, ast.FunctionDef) or counts[stmt.name] != 1 or stmt.decorator_list:
|
|
95
|
+
continue
|
|
96
|
+
args: ast.arguments = stmt.args
|
|
97
|
+
skipped: int = 1 if prefix else 0 # a method's `self`
|
|
98
|
+
named: list[ast.arg] = [*args.posonlyargs, *args.args][skipped:]
|
|
99
|
+
types: list[str] = [ast.unparse(arg.annotation) for arg in named if arg.annotation is not None]
|
|
100
|
+
plain: bool = not (args.defaults or args.kwonlyargs or args.vararg or args.kwarg)
|
|
101
|
+
if plain and len(types) == len(named) and not unusable & roots(f"[{', '.join(types)}]"):
|
|
102
|
+
found[f"{prefix}{stmt.name}"] = tuple(types)
|
|
103
|
+
return found
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def aliased(
|
|
107
|
+
target: ast.Name,
|
|
108
|
+
value: ast.expr,
|
|
109
|
+
site: Site,
|
|
110
|
+
known: Known,
|
|
111
|
+
typed: tuple[Mapping[str, str], _Infer],
|
|
112
|
+
) -> tuple[Inference, ast.expr] | None:
|
|
113
|
+
"""Infer a name bound to a function, a bound method or a lambda, at `site`: a `Callable[..., R]`.
|
|
114
|
+
|
|
115
|
+
`typed`: the scope's typed names, and what infers a value with them. Its parameters are listed
|
|
116
|
+
(`Callable[[A], R]`) where the function declares them all positional, and every call through
|
|
117
|
+
the name in its function passes them so.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The inference, and the call that gives `R` (whose guesses are its own); `None` for any other
|
|
121
|
+
value, one whose call's type its arguments decide or nothing declares, a class, a module that
|
|
122
|
+
can't name `Callable`, or a name an attribute is read of.
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(value, ast.Lambda):
|
|
126
|
+
return _lambda(target, value, site, known)
|
|
127
|
+
if not isinstance(value, ast.Name | ast.Attribute) or _is_class(value, known):
|
|
128
|
+
return None
|
|
129
|
+
call: ast.Call = ast.copy_location(ast.Call(value, [], []), value)
|
|
130
|
+
fixed: bool = fixed_by_callee(call, known, typed[0]) or (
|
|
131
|
+
stdlib.resolved(value, known.names.stdlib) in stdlib.RETURNS
|
|
132
|
+
)
|
|
133
|
+
found: Inference | None = typed[1](call) if fixed else _method(value, site, known)
|
|
134
|
+
plan: ImportPlan | None = known.names.plan
|
|
135
|
+
if found is None or plan is None or _read(target.id, site):
|
|
136
|
+
return None
|
|
137
|
+
spelled: str | None
|
|
138
|
+
if (spelled := plan.spell(_CALLABLE)) is None:
|
|
139
|
+
return None
|
|
140
|
+
taken: str = _taken(target.id, value, site, known, typed[1])
|
|
141
|
+
reason: str = f"`{ast.unparse(value)}`, whose call gives {found.reason}"
|
|
142
|
+
return Inference(f"{spelled}[{taken}, {found.annotation}]", reason, found.kinds | {KIND}), call
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _method(value: ast.expr, site: Site, known: Known) -> Inference | None:
|
|
146
|
+
"""Type a call of one of a class's own methods, named in its body (`items = _items`).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Its declared return, or `None` for anything else, anywhere else.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
declared: str | None = (
|
|
153
|
+
known.methods.get(site.owner or "", {}).get(value.id) if isinstance(value, ast.Name) else None
|
|
154
|
+
)
|
|
155
|
+
if declared is None or not isinstance(value, ast.Name):
|
|
156
|
+
return None
|
|
157
|
+
return Inference(declared, f"`{site.owner}.{value.id}`'s declared return type", frozenset({"method"}))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _taken(name: str, value: ast.Name | ast.Attribute, site: Site, known: Known, infer: _Infer) -> str:
|
|
161
|
+
"""Write what a callable bound to `name` takes: its parameters' types, or `...`.
|
|
162
|
+
|
|
163
|
+
Listed for one of the module's functions, or a method of its classes on a receiver whose type
|
|
164
|
+
is known, that `signatures` reads, bound in a function whose every call through the name passes
|
|
165
|
+
as many arguments, by position.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A `Callable`'s first argument.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
key: str | None = None
|
|
172
|
+
owner: Inference | None = None if isinstance(value, ast.Name) else infer(value.value)
|
|
173
|
+
if isinstance(value, ast.Name):
|
|
174
|
+
key = value.id
|
|
175
|
+
elif owner is not None:
|
|
176
|
+
key = f"{known.class_side.lineage.definer(owner.annotation, value.attr)}.{value.attr}"
|
|
177
|
+
taken: tuple[str, ...] | None = site.positional.get(key or "")
|
|
178
|
+
if taken is None or site.function is None:
|
|
179
|
+
return _OPEN
|
|
180
|
+
calls: list[ast.Call] = [
|
|
181
|
+
node
|
|
182
|
+
for node in walk(site.function)
|
|
183
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == name
|
|
184
|
+
]
|
|
185
|
+
positional: bool = all(
|
|
186
|
+
not call.keywords
|
|
187
|
+
and len(call.args) == len(taken)
|
|
188
|
+
and not any(isinstance(arg, ast.Starred) for arg in call.args)
|
|
189
|
+
for call in calls
|
|
190
|
+
)
|
|
191
|
+
return f"[{', '.join(taken)}]" if positional else _OPEN
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _lambda(
|
|
195
|
+
target: ast.Name,
|
|
196
|
+
value: ast.Lambda,
|
|
197
|
+
site: Site,
|
|
198
|
+
known: Known,
|
|
199
|
+
) -> tuple[Inference, ast.expr] | None:
|
|
200
|
+
"""Infer a name bound to a lambda: a `Callable` of what its body gives, whatever its names hold.
|
|
201
|
+
|
|
202
|
+
`Callable[[], R]` for one that takes nothing, `Callable[..., R]` otherwise: nothing types its
|
|
203
|
+
parameters, and its function's names are read only when it's called, so its body's type rests
|
|
204
|
+
on neither.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The inference, and the body (whose guesses are its own); or `None` (see `aliased`).
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
args: ast.arguments = value.args
|
|
211
|
+
params: bool = any((args.posonlyargs, args.args, args.kwonlyargs, args.vararg, args.kwarg))
|
|
212
|
+
# With none of the function's names: it reads them when it's called, whatever they hold then.
|
|
213
|
+
found: Inference | None = inference(value.body, known, {})
|
|
214
|
+
plan: ImportPlan | None = known.names.plan
|
|
215
|
+
spelled: str | None = None if plan is None or found is None else plan.spell(_CALLABLE)
|
|
216
|
+
if found is None or spelled is None or _read(target.id, site):
|
|
217
|
+
return None
|
|
218
|
+
taken: str = _OPEN if params else "[]"
|
|
219
|
+
reason: str = f"a lambda, whose body is {found.reason}"
|
|
220
|
+
return Inference(f"{spelled}[{taken}, {found.annotation}]", reason, found.kinds | {KIND}), value.body
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def called(
|
|
224
|
+
value: ast.Lambda,
|
|
225
|
+
name: str,
|
|
226
|
+
uses: Uses,
|
|
227
|
+
known: Known,
|
|
228
|
+
declared: Mapping[str, str],
|
|
229
|
+
) -> tuple[Inference, list[ast.expr]] | None:
|
|
230
|
+
"""Infer a lambda bound to `name` whose body rests on its parameters, by what its calls pass them.
|
|
231
|
+
|
|
232
|
+
Every use of the name in its function (`uses`) a call passing each parameter by position, each
|
|
233
|
+
one's arguments of one type: its body is typed with those, and none of the function's names. A
|
|
234
|
+
lambda of plain positional parameters, without defaults. A guess (`callers`): a call added
|
|
235
|
+
later may pass another type.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The inference (a `Callable[..., R]`), and the arguments it rests on; or `None`.
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
args: ast.arguments = value.args
|
|
242
|
+
params: list[ast.arg] = [*args.posonlyargs, *args.args]
|
|
243
|
+
passed: list[ast.expr] | None = _passed(name, uses, len(params))
|
|
244
|
+
if args.defaults or args.kwonlyargs or args.vararg or args.kwarg or name in uses.nested:
|
|
245
|
+
return None
|
|
246
|
+
if not (params and passed):
|
|
247
|
+
return None
|
|
248
|
+
given: list[Inference | None] = [inference(arg, known, declared) for arg in passed]
|
|
249
|
+
typed: dict[str, str | None] = {
|
|
250
|
+
param.arg: _one(
|
|
251
|
+
{None if each is None else each.annotation for each in islice(given, at, None, len(params))},
|
|
252
|
+
)
|
|
253
|
+
for at, param in enumerate(params)
|
|
254
|
+
}
|
|
255
|
+
found: Inference | None = (
|
|
256
|
+
None
|
|
257
|
+
if None in typed.values()
|
|
258
|
+
else inference(value.body, known, {param: text for param, text in typed.items() if text})
|
|
259
|
+
)
|
|
260
|
+
plan: ImportPlan | None = known.names.plan
|
|
261
|
+
spelled: str | None = None if plan is None or found is None else plan.spell(_CALLABLE)
|
|
262
|
+
if found is None or spelled is None:
|
|
263
|
+
return None
|
|
264
|
+
reason: str = f"a lambda, whose body is {found.reason}, given what its calls pass it"
|
|
265
|
+
kinds: frozenset[str] = found.kinds.union({KIND, _CALLERS}, *(each.kinds for each in given if each))
|
|
266
|
+
return Inference(f"{spelled}[{_OPEN}, {found.annotation}]", reason, kinds), passed
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _one(types: set[str | None]) -> str | None: # the one type every call passes, if they agree
|
|
270
|
+
return next(iter(types)) if len(types) == 1 else None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _passed(name: str, uses: Uses, count: int) -> list[ast.expr] | None:
|
|
274
|
+
"""List what every call of `name` in its function passes it: `count` arguments each, by position.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
The arguments, call after call; `None` where a use isn't such a call.
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
found: list[ast.expr] = []
|
|
281
|
+
node: ast.Name
|
|
282
|
+
for node in uses.names.get(name, []):
|
|
283
|
+
parent: ast.AST | None = uses.parents.get(id(node))
|
|
284
|
+
if (
|
|
285
|
+
not isinstance(parent, ast.Call)
|
|
286
|
+
or parent.func is not node
|
|
287
|
+
or parent.keywords
|
|
288
|
+
or len(parent.args) != count
|
|
289
|
+
or any(isinstance(arg, ast.Starred) for arg in parent.args)
|
|
290
|
+
):
|
|
291
|
+
return None
|
|
292
|
+
found.extend(parent.args)
|
|
293
|
+
return found
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _is_class(value: ast.expr, known: Known) -> bool:
|
|
297
|
+
"""Check whether a name is a class the module knows, or a builtin one it doesn't rebind.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Whether it is: calling it constructs one, and the name is a `type[C]`.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
name: str | None
|
|
304
|
+
if (name := dotted(value)) is None:
|
|
305
|
+
return False
|
|
306
|
+
return name in known.classes or (known.is_builtin(name) and isinstance(vars(builtins).get(name), type))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _read(name: str, site: Site) -> bool:
|
|
310
|
+
"""Check whether an attribute of `name` is read where it's bound (`name.cache_clear()`, `name.__name__`).
|
|
311
|
+
|
|
312
|
+
In its function; anywhere in the module, for a module's or a class body's name.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Whether one is.
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
if site.function is None:
|
|
319
|
+
return name in site.attributed
|
|
320
|
+
return any(
|
|
321
|
+
isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == name
|
|
322
|
+
for node in walk(site.function)
|
|
323
|
+
)
|
|
Binary file
|