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.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp313-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp313-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp313-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp313-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp313-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp313-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp313-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp313-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp313-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp313-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp313-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp313-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp313-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp313-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp313-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp313-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp313-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp313-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp313-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp313-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp313-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp313-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp313-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp313-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp313-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp313-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp313-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp313-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp313-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp313-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp313-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp313-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp313-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp313-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp313-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp313-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp313-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp313-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp313-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp313-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp313-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp313-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp313-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp313-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp313-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp313-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp313-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp313-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp313-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp313-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp313-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp313-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp313-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp313-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp313-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp313-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp313-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp313-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp313-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp313-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp313-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp313-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp313-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp313-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp313-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp313-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp313-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp313-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp313-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp313-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp313-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp313-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp313-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp313-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp313-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp313-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp313-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp313-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp313-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp313-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,580 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Fixes that add an import: how a module can name a library type, and where an import goes."""
3
+
4
+ import ast
5
+ import bisect
6
+ import re
7
+ import sys
8
+ from collections.abc import Iterator, Mapping, Sequence
9
+ from itertools import accumulate
10
+ from typing import Final, TypeAlias, cast
11
+ from weakref import WeakKeyDictionary
12
+
13
+ from constricter.fix.core.known import Checking, Guarded, ImportPlan, Origin, Returns
14
+ from constricter.rules.annotations import dotted, roots
15
+ from constricter.rules.syntax import FunctionDef, Start, import_bindings
16
+ from constricter.rules.walked import classes, of_type
17
+
18
+ _TYPE_CHECKING: Final = "TYPE_CHECKING"
19
+ _QUOTES: Final = ("'", '"') # what starts a string in an annotation: a name in one isn't a class's
20
+ _STDLIB: Final = sys.stdlib_module_names
21
+ _FUTURE: Final = "__future__"
22
+ _IMPORTORSKIP: Final = "pytest.importorskip" # a module it names may be missing
23
+ _MODNAME: Final = "modname" # its parameter
24
+ _DOT: Final = "."
25
+ _ANNOTATIONS: Final = "annotations" # `from __future__ import annotations` postpones them all
26
+ # Each module's names, for as long as its tree lives: the index reads them, then the check.
27
+ _Taken: TypeAlias = tuple[frozenset[str], frozenset[str]]
28
+ _TAKEN: Final[WeakKeyDictionary[ast.Module, _Taken]] = WeakKeyDictionary()
29
+ _Rebound: TypeAlias = dict[str, list[Start]]
30
+ _REBOUND: Final[WeakKeyDictionary[ast.Module, _Rebound]] = WeakKeyDictionary()
31
+ _Import: TypeAlias = ast.Import | ast.ImportFrom
32
+ _Inner: TypeAlias = tuple[_Import, ...]
33
+ _INNER: Final[WeakKeyDictionary[ast.Module, _Inner]] = WeakKeyDictionary()
34
+ # The nodes that bind a name: all `_taken` needs look at.
35
+ _BINDERS: Final = (
36
+ ast.Name,
37
+ ast.arg,
38
+ ast.FunctionDef,
39
+ ast.AsyncFunctionDef,
40
+ ast.ClassDef,
41
+ ast.alias,
42
+ ast.ExceptHandler,
43
+ ast.MatchAs,
44
+ )
45
+
46
+
47
+ def plan(tree: ast.Module) -> ImportPlan:
48
+ """Read what `tree` already imports, every name it binds, and where its imports end.
49
+
50
+ Only imports that run count as binding a name to use (not those under `if TYPE_CHECKING:`: a
51
+ module's own annotations are evaluated when it's imported); every name bound anywhere in the
52
+ file is taken, so an added import never shadows one (nor a builtin).
53
+
54
+ Returns:
55
+ A fresh plan for the module's fixes.
56
+
57
+ """
58
+ taken: frozenset[str]
59
+ values: frozenset[str]
60
+ taken, values = taken_names(tree)
61
+ bound: dict[str, str] = _bound(tree)
62
+ defined: dict[str, int] = _defined(tree)
63
+ return ImportPlan(
64
+ bound,
65
+ taken,
66
+ _after(tree),
67
+ defined,
68
+ checking=Checking(
69
+ _block(tree),
70
+ _lazy(tree, bound),
71
+ {
72
+ name: origin
73
+ for stmt in tree.body
74
+ if isinstance(stmt, ast.If) and _is_checking(stmt.test)
75
+ for name, origin, _ in import_bindings(stmt.body)
76
+ },
77
+ ),
78
+ postponed=_postponed(tree),
79
+ values=values,
80
+ )
81
+
82
+
83
+ def nested_classes(tree: ast.Module) -> dict[str, str]:
84
+ """Name the classes defined in another class's body: bare, names of that body alone.
85
+
86
+ Returns:
87
+ Each, mapped to the class that names it anywhere (`_Names`: `Group`, for `Group._Names`): one
88
+ of the module's own body, the only class defining that name; else to nothing.
89
+
90
+ """
91
+ top: frozenset[int] = frozenset(id(stmt) for stmt in tree.body)
92
+ found: dict[str, str] = {}
93
+ node: ast.ClassDef
94
+ for node in classes(tree):
95
+ stmt: ast.stmt
96
+ for stmt in node.body:
97
+ if isinstance(stmt, ast.ClassDef):
98
+ found[stmt.name] = node.name if id(node) in top and stmt.name not in found else ""
99
+ return found
100
+
101
+
102
+ def through(annotation: str, planned: ImportPlan, nested: Mapping[str, str]) -> str:
103
+ """Spell `annotation` for a function: a class of a class's body through that class (`Group._Names`).
104
+
105
+ Returns:
106
+ It, each such class the module binds nowhere else named through its class, where one names it.
107
+
108
+ """
109
+ name: str
110
+ for name in roots(annotation) & nested.keys() - planned.bound.keys() - planned.defined.keys():
111
+ if nested[name] and not any(quote in annotation for quote in _QUOTES):
112
+ annotation = re.sub(rf"(?<![\w.]){re.escape(name)}\b", f"{nested[name]}.{name}", annotation)
113
+ return annotation
114
+
115
+
116
+ def _private(statement: str) -> bool:
117
+ """Check whether an import to add takes a name its module keeps to itself (`from m import _T`).
118
+
119
+ Not the standard library's, whose stubs name such classes in what they declare
120
+ (`unittest.case._AssertRaisesContext`).
121
+
122
+ Returns:
123
+ Whether it does: one underscore before it, not a dunder's two.
124
+
125
+ """
126
+ module: str
127
+ name: str
128
+ module, _, name = statement.removeprefix("from ").rpartition(" import ")
129
+ return name.startswith("_") and not name.startswith("__") and module.partition(".")[0] not in _STDLIB
130
+
131
+
132
+ def unwritable(annotation: str, planned: ImportPlan, nested: Mapping[str, str]) -> bool:
133
+ """Check whether the module can't write `annotation`, whatever it imports.
134
+
135
+ It names a class of `nested` (see `nested_classes`) the module binds nowhere else, which no
136
+ function names bare; or an import it needs would take another module's private name.
137
+
138
+ Returns:
139
+ Whether it can't.
140
+
141
+ """
142
+ if roots(annotation) & nested.keys() - planned.bound.keys() - planned.defined.keys():
143
+ return True
144
+ return any(
145
+ _private(statement)
146
+ for statement in (*guarded_imports(annotation, planned), *imports_of(annotation, planned))
147
+ )
148
+
149
+
150
+ def added_origin(statement: str) -> Origin:
151
+ """Read what an import `ImportPlan.spell` added binds: `from m import T`, or `import m`.
152
+
153
+ Returns:
154
+ Its origin: `("m", "T")`, or `("m", None)`.
155
+
156
+ """
157
+ module: str
158
+ name: str
159
+ module, _, name = statement.removeprefix("from ").removeprefix("import ").partition(" import ")
160
+ return module, name or None
161
+
162
+
163
+ def added_dotted(statement: str) -> str:
164
+ """Spell what an added import binds, dotted: `m.T` for `from m import T`, `m` for `import m`.
165
+
166
+ Returns:
167
+ It.
168
+
169
+ """
170
+ module: str
171
+ name: str | None
172
+ module, name = added_origin(statement)
173
+ return f"{module}.{name}" if name else module
174
+
175
+
176
+ def present(found: ImportPlan, guarded: Mapping[str, Guarded]) -> frozenset[str]:
177
+ """Name those of `guarded` the module imports already as their statements would.
178
+
179
+ Under `if TYPE_CHECKING:`, or to run: a fix of its own may have added either.
180
+
181
+ Returns:
182
+ Each name whose import to add is one the module has.
183
+
184
+ """
185
+ return frozenset(
186
+ name
187
+ for name, each in guarded.items()
188
+ if each.statement is not None
189
+ and added_dotted(each.statement) in {found.checking.bound.get(name), found.bound.get(name)}
190
+ )
191
+
192
+
193
+ def _is_checking(test: ast.expr) -> bool:
194
+ """Check whether an `if`'s test is `TYPE_CHECKING` (or `typing.TYPE_CHECKING`).
195
+
196
+ Returns:
197
+ Whether it is.
198
+
199
+ """
200
+ return (isinstance(test, ast.Name) and test.id == _TYPE_CHECKING) or (
201
+ isinstance(test, ast.Attribute) and test.attr == _TYPE_CHECKING
202
+ )
203
+
204
+
205
+ def _block(tree: ast.Module) -> tuple[int, int]:
206
+ """Find the body of the module's first top-level `if TYPE_CHECKING:` with no `else`.
207
+
208
+ Returns:
209
+ Its first and last line (from 1), or zeros.
210
+
211
+ """
212
+ return next(
213
+ (
214
+ (stmt.body[0].lineno, stmt.end_lineno or stmt.lineno)
215
+ for stmt in tree.body
216
+ if isinstance(stmt, ast.If) and _is_checking(stmt.test) and not stmt.orelse
217
+ ),
218
+ (0, 0),
219
+ )
220
+
221
+
222
+ def checking(tree: ast.Module) -> frozenset[str]:
223
+ """Name what the module imports under a top-level `if` on a flag, which may not run.
224
+
225
+ `if TYPE_CHECKING:`, and any other name tested alone (`if MYPY_CHECK_RUNNING:`): what it imports
226
+ may be unbound when the module runs.
227
+
228
+ Returns:
229
+ The names they bind.
230
+
231
+ """
232
+ return frozenset(
233
+ alias.asname or alias.name.split(".", 1)[0]
234
+ for stmt in tree.body
235
+ if isinstance(stmt, ast.If) and isinstance(stmt.test, ast.Name | ast.Attribute)
236
+ for node in stmt.body
237
+ if isinstance(node, ast.Import | ast.ImportFrom)
238
+ for alias in node.names
239
+ )
240
+
241
+
242
+ def _postponed(tree: ast.Module) -> bool:
243
+ """Check whether the module has `from __future__ import annotations`.
244
+
245
+ Returns:
246
+ Whether it has.
247
+
248
+ """
249
+ return any(
250
+ isinstance(stmt, ast.ImportFrom)
251
+ and stmt.module == _FUTURE
252
+ and any(alias.name == _ANNOTATIONS for alias in stmt.names)
253
+ for stmt in tree.body
254
+ )
255
+
256
+
257
+ def _bound(tree: ast.Module) -> dict[str, str]:
258
+ """Map the names the module's own imports bind (at its top, or under a top-level `if`/`try`).
259
+
260
+ Returns:
261
+ Each name, mapped to what it is: `os` for `import os` (`import os.path` binds `os`),
262
+ `os.path` for `import os.path as p`, `io.BytesIO` for `from io import BytesIO`.
263
+
264
+ """
265
+ return {name: origin for name, origin, _ in import_bindings(_running(tree.body))}
266
+
267
+
268
+ def _defined(tree: ast.Module) -> dict[str, int]:
269
+ """Map each name the module binds at its top level (or under a top-level `if`/`try`) to its line.
270
+
271
+ Returns:
272
+ Each name, and the line it's first bound on.
273
+
274
+ """
275
+ found: dict[str, int] = {}
276
+ stmt: ast.stmt
277
+ for stmt in _running(tree.body):
278
+ name: str
279
+ for name in _names(stmt):
280
+ _ = found.setdefault(name, stmt.lineno)
281
+ return found
282
+
283
+
284
+ def _names(stmt: ast.stmt) -> list[str]:
285
+ """Name what one top-level statement binds, when it's plainly a name (not an unpacking's parts).
286
+
287
+ Returns:
288
+ The names.
289
+
290
+ """
291
+ name: str
292
+ targets: list[ast.expr]
293
+ match stmt:
294
+ case ast.Import() | ast.ImportFrom():
295
+ return [alias.asname or alias.name.split(".", 1)[0] for alias in stmt.names]
296
+ case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.ClassDef(name=name):
297
+ return [name]
298
+ case ast.AnnAssign(target=ast.Name(id=name)):
299
+ return [name]
300
+ case ast.Assign(targets=targets):
301
+ return [target.id for target in targets if isinstance(target, ast.Name)]
302
+ case _:
303
+ return []
304
+
305
+
306
+ def _running(body: list[ast.stmt]) -> Iterator[ast.stmt]:
307
+ """Walk the module's top-level statements, into `if` and `try` blocks but not `if TYPE_CHECKING:`.
308
+
309
+ Yields:
310
+ Each statement.
311
+
312
+ """
313
+ stmt: ast.stmt
314
+ test: ast.expr
315
+ for stmt in body:
316
+ yield stmt
317
+ match stmt:
318
+ case ast.If(test=test) if not _is_checking(test):
319
+ yield from _running(stmt.body + stmt.orelse)
320
+ case ast.Try() | ast.TryStar():
321
+ yield from _running(stmt.body + [s for h in stmt.handlers for s in h.body] + stmt.orelse)
322
+ case _:
323
+ pass
324
+
325
+
326
+ def inner_imports(tree: ast.Module) -> Sequence[ast.Import | ast.ImportFrom]:
327
+ """Find the imports inside the module's functions: what one binds, it binds there alone.
328
+
329
+ Returns:
330
+ Them, in source order.
331
+
332
+ """
333
+ found: _Inner | None
334
+ if (found := _INNER.get(tree)) is None:
335
+ found = _INNER[tree] = tuple(_read_inner_imports(tree))
336
+ return found
337
+
338
+
339
+ def _read_inner_imports(tree: ast.Module) -> Iterator[ast.Import | ast.ImportFrom]:
340
+ statements: list[_Import] = cast("list[_Import]", of_type(tree, ast.Import, ast.ImportFrom))
341
+ functions: list[FunctionDef] = cast(
342
+ "list[FunctionDef]",
343
+ of_type(tree, ast.FunctionDef, ast.AsyncFunctionDef),
344
+ )
345
+ spans: list[tuple[Start, Start]] = sorted(
346
+ ((node.lineno, node.col_offset), (node.end_lineno or node.lineno, node.end_col_offset or 0))
347
+ for node in functions
348
+ )
349
+ starts: list[Start] = [start for start, _ in spans]
350
+ # Where the last of the functions starting no later than each one ends.
351
+ ends: list[Start] = list(accumulate((end for _, end in spans), max))
352
+ stmt: _Import
353
+ for stmt in sorted(statements, key=lambda node: (node.lineno, node.col_offset)):
354
+ start: Start = (stmt.lineno, stmt.col_offset)
355
+ before: int = bisect.bisect_left(starts, start)
356
+ if before and start < ends[before - 1]:
357
+ yield stmt
358
+
359
+
360
+ def _lazy(tree: ast.Module, bound: dict[str, str]) -> frozenset[str]:
361
+ """Name the top-level packages only the module's functions import (see `ImportPlan.lazy`).
362
+
363
+ And those it imports by `pytest.importorskip("mod")`, anywhere: they may be missing when it runs.
364
+
365
+ Returns:
366
+ Them.
367
+
368
+ """
369
+ running: set[str] = {origin.partition(".")[0] for origin in bound.values()}
370
+ return (
371
+ frozenset(
372
+ module.partition(".")[0]
373
+ for stmt in inner_imports(tree)
374
+ for module in (
375
+ [alias.name for alias in stmt.names]
376
+ if isinstance(stmt, ast.Import)
377
+ else [stmt.module or ""] * (not stmt.level)
378
+ )
379
+ ).union(_skipped(tree, bound))
380
+ - running
381
+ )
382
+
383
+
384
+ def _skipped(tree: ast.Module, bound: Mapping[str, str]) -> Iterator[str]:
385
+ """Name the top-level packages `pytest.importorskip("mod")` imports, however the module names it.
386
+
387
+ Yields:
388
+ Each, once a call.
389
+
390
+ """
391
+ node: ast.Call
392
+ for node in cast("list[ast.Call]", of_type(tree, ast.Call)):
393
+ callee: str | None = dotted(node.func)
394
+ head: str
395
+ rest: str
396
+ head, _, rest = (callee or "").partition(_DOT)
397
+ named: list[ast.expr] = [*node.args[:1], *(k.value for k in node.keywords if k.arg == _MODNAME)]
398
+ if (
399
+ head in bound
400
+ and (f"{bound[head]}{_DOT}{rest}" if rest else bound[head]) == _IMPORTORSKIP
401
+ and len(named) == 1
402
+ and isinstance(named[0], ast.Constant)
403
+ and isinstance(named[0].value, str)
404
+ ):
405
+ yield named[0].value.partition(_DOT)[0]
406
+
407
+
408
+ def taken_names(tree: ast.Module) -> tuple[frozenset[str], frozenset[str]]:
409
+ """Find every name bound anywhere in the module: its own, a function's, a class's, a parameter's.
410
+
411
+ Returns:
412
+ Them all; and those bound by anything but an import or a class statement (an assignment, a
413
+ parameter, a `def`), which name a value, not a class or module, somewhere.
414
+
415
+ """
416
+ found: tuple[frozenset[str], frozenset[str]] | None
417
+ if (found := _TAKEN.get(tree)) is None:
418
+ found = _TAKEN[tree] = _read_taken_names(tree)
419
+ return found
420
+
421
+
422
+ def _read_taken_names(tree: ast.Module) -> tuple[frozenset[str], frozenset[str]]:
423
+ names: set[str] = set()
424
+ values: set[str] = set()
425
+ node: ast.AST
426
+ name: str
427
+ asname: str | None
428
+ for node in of_type(tree, *_BINDERS):
429
+ match node:
430
+ case ast.ClassDef(name=name):
431
+ names.add(name)
432
+ case ast.alias(name=name, asname=asname):
433
+ names.add(asname or name.split(".", 1)[0])
434
+ case (
435
+ ast.Name(id=name, ctx=ast.Store())
436
+ | ast.arg(arg=name)
437
+ | ast.FunctionDef(name=name)
438
+ | ast.AsyncFunctionDef(name=name)
439
+ ):
440
+ names.add(name)
441
+ values.add(name)
442
+ case ast.ExceptHandler(name=str() as name) | ast.MatchAs(name=str() as name):
443
+ names.add(name)
444
+ values.add(name)
445
+ case _:
446
+ pass
447
+ return frozenset(names), frozenset(values)
448
+
449
+
450
+ def rebound_names(tree: ast.Module) -> dict[str, list[Start]]:
451
+ """Find the names the module binds more than once, anywhere in it, and where.
452
+
453
+ One bound once (by its import, say) means the same thing throughout: no function shadows it.
454
+ One bound again is shadowed only by a function one of its bindings is in.
455
+
456
+ Returns:
457
+ Each such name, and where each of its bindings starts, in source order.
458
+
459
+ """
460
+ found: dict[str, list[Start]] | None
461
+ if (found := _REBOUND.get(tree)) is None:
462
+ found = _REBOUND[tree] = _read_rebound_names(tree)
463
+ return found
464
+
465
+
466
+ def _read_rebound_names(tree: ast.Module) -> dict[str, list[Start]]:
467
+ found: dict[str, list[Start]] = {}
468
+ node: ast.AST
469
+ name: str
470
+ asname: str | None
471
+ for node in of_type(tree, *_BINDERS):
472
+ match node:
473
+ case ast.alias(name=name, asname=asname):
474
+ found.setdefault(asname or name.split(".", 1)[0], []).append((node.lineno, node.col_offset))
475
+ case (
476
+ ast.Name(id=name, ctx=ast.Store() | ast.Del())
477
+ | ast.arg(arg=name)
478
+ | ast.FunctionDef(name=name)
479
+ | ast.AsyncFunctionDef(name=name)
480
+ | ast.ClassDef(name=name)
481
+ ):
482
+ found.setdefault(name, []).append((node.lineno, node.col_offset))
483
+ case ast.ExceptHandler(name=str() as name) | ast.MatchAs(name=str() as name):
484
+ found.setdefault(name, []).append((node.lineno, node.col_offset))
485
+ case _:
486
+ pass
487
+ return {name: sorted(starts) for name, starts in found.items() if len(starts) > 1}
488
+
489
+
490
+ def _after(tree: ast.Module) -> int:
491
+ """Find the line an added import goes after: the end of the imports the module starts with.
492
+
493
+ After its docstring and `from __future__` imports, before anything else (and so before an
494
+ `if TYPE_CHECKING:` block); the top of the file (0) if it starts with neither.
495
+
496
+ Returns:
497
+ That line (from 1), or 0.
498
+
499
+ """
500
+ after: int = 0
501
+ stmt: ast.stmt
502
+ index: int
503
+ for index, stmt in enumerate(tree.body):
504
+ is_docstring: bool = (
505
+ index == 0
506
+ and isinstance(stmt, ast.Expr)
507
+ and isinstance(stmt.value, ast.Constant)
508
+ and isinstance(stmt.value.value, str)
509
+ )
510
+ if not (is_docstring or isinstance(stmt, ast.Import | ast.ImportFrom)):
511
+ break
512
+ after = stmt.end_lineno or stmt.lineno
513
+ return after
514
+
515
+
516
+ def exported_names(
517
+ returns: Returns,
518
+ guarded: Mapping[str, Guarded],
519
+ added: Mapping[str, str],
520
+ ) -> dict[str, Origin]:
521
+ """Find what each name `returns`' types use refers to, where the module doesn't import it to run.
522
+
523
+ Imported for type checking alone (`guarded`), or by an import its fixes add (`added`): a library
524
+ type it doesn't import yet (`types.ModuleType`) is named for its importers too, or they'd type
525
+ its calls only on a second pass, once the import is in the source.
526
+
527
+ Returns:
528
+ Each such name's origin.
529
+
530
+ """
531
+ return {
532
+ name: guarded[name].origin if name in guarded else added_origin(added[name])
533
+ for annotation in (
534
+ *returns.calls.values(),
535
+ *(a for m in returns.methods.values() for a in m.values()),
536
+ )
537
+ for name in roots(annotation)
538
+ if name in guarded or name in added
539
+ }
540
+
541
+
542
+ def bound_within(func: FunctionDef) -> frozenset[str]:
543
+ """Name what a function's own body imports or defines as a class, at any depth.
544
+
545
+ Returns:
546
+ Each name: one nothing outside the function can spell.
547
+
548
+ """
549
+ found: set[str] = set()
550
+ node: ast.AST
551
+ for node in ast.walk(func):
552
+ if isinstance(node, ast.ClassDef):
553
+ found.add(node.name)
554
+ elif isinstance(node, ast.Import | ast.ImportFrom):
555
+ found.update((alias.asname or alias.name).split(".")[0] for alias in node.names)
556
+ return frozenset(found)
557
+
558
+
559
+ def imports_of(annotation: str, planned: ImportPlan) -> tuple[str, ...]:
560
+ """Find the imports `annotation` needs: those `planned` added for a name it's written with.
561
+
562
+ Returns:
563
+ Their statements, sorted.
564
+
565
+ """
566
+ # `annotation` is always `ast.unparse`'s own output (or a name `plan` spelled), so it parses.
567
+ return tuple(sorted({planned.added[root] for root in roots(annotation) if root in planned.added}))
568
+
569
+
570
+ def guarded_imports(annotation: str, planned: ImportPlan) -> tuple[str, ...]:
571
+ """Find the imports under `if TYPE_CHECKING:` `annotation` needs (see `Guarded`).
572
+
573
+ Returns:
574
+ Their statements, sorted.
575
+
576
+ """
577
+ statements: Iterator[str | None] = (
578
+ planned.guarded[root].statement for root in roots(annotation) if root in planned.guarded
579
+ )
580
+ return tuple(sorted({statement for statement in statements if statement is not None}))