python-constricter 0.4.1__cp313-cp313-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp313-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp313-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp313-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp313-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp313-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp313-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp313-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp313-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp313-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp313-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp313-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp313-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp313-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp313-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp313-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp313-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp313-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp313-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp313-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp313-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp313-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp313-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp313-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp313-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp313-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp313-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp313-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp313-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp313-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp313-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp313-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp313-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp313-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp313-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp313-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp313-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp313-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp313-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp313-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp313-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp313-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp313-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp313-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp313-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp313-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp313-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp313-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp313-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp313-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp313-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp313-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp313-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp313-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp313-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp313-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp313-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp313-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp313-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp313-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp313-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp313-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp313-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp313-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp313-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp313-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp313-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp313-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp313-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp313-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp313-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp313-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp313-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp313-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp313-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp313-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp313-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp313-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp313-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp313-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp313-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,91 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A checked file's classes' methods defined with `@overload`, for the calls their arguments decide.
3
+
4
+ On a receiver typed as the class, as the calling file spells it: read and written as its functions'
5
+ overloads are (see `constricter.fix.index.own_overloads`), each under `Class.method`.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Final
10
+
11
+ from constricter.fix.core.known import Guarded
12
+ from constricter.fix.core.signatures import ReadSignature
13
+ from constricter.fix.index import crossed, own_overloads
14
+ from constricter.fix.index.modules import SUFFIX, Index, Module, module_name
15
+ from constricter.fix.index.stubs import stubbed
16
+ from constricter.rules.annotations import roots
17
+
18
+ if TYPE_CHECKING:
19
+ from constricter.fix.index.stubs.declared import Signature
20
+
21
+ _SELF: Final = "Self"
22
+
23
+
24
+ def overloaded(
25
+ catalog: Index,
26
+ path: Path,
27
+ guarded: dict[str, Guarded],
28
+ ) -> dict[str, tuple[ReadSignature, ...]]:
29
+ """Find the overloaded methods the file at `path` calls, of the checked files' classes it names.
30
+
31
+ `guarded` records the names their returns need imported for type checking.
32
+
33
+ Returns:
34
+ Each one's signatures, by the class as the file writes it and the method (`Frame.get`); none
35
+ for a file `catalog` doesn't have.
36
+
37
+ """
38
+ target: Module | None
39
+ if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
40
+ return {}
41
+ found: dict[str, tuple[ReadSignature, ...]] = {}
42
+ key: str
43
+ defined: tuple[Module, str]
44
+ named: dict[str, tuple[Module, str]] = {
45
+ **dict(crossed.spelled_classes(catalog, target, (target.names, target.attributes), set())),
46
+ **{name: (target, name) for name in target.bases}, # its own
47
+ }
48
+ for key, defined in named.items():
49
+ name: str
50
+ # Those it calls of the class's overloaded ones: few, of the thousands of classes it can name.
51
+ for name in sorted(_methods(catalog, defined[0]).get(defined[1], frozenset()) & target.method_calls):
52
+ entry: str = f"{defined[1]}.{name}"
53
+ written: tuple[Signature, ...] = defined[0].overloads.get(entry, ())
54
+ # One returning `Self` is its receiver's class: not the type the signature writes; one
55
+ # declaring its `self` is for some receivers alone.
56
+ if not written or any(
57
+ each.self_type is not None or _SELF in roots(each.returns or _SELF) for each in written
58
+ ):
59
+ continue
60
+ found[f"{key}.{name}"] = own_overloads.spelled(
61
+ catalog,
62
+ target,
63
+ (defined[0], entry),
64
+ stubbed.read_signatures(catalog.modules, defined[0], entry),
65
+ guarded,
66
+ )
67
+ return {entry: read for entry, read in found.items() if read}
68
+
69
+
70
+ def _methods(catalog: Index, module: Module) -> dict[str, frozenset[str]]:
71
+ """Group the methods a checked module's classes define with `@overload` by class, once for the index.
72
+
73
+ Returns:
74
+ Each class's, by its name (`Frame`, of `Frame.get`); none for an installed module.
75
+
76
+ """
77
+ key: tuple[str, str] = (module.name, module.folder)
78
+ found: dict[str, frozenset[str]] | None
79
+ if (found := None if catalog.memo is None else catalog.memo.overloaded.get(key)) is None:
80
+ grouped: dict[str, set[str]] = {}
81
+ entry: str
82
+ for entry in () if module.installed else module.overloads:
83
+ owner: str
84
+ method: str
85
+ owner, _, method = entry.rpartition(".")
86
+ if owner:
87
+ grouped.setdefault(owner, set()).add(method)
88
+ found = {owner: frozenset(methods) for owner, methods in grouped.items()}
89
+ if catalog.memo is not None:
90
+ catalog.memo.overloaded[key] = found
91
+ return found
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A checked file's functions defined with `@overload`, for the calls their arguments decide.
3
+
4
+ Their signatures are read as an installed package's are (see `constricter.fix.index.stubs.stubbed`), but
5
+ for what each returns: a type the calling file writes as it would any other checked file's.
6
+ """
7
+
8
+ import ast
9
+
10
+ from constricter.fix.core.known import SPELLED, Guarded
11
+ from constricter.fix.core.signatures import ReadSignature
12
+ from constricter.fix.index import project
13
+ from constricter.fix.index.modules import Index, Module
14
+ from constricter.fix.index.stubs.declared import Signature
15
+
16
+
17
+ def written(module: Module, name: str) -> tuple[Signature, ...]:
18
+ """Find a function's signatures as written: an installed module's, or a checked file's overloads.
19
+
20
+ Returns:
21
+ Them.
22
+
23
+ """
24
+ return module.overloads[name] if module.declared is None else module.declared.signatures[name]
25
+
26
+
27
+ def spelled(
28
+ catalog: Index,
29
+ target: Module,
30
+ defined: tuple[Module, str],
31
+ read: tuple[ReadSignature, ...],
32
+ guarded: dict[str, Guarded] | None,
33
+ ) -> tuple[ReadSignature, ...]:
34
+ """Write a checked file's overloads' returns as `target` can (see `project.spelled_in`).
35
+
36
+ Each is the type itself then (`SPELLED`), not a template: no overload of a checked file's binds
37
+ a type variable.
38
+
39
+ Returns:
40
+ The signatures; none if a return isn't declared, or `target` can't write it (a type
41
+ variable, a generic class without its arguments, a name it binds otherwise).
42
+
43
+ """
44
+ found: list[ReadSignature] = []
45
+ names: dict[str, Guarded] = dict(guarded or {})
46
+ signature: Signature
47
+ each: ReadSignature
48
+ for signature, each in zip(defined[0].overloads[defined[1]], read, strict=True):
49
+ text: str | None = (
50
+ None
51
+ if signature.returns is None
52
+ else project.spelled_in(catalog, target, defined[0], _unquoted(signature.returns), names)
53
+ )
54
+ if text is None:
55
+ return ()
56
+ found.append(each._replace(returns=f"{SPELLED}{text}"))
57
+ if guarded is not None:
58
+ guarded.update(names)
59
+ return tuple(found)
60
+
61
+
62
+ def _unquoted(returns: str) -> str:
63
+ """Read a return written in quotes (`-> "Frame"`) as the type it names.
64
+
65
+ Returns:
66
+ Its text; any other return as it is.
67
+
68
+ """
69
+ node: ast.expr = ast.parse(returns, mode="eval").body
70
+ return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str) else returns
@@ -0,0 +1,330 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A checked file's classes in its overloads' parameters, and where each class a calling file names is from.
3
+
4
+ A parameter typed as nothing but checked files' classes (`frame: DataFrame`), or as an iterable of
5
+ them (`objs: Iterable[DataFrame] | Mapping[K, DataFrame]`), takes an argument by its class's
6
+ lineage: its own path and its bases', through the checked files, as an installed class's is
7
+ matched (see `constricter.fix.index.stubs.stubbed`). `concat([df, df])` then picks the overload taking
8
+ `DataFrame`s, and not the one taking `Series`.
9
+ """
10
+
11
+ import ast
12
+ from collections.abc import Callable, Iterator, Mapping, Sequence
13
+ from pathlib import Path
14
+ from types import MappingProxyType
15
+ from typing import Final, NamedTuple, TypeAlias
16
+
17
+ from constricter.fix.core.known import Guarded, Origin
18
+ from constricter.fix.core.signatures import OWN, OWN_ELEMENTS, Accepts
19
+ from constricter.fix.index import plain, project
20
+ from constricter.fix.index.modules import SUFFIX, Index, Memo, Module, classes_under, exported, module_name
21
+ from constricter.fix.index.stubs.atoms import BUILTINS_MODULE, CLASS, Atom, Scope
22
+ from constricter.fix.libraries import stdlib
23
+ from constricter.fix.values import classvars
24
+ from constricter.overrides import override
25
+
26
+ _NONE_ATOM: Final = "none"
27
+ _LITERAL_ATOM: Final = "literal"
28
+ _UNFOLLOWED: Final = "?" # a lineage's base that can't be followed
29
+ _DEPTH: Final = 8 # how many classes a line of bases is followed through
30
+ # Generic classes of one argument whose instance a builtin sequence or set argument is, by its elements.
31
+ _ITERABLES: Final = frozenset(
32
+ {"Iterable", "Iterator", "Collection", "Sequence", "MutableSequence", "list", "List"}
33
+ | {"set", "Set", "AbstractSet", "frozenset", "FrozenSet"},
34
+ )
35
+ # Those a builtin sequence or set isn't: they neither take one nor say what its elements are.
36
+ _MAPPINGS: Final = frozenset({"Mapping", "MutableMapping", "dict", "Dict"})
37
+ _Atoms: TypeAlias = Callable[[ast.expr, Scope], Iterator[Atom]]
38
+ _Lines: TypeAlias = Mapping[str, tuple[str, ...]] # each class's line of bases, by how a file names it
39
+ _NO_LINES: Final[_Lines] = MappingProxyType({})
40
+
41
+
42
+ def accept(modules: Mapping[str, Module], atoms: Sequence[Atom], read: _Atoms, found: Accepts) -> None:
43
+ """Add which checked files' classes a parameter takes to `found`, where it takes nothing else.
44
+
45
+ `atoms`: its annotation's members; `read`: what reads a type argument's. `OWN`: the classes
46
+ (each `module.Class`) an argument must be one of, for a parameter whose every member is one,
47
+ `None` or a builtin class; `OWN_ELEMENTS`: those a builtin sequence's or set's elements must
48
+ be, for one whose every member is an iterable of such classes, a mapping or `None`. Neither
49
+ where a member could take anything else.
50
+ """
51
+ direct: list[str] | None
52
+ if direct := _classes(modules, atoms):
53
+ found[OWN] = direct
54
+ elements: list[str] = []
55
+ atom: Atom
56
+ for atom in atoms:
57
+ name: str = atom.origin[1] or ""
58
+ if atom.kind == _NONE_ATOM or (atom.kind == CLASS and name in _MAPPINGS):
59
+ continue
60
+ inner: list[str] | None = (
61
+ _classes(modules, list(read(atom.args[0], atom.scope)))
62
+ if atom.kind == CLASS and name in _ITERABLES and len(atom.args) == 1 and atom.scope is not None
63
+ else None
64
+ )
65
+ if not inner:
66
+ return
67
+ elements.extend(inner)
68
+ if elements:
69
+ found[OWN_ELEMENTS] = sorted(set(elements))
70
+
71
+
72
+ def _classes(modules: Mapping[str, Module], atoms: Sequence[Atom]) -> list[str] | None:
73
+ """Name the checked files' classes a union's members are, where each is one, `None` or a builtin class.
74
+
75
+ Returns:
76
+ Their paths, sorted; `None` where a member is anything else (an alias left unread, a type
77
+ variable, another package's class).
78
+
79
+ """
80
+ found: set[str] = set()
81
+ atom: Atom
82
+ for atom in atoms:
83
+ module: Module | None = modules.get(atom.origin[0])
84
+ if (
85
+ atom.kind == CLASS
86
+ and module is not None
87
+ and not module.installed
88
+ and atom.origin[1] in module.bases
89
+ ):
90
+ found.add(f"{atom.origin[0]}.{atom.origin[1]}")
91
+ elif atom.kind not in {_NONE_ATOM, _LITERAL_ATOM} and not (
92
+ atom.kind == CLASS and atom.origin[0] == BUILTINS_MODULE and not atom.args
93
+ ):
94
+ return None
95
+ return sorted(found)
96
+
97
+
98
+ def lineages(
99
+ catalog: Index,
100
+ path: Path,
101
+ guarded: Mapping[str, Guarded],
102
+ installed: _Lines = _NO_LINES,
103
+ ) -> "Lineages":
104
+ """Find where each checked file's class the file at `path` names comes from, by how it names it.
105
+
106
+ Its own classes, those it imports (for type checking too, `guarded`), and those of the modules
107
+ it imports; and `installed`, installed packages' classes, which go first.
108
+
109
+ Returns:
110
+ Each one's path and its bases', through the checked files, `?` last where a base can't be
111
+ followed (another package's, a computed one); none for a file `catalog` doesn't have.
112
+
113
+ """
114
+ target: Module | None
115
+ if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
116
+ return Lineages(catalog, Named({}, {}, frozenset()), installed)
117
+ own: dict[str, tuple[str, ...]] = {
118
+ name: _lined(catalog, f"{target.name}.{name}") for name in target.bases
119
+ }
120
+ # Those it names for type checking alone, then by its own imports, which the index keeps.
121
+ checking: dict[str, Origin] = {
122
+ name: each.origin for name, each in guarded.items() if name not in target.names
123
+ }
124
+ under: dict[str, str] = {}
125
+ names: Mapping[str, Origin]
126
+ for names in (checking, target.names):
127
+ local: str
128
+ origin: Origin
129
+ for local, origin in project.resolved(catalog, names):
130
+ defined: tuple[Module, str] | None
131
+ if origin[1] is None:
132
+ under[local] = origin[0]
133
+ continue
134
+ defined = None if origin[0] == target.name else project.definition(catalog.modules, origin, CLASS)
135
+ if defined is not None and not defined[0].installed:
136
+ own[local] = _lined(catalog, f"{defined[0].name}.{defined[1]}")
137
+ return Lineages(catalog, Named(own, under, target.attributes), installed)
138
+
139
+
140
+ class Named(NamedTuple):
141
+ """The classes a file names itself, by its own or imported name, and where it can name more.
142
+
143
+ `under`: each module it imports, by how it names it, a class of whose (or of whose submodules')
144
+ it may name by that (`pkg.m.Row`); `attributes`: the attributes it takes of anything, which a
145
+ class its package re-exports must be one of (`pkg.Row`).
146
+ """
147
+
148
+ own: dict[str, tuple[str, ...]]
149
+ under: dict[str, str]
150
+ attributes: frozenset[str]
151
+
152
+
153
+ class Lineages(Mapping[str, tuple[str, ...]]):
154
+ """Each class a file names, by how it names it, and its line of bases (see `lineages`).
155
+
156
+ Those it names itself or imports by name are worked out at once; one of a module it imports
157
+ (`test.support.Foo`, any of a package's) only when it's asked for: a file may name any of
158
+ thousands, and names a few. Two are equal where the file names the same, the index aside.
159
+ """
160
+
161
+ def __init__(self, catalog: Index, named: Named, installed: Mapping[str, tuple[str, ...]]) -> None:
162
+ """Keep what `lineages` found, `installed` first, and the index the rest are found in."""
163
+ self.catalog: Index = catalog
164
+ self.named: Named = named
165
+ self.installed: Mapping[str, tuple[str, ...]] = installed
166
+ self.asked: dict[str, tuple[str, ...] | None] = {}
167
+
168
+ @override
169
+ def __getitem__(self, key: str) -> tuple[str, ...]:
170
+ """Find a class's line of bases, as `lineages` would have.
171
+
172
+ Returns:
173
+ It.
174
+
175
+ Raises:
176
+ KeyError: the file names no such class.
177
+
178
+ """
179
+ found: tuple[str, ...] | None = self.installed.get(key) or self.named.own.get(key)
180
+ if found is None:
181
+ if key not in self.asked:
182
+ self.asked[key] = self._under(key)
183
+ found = self.asked[key]
184
+ if found is None:
185
+ raise KeyError(key)
186
+ return found
187
+
188
+ def _under(self, key: str) -> tuple[str, ...] | None:
189
+ """Find a class `key` names through a module the file imports, a checked file's.
190
+
191
+ `local.Row` for one the module defines, `local.m.Row` for one a submodule does, and
192
+ `local.Row` for one it imports from another of its package's, a name the file takes.
193
+
194
+ Returns:
195
+ Its line of bases, or `None`.
196
+
197
+ """
198
+ parts: list[str] = key.split(".")
199
+ cut: int
200
+ for cut in range(len(parts) - 1, 0, -1):
201
+ package: str | None
202
+ if (package := self.named.under.get(".".join(parts[:cut]))) is None:
203
+ continue
204
+ rest: list[str] = parts[cut:]
205
+ modules: Mapping[str, Module] = self.catalog.modules
206
+ defined: tuple[Module, str] | None = project.definition(
207
+ modules,
208
+ (".".join([package, *rest[:-1]]), rest[-1]),
209
+ CLASS,
210
+ 1,
211
+ )
212
+ top: Module | None = modules.get(package)
213
+ where: Origin | None = None if top is None or len(rest) != 1 else exported(top).get(rest[0])
214
+ if defined is None and where is not None and where[1] is not None and where[0] != package:
215
+ defined = (
216
+ project.definition(modules, where, CLASS) if rest[0] in self.named.attributes else None
217
+ )
218
+ if defined is None or defined[0].installed:
219
+ return None
220
+ return _lined(self.catalog, f"{defined[0].name}.{defined[1]}")
221
+ return None
222
+
223
+ @override
224
+ def __iter__(self) -> Iterator[str]:
225
+ """Name every class the file can, its own and installed ones first: each one asked for.
226
+
227
+ Yields:
228
+ Each one's name, as the file names it.
229
+
230
+ """
231
+ yield from self.installed
232
+ yield from (name for name in self.named.own if name not in self.installed)
233
+ local: str
234
+ package: str
235
+ for local, package in self.named.under.items():
236
+ top: Module | None = self.catalog.modules.get(package)
237
+ spelled: list[str] = [key for key, _ in classes_under(self.catalog, local, package, CLASS)]
238
+ spelled += [
239
+ f"{local}.{name}"
240
+ for name in (() if top is None else exported(top))
241
+ if name in self.named.attributes
242
+ ]
243
+ key: str
244
+ for key in spelled:
245
+ if key not in self.installed and key not in self.named.own and key in self:
246
+ yield key
247
+
248
+ @override
249
+ def __len__(self) -> int:
250
+ """Count the classes the file can name (see `__iter__`).
251
+
252
+ Returns:
253
+ How many.
254
+
255
+ """
256
+ return sum(1 for _ in self)
257
+
258
+ @override
259
+ def __eq__(self, other: object) -> bool:
260
+ """Compare two by what decides them: the classes named at once and the modules the rest are under.
261
+
262
+ Returns:
263
+ Whether they're equal.
264
+
265
+ """
266
+ if isinstance(other, Lineages):
267
+ return (self.named, dict(self.installed)) == (other.named, dict(other.installed))
268
+ return super().__eq__(other)
269
+
270
+ @override
271
+ def __hash__(self) -> int:
272
+ """Hash it by part of what `__eq__` compares: the modules the file names classes under.
273
+
274
+ Returns:
275
+ The hash.
276
+
277
+ """
278
+ return hash((tuple(self.named.under.items()), self.named.attributes))
279
+
280
+ @override
281
+ def __reduce__(self) -> tuple[type[dict[str, tuple[str, ...]]], tuple[dict[str, tuple[str, ...]]]]:
282
+ """Pickle what's worked out so far, as a `dict`: the index stays behind.
283
+
284
+ Returns:
285
+ How.
286
+
287
+ """
288
+ asked: dict[str, tuple[str, ...]] = {
289
+ key: line for key, line in self.asked.items() if line is not None
290
+ }
291
+ return dict, ({**asked, **self.named.own, **self.installed},)
292
+
293
+
294
+ def _lined(catalog: Index, defined: str) -> tuple[str, ...]:
295
+ """Follow a checked file's class up its bases (see `_line`), once for the index.
296
+
297
+ Returns:
298
+ Its path and its bases', each once.
299
+
300
+ """
301
+ memo: Memo | None = catalog.memo
302
+ line: tuple[str, ...] | None
303
+ if (line := None if memo is None else memo.lines.get(defined)) is None:
304
+ line = tuple(dict.fromkeys(_line(catalog.modules, defined, _DEPTH)))
305
+ if memo is not None:
306
+ memo.lines[defined] = line
307
+ return line
308
+
309
+
310
+ def _line(modules: Mapping[str, Module], defined: str, depth: int) -> Iterator[str]:
311
+ """Follow a checked file's class (`defined`: its dotted path) up its bases.
312
+
313
+ Yields:
314
+ Its path, then its bases' in turn; `?` for a base that isn't a checked file's class or the
315
+ standard library's, or one too deep.
316
+
317
+ """
318
+ module: Module | None = modules.get(defined.rpartition(".")[0])
319
+ name: str = defined.rpartition(".")[2]
320
+ if module is None or module.installed or name not in module.bases:
321
+ if stdlib.held_whole(defined) is None and defined.rpartition(".")[0] != BUILTINS_MODULE:
322
+ yield _UNFOLLOWED
323
+ return
324
+ yield defined
325
+ base: str
326
+ for base in module.bases[name]:
327
+ if base == classvars.SPECIAL or not depth:
328
+ yield _UNFOLLOWED
329
+ else:
330
+ yield from _line(modules, plain.resolved(modules, module, base), depth - 1)
@@ -0,0 +1,81 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A module's plain top-level functions with a parameter left unannotated, and their parameters.
3
+
4
+ What `constricter.fix.index.callers` matches each call's arguments to (`Param`).
5
+ """
6
+
7
+ import ast
8
+ from typing import Final, TypeAlias
9
+
10
+ # A function's parameter: its name, kind (`p` positional only, `e` either, `k` keyword only), and
11
+ # whether it has a default and an annotation.
12
+ Param: TypeAlias = tuple[str, str, bool, bool]
13
+ _POSITIONAL: Final = "p"
14
+ _EITHER: Final = "e"
15
+ _KEYWORD: Final = "k"
16
+
17
+
18
+ def open_functions(tree: ast.Module) -> dict[str, tuple[Param, ...]]:
19
+ """Find a module's plain top-level functions with a parameter left unannotated.
20
+
21
+ Plain: not decorated (a decorator may change how it's called), without `*args` or `**kwargs`
22
+ (whose arguments can't be matched), and defined once.
23
+
24
+ Returns:
25
+ Each one's parameters, by its name.
26
+
27
+ """
28
+ counts: dict[str, int] = {}
29
+ stmt: ast.stmt
30
+ for stmt in tree.body:
31
+ if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
32
+ counts[stmt.name] = counts.get(stmt.name, 0) + 1
33
+ return {
34
+ stmt.name: _params(stmt.args)
35
+ for stmt in tree.body
36
+ if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
37
+ and counts[stmt.name] == 1
38
+ and _plain(stmt)
39
+ }
40
+
41
+
42
+ def _plain(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
43
+ """Check a function is undecorated, without `*args` or `**kwargs`, and has an unannotated parameter.
44
+
45
+ Returns:
46
+ Whether it is.
47
+
48
+ """
49
+ args: ast.arguments = function.args
50
+ return (
51
+ not function.decorator_list
52
+ and args.vararg is None
53
+ and args.kwarg is None
54
+ and any(arg.annotation is None for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs))
55
+ )
56
+
57
+
58
+ def _params(args: ast.arguments) -> tuple[Param, ...]:
59
+ """Read a function's parameters (see `Param`).
60
+
61
+ Returns:
62
+ Them, in order.
63
+
64
+ """
65
+ positional: list[ast.arg] = [*args.posonlyargs, *args.args]
66
+ first_default: int = len(positional) - len(args.defaults)
67
+ return (
68
+ *(
69
+ (
70
+ arg.arg,
71
+ _POSITIONAL if at < len(args.posonlyargs) else _EITHER,
72
+ at >= first_default,
73
+ arg.annotation is not None,
74
+ )
75
+ for at, arg in enumerate(positional)
76
+ ),
77
+ *(
78
+ (arg.arg, _KEYWORD, default is not None, arg.annotation is not None)
79
+ for arg, default in zip(args.kwonlyargs, args.kw_defaults, strict=True)
80
+ ),
81
+ )