python-constricter 0.4.1__cp311-cp311-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__.cp311-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-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__.cp311-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-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__.cp311-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-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,652 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Binding the names a statement binds: each target typed, or declared before the statement.
3
+
4
+ `bind` is what `checker` calls for every statement: a plain `name = value` goes through the scope
5
+ (`Scope.assign`); a loop's or an unpacking's targets are split from the value's type and declared on a
6
+ line of their own before it; a loop typed only by its `# type:` comment (LVA003) declares that type
7
+ instead; `with manager as name` declares `name` first; and a `match`'s captures and an augmented
8
+ assignment are bound as they are. `walruses` binds a statement's `:=` targets, each declared before
9
+ it too.
10
+ """
11
+
12
+ import ast
13
+ from collections.abc import Iterator, Mapping
14
+ from typing import Final, TypeAlias, cast
15
+
16
+ from constricter.fix.core.known import Inference, Known
17
+ from constricter.fix.libraries.opened import opened
18
+ from constricter.fix.values import shapes
19
+ from constricter.fix.values.doubts import bare
20
+ from constricter.fix.values.entered import Entered, entered, entered_async, entering
21
+ from constricter.fix.values.inference import inference
22
+ from constricter.fix.values.loops import LoopPart, looped, looped_parts
23
+ from constricter.fix.values.targets import iterated, unpacked
24
+ from constricter.offences import COMMENT_TYPED_TARGET, UNTYPED_TARGET, VAGUE_TYPE, Edit, Fix, Offence, at
25
+ from constricter.rules.flow import augmented, members
26
+ from constricter.rules.scope import Scope, certain_type, guessed_type, guesses_in
27
+ from constricter.rules.scoped import Bare, Late
28
+ from constricter.rules.syntax import captures, comment_type, target_names, type_comment_span
29
+
30
+ _COMMENT: Final = "comment" # the fix kind of LVA003's declaration
31
+ _UNPACK: Final = frozenset({"unpack"}) # the fix kind of an unpacking's split
32
+ _TUPLE: Final = ("tuple[", "Tuple[") # how a tuple's type starts
33
+ _ANY: Final = ", ...]" # and how one of any length ends
34
+ # A name's inference (`None`: unknown), whether it's a guess, and what the guess rests on.
35
+ # What `Scope.valued` gives.
36
+ _Valued: TypeAlias = tuple[Inference | None, bool, frozenset[str]]
37
+ _Named: TypeAlias = tuple[ast.Name, _Valued]
38
+ _COMPREHENSIONS: Final = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
39
+ # Statements a declaration can't go before: a decorator's line is its definition's.
40
+ _DEFINITIONS: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
41
+
42
+
43
+ def bind(scope: Scope, stmt: ast.stmt) -> None:
44
+ """Bind the names `stmt` binds that need typing, reporting the untyped ones."""
45
+ target: ast.expr
46
+ items: list[ast.withitem]
47
+ comment: str | None
48
+ name: str
49
+ single: ast.Name
50
+ value: ast.expr
51
+ cases: list[ast.match_case]
52
+ op: ast.operator
53
+ match stmt:
54
+ case ast.Assign(targets=[ast.Name() as single], value=value, type_comment=comment):
55
+ scope.assign(single, scope.unannotated(comment), value)
56
+ case ast.Assign(targets=[ast.Tuple() | ast.List() as target], value=value):
57
+ _bind_unpacked(scope, stmt, target, value)
58
+ case ast.Assign():
59
+ _bind_assigned(scope, stmt)
60
+ case ast.With(items=items, type_comment=comment) | ast.AsyncWith(items=items, type_comment=comment):
61
+ _bind_with(scope, stmt, items, scope.unannotated(comment))
62
+ case (
63
+ ast.For(target=target, iter=value, type_comment=None)
64
+ | ast.AsyncFor(
65
+ target=target,
66
+ iter=value,
67
+ type_comment=None,
68
+ )
69
+ ):
70
+ _bind_loop(scope, stmt, target, value)
71
+ case (
72
+ ast.For(target=target, type_comment=str() as comment)
73
+ | ast.AsyncFor(
74
+ target=target,
75
+ type_comment=str() as comment,
76
+ )
77
+ ):
78
+ _bind_commented(scope, stmt, target, comment)
79
+ case ast.Match(cases=cases):
80
+ _bind_captures(scope, cases)
81
+ case ast.AugAssign(target=ast.Name(id=name) as single, op=op, value=value):
82
+ own: str | None = None if name in scope.inferred.guesses else scope.inferred.types.get(name)
83
+ bound: str | None = augmented(op, certain_type(scope, value), own)
84
+ scope.lifetime(name).bind(at(single), bound, None if bound else _augmented_guess(scope, stmt))
85
+ scope.inferred.rebound(name, bound)
86
+ case _:
87
+ pass
88
+
89
+
90
+ def walruses(scope: Scope, part: ast.AST, before: ast.stmt) -> None:
91
+ """Bind the `:=` targets in `part`, an expression of a statement, each offered a declaration.
92
+
93
+ Comprehensions included, lambdas excluded. A name can't be annotated where `:=` binds it: it's
94
+ declared on a line of its own before `before` (its statement, or the `if` an `elif` belongs to),
95
+ typed as a plain assignment's is. Not one inside a comprehension, whose value may read the
96
+ comprehension's own names, nor where a line can't go before the statement: a definition (its
97
+ decorators' lines are its own), or one that doesn't start its line (`else: x = (y := 1)`).
98
+ Asked only in a module with a `:=` (`Settings.walruses`).
99
+ """
100
+ hidden: set[int] = _inside(part, ast.Lambda)
101
+ line: bytes = scope.settings.lines[before.lineno - 1].encode() if scope.settings.lines else b""
102
+ plain: bool = isinstance(before, _DEFINITIONS) or bool(line[: before.col_offset].strip())
103
+ comprehended: set[int] = set() if plain else _inside(part, *_COMPREHENSIONS)
104
+ code: str = scope.kind.unannotated
105
+ node: ast.AST
106
+ for node in ast.walk(part):
107
+ if not isinstance(node, ast.NamedExpr) or id(node) in hidden:
108
+ continue
109
+ if plain or id(node) in comprehended:
110
+ scope.bind(node.target.id, at(node.target), code)
111
+ else:
112
+ _bind_declaration(scope, before, node.target, code, scope.valued(node.value, node.lineno))
113
+
114
+
115
+ def _inside(part: ast.AST, *kinds: type[ast.AST]) -> set[int]:
116
+ """Find every node inside a node of one of `kinds`, in `part`.
117
+
118
+ Returns:
119
+ Their `id()`s.
120
+
121
+ """
122
+ return {id(inner) for outer in ast.walk(part) if isinstance(outer, kinds) for inner in ast.walk(outer)}
123
+
124
+
125
+ def _bind_assigned(scope: Scope, stmt: ast.Assign) -> None:
126
+ """Bind a chained assignment's names (`a = b = 0`), each offered a declaration before it, or others'."""
127
+ code: str | None = scope.unannotated(stmt.type_comment)
128
+ if not all(isinstance(target, ast.Name) for target in stmt.targets):
129
+ _bind_targets(scope, stmt.targets, code)
130
+ return
131
+ target: ast.expr
132
+ for target in stmt.targets:
133
+ scope.assign(cast("ast.Name", target), code, stmt.value, stmt)
134
+
135
+
136
+ def _bind_loop(scope: Scope, stmt: ast.For | ast.AsyncFor, target: ast.expr, value: ast.expr) -> None:
137
+ """Bind a loop's target (see `_bind_declared`), over `enumerate` or `zip` one part at a time.
138
+
139
+ `for i, x in enumerate(xs)` declares `i: int` even when `xs`'s elements aren't known, and a
140
+ guess about them makes only `x`'s fix one.
141
+ """
142
+ parts: list[LoopPart] | None = looped_parts(
143
+ value,
144
+ scope.settings.known,
145
+ scope.inferred.types,
146
+ inference,
147
+ )
148
+ elements: list[ast.expr]
149
+ match target:
150
+ case ast.Tuple(elts=elements) | ast.List(elts=elements) if (
151
+ parts is not None
152
+ and len(elements) == len(parts)
153
+ and not any(isinstance(element, ast.Starred) for element in elements)
154
+ ):
155
+ element: ast.expr
156
+ typed: Inference | None
157
+ bases: list[ast.expr]
158
+ for element, (typed, bases) in zip(elements, parts, strict=True):
159
+ _bind_declared(scope, stmt, element, typed, bases)
160
+ case _:
161
+ _bind_declared(
162
+ scope,
163
+ stmt,
164
+ target,
165
+ looped(value, scope.settings.known, scope.inferred.types, inference),
166
+ iterated(value),
167
+ )
168
+
169
+
170
+ def _bind_unpacked(scope: Scope, stmt: ast.Assign, target: ast.expr, value: ast.expr) -> None:
171
+ """Bind an unpacking's names, offering to declare each before `stmt` (see `_unpacked`).
172
+
173
+ Every name is typed before any is bound, as its value is evaluated: `a, b = b, a`.
174
+ """
175
+ code: str | None = scope.unannotated(stmt.type_comment)
176
+ name: ast.Name
177
+ typed: _Valued
178
+ for name, typed in list(_unpacked(scope, stmt, target, value)):
179
+ _bind_declaration(scope, stmt, name, code, typed)
180
+
181
+
182
+ def _source(stmt: ast.stmt, name: ast.Name) -> ast.expr | None:
183
+ """Find what a loop iterates, an unpacking splits or a `with` enters to bind `name`.
184
+
185
+ Returns:
186
+ It, or `None` for a name `stmt` binds otherwise (a `:=`'s, a chained assignment's).
187
+
188
+ """
189
+ target: ast.expr
190
+ value: ast.expr
191
+ items: list[ast.withitem]
192
+ pairs: Iterator[tuple[ast.Name, ast.expr]]
193
+ match stmt:
194
+ case ast.For(target=target, iter=value) | ast.AsyncFor(target=target, iter=value):
195
+ pairs = ((each, value) for each in target_names(target))
196
+ case ast.Assign(targets=[target], value=value):
197
+ pairs = _sources(target, value)
198
+ case ast.With(items=items) | ast.AsyncWith(items=items):
199
+ pairs = (
200
+ (each, item.context_expr)
201
+ for item in items
202
+ if item.optional_vars is not None
203
+ for each in target_names(item.optional_vars)
204
+ )
205
+ case _:
206
+ return None
207
+ return next((taken for each, taken in pairs if each is name), None)
208
+
209
+
210
+ def _sources(target: ast.expr, value: ast.expr) -> Iterator[tuple[ast.Name, ast.expr]]:
211
+ """Pair each name an unpacking of `value` into `target` binds with what it's taken from.
212
+
213
+ Yields:
214
+ Each name, with its own value where a display of as many is split with the target (see
215
+ `_unpacked`), else with `value`.
216
+
217
+ """
218
+ targets: list[ast.expr]
219
+ values: list[ast.expr]
220
+ match (target, value):
221
+ case (
222
+ ast.Tuple(elts=targets) | ast.List(elts=targets),
223
+ ast.Tuple(elts=values) | ast.List(elts=values),
224
+ ) if len(targets) == len(values) and not any(
225
+ isinstance(part, ast.Starred) for part in (*targets, *values)
226
+ ):
227
+ part: ast.expr
228
+ item: ast.expr
229
+ for part, item in zip(targets, values, strict=True):
230
+ yield from _sources(part, item)
231
+ case _:
232
+ name: ast.Name
233
+ for name in target_names(target):
234
+ yield name, value
235
+
236
+
237
+ def _unpacked(scope: Scope, stmt: ast.Assign, target: ast.expr, value: ast.expr) -> Iterator[_Named]:
238
+ """Type each name an unpacking of `value` into `target` binds.
239
+
240
+ A display of as many values is split with its target, each name typed by its own value as a
241
+ plain assignment's is (`a, b = x, 1` declares `b: int` whatever `x` is); any other value's type
242
+ is split over the names (see `unpacked`), or its elements' is (`a, b = range(2)`).
243
+
244
+ Yields:
245
+ Each name, with its inference, whether that's a guess, and what the guess rests on.
246
+
247
+ """
248
+ targets: list[ast.expr]
249
+ values: list[ast.expr]
250
+ name: ast.Name
251
+ match (target, value):
252
+ case (
253
+ ast.Tuple(elts=targets) | ast.List(elts=targets),
254
+ ast.Tuple(elts=values) | ast.List(elts=values),
255
+ ) if len(targets) == len(values) and not any(
256
+ isinstance(part, ast.Starred) for part in (*targets, *values)
257
+ ):
258
+ part: ast.expr
259
+ item: ast.expr
260
+ for part, item in zip(targets, values, strict=True):
261
+ yield from _unpacked(scope, stmt, part, item)
262
+ case (ast.Name() as name, _):
263
+ fix: Inference | None
264
+ unsafe: bool
265
+ origins: frozenset[str]
266
+ fix, unsafe, origins = scope.valued(value, stmt.lineno)
267
+ yield name, (None if fix is None else fix._replace(kinds=fix.kinds | _UNPACK), unsafe, origins)
268
+ case _:
269
+ yield from _unpacked_whole(scope, stmt, target, value)
270
+
271
+
272
+ def _unpacked_whole(scope: Scope, stmt: ast.Assign, target: ast.expr, value: ast.expr) -> Iterator[_Named]:
273
+ """Type each name an unpacking of `value` binds by `value`'s own type, split over them.
274
+
275
+ Its type as any value's is inferred (a call's declared return with a vague part too: a part
276
+ vaguer than `vague` allows is its name's type, and no fix); else what iterating it gives, each
277
+ name an element.
278
+
279
+ Yields:
280
+ Each name, with its inference, whether that's a guess, and what the guess rests on.
281
+
282
+ """
283
+ known: Known = scope.settings.known
284
+ typed: Inference | None = inference(value, known, scope.inferred.types)
285
+ element: Inference | None
286
+ if (element := None if typed else looped(value, known, scope.inferred.types, inference)) is not None:
287
+ typed = element._replace(annotation=f"tuple[{element.annotation}, ...]")
288
+ yield from _split(scope, stmt, target, typed, iterated(value))
289
+ return
290
+ yield from _split(scope, stmt, target, typed, [value])
291
+
292
+
293
+ def _bind_declared(
294
+ scope: Scope,
295
+ stmt: ast.For | ast.AsyncFor,
296
+ target: ast.expr,
297
+ typed: Inference | None,
298
+ bases: list[ast.expr],
299
+ ) -> None:
300
+ """Bind each name in a loop's `target`, offering to declare each before `stmt` (see `_split`)."""
301
+ name: ast.Name
302
+ part: _Valued
303
+ for name, part in _split(scope, stmt, target, typed, bases):
304
+ _bind_declaration(scope, stmt, name, UNTYPED_TARGET, part)
305
+
306
+
307
+ def _split(
308
+ scope: Scope,
309
+ stmt: ast.stmt,
310
+ target: ast.expr,
311
+ typed: Inference | None,
312
+ bases: list[ast.expr],
313
+ ) -> Iterator[_Named]:
314
+ """Type each name in `target` (a loop's, or an unpacking's).
315
+
316
+ `typed` is what the whole target gets (a loop's element, an unpacked value's type), split over
317
+ its names (see `unpacked`); a name whose part isn't known gets no fix. The fixes are guesses if
318
+ any of `bases`, the values `typed` came from, is.
319
+
320
+ Yields:
321
+ Each name, with its inference, whether that's a guess, and what the guess rests on.
322
+
323
+ """
324
+ unsafe: bool
325
+ origins: frozenset[str]
326
+ unsafe, origins = (False, frozenset()) if typed is None else guesses_in(scope, bases)
327
+ # An unpacking's names are split from the value's type; a loop's are what it iterates (`loop`).
328
+ split: frozenset[str] = frozenset() if isinstance(stmt, ast.For | ast.AsyncFor) else _UNPACK
329
+ name: ast.Name
330
+ annotation: str | None
331
+ whole: str | None = None if typed is None else typed.annotation
332
+ tuples: Mapping[str, str] = scope.settings.known.indirect.tuples
333
+ parts: list[tuple[ast.Name, str | None]] = unpacked(target, whole, tuples)
334
+ mixed: bool = _by_position(parts, whole or "", tuples)
335
+ for name, annotation in parts:
336
+ part: Inference | None = (
337
+ None
338
+ if typed is None or annotation is None or mixed
339
+ else Inference(annotation, typed.reason, typed.kinds | split)
340
+ )
341
+ yield name, (part, unsafe, origins)
342
+
343
+
344
+ def _by_position(parts: list[tuple[ast.Name, str | None]], whole: str, tuples: Mapping[str, str]) -> bool:
345
+ """Whether `parts`, split from `whole`, share a union that may be by position.
346
+
347
+ A union over several names, of anything but a tuple of that many, is by position as often as
348
+ not (`name, length`, of `[["prefix", 24], ...]`): each has no fix.
349
+
350
+ Returns:
351
+ Whether each name's part is the same union, of a `whole` that isn't a tuple of that many.
352
+
353
+ """
354
+ fixed: bool = tuples.get(whole, whole).startswith(_TUPLE) and not whole.endswith(_ANY)
355
+ return (
356
+ not fixed
357
+ and len(parts) > 1
358
+ and len({annotation for _, annotation in parts}) == 1
359
+ and len(members(parts[0][1] or "") or ()) > 1
360
+ )
361
+
362
+
363
+ def _bind_declaration(
364
+ scope: Scope,
365
+ stmt: ast.stmt,
366
+ name: ast.Name,
367
+ code: str | None,
368
+ typed: _Valued,
369
+ ) -> None:
370
+ """Bind one name a statement binds, offering to declare it before `stmt` as `typed` has it.
371
+
372
+ `typed`: its inference (`None`: unknown, when the type checker's hint is asked), whether that's a
373
+ guess, and what the guess rests on. A loop's, an unpacking's or a `with`'s name left with no fix
374
+ is noted with what it's taken from (see `_source`), for `late.widens`, whose wider type is
375
+ declared before `stmt` too.
376
+ """
377
+ found: Inference | None
378
+ unsafe: bool
379
+ origins: frozenset[str]
380
+ found, unsafe, origins = typed
381
+ hint: Inference | None
382
+ if (found is None or not scope.writable(found)) and (hint := scope.hint(name)) is not None:
383
+ found, unsafe, origins = hint, True, hint.kinds
384
+ fix: Fix | None = None
385
+ noted: Bare | None = scope.assignments.bare.get(name.id)
386
+ if found is not None:
387
+ fix = scope.offer(
388
+ found,
389
+ origins,
390
+ unsafe=unsafe,
391
+ edit=Edit.DECLARE,
392
+ span=(stmt.lineno, stmt.col_offset),
393
+ )
394
+ # What the rest of the scope infers from `name` knows its type, as for `name = value`.
395
+ scope.inferred.learn(
396
+ name.id,
397
+ found.annotation,
398
+ origins if unsafe else None,
399
+ # A marked declaration with no value bound it to nothing.
400
+ again=name.id in scope.declared and noted is None,
401
+ )
402
+ if found is not None and noted is not None and noted[1] is None:
403
+ scope.assignments.bare[name.id] = (noted[0], found)
404
+ source: ast.expr | None
405
+ if (source := None if fix or name.id in scope.declared else _source(stmt, name)) is not None:
406
+ scope.assignments.plain[name.id] = (name, source, found)
407
+ scope.assignments.chained[name.id] = (stmt.lineno, stmt.col_offset)
408
+ typed_as: str | Late | None = None
409
+ if found is not None and scope.writable(found):
410
+ typed_as = (found.annotation, origins) if unsafe else found.annotation
411
+ scope.bind(name.id, at(name), code, fix, typed_as)
412
+
413
+
414
+ def _bind_commented(scope: Scope, stmt: ast.For | ast.AsyncFor, target: ast.expr, comment: str) -> None:
415
+ """Bind a loop's target typed only by its `# type:` comment (LVA003), offering to declare it instead.
416
+
417
+ The comment's type (`int`, or `int, str` for a tuple target) is split over the target's names as
418
+ an unpacking's is; each is declared before the loop, and the comment dropped, since a type checker
419
+ would see the name declared twice. Only for a loop whose header is on one line, where the comment
420
+ is found after its iterable.
421
+ """
422
+ drop: tuple[int, int] | None = type_comment_span(scope.settings.lines, stmt)
423
+ annotation: str | None = comment_type(comment) if drop is not None else None
424
+ name: ast.Name
425
+ part: str | None
426
+ for name, part in unpacked(target, annotation):
427
+ fix: Fix | None = None
428
+ if part is not None and drop is not None:
429
+ fix = scope.offer(
430
+ Inference(part, "its `# type:` comment", frozenset({_COMMENT})),
431
+ frozenset(),
432
+ unsafe=False,
433
+ edit=Edit.DECLARE,
434
+ span=(stmt.lineno, stmt.col_offset),
435
+ )
436
+ fix = None if fix is None else fix._replace(drop=drop)
437
+ scope.bind(name.id, at(name), COMMENT_TYPED_TARGET, fix)
438
+
439
+
440
+ def _bind_with(scope: Scope, stmt: ast.stmt, items: list[ast.withitem], code: str | None) -> None:
441
+ """Bind each `with` item's target, offering to declare `with manager as name`'s `name` first.
442
+
443
+ As what the manager's `__enter__` returns (see `constricter.fix.values.entered`); the file object
444
+ `open` gives, which is its own context manager, by its literal mode. A target that unpacks takes
445
+ that type split over its names, as an unpacking's are (but for a vague part). An `async with`'s
446
+ is typed by a standard-library manager's `__aenter__` alone, and not split.
447
+ """
448
+ item: ast.withitem
449
+ name: ast.Name
450
+ target: ast.expr
451
+ for item in items:
452
+ typed: _Valued = _entered(scope, item.context_expr, asynchronous=isinstance(stmt, ast.AsyncWith))
453
+ match item.optional_vars:
454
+ case ast.Name() as name:
455
+ _bind_declaration(scope, stmt, name, code, typed)
456
+ case None:
457
+ pass
458
+ case target if isinstance(stmt, ast.With):
459
+ named: _Named
460
+ for named in _entered_parts(scope, target, item.context_expr, typed):
461
+ _bind_declaration(scope, stmt, named[0], code, named[1])
462
+ case target:
463
+ _bind_targets(scope, [target], code)
464
+
465
+
466
+ def _entered_parts(scope: Scope, target: ast.expr, manager: ast.expr, typed: _Valued) -> Iterator[_Named]:
467
+ """Type each name a `with` statement's unpacking `target` binds, entering `manager`.
468
+
469
+ `typed`: what the whole target gets (see `_entered`), split over its names as an unpacking's
470
+ value is; where that's unknown, an `__enter__` declared to return a vague part is split instead
471
+ (see `shapes.partly`). A part vaguer than `vague` allows is its name's type, and no fix.
472
+
473
+ Yields:
474
+ Each name, with its inference, whether that's a guess, and what the guess rests on.
475
+
476
+ """
477
+ known: Known = scope.settings.known
478
+ whole: Inference | None = typed[0]
479
+ doubt: tuple[bool, frozenset[str]] = typed[1:]
480
+ if whole is None:
481
+ whole = shapes.partly(
482
+ entering(manager),
483
+ known,
484
+ lambda part: inference(part, known, scope.inferred.types),
485
+ )
486
+ doubt = guesses_in(scope, [manager])
487
+ name: ast.Name
488
+ part: str | None
489
+ for name, part in unpacked(target, None if whole is None else whole.annotation, known.indirect.tuples):
490
+ split: Inference | None = (
491
+ None if whole is None or part is None else Inference(part, whole.reason, whole.kinds | _UNPACK)
492
+ )
493
+ yield name, (split, *doubt)
494
+
495
+
496
+ def _entered(scope: Scope, manager: ast.expr, *, asynchronous: bool = False) -> _Valued:
497
+ """Infer what a `with` statement binds its target to, entering `manager`.
498
+
499
+ Returns:
500
+ The inference, whether it's a guess (the manager's type is one), and what the guess rests on;
501
+ none for a generic class written without its arguments.
502
+
503
+ """
504
+ known: Known = scope.settings.known
505
+ file: Inference | None
506
+ if not asynchronous and (file := opened(manager, known)) is not None:
507
+ return file, False, frozenset()
508
+ found: Entered | None = (
509
+ entered_async(manager, known, scope.inferred.types)
510
+ if asynchronous
511
+ else entered(manager, known, scope.inferred.types, scope.settings.facts.managers)
512
+ )
513
+ if found is None or bare(found[0].annotation, scope.settings.facts.generics):
514
+ return None, False, frozenset()
515
+ unsafe: bool
516
+ origins: frozenset[str]
517
+ unsafe, origins = guesses_in(scope, found[1])
518
+ return found[0], unsafe or bool(found[2]), origins | found[2]
519
+
520
+
521
+ def _bind_targets(scope: Scope, targets: list[ast.expr], code: str | None) -> None:
522
+ """Bind every name in `targets`, reporting `code` for each first binding."""
523
+ target: ast.expr
524
+ name: ast.Name
525
+ for target in targets:
526
+ for name in target_names(target):
527
+ scope.bind(name.id, at(name), code)
528
+
529
+
530
+ def _bind_captures(scope: Scope, cases: list[ast.match_case]) -> None:
531
+ """Bind every name the `case` patterns capture: LVA002 unless declared first."""
532
+ case: ast.match_case
533
+ name: str
534
+ where: tuple[int, int]
535
+ for case in cases:
536
+ for name, where in captures(case.pattern, scope.settings.lines):
537
+ scope.bind(name, where, UNTYPED_TARGET)
538
+
539
+
540
+ def _augmented_guess(scope: Scope, stmt: ast.AugAssign) -> Late | None:
541
+ """Guess the type `name op= value` binds, where the name's type or the value's is only a guess.
542
+
543
+ As the statement binds it once `--fix` has written those guesses, and they're declared.
544
+
545
+ Returns:
546
+ The type and what the guess rests on, or `None` if it's unknown still.
547
+
548
+ """
549
+ name: str = cast("ast.Name", stmt.target).id
550
+ operand: Late | None = guessed_type(scope, stmt.value)
551
+ certain: str | None = None if operand else certain_type(scope, stmt.value)
552
+ bound: str | None = augmented(stmt.op, operand[0] if operand else certain, scope.inferred.types.get(name))
553
+ none: frozenset[str] = frozenset()
554
+ own: frozenset[str] = scope.inferred.origins.get(name, none) if name in scope.inferred.guesses else none
555
+ origins: frozenset[str] = own | (operand[1] if operand else none)
556
+ return (bound, origins) if bound is not None and origins else None
557
+
558
+
559
+ def annotated(scope: Scope, stmt: ast.AnnAssign, target: ast.Name) -> None:
560
+ """Bind the name an annotated assignment declares (`name: T`, `name: T = value`).
561
+
562
+ One a widening annotated (its statement's last line is marked: see `widened`) is typed by its
563
+ value where `--fix` now knows it (see `_rewidened`), and has no type otherwise: a second pass
564
+ then finds nothing the first didn't.
565
+ """
566
+ name: str = target.id
567
+ annotation: ast.expr = stmt.annotation
568
+ fresh: bool = name not in scope.declared
569
+ marked: bool = stmt.end_lineno in scope.settings.known.limits.marks
570
+ scope.declare(name)
571
+ scope.annotation(name, annotation, marked=marked)
572
+ if fresh and marked and _rewidened(scope, stmt, target):
573
+ return
574
+ held: str | None = None if stmt.value is None else certain_type(scope, stmt.value)
575
+ if not marked: # a widening's type says nothing of the value: what's read of the name has none
576
+ scope.inferred.declare(name, annotation, held)
577
+ scope.lifetime(name).declare(ast.unparse(annotation), at(target), _span(annotation, target))
578
+ if stmt.value is not None:
579
+ scope.lifetime(name).bind(at(target), held)
580
+ scope.assigned(name, at(target))
581
+
582
+
583
+ def _rewidened(scope: Scope, stmt: ast.AnnAssign, target: ast.Name) -> bool:
584
+ """Offer a marked annotation's value's own type in its place (LVA005), where `--fix` now knows it.
585
+
586
+ The annotation is a widening's (see `widened`): the name is first bound here, as it was before
587
+ it was written. Its fix writes over the annotation and deletes the mark; the name is then what
588
+ its value makes it, to the rest of the scope.
589
+
590
+ Returns:
591
+ Whether it's offered; not for an annotation over several lines, a value `--fix` still can't
592
+ type, or a scope it leaves alone. One with no value is noted, for `redeclared`.
593
+
594
+ """
595
+ name: str = target.id
596
+ span: tuple[int, int] | None = _span(stmt.annotation, target)
597
+ scope.assignments.widened.add(name)
598
+ if stmt.value is None and span is not None and scope.kind.fixable:
599
+ scope.assignments.bare[name] = (stmt, None)
600
+ if stmt.value is None or span is None or not scope.kind.fixable:
601
+ return False
602
+ found: Inference | None
603
+ unsafe: bool
604
+ origins: frozenset[str]
605
+ found, unsafe, origins = scope.valued(stmt.value, target.lineno)
606
+ fix: Fix | None = (
607
+ None if found is None else scope.offer(found, origins, unsafe=unsafe, edit=Edit.REPLACE, span=span)
608
+ )
609
+ if found is None or fix is None:
610
+ return False
611
+ last: int = stmt.end_lineno or target.lineno
612
+ marked: Fix = fix._replace(drop=scope.settings.known.limits.marks[last], mark=last)
613
+ scope.offences.append(Offence(*at(stmt.annotation), name, VAGUE_TYPE, marked))
614
+ scope.lifetime(name).bind(at(target), None if unsafe else found.annotation)
615
+ scope.assigned(name, at(target))
616
+ scope.inferred.learn(name, found.annotation, origins if unsafe else None)
617
+ return True
618
+
619
+
620
+ def redeclared(scope: Scope) -> None:
621
+ """Offer each marked declaration with no value its name's type in its place (LVA005), once known.
622
+
623
+ `x: Any`, marked, before `for x in rows:`: where the loop, the unpacking or the `with` that
624
+ binds the name types it now, and nothing binds it again. The fix writes over the annotation and
625
+ deletes the mark, as `_rewidened`'s does; run once the scope is checked, and again without effect.
626
+ """
627
+ name: str
628
+ stmt: ast.AnnAssign
629
+ found: Inference | None
630
+ for name, (stmt, found) in list(scope.assignments.bare.items()):
631
+ del scope.assignments.bare[name]
632
+ span: tuple[int, int] | None = _span(stmt.annotation, stmt.target)
633
+ if found is None or span is None or scope.flow[name].escaped or len(scope.flow[name].bindings) != 1:
634
+ continue
635
+ guessed: bool = name in scope.inferred.guesses
636
+ origins: frozenset[str] = scope.inferred.origins.get(name, frozenset()) if guessed else frozenset()
637
+ fix: Fix | None
638
+ if (fix := scope.offer(found, origins, unsafe=guessed, edit=Edit.REPLACE, span=span)) is not None:
639
+ last: int = stmt.end_lineno or stmt.lineno
640
+ marked: Fix = fix._replace(drop=scope.settings.known.limits.marks[last], mark=last)
641
+ scope.offences.append(Offence(*at(stmt.annotation), name, VAGUE_TYPE, marked))
642
+
643
+
644
+ def _span(annotation: ast.expr, target: ast.expr) -> tuple[int, int] | None:
645
+ """Find an annotation's columns, if it's all on its target's line (so `--fix` can rewrite it).
646
+
647
+ Returns:
648
+ Its start and end columns (UTF-8 bytes, as `ast` counts), or `None`.
649
+
650
+ """
651
+ one_line: bool = annotation.lineno == annotation.end_lineno == target.lineno
652
+ return (annotation.col_offset, annotation.end_col_offset or 0) if one_line else None
Binary file