python-constricter 0.4.1__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-win_amd64.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win_amd64.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win_amd64.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win_amd64.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win_amd64.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win_amd64.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win_amd64.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win_amd64.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win_amd64.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win_amd64.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win_amd64.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win_amd64.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win_amd64.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win_amd64.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win_amd64.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win_amd64.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win_amd64.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win_amd64.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win_amd64.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win_amd64.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win_amd64.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win_amd64.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win_amd64.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win_amd64.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win_amd64.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win_amd64.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win_amd64.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win_amd64.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win_amd64.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win_amd64.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win_amd64.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win_amd64.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win_amd64.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win_amd64.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win_amd64.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win_amd64.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win_amd64.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win_amd64.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win_amd64.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win_amd64.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win_amd64.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win_amd64.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win_amd64.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win_amd64.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win_amd64.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win_amd64.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win_amd64.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win_amd64.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-win_amd64.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-win_amd64.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win_amd64.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win_amd64.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win_amd64.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win_amd64.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win_amd64.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win_amd64.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win_amd64.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win_amd64.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win_amd64.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win_amd64.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win_amd64.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win_amd64.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win_amd64.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win_amd64.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win_amd64.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win_amd64.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win_amd64.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win_amd64.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win_amd64.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win_amd64.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win_amd64.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win_amd64.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win_amd64.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-win_amd64.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-win_amd64.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win_amd64.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win_amd64.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win_amd64.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win_amd64.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win_amd64.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win_amd64.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win_amd64.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win_amd64.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win_amd64.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win_amd64.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win_amd64.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win_amd64.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win_amd64.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win_amd64.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win_amd64.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win_amd64.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win_amd64.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win_amd64.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win_amd64.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win_amd64.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win_amd64.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win_amd64.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win_amd64.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-win_amd64.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,189 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a module is checked with (`Settings`): its options, and what `--fix` knows of it.
3
+
4
+ Its own tables (`Tables`) joined with what other checked files say of it (`Outside`): functions'
5
+ returns, classes' members and lines of bases, the standard library's classes it takes from, what a
6
+ type checker sees otherwise (`facts`), and its parameters' types given from outside. A type the
7
+ module's own code writes with a type variable it imports is left out: `--fix` sees only its own.
8
+ """
9
+
10
+ import ast
11
+ from collections.abc import Mapping, Sequence
12
+ from dataclasses import replace
13
+ from typing import Final, cast
14
+
15
+ from constricter.fix.core import imports, inherited
16
+ from constricter.fix.core.known import (
17
+ Classes,
18
+ ClassSide,
19
+ Indirect,
20
+ Known,
21
+ LibraryNames,
22
+ Limits,
23
+ Outside,
24
+ Partial,
25
+ )
26
+ from constricter.fix.libraries import stdlib
27
+ from constricter.fix.values import callables, classvars, entered, fills, stored
28
+ from constricter.fix.values.doubts import facts, says_self
29
+ from constricter.offences import (
30
+ STARRED_SUBSCRIPTS,
31
+ Checks,
32
+ )
33
+ from constricter.rules import late, widened
34
+ from constricter.rules.annotations import free_of, free_of_all
35
+ from constricter.rules.calls import keyed, marked
36
+ from constricter.rules.declarations import (
37
+ awaited_returns,
38
+ casts,
39
+ class_attributes,
40
+ defined_type_vars,
41
+ factories,
42
+ self_returns,
43
+ )
44
+ from constricter.rules.decorators import passing
45
+ from constricter.rules.flow import Hierarchy
46
+ from constricter.rules.scoped import Seeded, Settings
47
+ from constricter.rules.syntax import (
48
+ owners,
49
+ python2_compatible,
50
+ )
51
+ from constricter.rules.tables import Tables
52
+ from constricter.rules.walked import of_type
53
+
54
+ TYPING_ALIAS: Final = (3, 10) # the first Python whose `typing` has `TypeAlias`
55
+
56
+
57
+ def module_settings(
58
+ tree: ast.Module,
59
+ checks: Checks,
60
+ lines: Sequence[str],
61
+ own: Tables,
62
+ outside: Outside | None = None,
63
+ ) -> Settings:
64
+ """Work out what the module `tree` is checked with (see the module docstring).
65
+
66
+ `checks`: its options; `lines`: its source lines; `own`: its own tables; `outside`: what the CLI
67
+ knows of it from the other checked files, if it's one of them.
68
+
69
+ Returns:
70
+ Its settings.
71
+
72
+ """
73
+ stored.hand(tree, {} if outside is None else outside.emptied)
74
+ # The functions' declared returns: other checked files' (see `Outside`), then the module's own.
75
+ calls: dict[str, str] = {**({} if outside is None else outside.calls), **own.returns, **own.side_calls}
76
+ imported: Classes | None = None if outside is None else outside.classes
77
+ # The bases whose members are known whole: the standard library's, and other checked files' below.
78
+ bases: dict[str, str] = stdlib.bases(tree, stdlib.origins(tree))
79
+ beyond: Mapping[str, inherited.Beyond] = {} if outside is None else outside.beyond
80
+ library: dict[str, inherited.Library] = {
81
+ text: (stdlib.members(path), stdlib.lines(path))
82
+ for text, path in (*bases.items(), *((end, end) for end in inherited.several(beyond)))
83
+ if path
84
+ }
85
+ # The file's own types that mention a type variable it imports (`--fix` sees only its own).
86
+ free: frozenset[str] = frozenset() if outside is None else outside.type_vars
87
+ selfish: dict[str, frozenset[str]] = self_returns(tree)
88
+ return Settings(
89
+ checks._replace(type_comments=checks.type_comments or python2_compatible(tree)),
90
+ lines,
91
+ Known(
92
+ free_of(calls, free),
93
+ factories(tree) | (frozenset[str]() if outside is None else outside.values),
94
+ {**(imported.attributes if imported else {}), **free_of_all(own.classes, free)},
95
+ {**(imported.methods if imported else {}), **free_of_all(own.methods, free)},
96
+ Indirect(
97
+ {**({} if outside is None else outside.awaits), **free_of(awaited_returns(tree), free)},
98
+ _partial(own, free, outside),
99
+ {**({} if outside is None else outside.tuples), **free_of(own.tuples, free)},
100
+ {**({} if outside is None else outside.unions), **own.unions},
101
+ {**({} if outside is None else outside.takers), **fills.takers(tree)},
102
+ ),
103
+ ClassSide(
104
+ free_of_all(own.order.flattened(class_attributes(tree)), free),
105
+ free_of_all(own.sides, free),
106
+ inherited.lineage(
107
+ tree,
108
+ selfish,
109
+ # A base whose line the index followed is in sight too, another package's included.
110
+ frozenset(bases).union(imported.methods if imported else (), beyond),
111
+ beyond,
112
+ library,
113
+ ),
114
+ classvars.variables(tree, classvars.imported(tree), outside, checks.plain_bases),
115
+ classvars.own_plain(tree, classvars.imported(tree), outside, checks.plain_bases),
116
+ ),
117
+ LibraryNames(
118
+ casts(tree),
119
+ stdlib.origins(tree),
120
+ # What annotations alone can name: its own imports for type checking, and other files' types'.
121
+ replace(
122
+ imports.plan(tree),
123
+ guarded={} if outside is None else {**outside.checking, **outside.guarded},
124
+ ),
125
+ {} if outside is None else outside.overloaded,
126
+ frozenset() if outside is None else outside.installed_classes,
127
+ {} if outside is None else outside.installed_parameters,
128
+ {} if outside is None else outside.installed_lineage,
129
+ {} if outside is None else outside.installed_aliases,
130
+ checks.min_python is not None and checks.min_python >= STARRED_SUBSCRIPTS,
131
+ checks.min_python is None or checks.min_python >= TYPING_ALIAS,
132
+ ),
133
+ Limits(checks.max_length, checks.vague, widened.marks(lines)),
134
+ vouched=passing(tree, defined_type_vars(tree)).union(
135
+ frozenset() if outside is None else outside.vouched,
136
+ ),
137
+ ),
138
+ Hierarchy.for_module(
139
+ tree,
140
+ {name: frozenset(wider) for name, wider in checks.narrower},
141
+ () if outside is None else outside.same,
142
+ ),
143
+ owners(tree, says_self),
144
+ tuple(
145
+ sorted(
146
+ (node.lineno, node.col_offset)
147
+ for node in cast("list[ast.NamedExpr]", of_type(tree, ast.NamedExpr))
148
+ ),
149
+ ),
150
+ () if outside is None else outside.hints,
151
+ facts(
152
+ tree,
153
+ selfish,
154
+ stdlib.generics(stdlib.origins(tree)) | (outside.generics if outside else frozenset[str]()),
155
+ {
156
+ **({} if outside is None else outside.managers),
157
+ **{name: (yielded, frozenset[str]()) for name, yielded in entered.managers(tree).items()},
158
+ },
159
+ defined_type_vars(tree) | free,
160
+ )._replace(
161
+ untyped=frozenset() if outside is None else outside.untyped,
162
+ attributed=frozenset(
163
+ node.value.id
164
+ for node in cast("list[ast.Attribute]", of_type(tree, ast.Attribute))
165
+ if isinstance(node.value, ast.Name)
166
+ ),
167
+ positional=callables.signatures(tree),
168
+ excused=late.excusing(lines),
169
+ ),
170
+ Seeded(
171
+ keyed(tree, {} if outside is None else outside.parameters),
172
+ {} if outside is None else outside.fixtures,
173
+ marked=marked(tree),
174
+ ),
175
+ )
176
+
177
+
178
+ def _partial(own: Tables, free: frozenset[str], outside: Outside | None) -> Partial:
179
+ """Gather the returns only an unpacking can use: other checked files', then the module's own.
180
+
181
+ Returns:
182
+ Them, less the module's that mention a type variable it imports (`free`).
183
+
184
+ """
185
+ given: Partial = Partial() if outside is None else outside.partial
186
+ return Partial(
187
+ {**given.calls, **free_of(own.partial, free)},
188
+ {**given.methods, **free_of_all(own.partial_methods, free)},
189
+ )
@@ -0,0 +1,472 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Walking the syntax the rules read: functions and methods, nested statements, binding targets."""
3
+
4
+ import ast
5
+ import bisect
6
+ import re
7
+ from collections.abc import Callable, Iterable, Iterator, Sequence
8
+ from typing import Final, TypeAlias, cast
9
+
10
+ from constricter.offences import at
11
+ from constricter.rules.walked import children as child_nodes
12
+ from constricter.rules.walked import classes, of_type
13
+
14
+ # A `# type:` comment, as a loop header writes one (LVA003).
15
+ _TYPE_COMMENT: Final = re.compile(rb"#\s*type:")
16
+
17
+ FunctionDef: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
18
+ FUNCTION_DEFS: tuple[type[ast.FunctionDef], type[ast.AsyncFunctionDef]] = (
19
+ ast.FunctionDef,
20
+ ast.AsyncFunctionDef,
21
+ )
22
+ # Statements whose nested statements may not all run (or not only once).
23
+ BRANCHING: Final = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ast.TryStar, ast.Match)
24
+ _LEAVING: Final = (ast.Return, ast.Raise, ast.Continue, ast.Break)
25
+ # The nodes with a scope of their own: what's inside one isn't the enclosing function's.
26
+ NESTED_SCOPES: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)
27
+ _END: Final = 1 << 62 # past any line: a module's span has no end
28
+ Start: TypeAlias = tuple[int, int] # where a node starts: its line and column
29
+ _FUTURE: Final = "__future__"
30
+ _SELF: Final = "self"
31
+ # `from __future__` features only code that also runs on Python 2 imports: its type comments count.
32
+ _PYTHON2_FUTURES: frozenset[str] = frozenset(
33
+ {
34
+ "nested_scopes",
35
+ "generators",
36
+ "division",
37
+ "absolute_import",
38
+ "with_statement",
39
+ "print_function",
40
+ "unicode_literals",
41
+ },
42
+ )
43
+
44
+
45
+ def python2_compatible(tree: ast.Module) -> bool:
46
+ """Check for a `from __future__` import only Python 2 needs.
47
+
48
+ Returns:
49
+ Whether it marks the module as written for Python 2.
50
+
51
+ """
52
+ return any(
53
+ isinstance(stmt, ast.ImportFrom)
54
+ and stmt.module == _FUTURE
55
+ and any(alias.name in _PYTHON2_FUTURES for alias in stmt.names)
56
+ for stmt in tree.body
57
+ )
58
+
59
+
60
+ def collect_functions(body: list[ast.stmt], into: list[FunctionDef]) -> None:
61
+ """Collect functions in a module or class body, through compound statements and classes."""
62
+ stmt: ast.stmt
63
+ for stmt in body:
64
+ if isinstance(stmt, FUNCTION_DEFS):
65
+ into.append(stmt)
66
+ elif isinstance(stmt, ast.ClassDef):
67
+ collect_functions(stmt.body, into)
68
+ else:
69
+ collect_functions(child_statements(stmt), into)
70
+
71
+
72
+ def owners(tree: ast.Module, selfish: Callable[[FunctionDef], bool]) -> dict[int, str]:
73
+ """Map each direct method of a class to that class's name, by the method's `id`.
74
+
75
+ For `--fix` to type a method's `self`. A method is a function directly in a class's body,
76
+ however deep through `if`/`try`/..., but not through a nested class's or function's own body.
77
+ A function defined in a method reads the method's `self` (see `_self_readers`): it's mapped
78
+ too, unless the method is one `selfish` says types its `self` otherwise (as `Self`).
79
+
80
+ Returns:
81
+ Each such function, by `id()`, mapped to its class's name.
82
+
83
+ """
84
+ found: dict[int, str] = {}
85
+ bindings: list[Start] = _self_bindings(tree)
86
+ node: ast.AST
87
+ methods: list[FunctionDef]
88
+ method: FunctionDef
89
+ for node in classes(tree):
90
+ methods = []
91
+ _direct_methods(node.body, methods)
92
+ for method in methods:
93
+ found[id(method)] = node.name
94
+ if (
95
+ _SELF in _first_parameter(method)
96
+ and not _binds_self(method, bindings)
97
+ and not selfish(method)
98
+ ):
99
+ found.update((id(inner), node.name) for inner in _self_readers(method, bindings))
100
+ return found
101
+
102
+
103
+ def _self_bindings(tree: ast.Module) -> list[Start]:
104
+ """Find where the module binds a name `self` other than as a parameter (few do, anywhere).
105
+
106
+ Returns:
107
+ Where each such store, deletion or import starts, in source order.
108
+
109
+ """
110
+ return sorted(
111
+ (node.lineno, node.col_offset)
112
+ for node in cast("list[ast.Name | ast.alias]", of_type(tree, ast.Name, ast.alias))
113
+ if (
114
+ (node.id == _SELF and not isinstance(node.ctx, ast.Load))
115
+ if isinstance(node, ast.Name)
116
+ else (node.asname or node.name) == _SELF
117
+ )
118
+ )
119
+
120
+
121
+ def _first_parameter(function: FunctionDef) -> list[str]:
122
+ """Name a function's first positional parameter.
123
+
124
+ Returns:
125
+ It, alone; nothing for a function without one.
126
+
127
+ """
128
+ return [arg.arg for arg in (*function.args.posonlyargs, *function.args.args)][:1]
129
+
130
+
131
+ def _self_readers(function: FunctionDef, bindings: Sequence[Start]) -> Iterator[FunctionDef]:
132
+ """Walk the functions defined in `function`, however deep, that read its `self`.
133
+
134
+ One taking no parameter `self` and binding none, in a function that does neither (but
135
+ `function` itself, whose `self` it is). `bindings`: the module's `_self_bindings`.
136
+
137
+ Yields:
138
+ Each.
139
+
140
+ """
141
+ nested: list[FunctionDef] = []
142
+ _direct_methods(function.body, nested)
143
+ inner: FunctionDef
144
+ for inner in nested:
145
+ args: ast.arguments = inner.args
146
+ params: list[ast.arg | None] = [
147
+ *args.posonlyargs,
148
+ *args.args,
149
+ *args.kwonlyargs,
150
+ args.vararg,
151
+ args.kwarg,
152
+ ]
153
+ if not any(arg is not None and arg.arg == _SELF for arg in params) and not _binds_self(
154
+ inner,
155
+ bindings,
156
+ ):
157
+ yield inner
158
+ yield from _self_readers(inner, bindings)
159
+
160
+
161
+ def _binds_self(function: FunctionDef, bindings: Sequence[Start]) -> bool:
162
+ """Check whether a function's own body binds `self` (an assignment, a loop, an import, ...).
163
+
164
+ `bindings`: where the module binds one at all (see `_self_bindings`): a function none is in
165
+ isn't walked.
166
+
167
+ Returns:
168
+ Whether it does.
169
+
170
+ """
171
+ return has_within(bindings, function) and any(
172
+ (isinstance(node, ast.Name) and node.id == _SELF and not isinstance(node.ctx, ast.Load))
173
+ or (isinstance(node, ast.alias) and (node.asname or node.name) == _SELF)
174
+ for node in own_nodes(function.body)
175
+ )
176
+
177
+
178
+ def _direct_methods(body: list[ast.stmt], into: list[FunctionDef]) -> None:
179
+ """Collect the functions directly in a class's body, through compound statements.
180
+
181
+ A nested class's own methods aren't included.
182
+ """
183
+ stmt: ast.stmt
184
+ for stmt in body:
185
+ if isinstance(stmt, FUNCTION_DEFS):
186
+ into.append(stmt)
187
+ elif not isinstance(stmt, ast.ClassDef):
188
+ _direct_methods(child_statements(stmt), into)
189
+
190
+
191
+ def import_bindings(body: Iterable[ast.stmt]) -> Iterator[tuple[str, str, str]]:
192
+ """Walk the absolute imports among `body`, and what each name they bind is.
193
+
194
+ `import os` binds `os` to `os` (and `import os.path` binds `os` too); `import os.path as p`
195
+ binds `p` to `os.path`; `from os import getpid as pid` binds `pid` to `os.getpid`. A relative
196
+ import names no module here.
197
+
198
+ Yields:
199
+ Each bound name, its dotted origin, and the module it comes from: an `import`'s top-level
200
+ package, a `from` import's module.
201
+
202
+ """
203
+ stmt: ast.stmt
204
+ module: str
205
+ alias: ast.alias
206
+ for stmt in body:
207
+ match stmt:
208
+ case ast.Import():
209
+ for alias in stmt.names:
210
+ top: str = alias.name.split(".", 1)[0]
211
+ yield alias.asname or top, alias.name if alias.asname else top, top
212
+ case ast.ImportFrom(module=str() as module, level=0):
213
+ for alias in stmt.names:
214
+ yield alias.asname or alias.name, f"{module}.{alias.name}", module
215
+ case _:
216
+ pass
217
+
218
+
219
+ def within(starts: Sequence[Start], node: ast.AST, begin: Start | None = None) -> slice:
220
+ """Find which of `starts` (in source order) are within `node`'s span of the source.
221
+
222
+ `begin`, if given, is where the span starts instead (a function's first decorator, say). A node
223
+ with no position (the module) spans everything.
224
+
225
+ Returns:
226
+ Their slice.
227
+
228
+ """
229
+ return slice(*_bounds(starts, node, begin))
230
+
231
+
232
+ def has_within(starts: Sequence[Start], node: ast.AST) -> bool:
233
+ """Check whether any of `starts` (in source order) is within `node`'s span (see `within`).
234
+
235
+ Returns:
236
+ Whether one is.
237
+
238
+ """
239
+ first: int
240
+ stop: int
241
+ first, stop = _bounds(starts, node, None)
242
+ return stop > first
243
+
244
+
245
+ def _bounds(starts: Sequence[Start], node: ast.AST, begin: Start | None) -> tuple[int, int]:
246
+ """Find where `node`'s span (from `begin`, if given) starts and stops among `starts`.
247
+
248
+ Returns:
249
+ The first index within it, and the first past it.
250
+
251
+ """
252
+ first: Start = begin or (getattr(node, "lineno", 0), getattr(node, "col_offset", 0))
253
+ end: Start = (getattr(node, "end_lineno", _END) or _END, getattr(node, "end_col_offset", 0) or 0)
254
+ return bisect.bisect_left(starts, first), bisect.bisect_left(starts, end)
255
+
256
+
257
+ def own_nodes(found: Sequence[ast.AST], parents: dict[int, ast.AST] | None = None) -> Iterator[ast.AST]:
258
+ """Walk `found`, depth first in source order, without entering a nested function, lambda or class.
259
+
260
+ `parents`, if given, records each node's parent (by `id()`) before the node is yielded.
261
+
262
+ Yields:
263
+ Each node, a nested scope's own too (but not what's inside it).
264
+
265
+ """
266
+ # A stack, not a recursion: a nested generator passes each node up through every level above it.
267
+ waiting: list[ast.AST | None] = [None, *reversed(found)] # `None` at its bottom ends it
268
+ node: ast.AST
269
+ for node in iter(waiting.pop, None):
270
+ yield node
271
+ if not isinstance(node, NESTED_SCOPES):
272
+ below: list[ast.AST] = child_nodes(node)
273
+ if parents is not None:
274
+ parents.update((id(child), node) for child in below)
275
+ waiting.extend(reversed(below))
276
+
277
+
278
+ def top_level(body: Sequence[ast.stmt]) -> Iterator[ast.stmt]:
279
+ """Walk a module's top-level statements, those under its `if`s and `try`s too.
280
+
281
+ Yields:
282
+ Each.
283
+
284
+ """
285
+ stmt: ast.stmt
286
+ for stmt in body:
287
+ yield stmt
288
+ if isinstance(stmt, ast.If | ast.Try | ast.TryStar):
289
+ yield from top_level(child_statements(stmt))
290
+
291
+
292
+ def child_statements(stmt: ast.stmt) -> list[ast.stmt]:
293
+ """Collect the statements nested directly in `stmt`.
294
+
295
+ Returns:
296
+ Them, in source order.
297
+
298
+ """
299
+ children: list[ast.stmt] = []
300
+ handler: ast.ExceptHandler
301
+ case: ast.match_case
302
+ match stmt:
303
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
304
+ children += stmt.body + stmt.orelse
305
+ case ast.With() | ast.AsyncWith():
306
+ children += stmt.body
307
+ case ast.Try() | ast.TryStar():
308
+ children += stmt.body
309
+ for handler in stmt.handlers:
310
+ children += handler.body
311
+ children += stmt.orelse + stmt.finalbody
312
+ case ast.Match():
313
+ for case in stmt.cases:
314
+ children += case.body
315
+ case _:
316
+ pass
317
+ return children
318
+
319
+
320
+ def arm_ends(stmt: ast.stmt) -> frozenset[int]:
321
+ """Find where each arm of a branching statement ends: a body, an `else`, a handler, a `match` case.
322
+
323
+ Returns:
324
+ Each arm's last statement, by its `id()`; not one that leaves (`return`, `raise`, ...), past
325
+ which its arm doesn't go on.
326
+
327
+ """
328
+ blocks: list[list[ast.stmt]]
329
+ match stmt:
330
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
331
+ blocks = [stmt.body, stmt.orelse]
332
+ case ast.Try() | ast.TryStar():
333
+ blocks = [stmt.body, *(handler.body for handler in stmt.handlers), stmt.orelse]
334
+ case _: # a `match`, the one other branching statement
335
+ blocks = [case.body for case in cast("ast.Match", stmt).cases]
336
+ return frozenset(id(block[-1]) for block in blocks if block and not isinstance(block[-1], _LEAVING))
337
+
338
+
339
+ def expressions(stmt: ast.stmt) -> Iterator[ast.AST]:
340
+ """Walk the parts of `stmt` that aren't statements.
341
+
342
+ Yields:
343
+ Each, as where a `:=` can bind.
344
+
345
+ """
346
+ child: ast.AST
347
+ for child in child_nodes(stmt):
348
+ if isinstance(child, ast.match_case | ast.ExceptHandler):
349
+ yield from (part for part in child_nodes(child) if not isinstance(part, ast.stmt))
350
+ elif not isinstance(child, ast.stmt):
351
+ yield child
352
+
353
+
354
+ def target_names(target: ast.expr) -> Iterator[ast.Name]:
355
+ """Walk an assignment target.
356
+
357
+ Yields:
358
+ Each plain name it binds.
359
+
360
+ """
361
+ elements: list[ast.expr]
362
+ element: ast.expr
363
+ value: ast.expr
364
+ match target:
365
+ case ast.Name():
366
+ yield target
367
+ case ast.Tuple(elts=elements) | ast.List(elts=elements):
368
+ for element in elements:
369
+ yield from target_names(element)
370
+ case ast.Starred(value=value):
371
+ yield from target_names(value)
372
+ case _:
373
+ return
374
+
375
+
376
+ def captures(pattern: ast.pattern, lines: Sequence[str]) -> Iterator[tuple[str, tuple[int, int]]]:
377
+ """Walk a `case` pattern.
378
+
379
+ Yields:
380
+ Each name it captures, with where it's bound.
381
+
382
+ """
383
+ node: ast.AST
384
+ name: str
385
+ for node in ast.walk(pattern):
386
+ match node:
387
+ case ast.MatchAs(name=str() as name) | ast.MatchStar(name=str() as name):
388
+ yield name, at(node)
389
+ case ast.MatchMapping(rest=str() as name):
390
+ yield name, _rest_at(node, name, lines)
391
+ case _:
392
+ pass
393
+
394
+
395
+ _REST: Final = re.compile(rb"\*\*\s*(\w+)\b")
396
+
397
+
398
+ def _rest_at(node: ast.MatchMapping, name: str, lines: Sequence[str]) -> tuple[int, int]:
399
+ """Find `**name` in a mapping pattern's source.
400
+
401
+ Returns:
402
+ Its position (as `ast` gives it, a byte column), or else the pattern's start.
403
+
404
+ """
405
+ target: bytes = name.encode()
406
+ number: int
407
+ encoded: bytes
408
+ start: int
409
+ found: re.Match[bytes]
410
+ for number in range(node.lineno, min(node.end_lineno or node.lineno, len(lines)) + 1):
411
+ encoded = lines[number - 1].encode()
412
+ start = node.col_offset if number == node.lineno else 0
413
+ for found in _REST.finditer(encoded, start):
414
+ if found.group(1) == target:
415
+ return number, found.start(1)
416
+ return at(node)
417
+
418
+
419
+ def comment_type(comment: str) -> str | None:
420
+ """Read a loop's `# type:` comment as an annotation (`int, str` is a tuple's parts).
421
+
422
+ Returns:
423
+ It, or `None` if it doesn't parse as one.
424
+
425
+ """
426
+ text: str = comment.split("#", 1)[0].strip() # a trailing comment (a noqa one, say) isn't part of it
427
+ try:
428
+ parsed: ast.expr = ast.parse(text, mode="eval").body
429
+ except SyntaxError:
430
+ return None
431
+ if isinstance(parsed, ast.Tuple):
432
+ return f"tuple[{', '.join(ast.unparse(part) for part in parsed.elts)}]"
433
+ return ast.unparse(parsed)
434
+
435
+
436
+ def declared_return(func: FunctionDef) -> ast.expr | None:
437
+ """Read the return type a function declares: its annotation, or its `# type:` signature comment's.
438
+
439
+ `def find(): # type: () -> List[str]` declares a `List[str]`, as a type checker reads it.
440
+
441
+ Returns:
442
+ It, or `None` for a function declaring none (or a comment that isn't a signature).
443
+
444
+ """
445
+ if func.returns is not None or not func.type_comment:
446
+ return func.returns
447
+ try:
448
+ return ast.parse(func.type_comment, mode="func_type").returns
449
+ except SyntaxError:
450
+ return None
451
+
452
+
453
+ def type_comment_span(lines: Sequence[str], stmt: ast.For | ast.AsyncFor) -> tuple[int, int] | None:
454
+ """Find the columns (UTF-8 bytes) of a one-line loop header's `# type:` comment, to delete it.
455
+
456
+ From the end of the header's code (or, with a comment after it, `# type: int # noqa`, from the
457
+ `#`), up to that comment or the end of the line, so the other comment stays.
458
+
459
+ Returns:
460
+ Them, or `None` if the header spans lines or the comment isn't found after the iterable.
461
+
462
+ """
463
+ if not lines or stmt.body[0].lineno == stmt.lineno or stmt.iter.end_lineno != stmt.lineno:
464
+ return None
465
+ raw: bytes = lines[stmt.lineno - 1].encode()
466
+ found: re.Match[bytes] | None
467
+ if (found := _TYPE_COMMENT.search(raw, stmt.iter.end_col_offset or 0)) is None:
468
+ return None
469
+ after: int
470
+ if (after := raw.find(b"#", found.end())) >= 0:
471
+ return found.start(), after
472
+ return len(raw[: found.start()].rstrip()), len(raw.rstrip(b"\r\n"))