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
constricter/empty.py ADDED
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """An empty mapping nothing can change: a record's default, where a `{}` is a dict every record shares."""
3
+
4
+ from collections.abc import Iterator, Mapping
5
+ from typing import NoReturn, TypeVar
6
+
7
+ from constricter.overrides import override
8
+
9
+ _K = TypeVar("_K")
10
+ _V = TypeVar("_V")
11
+
12
+
13
+ class Empty(Mapping[_K, _V]):
14
+ """A mapping of nothing, read-only, equal to `{}`, and pickled as itself: workers are sent records."""
15
+
16
+ __slots__: tuple[()] = ()
17
+
18
+ @override
19
+ def __getitem__(self, key: _K) -> NoReturn:
20
+ """Find nothing.
21
+
22
+ Raises:
23
+ KeyError: always.
24
+
25
+ """
26
+ raise KeyError(key)
27
+
28
+ @override
29
+ def __iter__(self) -> Iterator[_K]:
30
+ """Yield nothing.
31
+
32
+ Returns:
33
+ An empty iterator.
34
+
35
+ """
36
+ return iter(())
37
+
38
+ @override
39
+ def __len__(self) -> int:
40
+ """Count nothing.
41
+
42
+ Returns:
43
+ 0.
44
+
45
+ """
46
+ return 0
47
+
48
+ @override
49
+ def __reduce__(self) -> "tuple[type[Empty[_K, _V]], tuple[()]]":
50
+ """Pickle as a new empty mapping.
51
+
52
+ Returns:
53
+ How.
54
+
55
+ """
56
+ return Empty, ()
57
+
58
+ @override
59
+ def __repr__(self) -> str:
60
+ """Write it as it's made.
61
+
62
+ Returns:
63
+ `Empty()`.
64
+
65
+ """
66
+ return "Empty()"
Binary file
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix`: what a value makes its annotation, and adding it to the source."""
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What every part of `--fix` shares: what it knows and infers, how a module names a type, writing a fix."""
@@ -0,0 +1,56 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a value was inferred to be, kept until its scope types another name.
3
+
4
+ A statement's value is asked for its type, then for whether that's a guess, then as a part of the
5
+ next value (a `with` statement's manager, three times): `asked` answers again what `keep` was told,
6
+ for the same value (by `id`), the same scope's types (`Typed`) unchanged since, and the same number
7
+ of imports the module's plan has added or guarded (an import `--fix` adds can resolve a name the
8
+ next time).
9
+ """
10
+
11
+ import ast
12
+ from collections.abc import Mapping
13
+ from typing import Final, TypeAlias
14
+
15
+ from constricter.fix.core.known import ImportPlan, Inference, Known, Typed
16
+
17
+ # What was asked: the value and the scope's types (by `id`), how often those had changed, and how
18
+ # many imports the module's plan had added or guarded.
19
+ _Asked: TypeAlias = tuple[int, int, int, int]
20
+ # Its answer, with what was asked kept alive beside it: an `id` is only theirs while they live.
21
+ _Answered: TypeAlias = tuple[ast.expr, Known, Typed, Inference | None]
22
+ _ANSWERS: Final[dict[_Asked, _Answered]] = {}
23
+ _KEPT: Final = 2048 # then all are dropped: a few functions' worth
24
+
25
+
26
+ def _key(value: ast.expr, known: Known, declared: Typed) -> _Asked:
27
+ plan: ImportPlan | None = known.names.plan
28
+ return (
29
+ id(value),
30
+ id(declared),
31
+ declared.version,
32
+ 0 if plan is None else len(plan.added) + len(plan.guarded),
33
+ )
34
+
35
+
36
+ def asked(value: ast.expr, known: Known, declared: Mapping[str, str]) -> tuple[Inference | None] | None:
37
+ """Find what `value` was inferred to be, if it was asked before with nothing changed since.
38
+
39
+ Returns:
40
+ The inference (or `None`, for no type), in a tuple; `None` if it's yet to be worked out.
41
+
42
+ """
43
+ held: _Answered | None = (
44
+ _ANSWERS.get(_key(value, known, declared)) if isinstance(declared, Typed) else None
45
+ )
46
+ if held is None or held[0] is not value or held[1] is not known or held[2] is not declared:
47
+ return None
48
+ return (held[3],)
49
+
50
+
51
+ def keep(value: ast.expr, known: Known, declared: Mapping[str, str], found: Inference | None) -> None:
52
+ """Keep what `value` was inferred to be, for a scope's own types (any other mapping may change unseen)."""
53
+ if isinstance(declared, Typed):
54
+ if len(_ANSWERS) >= _KEPT:
55
+ _ANSWERS.clear()
56
+ _ANSWERS[_key(value, known, declared)] = (value, known, declared, found)
@@ -0,0 +1,317 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix`: write the annotations offences offer, to a file's lines or a notebook cell's."""
3
+
4
+ import contextlib
5
+ import re
6
+ import unicodedata
7
+ from collections.abc import Sequence
8
+ from operator import itemgetter
9
+ from typing import Final, NamedTuple, TypeAlias
10
+
11
+ from constricter.offences import MARK, WIDEN_KINDS, Edit, Fix, Offence
12
+
13
+ _HEADER: Final = 2 # a file's shebang and coding lines come first, if it has them
14
+ # The last lines `_missing` read, stripped each way (see there): the lines, their count, and the set.
15
+ _Present: TypeAlias = tuple[Sequence[str], int, frozenset[str]]
16
+ _PRESENT: Final[dict[str | None, _Present]] = {}
17
+ _HEADER_LINE: Final = re.compile(r"#!|#.*coding[:=]")
18
+ _IDENTIFIER: Final = re.compile(r"\w+")
19
+ _NORMAL: Final = "NFKC" # how Python normalises an identifier: `ast` has a full-width `w` as `w`
20
+ # Lines to insert, and the number of lines before them.
21
+ _Run: TypeAlias = tuple[int, list[str]]
22
+ # What orders the edits: a line, what's made there first (`_IMPORTS`, then an edit in the line,
23
+ # then a `_DECLARATION` before it), and an edit's own line and column.
24
+ _Place: TypeAlias = tuple[int, int, int, int]
25
+ # An edit's place, and its offence (`None`: a run of imports).
26
+ _Placed: TypeAlias = tuple[_Place, Offence | None]
27
+ _IMPORTS: Final = 2
28
+ _INLINE: Final = 1
29
+ _DECLARATION: Final = 0
30
+
31
+
32
+ class Replacement(NamedTuple):
33
+ """One fix as a text edit on `line` (from 1): after `prefix`, `deleted` becomes `text`.
34
+
35
+ A declaration inserts a whole line before its statement's: `prefix` and `deleted` are empty.
36
+ """
37
+
38
+ line: int
39
+ prefix: str # the line's text before the edit
40
+ deleted: str
41
+ text: str
42
+
43
+ @property
44
+ def columns(self) -> tuple[int, int]:
45
+ """Where the edit starts and ends, in characters from 0."""
46
+ return len(self.prefix), len(self.prefix) + len(self.deleted)
47
+
48
+ @property
49
+ def byte_columns(self) -> tuple[int, int]:
50
+ """Where the edit starts and ends, in UTF-8 bytes from 0."""
51
+ start: int = len(self.prefix.encode())
52
+ return start, start + len(self.deleted.encode())
53
+
54
+
55
+ def replacement(lines: Sequence[str], offence: Offence) -> Replacement | None:
56
+ """Turn `offence`'s edit into a text edit on `lines` (indexed from 1 by its line).
57
+
58
+ An `Edit.ANNOTATE` adds `: T` after the name; an `Edit.REPLACE` writes over the annotation
59
+ between its span's columns on the offence's line (an empty annotation deletes it); an
60
+ `Edit.DECLARE` inserts `name: T` on a line of its own before the statement its span names, at
61
+ that statement's indentation. A declaration's `drop` is a second edit: see `dropped`.
62
+
63
+ Returns:
64
+ The edit; none without a fix, or when its columns don't land on UTF-8 character boundaries in
65
+ its line (rare: `ast`'s column and a re-encoded line's bytes can disagree for some non-ASCII
66
+ source), rather than splitting a multi-byte character and corrupting the line.
67
+
68
+ """
69
+ fix: Fix | None
70
+ if (fix := offence.edit) is None:
71
+ return None
72
+ with contextlib.suppress(UnicodeDecodeError, IndexError):
73
+ if fix.edit is Edit.DECLARE:
74
+ statement: str = lines[fix.span[0] - 1]
75
+ indent: str = statement[: len(statement) - len(statement.lstrip())]
76
+ ending: str = statement.removeprefix(statement.rstrip("\r\n")) or "\n"
77
+ mark: str = f" {MARK}" if fix.kinds & WIDEN_KINDS else ""
78
+ return Replacement(fix.span[0], "", "", f"{indent}{offence.name}: {fix.annotation}{mark}{ending}")
79
+ raw: bytes = lines[offence.line - 1].encode()
80
+ start: int = fix.span[0] if fix.edit is Edit.REPLACE else _name_end(raw, offence)
81
+ end: int = fix.span[1] if fix.edit is Edit.REPLACE else start
82
+ written: str = fix.annotation if fix.edit is Edit.REPLACE else f": {fix.annotation}"
83
+ return Replacement(offence.line, raw[:start].decode(), raw[start:end].decode(), written)
84
+ return None
85
+
86
+
87
+ def _name_end(raw: bytes, offence: Offence) -> int:
88
+ """Find where the offence's name ends in its line `raw`, as the source spells it.
89
+
90
+ The source may spell it longer than `ast` has it, which normalises an identifier (a full-width
91
+ `w` is `w`).
92
+
93
+ Returns:
94
+ The column after it.
95
+
96
+ Raises:
97
+ IndexError: The line doesn't have the name there.
98
+
99
+ """
100
+ rest: str = raw[offence.col :].decode()
101
+ if rest.startswith(offence.name):
102
+ return offence.col + len(offence.name.encode())
103
+ spelled: re.Match[str] | None = _IDENTIFIER.match(rest)
104
+ if spelled is None or unicodedata.normalize(_NORMAL, spelled.group()) != offence.name:
105
+ raise IndexError(offence.col)
106
+ return offence.col + len(spelled.group().encode())
107
+
108
+
109
+ def dropped(lines: Sequence[str], offence: Offence) -> Replacement | None:
110
+ """Turn a fix's `drop` into a text edit: deleting those columns of its statement's line.
111
+
112
+ A declaration's statement is the one it goes before; a replaced annotation's ends on the fix's
113
+ `mark` line (the offence's own, if none).
114
+
115
+ Returns:
116
+ The edit, or none without one (or with columns that don't land on characters, as
117
+ `replacement`'s).
118
+
119
+ """
120
+ fix: Fix | None = offence.edit
121
+ if fix is None or fix.drop is None:
122
+ return None
123
+ with contextlib.suppress(UnicodeDecodeError, IndexError):
124
+ line: int = fix.span[0] if fix.edit is Edit.DECLARE else fix.mark or offence.line
125
+ raw: bytes = lines[line - 1].encode()
126
+ start: int
127
+ end: int
128
+ start, end = fix.drop
129
+ return Replacement(line, raw[:start].decode(), raw[start:end].decode(), "")
130
+ return None
131
+
132
+
133
+ def marked(lines: Sequence[str], offence: Offence) -> Replacement | None:
134
+ """Turn a widening's mark into a text edit: `MARK` at the end of its statement's last line.
135
+
136
+ Returns:
137
+ The edit, or none for any other fix, or a declaration (whose own line `replacement` marks).
138
+
139
+ """
140
+ fix: Fix | None = offence.edit
141
+ if fix is None or fix.edit is not Edit.ANNOTATE or not fix.kinds & WIDEN_KINDS:
142
+ return None
143
+ line: int = fix.mark or offence.line
144
+ return Replacement(line, lines[line - 1].rstrip("\r\n"), "", f" {MARK}")
145
+
146
+
147
+ def replacements(lines: Sequence[str], offence: Offence) -> tuple[Replacement, ...]:
148
+ """Turn `offence`'s fix into its text edits: `replacement`'s, `dropped`'s, `marked`'s, then its imports'.
149
+
150
+ Returns:
151
+ Them; none without a fix.
152
+
153
+ """
154
+ edits: list[Replacement | None] = [
155
+ replacement(lines, offence),
156
+ dropped(lines, offence),
157
+ marked(lines, offence),
158
+ ]
159
+ line: int
160
+ added: list[str]
161
+ for line, added in _imported(lines, [offence]):
162
+ edits.append(Replacement(line + 1, "", "", "".join(added)))
163
+ return tuple(edit for edit in edits if edit is not None)
164
+
165
+
166
+ def inserted(lines: Sequence[str], offences: Sequence[Offence]) -> list[int]:
167
+ """Find where `apply` adds lines to `lines`: each declaration, and the imports the fixes need.
168
+
169
+ Returns:
170
+ For each line it adds, the line (from 1) it goes before, sorted.
171
+
172
+ """
173
+ found: list[int] = [
174
+ o.edit.span[0] for o in offences if o.edit is not None and o.edit.edit is Edit.DECLARE
175
+ ]
176
+ line: int
177
+ added: list[str]
178
+ for line, added in _imported(lines, offences):
179
+ found.extend([line + 1] * len(added))
180
+ return sorted(found)
181
+
182
+
183
+ def _imported(lines: Sequence[str], offences: Sequence[Offence]) -> list["_Run"]:
184
+ """Place the imports `offences`' fixes need that `lines` doesn't have yet, each once.
185
+
186
+ Those for type checking alone go at the end of the body of the module's `if TYPE_CHECKING:`,
187
+ or, with none, in a new one after the others.
188
+
189
+ Returns:
190
+ Each run of lines to insert, with the number of lines before it, the last run first.
191
+
192
+ """
193
+ fixes: list[Fix]
194
+ if not (fixes := [o.edit for o in offences if o.edit and (o.edit.imports or o.edit.guarded)]):
195
+ return []
196
+ ending: str = _ending(lines)
197
+ statements: list[str] = _missing(lines, sorted({s for fix in fixes for s in fix.imports}), "\r\n")
198
+ guarded: list[str] = _missing(lines, sorted({s for fix in fixes for s in fix.guarded}), None)
199
+ line: int = _import_line(lines, next((fix.after for fix in fixes if fix.imports), fixes[0].after))
200
+ runs: list[_Run] = []
201
+ block: tuple[int, int] = fixes[0].block
202
+ if guarded and block != (0, 0):
203
+ indent: str = lines[block[0] - 1][: len(lines[block[0] - 1]) - len(lines[block[0] - 1].lstrip())]
204
+ runs.append((block[1], [f"{indent}{statement}{ending}" for statement in guarded]))
205
+ elif guarded:
206
+ guard: str = next(fix.guard for fix in fixes if fix.guarded)
207
+ statements += [f"if {guard}:", *(f" {statement}" for statement in guarded)]
208
+ if statements:
209
+ runs.append((line, [f"{statement}{ending}" for statement in statements]))
210
+ return runs
211
+
212
+
213
+ def _missing(lines: Sequence[str], statements: Sequence[str], stripped: str | None) -> list[str]:
214
+ """Find the import statements `lines` doesn't have yet, as a line of their own.
215
+
216
+ `stripped`: what a line may have around the statement: its line ending alone, for an import
217
+ that must run (an indented one is some block's, or a string's text, and may not), or any
218
+ whitespace (`None`), for one under `if TYPE_CHECKING:`.
219
+
220
+ Returns:
221
+ Them, in order.
222
+
223
+ """
224
+ if not statements: # most fixes add none: the file's lines needn't be read for them
225
+ return []
226
+ # A file's fixes are asked one at a time, of the same lines: stripped once for them all.
227
+ held: _Present | None = _PRESENT.get(stripped)
228
+ if held is None or held[0] is not lines or held[1] != len(lines):
229
+ held = _PRESENT[stripped] = (
230
+ lines,
231
+ len(lines),
232
+ frozenset(line.rstrip(stripped) if stripped else line.strip() for line in lines),
233
+ )
234
+ return [statement for statement in statements if statement not in held[2]]
235
+
236
+
237
+ def _import_line(lines: Sequence[str], after: int) -> int:
238
+ """Place added imports after line `after` (from 1), or at the top below a shebang or coding line.
239
+
240
+ Returns:
241
+ The number of lines before them.
242
+
243
+ """
244
+ if after:
245
+ return after
246
+ header: Sequence[str] = lines[:_HEADER]
247
+ return next((index for index, line in enumerate(header) if not _HEADER_LINE.match(line)), len(header))
248
+
249
+
250
+ def _ending(lines: Sequence[str]) -> str:
251
+ """Find the file's line ending, from its first line.
252
+
253
+ Returns:
254
+ It (a newline for an empty file).
255
+
256
+ """
257
+ return next((line.removeprefix(line.rstrip("\r\n")) for line in lines[:1]), "") or "\n"
258
+
259
+
260
+ def apply(lines: Sequence[str], offences: Sequence[Offence]) -> list[str]:
261
+ """Return `lines` with each fixable offence's edit made (see `replacement`).
262
+
263
+ The drops go first: each deletes the end of a statement's line (its type comment), so moves no
264
+ other edit, and one statement's is made once however many of its names declare. The rest, and
265
+ the imports the fixes need (each once, see `_imported`), are made from the last to the first, so
266
+ no edit moves one still to be made: on one line, the edits in it before the declarations that
267
+ go before it (`y: int`, then `x: int = f(y := 3)`), which keep their names' order; one
268
+ `replacement` can't make is left unmade.
269
+
270
+ Returns:
271
+ New lines; `lines` is left as it was. An offence's `line` indexes `lines` from 1.
272
+
273
+ """
274
+ text: list[str] = list(lines)
275
+ o: Offence
276
+ edit: Replacement | None
277
+ mark: Replacement | None
278
+ drops: dict[tuple[int, int, int], Replacement] = {}
279
+ for o in offences:
280
+ if (edit := dropped(text, o)) is not None:
281
+ drops[edit.line, *edit.byte_columns] = edit
282
+ for edit in (drops[key] for key in sorted(drops, reverse=True)):
283
+ kept: int = edit.columns[1]
284
+ text[edit.line - 1] = edit.prefix + text[edit.line - 1][kept:]
285
+ # Each run of imports goes after its line: before any edit on that line, in this order.
286
+ runs: dict[_Place, list[str]] = {(line, _IMPORTS, 0, 0): run for line, run in _imported(text, offences)}
287
+ edits: list[_Placed] = [(_position(o), o) for o in offences if o.edit]
288
+ placed: _Place
289
+ fixed: Offence | None
290
+ for placed, fixed in sorted([*edits, *((key, None) for key in runs)], key=itemgetter(0), reverse=True):
291
+ if fixed is None:
292
+ after: int = placed[0]
293
+ text[after:after] = runs[placed]
294
+ elif (edit := replacement(text, fixed)) is None:
295
+ continue
296
+ elif fixed.edit and fixed.edit.edit is Edit.DECLARE:
297
+ text.insert(edit.line - 1, edit.text) # a declaration: a line of its own
298
+ else:
299
+ end: int = edit.columns[1]
300
+ text[edit.line - 1] = edit.prefix + edit.text + text[edit.line - 1][end:]
301
+ if (mark := marked(text, fixed)) is not None:
302
+ ending: str = text[mark.line - 1].removeprefix(mark.prefix)
303
+ text[mark.line - 1] = mark.prefix + mark.text + ending
304
+ return text
305
+
306
+
307
+ def _position(offence: Offence) -> _Place:
308
+ """Place an offence's edit in the file, to order the edits by (see `_Place`).
309
+
310
+ Returns:
311
+ Its place: a declaration's line is the statement's it goes before.
312
+
313
+ """
314
+ fix: Fix | None = offence.edit
315
+ if fix is not None and fix.edit is Edit.DECLARE:
316
+ return fix.span[0], _DECLARATION, offence.line, offence.col
317
+ return offence.line, _INLINE, offence.line, offence.col