python-constricter 0.4.1__cp312-cp312-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__.cp312-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp312-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp312-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp312-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp312-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp312-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp312-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp312-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp312-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp312-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp312-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp312-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp312-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp312-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp312-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp312-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp312-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp312-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp312-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp312-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp312-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp312-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp312-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp312-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp312-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp312-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp312-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp312-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp312-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp312-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp312-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__.cp312-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp312-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp312-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp312-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp312-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp312-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp312-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp312-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp312-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp312-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp312-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp312-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp312-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp312-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp312-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp312-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp312-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp312-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp312-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp312-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp312-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp312-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp312-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp312-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__.cp312-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp312-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp312-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp312-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp312-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp312-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp312-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp312-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp312-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp312-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp312-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp312-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp312-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp312-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp312-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp312-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp312-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp312-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp312-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp312-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp312-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp312-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp312-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp312-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp312-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,677 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Where a type checker sees a value otherwise than `--fix` infers it, and what `--fix` does then.
|
|
3
|
+
|
|
4
|
+
- A copy, attribute or subscript of a union may be narrowed where it's read (`if x is not None:`,
|
|
5
|
+
`isinstance`), which a type checker sees and `--fix` doesn't; so may a comprehension's elements, by
|
|
6
|
+
its condition (`[c for c in cs if isinstance(c, Column)]`, whatever their type), and a read whose
|
|
7
|
+
type a call takes as its own (`deque([x])`). Their fixes are guesses.
|
|
8
|
+
- An ALL_CAPS module-level name is a constant to pyright, which keeps its literal's type
|
|
9
|
+
(`Literal["r"]`) where `str` would widen it: a guess too, where the module passes it to a call
|
|
10
|
+
or a default (see `passed`), for a parameter that may take only some values.
|
|
11
|
+
- `self`, and a method declared to return `Self` called on `self` or `cls`, are `Self`, not the class
|
|
12
|
+
(a subclass's `self` isn't its base): written as the module already spells `Self`, or not at all.
|
|
13
|
+
- A generic class the module defines, written bare (`list[Mapper]`, as `self` makes one), is missing
|
|
14
|
+
its type arguments: not offered.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import bisect
|
|
19
|
+
from collections.abc import Mapping, Sequence
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from functools import lru_cache
|
|
22
|
+
from types import MappingProxyType
|
|
23
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
24
|
+
|
|
25
|
+
from constricter.fix.core.imports import checking, inner_imports, nested_classes, rebound_names
|
|
26
|
+
from constricter.fix.core.known import ImportPlan, Inference, Passed
|
|
27
|
+
from constricter.fix.values.narrowed import Regions, regions
|
|
28
|
+
from constricter.rules.annotations import node_name, roots
|
|
29
|
+
from constricter.rules.declarations import generic_classes
|
|
30
|
+
from constricter.rules.flow import members
|
|
31
|
+
from constricter.rules.quoted import parsed
|
|
32
|
+
from constricter.rules.syntax import FunctionDef, Start, within
|
|
33
|
+
from constricter.rules.walked import classes, of_type, walk
|
|
34
|
+
|
|
35
|
+
# What a copy, an attribute and a subscript of a narrowable union rest on, as guesses.
|
|
36
|
+
_Read: TypeAlias = type[ast.expr]
|
|
37
|
+
_READS: Final[dict[_Read, str]] = {
|
|
38
|
+
ast.Name: "copy",
|
|
39
|
+
ast.Attribute: "attribute",
|
|
40
|
+
ast.Subscript: "subscript",
|
|
41
|
+
}
|
|
42
|
+
# Where a function tests a value, a type checker may narrow it (`isinstance`, `is None`, a `TypeGuard`).
|
|
43
|
+
_TESTS: Final = (ast.If, ast.While, ast.Assert, ast.IfExp, ast.comprehension, ast.Match)
|
|
44
|
+
_SELF: Final = "Self"
|
|
45
|
+
_STDLIB: Final = "stdlib" # the fix kind of what typeshed declares
|
|
46
|
+
_COMPREHENSION: Final = "comprehension"
|
|
47
|
+
_LITERAL: Final = "literal"
|
|
48
|
+
_NONE: Final = "None"
|
|
49
|
+
_OPTIONAL: Final = "Optional"
|
|
50
|
+
_COMPREHENSIONS: Final = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
51
|
+
_Comprehension: TypeAlias = ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp
|
|
52
|
+
_UNIONS: Final = frozenset({"Optional", "Union"})
|
|
53
|
+
_SELF_ORIGINS: Final = frozenset({"typing.Self", "typing_extensions.Self"})
|
|
54
|
+
_SELF_ITSELF: Final = "{}" # how a value that is `Self` is written, `{}` standing for it
|
|
55
|
+
_CLASS_OF: Final = "type[{}]" # how `type(self)` is
|
|
56
|
+
_INSTANCE: Final = "self" # a method's first parameter: any other name is a classmethod's class
|
|
57
|
+
_EXTENSIONS: Final = "typing_extensions"
|
|
58
|
+
_TYPING: Final = frozenset({"typing", _EXTENSIONS})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, eq=False) # hashed by identity: a module's, cached with each function
|
|
62
|
+
class Tests:
|
|
63
|
+
"""What a module's tests (see `_TESTS`) read, where a type checker may narrow it (see `tests`)."""
|
|
64
|
+
|
|
65
|
+
starts: tuple[Start, ...] = () # where each read's test starts, in source order
|
|
66
|
+
texts: tuple[str, ...] = () # each read, as source text
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Facts(NamedTuple):
|
|
70
|
+
"""What of a module decides what a type checker sees otherwise than `--fix` infers.
|
|
71
|
+
|
|
72
|
+
Each class's methods declared to return a bare `Self` (`self_returns`), its generic classes
|
|
73
|
+
(`generic_classes`), and the names it passes to a call or a default (`passed`).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
selfish: Mapping[str, frozenset[str]] = MappingProxyType({})
|
|
77
|
+
generics: frozenset[str] = frozenset()
|
|
78
|
+
passed: frozenset[str] = frozenset()
|
|
79
|
+
# What it imports under a top-level `if` on a flag (`TYPE_CHECKING`), unbound when it runs.
|
|
80
|
+
checking: frozenset[str] = frozenset()
|
|
81
|
+
tests: Tests = Tests() # what its tests read (see `tests`)
|
|
82
|
+
narrowed: Regions = MappingProxyType({}) # where each value is narrowed (see `narrowed.regions`)
|
|
83
|
+
inner: tuple[int, ...] = () # the lines functions and lambdas start on, sorted (see `inner_starts`)
|
|
84
|
+
# The functions `@contextmanager` makes context managers, its own and those it imports: what
|
|
85
|
+
# `with` gives of each, and what that rests on if it's a guess (see `entered`).
|
|
86
|
+
managers: Mapping[str, Passed] = MappingProxyType({})
|
|
87
|
+
type_vars: frozenset[str] = frozenset() # its type variables, its own and those it imports
|
|
88
|
+
# Those each class's bases name (`class Row(Generic[_TP])`), bound in its methods.
|
|
89
|
+
bound: Mapping[str, frozenset[str]] = MappingProxyType({})
|
|
90
|
+
# The names it binds more than once, and where (see `imports.rebound_names`): all a function
|
|
91
|
+
# can shadow, each only where one of its bindings is.
|
|
92
|
+
rebound: Mapping[str, Sequence[Start]] | None = None
|
|
93
|
+
# Where its functions' own imports start, in source order (see `imports.inner_imports`).
|
|
94
|
+
lazy: Sequence[Start] | None = None
|
|
95
|
+
# The other checked files' functions it calls that declare no return (see `Outside.untyped`).
|
|
96
|
+
untyped: frozenset[str] = frozenset()
|
|
97
|
+
# The names it reads an attribute of (`run`, in `run.cache_clear()`), anywhere in it.
|
|
98
|
+
attributed: frozenset[str] = frozenset()
|
|
99
|
+
# Its functions' parameters' types, where a `Callable` can list them (see `callables.signatures`).
|
|
100
|
+
positional: Mapping[str, tuple[str, ...]] = MappingProxyType({})
|
|
101
|
+
# The classes defined in a class's body, and the class naming each (see `imports.nested_classes`).
|
|
102
|
+
nested: Mapping[str, str] = MappingProxyType({})
|
|
103
|
+
excused: tuple[int, ...] = () # the lines (from 0) a type-ignore comment is on, sorted (`late.excusing`)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def facts(
|
|
107
|
+
tree: ast.Module,
|
|
108
|
+
selfish: Mapping[str, frozenset[str]],
|
|
109
|
+
generics: frozenset[str],
|
|
110
|
+
managers: Mapping[str, Passed],
|
|
111
|
+
type_vars: frozenset[str],
|
|
112
|
+
) -> Facts:
|
|
113
|
+
"""Read a module's `Facts`.
|
|
114
|
+
|
|
115
|
+
`selfish`: its `self_returns`; `generics`: the generic classes it names that others define;
|
|
116
|
+
`managers`: its `entered.managers`, and those it imports; `type_vars`: its type variables.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Them.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
return Facts(
|
|
123
|
+
selfish,
|
|
124
|
+
generic_classes(tree) | generics,
|
|
125
|
+
passed(tree),
|
|
126
|
+
checking(tree),
|
|
127
|
+
tests(tree),
|
|
128
|
+
regions(tree),
|
|
129
|
+
inner_starts(tree),
|
|
130
|
+
managers,
|
|
131
|
+
type_vars,
|
|
132
|
+
_bound_vars(tree, type_vars),
|
|
133
|
+
rebound_names(tree),
|
|
134
|
+
[(stmt.lineno, stmt.col_offset) for stmt in inner_imports(tree)],
|
|
135
|
+
nested=nested_classes(tree),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _bound_vars(tree: ast.Module, type_vars: frozenset[str]) -> dict[str, frozenset[str]]:
|
|
140
|
+
"""Map each class to the type variables its bases name, which its methods may use.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Each such class's name, and them.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
found: dict[str, frozenset[str]] = {}
|
|
147
|
+
node: ast.ClassDef
|
|
148
|
+
for node in classes(tree) if type_vars else ():
|
|
149
|
+
named: frozenset[str] = frozenset(
|
|
150
|
+
name.id for base in node.bases for name in walk(base) if isinstance(name, ast.Name)
|
|
151
|
+
)
|
|
152
|
+
if named & type_vars:
|
|
153
|
+
found[node.name] = found.get(node.name, frozenset()) | (named & type_vars)
|
|
154
|
+
return found
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def undeclared(
|
|
158
|
+
annotation: str,
|
|
159
|
+
function: FunctionDef | None,
|
|
160
|
+
known: frozenset[str],
|
|
161
|
+
bound: frozenset[str],
|
|
162
|
+
) -> bool:
|
|
163
|
+
"""Check whether an annotation names a type variable its function doesn't declare.
|
|
164
|
+
|
|
165
|
+
One of the module's (`known`) that neither the function's signature names nor its class binds
|
|
166
|
+
(`bound`) is unbound there, to a type checker (`proc: type[Row[_TP]]`); a module body declares
|
|
167
|
+
none.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Whether it does.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
named: frozenset[str] = roots(annotation) & known
|
|
174
|
+
if not named or function is None:
|
|
175
|
+
return bool(named)
|
|
176
|
+
declared: set[str] = {
|
|
177
|
+
node.id for part in _signature(function) for node in ast.walk(part) if isinstance(node, ast.Name)
|
|
178
|
+
}
|
|
179
|
+
return not named <= (bound | declared)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Owner(NamedTuple):
|
|
183
|
+
"""The class a method belongs to, the name its instance (or class) goes by, and its `Self` methods."""
|
|
184
|
+
|
|
185
|
+
name: str
|
|
186
|
+
first: str # `self`, or a classmethod's first parameter
|
|
187
|
+
selfish: frozenset[str] # its methods declared to return a bare `Self`, those it inherits too
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def doubts(value: ast.expr, found: Inference, *, constant: bool, narrowed: frozenset[str]) -> frozenset[str]:
|
|
191
|
+
"""Find what makes `found`, a certain inference of `value`, a guess (see the module docstring).
|
|
192
|
+
|
|
193
|
+
`constant`: whether the value is bound to an ALL_CAPS module-level name; `narrowed`: what the
|
|
194
|
+
function tests somewhere (`isinstance(x, C)`, `x is None`, `is_c(x)`), where a type checker
|
|
195
|
+
narrows it, whatever its type: the value itself, or a read whose type it takes (`found.reads`).
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The guessing mechanisms (`FIX_KINDS`) it then rests on; none if it stays certain.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
kind: str | None = _READS.get(type(value))
|
|
202
|
+
if kind is not None and (ast.unparse(value) in narrowed or len(members(found.annotation) or ()) > 1):
|
|
203
|
+
return frozenset({kind})
|
|
204
|
+
if narrowed.intersection(found.reads):
|
|
205
|
+
return frozenset({_READS[ast.Name]})
|
|
206
|
+
if (
|
|
207
|
+
isinstance(value, _COMPREHENSIONS)
|
|
208
|
+
and any(g.ifs for g in value.generators)
|
|
209
|
+
and (_has_union(found) or _sifted(value))
|
|
210
|
+
):
|
|
211
|
+
return frozenset({_COMPREHENSION})
|
|
212
|
+
if constant and _literal(value):
|
|
213
|
+
return frozenset({_LITERAL})
|
|
214
|
+
return frozenset()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def narrowed_first(value: ast.expr, found: Inference) -> bool:
|
|
218
|
+
"""Check whether code reading `value` almost always narrows it first, so `found` is best not offered.
|
|
219
|
+
|
|
220
|
+
A copy, attribute or subscript of an `X | None`, and a filtered comprehension over one, is nearly
|
|
221
|
+
always checked for `None` before it's used (an `assert`, an early `return`, a walrus), and a
|
|
222
|
+
checker then takes it for the `X` it's narrowed to: declaring the union breaks that (but see
|
|
223
|
+
`as_declared`). A bare `None` (a copy of a name only ever `None`) says nothing.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Whether it is.
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
if found.annotation == _NONE:
|
|
230
|
+
return True
|
|
231
|
+
optional: bool = _NONE in (members(found.annotation) or ())
|
|
232
|
+
filtered: bool = isinstance(value, _COMPREHENSIONS) and any(g.ifs for g in value.generators)
|
|
233
|
+
return (isinstance(value, tuple(_READS)) and optional) or (filtered and _has_none(found.annotation))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def as_declared(value: ast.expr, function: FunctionDef | None, found: Tests) -> bool:
|
|
237
|
+
"""Check whether a read (`x`, `self.x`, `d[k]`) has, to a type checker, the type it's declared.
|
|
238
|
+
|
|
239
|
+
Nothing in its function narrows it: no test of it or of what it's read of (`self.conn`, in
|
|
240
|
+
`self.conn.pool`), and no store of either, which narrows as a test does.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Whether it has; not for a module's or a class body's read.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
if function is None or not isinstance(value, tuple(_READS)):
|
|
247
|
+
return False
|
|
248
|
+
text: str = ast.unparse(value)
|
|
249
|
+
return not any(
|
|
250
|
+
text == each or text.startswith((f"{each}.", f"{each}["))
|
|
251
|
+
for each in tested(function, found) | _stored(function)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@lru_cache(maxsize=64) # asked of each read of an `X | None` in the function's scope
|
|
256
|
+
def _stored(function: FunctionDef) -> frozenset[str]:
|
|
257
|
+
"""List what a function stores, the functions inside it too: `x`, `self.x`, `d[k]`.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Each, as source text.
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
return frozenset(
|
|
264
|
+
ast.unparse(node)
|
|
265
|
+
for node in walk(function)
|
|
266
|
+
if isinstance(node, ast.Name | ast.Attribute | ast.Subscript) and not isinstance(node.ctx, ast.Load)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _sifted(value: _Comprehension) -> bool:
|
|
271
|
+
"""Check whether a comprehension keeps only what's of some class: `[x for x in xs if isinstance(x, C)]`.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Whether one of its conditions is an `isinstance` of a name it binds: its elements are narrowed.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
bound: set[str] = {
|
|
278
|
+
node.id for each in value.generators for node in ast.walk(each.target) if isinstance(node, ast.Name)
|
|
279
|
+
}
|
|
280
|
+
return any(
|
|
281
|
+
_instance_checked(node) in bound
|
|
282
|
+
for each in value.generators
|
|
283
|
+
for test in each.ifs
|
|
284
|
+
for node in ast.walk(test)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _instance_checked(node: ast.AST) -> str: # the `x` of `isinstance(x, C)`, else nothing
|
|
289
|
+
name: str
|
|
290
|
+
match node:
|
|
291
|
+
case ast.Call(func=ast.Name(id="isinstance"), args=[ast.Name(id=name), *_]):
|
|
292
|
+
return name
|
|
293
|
+
case _:
|
|
294
|
+
return ""
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _has_none(annotation: str) -> bool:
|
|
298
|
+
"""Check whether an annotation allows `None` anywhere in it (`list[int | None]`, `list[Optional[str]]`).
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Whether it does.
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
# `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
305
|
+
return any(
|
|
306
|
+
(isinstance(node, ast.Constant) and node.value is None)
|
|
307
|
+
or (isinstance(node, ast.Name | ast.Attribute) and node_name(node) == _OPTIONAL)
|
|
308
|
+
for node in ast.walk(ast.parse(annotation, mode="eval"))
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def inner_starts(tree: ast.Module) -> tuple[int, ...]:
|
|
313
|
+
"""List the lines the module's functions and lambdas start on, for `contains_inner` to look up.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Them, sorted.
|
|
317
|
+
|
|
318
|
+
"""
|
|
319
|
+
inner: list[FunctionDef | ast.Lambda] = cast(
|
|
320
|
+
"list[FunctionDef | ast.Lambda]",
|
|
321
|
+
of_type(tree, ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda),
|
|
322
|
+
)
|
|
323
|
+
return tuple(sorted(node.lineno for node in inner))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def contains_inner(function: FunctionDef, starts: tuple[int, ...]) -> bool:
|
|
327
|
+
"""Check whether a function has a function or lambda inside it, by the lines they start on.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Whether one starts after its own line and by its last: most functions have none, and needn't
|
|
331
|
+
be walked for one.
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
return bisect.bisect_right(starts, function.lineno) < bisect.bisect_right(
|
|
335
|
+
starts,
|
|
336
|
+
function.end_lineno or function.lineno,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def tests(tree: ast.Module) -> Tests:
|
|
341
|
+
"""Find what the module's tests read, once for all its functions (see `tested`).
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Each name, attribute and subscript in a test (an `if`'s condition, a comprehension's, a
|
|
345
|
+
`match`'s subject), as source text, by where its test starts.
|
|
346
|
+
|
|
347
|
+
"""
|
|
348
|
+
found: list[tuple[Start, str]] = sorted(
|
|
349
|
+
((test.lineno, test.col_offset), ast.unparse(read))
|
|
350
|
+
for node in of_type(tree, *_TESTS)
|
|
351
|
+
for test in _tested_parts(node)
|
|
352
|
+
for read in walk(test)
|
|
353
|
+
if isinstance(read, ast.Name | ast.Attribute | ast.Subscript)
|
|
354
|
+
)
|
|
355
|
+
return Tests(tuple(start for start, _ in found), tuple(text for _, text in found))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@lru_cache(maxsize=64) # asked by each assignment in the function's scope
|
|
359
|
+
def tested(function: FunctionDef, found: Tests) -> frozenset[str]:
|
|
360
|
+
"""List what a function tests, where a type checker may narrow it: `x`, `self.x`, `d[k]`.
|
|
361
|
+
|
|
362
|
+
Its own tests and those of the functions inside it, its decorators' too; from the module's
|
|
363
|
+
(`found`, see `tests`), by the function's span of the source.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Each name, attribute and subscript in a test, as source text.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
begin: Start | None = (
|
|
370
|
+
(function.decorator_list[0].lineno, function.decorator_list[0].col_offset)
|
|
371
|
+
if function.decorator_list
|
|
372
|
+
else None
|
|
373
|
+
)
|
|
374
|
+
return frozenset(found.texts[within(found.starts, function, begin)])
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@lru_cache(maxsize=64) # asked by each hinted union in the function's scope
|
|
378
|
+
def narrowing(function: FunctionDef) -> frozenset[str]:
|
|
379
|
+
"""List the names a function's tests narrow themselves: `x is None`, `isinstance(x, C)`, `if x`.
|
|
380
|
+
|
|
381
|
+
Not one a test only reads (`if x.ready`), as `tested` lists too.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Them.
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
return frozenset(
|
|
388
|
+
name
|
|
389
|
+
for node in walk(function)
|
|
390
|
+
if isinstance(node, _TESTS)
|
|
391
|
+
for test in _tested_parts(node)
|
|
392
|
+
for name in _narrowed(test)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _narrowed(test: ast.expr) -> list[str]:
|
|
397
|
+
"""Find the names one test narrows, through `and`, `or` and `not`.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Them.
|
|
401
|
+
|
|
402
|
+
"""
|
|
403
|
+
name: str
|
|
404
|
+
values: list[ast.expr]
|
|
405
|
+
operand: ast.expr
|
|
406
|
+
match test:
|
|
407
|
+
case ast.BoolOp(values=values):
|
|
408
|
+
return [name for value in values for name in _narrowed(value)]
|
|
409
|
+
case ast.UnaryOp(op=ast.Not(), operand=operand):
|
|
410
|
+
return _narrowed(operand)
|
|
411
|
+
case (
|
|
412
|
+
ast.Name(id=name)
|
|
413
|
+
| ast.Compare(left=ast.Name(id=name), comparators=[ast.Constant(value=None)])
|
|
414
|
+
| ast.Call(func=ast.Name(id="isinstance"), args=[ast.Name(id=name), *_])
|
|
415
|
+
):
|
|
416
|
+
return [name]
|
|
417
|
+
case _:
|
|
418
|
+
return []
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _tested_parts(node: ast.AST) -> list[ast.expr]:
|
|
422
|
+
"""Find what one of `_TESTS` tests: an `if`'s condition, a comprehension's, a `match`'s subject.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Them.
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
test: ast.expr
|
|
429
|
+
ifs: list[ast.expr]
|
|
430
|
+
match node:
|
|
431
|
+
case ast.If(test=test) | ast.While(test=test) | ast.Assert(test=test) | ast.IfExp(test=test):
|
|
432
|
+
return [test]
|
|
433
|
+
case ast.comprehension(ifs=ifs):
|
|
434
|
+
return list(ifs)
|
|
435
|
+
case _: # a `match` (see `_TESTS`)
|
|
436
|
+
return [cast("ast.Match", node).subject]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def says_self(function: FunctionDef) -> bool:
|
|
440
|
+
"""Check whether a function's signature mentions `Self` (`-> Self`, `other: Self`, `list[Self]`).
|
|
441
|
+
|
|
442
|
+
In any other method, mypy takes `self` for its class, not `Self`.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Whether it does.
|
|
446
|
+
|
|
447
|
+
"""
|
|
448
|
+
return any(
|
|
449
|
+
node_name(node) == _SELF
|
|
450
|
+
for annotation in _signature(function)
|
|
451
|
+
for node in ast.walk(annotation)
|
|
452
|
+
if isinstance(node, ast.Name | ast.Attribute)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _signature(function: FunctionDef) -> list[ast.expr]:
|
|
457
|
+
"""List the annotations of a function's signature: its parameters', and its return's.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Them, one in quotes as the expression it quotes.
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
args: ast.arguments = function.args
|
|
464
|
+
annotations: list[ast.expr | None] = [
|
|
465
|
+
function.returns,
|
|
466
|
+
*(
|
|
467
|
+
arg.annotation
|
|
468
|
+
for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg)
|
|
469
|
+
if arg
|
|
470
|
+
),
|
|
471
|
+
]
|
|
472
|
+
return [parsed(annotation) for annotation in annotations if annotation is not None]
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def passed(tree: ast.Module) -> frozenset[str]:
|
|
476
|
+
"""Find the names a module passes to a call (`f(X)`, `f(code=X)`) or gives a parameter as its default.
|
|
477
|
+
|
|
478
|
+
Where a constant's `Literal` type matters: a parameter may take only some values
|
|
479
|
+
(`code: Literal["invalid-type", ...]`), which `str` doesn't say.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Them.
|
|
483
|
+
|
|
484
|
+
"""
|
|
485
|
+
names: set[str] = set()
|
|
486
|
+
node: ast.AST
|
|
487
|
+
for node in of_type(tree, ast.Call, ast.arguments):
|
|
488
|
+
values: list[ast.expr | None] = (
|
|
489
|
+
[*node.args, *(keyword.value for keyword in node.keywords)]
|
|
490
|
+
if isinstance(node, ast.Call)
|
|
491
|
+
else [*cast("ast.arguments", node).defaults, *cast("ast.arguments", node).kw_defaults]
|
|
492
|
+
)
|
|
493
|
+
names.update(value.id for value in values if isinstance(value, ast.Name))
|
|
494
|
+
return frozenset(names)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def is_constant(name: str) -> bool:
|
|
498
|
+
"""Check whether pyright takes a name for a constant: letters all capitals (`MAX`, `_DEFAULT_MODE`).
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Whether it does.
|
|
502
|
+
|
|
503
|
+
"""
|
|
504
|
+
return name.upper() == name and any(char.isalpha() for char in name)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def corrected(
|
|
508
|
+
value: ast.expr,
|
|
509
|
+
found: Inference,
|
|
510
|
+
owner: Owner | None,
|
|
511
|
+
generics: frozenset[str],
|
|
512
|
+
plan: ImportPlan | None,
|
|
513
|
+
) -> Inference | None:
|
|
514
|
+
"""Write `found` as a type checker sees it: `Self` where the value is one; nothing for a bare generic.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
The inference to offer, or `None` if there's none to.
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
written: str | None
|
|
521
|
+
if (written := None if owner is None else _selfish(value, found, owner)) is not None:
|
|
522
|
+
spelled: str | None = None if plan is None else spelled_self(plan)
|
|
523
|
+
return (
|
|
524
|
+
None
|
|
525
|
+
if spelled is None
|
|
526
|
+
else found._replace(annotation=written.format(spelled), reason=f"{found.reason}: `Self`")
|
|
527
|
+
)
|
|
528
|
+
return None if bare(found.annotation, generics) else found
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _selfish(value: ast.expr, found: Inference, owner: Owner) -> str | None:
|
|
532
|
+
"""Find how `value`'s type is written with `Self`, if it's one typed as the class.
|
|
533
|
+
|
|
534
|
+
`self`, or a `Self` method called on `self`, `cls` or `type(self)`; `type(self)` or
|
|
535
|
+
`self.__class__`, a `type[Self]`; what `cls()`, either of those called, or `__new__` given one
|
|
536
|
+
constructs; a standard-library function's call given `self` that returns its class
|
|
537
|
+
(`copy.copy(self)`); and a conditional of two of them (`self if inplace else self.copy()`).
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Its annotation, `{}` standing for `Self`; or `None` if it isn't one.
|
|
541
|
+
|
|
542
|
+
"""
|
|
543
|
+
receiver: ast.expr
|
|
544
|
+
method: str
|
|
545
|
+
func: ast.expr
|
|
546
|
+
args: list[ast.expr]
|
|
547
|
+
template: str | None = None
|
|
548
|
+
match value:
|
|
549
|
+
case ast.Name() | ast.Call(func=ast.Name()) | ast.Attribute() if (
|
|
550
|
+
template := _own(value, owner.first)
|
|
551
|
+
) is not None:
|
|
552
|
+
pass
|
|
553
|
+
case ast.Call(func=func) if _constructs(func, owner.first):
|
|
554
|
+
template = _SELF_ITSELF
|
|
555
|
+
case ast.Call(func=ast.Attribute(attr="__new__"), args=[func, *_]) if _constructs(func, owner.first):
|
|
556
|
+
template = _SELF_ITSELF
|
|
557
|
+
case ast.Call(func=ast.Attribute(value=receiver, attr=method)) if (
|
|
558
|
+
method in owner.selfish and _own(receiver, owner.first) is not None
|
|
559
|
+
):
|
|
560
|
+
template = _SELF_ITSELF
|
|
561
|
+
# The class only by a type variable its argument binds: the standard library names no other's.
|
|
562
|
+
case ast.Call(args=[*args]) if _STDLIB in found.kinds and any(
|
|
563
|
+
_own(arg, owner.first) == _SELF_ITSELF for arg in args
|
|
564
|
+
):
|
|
565
|
+
template = _SELF_ITSELF
|
|
566
|
+
case ast.IfExp() if {_selfish(side, found, owner) for side in (value.body, value.orelse)} == {
|
|
567
|
+
_SELF_ITSELF,
|
|
568
|
+
}:
|
|
569
|
+
template = _SELF_ITSELF
|
|
570
|
+
case _:
|
|
571
|
+
pass
|
|
572
|
+
return template if template is not None and found.annotation == template.format(owner.name) else None
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _constructs(func: ast.expr, first: str) -> bool:
|
|
576
|
+
"""Check whether a callee is the method's own class: a classmethod's `cls`, or `type(self)`.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
Whether it is.
|
|
580
|
+
|
|
581
|
+
"""
|
|
582
|
+
return (isinstance(func, ast.Name) and func.id == first != _INSTANCE) or _own(func, first) == _CLASS_OF
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _own(value: ast.expr, first: str) -> str | None:
|
|
586
|
+
"""Find how a method's own instance or class is typed with `Self`: `self` or `cls`, or its class.
|
|
587
|
+
|
|
588
|
+
Its class: `type(self)`, or `self.__class__`.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Its annotation, `{}` standing for `Self`; or `None` if `value` is neither.
|
|
592
|
+
|
|
593
|
+
"""
|
|
594
|
+
name: str
|
|
595
|
+
match value:
|
|
596
|
+
case ast.Name(id=name) if name == first:
|
|
597
|
+
return _SELF_ITSELF
|
|
598
|
+
case (
|
|
599
|
+
ast.Call(func=ast.Name(id="type"), args=[ast.Name(id=name)])
|
|
600
|
+
| ast.Attribute(value=ast.Name(id=name), attr="__class__")
|
|
601
|
+
) if name == first:
|
|
602
|
+
return _CLASS_OF
|
|
603
|
+
case _:
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def spelled_self(plan: ImportPlan) -> str | None:
|
|
608
|
+
"""Name `Self` as the module already imports it (`Self`, `te.Self`, `typing.Self`).
|
|
609
|
+
|
|
610
|
+
Through an import for type checking alone too, and `typing_extensions` before `typing`. Not an
|
|
611
|
+
import added for it: `typing.Self` is Python 3.11's, and the module may run on older ones.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
The name, or `None` if it doesn't import it.
|
|
615
|
+
|
|
616
|
+
"""
|
|
617
|
+
bound: dict[str, str] = {**plan.checking.bound, **plan.bound}
|
|
618
|
+
modules: dict[str, str] = {origin: name for name, origin in bound.items() if origin in _TYPING}
|
|
619
|
+
name: str | None = next((name for name, origin in bound.items() if origin in _SELF_ORIGINS), None)
|
|
620
|
+
if name is None and modules:
|
|
621
|
+
name = f"{modules.get(_EXTENSIONS) or next(iter(modules.values()))}.Self"
|
|
622
|
+
return name
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def bare(annotation: str, generics: frozenset[str]) -> bool:
|
|
626
|
+
"""Check whether an annotation names one of `generics` (as the module spells them) unsubscripted.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Whether it does: `Box`, `util.OrderedSet`, but not `Box[int]`.
|
|
630
|
+
|
|
631
|
+
"""
|
|
632
|
+
return bool(generics) and _bare(annotation, generics)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@lru_cache(maxsize=4096) # a module's fixes are a few types, each asked again and again
|
|
636
|
+
def _bare(annotation: str, generics: frozenset[str]) -> bool:
|
|
637
|
+
# `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
638
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
639
|
+
inner: set[int] = {
|
|
640
|
+
id(node.value) for node in walk(tree) if isinstance(node, ast.Subscript | ast.Attribute)
|
|
641
|
+
}
|
|
642
|
+
return any(
|
|
643
|
+
isinstance(node, ast.Name | ast.Attribute) and id(node) not in inner and ast.unparse(node) in generics
|
|
644
|
+
for node in walk(tree)
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _has_union(found: Inference) -> bool:
|
|
649
|
+
"""Check whether an annotation has a union anywhere in it (`list[int | None]`, `Optional[str]`).
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Whether it has.
|
|
653
|
+
|
|
654
|
+
"""
|
|
655
|
+
# `found.annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
656
|
+
return any(
|
|
657
|
+
(isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr))
|
|
658
|
+
or (isinstance(node, ast.Name) and node.id in _UNIONS)
|
|
659
|
+
or (isinstance(node, ast.Attribute) and node.attr in _UNIONS)
|
|
660
|
+
for node in ast.walk(ast.parse(found.annotation, mode="eval"))
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _literal(value: ast.expr) -> bool:
|
|
665
|
+
"""Check whether `value` is a literal whose own type a `Literal` can be (a string, a number, a `bool`).
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
Whether it is.
|
|
669
|
+
|
|
670
|
+
"""
|
|
671
|
+
match value:
|
|
672
|
+
case ast.Constant(value=str() | bytes() | int()): # `bool` is an `int`
|
|
673
|
+
return True
|
|
674
|
+
case ast.UnaryOp(op=ast.USub(), operand=ast.Constant(value=int())):
|
|
675
|
+
return True
|
|
676
|
+
case _:
|
|
677
|
+
return False
|
|
Binary file
|