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,327 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What crosses into a file from the other checked files it imports, for `--fix`.
3
+
4
+ `imported` gives a file the return types of the functions it imports (see `project.calls`), and
5
+ each imported class's attributes and methods' returns, its plain classes' variables, and which are
6
+ generic, each keyed as the file spells the class (`spelled_classes`): only where every name in a
7
+ type means the same in the file as where it was written (see `project.portable`).
8
+ """
9
+
10
+ from collections.abc import Iterator, Mapping
11
+ from pathlib import Path
12
+ from typing import NamedTuple, TypeAlias
13
+
14
+ from constricter.fix.core.known import Classes, Guarded, Origin, Partial, Returns
15
+ from constricter.fix.index.modules import (
16
+ SUFFIX,
17
+ Index,
18
+ Memo,
19
+ Module,
20
+ classes_under,
21
+ exported,
22
+ module_name,
23
+ )
24
+ from constricter.fix.index.project import (
25
+ BUILTINS,
26
+ CLASS,
27
+ HOPS,
28
+ NO_TYPE,
29
+ PARTIAL,
30
+ Imported,
31
+ calls,
32
+ definition,
33
+ is_generic,
34
+ portable,
35
+ resolved,
36
+ returned,
37
+ typed_calls,
38
+ )
39
+ from constricter.rules.annotations import roots
40
+
41
+ _Named: TypeAlias = tuple[Mapping[str, Origin], frozenset[str] | None] # see `spelled_classes`
42
+
43
+
44
+ def imported(
45
+ catalog: Index,
46
+ path: Path,
47
+ seeded: tuple[Mapping[str, Guarded], frozenset[str]] | None = None,
48
+ ) -> Imported:
49
+ """Return, for the file at `path`, what it imports from other checked files that it can name.
50
+
51
+ Each function's return type (`calls`), and each class's attributes and methods' returns, keyed as
52
+ the file spells the class (`Row`, `m.Row`); a type naming the class itself (a `Self` return) is
53
+ spelled that way too. `seeded`: the names its fixtures' types are written with, imported for type
54
+ checking alone (see `fixtures.visible`), whose classes' members it takes as well; and the
55
+ attributes those types take of a module it imports (`MonkeyPatch`, in `pytest.MonkeyPatch`).
56
+
57
+ Returns:
58
+ Them, and the names their types need imported for type checking alone (see `Guarded`); nothing
59
+ for a file `catalog` doesn't have (a notebook, standard input).
60
+
61
+ """
62
+ taken: _Taken = _Taken({}, {}, {}, {}, set())
63
+ guarded: dict[str, Guarded] = {} if seeded is None else dict(seeded[0])
64
+ target: Module | None
65
+ if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
66
+ return Imported({}, Classes(taken.attributes, taken.methods))
67
+ # Its own attributes as they are where the fixtures' add none: the index keeps what they find.
68
+ attributes: frozenset[str] = (
69
+ target.attributes
70
+ if seeded is None or seeded[1] <= target.attributes
71
+ else target.attributes | seeded[1]
72
+ )
73
+ checking: dict[str, Origin] = {
74
+ name: found.origin for name, found in guarded.items() if name not in target.names
75
+ }
76
+ named: _Named = ({**checking, **target.names}, attributes)
77
+ _take(catalog, target, (checking, attributes), taken, guarded)
78
+ _take(catalog, target, (target.names, attributes), taken, guarded)
79
+ found: tuple[dict[str, str], Returns, dict[str, str]] = (
80
+ calls(catalog, path, guarded),
81
+ returned(catalog, path, guarded),
82
+ {name: each for name, each, _ in typed_calls(catalog, path, PARTIAL, guarded)},
83
+ )
84
+ known: set[str] = set(named[0])
85
+ for _ in range(HOPS): # a class those types need imported is a receiver's too, needing more in turn
86
+ fresh: dict[str, Origin] = {name: each.origin for name, each in guarded.items() if name not in known}
87
+ # Any class their packages re-export: the types naming them aren't written in the file yet.
88
+ _take(catalog, target, (fresh, None), taken, guarded)
89
+ known.update(fresh)
90
+ return Imported(
91
+ found[0],
92
+ Classes(taken.attributes, taken.methods),
93
+ found[1],
94
+ guarded,
95
+ frozenset(taken.generics),
96
+ taken.members,
97
+ Partial(found[2], {name: each for name, each in taken.partial.items() if each}),
98
+ )
99
+
100
+
101
+ def guarded_classes(catalog: Index, path: Path, found: Imported, guarded: Mapping[str, Guarded]) -> Classes:
102
+ """Add the members of the classes `guarded` names that `found` hasn't taken, as the file names them.
103
+
104
+ Those of the imports later steps add for type checking (an overloaded method's return):
105
+ `df = pd.DataFrame(...).set_index("C")`, typed `DataFrame` by its import: `df.explode()` is one of
106
+ that class's methods in the same pass, not only once the import is written.
107
+
108
+ Returns:
109
+ `found`'s classes, with theirs.
110
+
111
+ """
112
+ target: Module | None
113
+ if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
114
+ return found.classes
115
+ fresh: dict[str, Origin] = {
116
+ name: each.origin
117
+ for name, each in guarded.items()
118
+ if name not in found.guarded and name not in target.names and name not in found.classes.methods
119
+ }
120
+ if not fresh:
121
+ return found.classes
122
+ taken: _Taken = _Taken(
123
+ {key: dict(each) for key, each in found.classes.attributes.items()},
124
+ {key: dict(each) for key, each in found.classes.methods.items()},
125
+ {},
126
+ {},
127
+ set(),
128
+ )
129
+ _take(catalog, target, (fresh, None), taken, dict(guarded))
130
+ return Classes(taken.attributes, taken.methods)
131
+
132
+
133
+ class _Taken(NamedTuple):
134
+ """What `imported` takes of the classes a file names, each keyed as the file spells the class."""
135
+
136
+ attributes: dict[str, dict[str, str]]
137
+ methods: dict[str, dict[str, str]]
138
+ partial: dict[str, dict[str, str]]
139
+ members: dict[str, Mapping[str, str]]
140
+ generics: set[str]
141
+
142
+
143
+ def _take(catalog: Index, target: Module, named: _Named, taken: _Taken, guarded: dict[str, Guarded]) -> None:
144
+ """Add to `taken` the members `target` uses of the classes `named` finds (see `spelled_classes`)."""
145
+ key: str
146
+ defined: tuple[Module, str]
147
+ for key, defined in spelled_classes(catalog, target, named, taken.generics):
148
+ if defined[1] in defined[0].generics:
149
+ taken.generics.add(key)
150
+ if defined[1] in defined[0].plain and defined[1] in defined[0].members:
151
+ taken.members[key] = _variables(catalog.modules, (target, defined), key, guarded)
152
+ if _declared_members(catalog, defined).isdisjoint(target.attributes):
153
+ # Most classes it can name: it takes none of their members, and asks whether it names one
154
+ # only by what it writes (`m.Row(...)` constructs one).
155
+ if _written(key, target.attributes):
156
+ taken.attributes[key], taken.methods[key], taken.partial[key] = {}, {}, {}
157
+ continue
158
+ taken.attributes[key], taken.methods[key], taken.partial[key] = (
159
+ _used(catalog.modules, (target, defined), key, table.get(defined[1]), guarded)
160
+ for table in (defined[0].classes, defined[0].methods, defined[0].partial_methods)
161
+ )
162
+
163
+
164
+ def _written(spelled: str, attributes: frozenset[str]) -> bool:
165
+ """Check whether a file can write a class as `spelled`: each part after its first an attribute it takes.
166
+
167
+ Returns:
168
+ Whether it can.
169
+
170
+ """
171
+ return all(part in attributes for part in spelled.split(".")[1:])
172
+
173
+
174
+ def _variables(
175
+ modules: Mapping[str, Module],
176
+ where: tuple[Module, tuple[Module, str]],
177
+ key: str,
178
+ guarded: dict[str, Guarded],
179
+ ) -> dict[str, str]:
180
+ """Write a plain class's variables' types as the file (`where[0]`) can (see `portable`).
181
+
182
+ Returns:
183
+ Each one's type: its own, made of builtins alone; one naming anything else, as the file
184
+ spells it, or `NO_TYPE` where it can't, which still holds a class under it to no other.
185
+
186
+ """
187
+ typed: Mapping[str, str] = where[1][0].members[where[1][1]]
188
+ named: dict[str, str] = {name: text for name, text in typed.items() if not roots(text) <= BUILTINS}
189
+ written: dict[str, str] = portable(modules, where, key, named, guarded) if named else {}
190
+ return {name: written.get(name, NO_TYPE) if name in named else text for name, text in typed.items()}
191
+
192
+
193
+ def _declared_members(catalog: Index, defined: tuple[Module, str]) -> frozenset[str]:
194
+ """Name the members a class declares a type for, once for the index.
195
+
196
+ Returns:
197
+ Its attributes' and methods' names.
198
+
199
+ """
200
+ module: Module
201
+ name: str
202
+ module, name = defined
203
+ memo: Memo | None = catalog.memo
204
+ found: frozenset[str] | None
205
+ if (found := None if memo is None else memo.members.get((module.name, name))) is None:
206
+ found = frozenset[str]().union(
207
+ *(table.get(name, ()) for table in (module.classes, module.methods, module.partial_methods)),
208
+ )
209
+ if memo is not None:
210
+ memo.members[module.name, name] = found
211
+ return found
212
+
213
+
214
+ def _used(
215
+ modules: Mapping[str, Module],
216
+ where: tuple[Module, tuple[Module, str]],
217
+ key: str,
218
+ members: Mapping[str, str] | None,
219
+ guarded: dict[str, Guarded],
220
+ ) -> dict[str, str]:
221
+ """Keep the types of a class's `members` that the file (`where[0]`) takes of something, and can write.
222
+
223
+ Returns:
224
+ Each kept member's type (see `portable`): none for most classes it can name, whose members it
225
+ doesn't use.
226
+
227
+ """
228
+ used: frozenset[str] = where[0].attributes
229
+ taken: dict[str, str] = {name: typed for name, typed in (members or {}).items() if name in used}
230
+ return portable(modules, where, key, taken, guarded) if taken else {}
231
+
232
+
233
+ def spelled_classes(
234
+ catalog: Index,
235
+ target: Module,
236
+ named: _Named,
237
+ generics: set[str],
238
+ ) -> Iterator[tuple[str, tuple[Module, str]]]:
239
+ """Find the classes other checked files define that `target` names, each as it spells it.
240
+
241
+ `named`: the names it's to find them by, and the attributes it takes of anything (`None`: any).
242
+ `Row` after `from m import Row`; `m.Row`, or `pkg.m.Row` after `import pkg.m`, for a module's,
243
+ and `pkg.Row` for one `pkg` re-exports, where `Row` is such an attribute.
244
+ The generic classes a module it imports re-exports are added to `generics`, as it spells them.
245
+
246
+ Yields:
247
+ Each spelling, and the module defining the class and its name there.
248
+
249
+ """
250
+ memo: Memo | None = catalog.memo
251
+ if memo is None or named[0] is not target.names or named[1] is not target.attributes:
252
+ yield from _spelled_classes(catalog, target, named, generics)
253
+ return
254
+ kept: tuple[str, str] # with its folder: several `conftest.py`s share the one name
255
+ if (kept := (target.name, target.folder)) not in memo.spelled:
256
+ added: set[str] = set()
257
+ memo.spelled[kept] = (
258
+ [
259
+ (key, defined[0].name, defined[1])
260
+ for key, defined in _spelled_classes(catalog, target, named, added)
261
+ ],
262
+ sorted(added),
263
+ )
264
+ generics.update(memo.spelled[kept][1])
265
+ key: str
266
+ module: str
267
+ name: str
268
+ for key, module, name in memo.spelled[kept][0]:
269
+ yield key, (catalog.modules[module], name)
270
+
271
+
272
+ def _spelled_classes(
273
+ catalog: Index,
274
+ target: Module,
275
+ named: _Named,
276
+ generics: set[str],
277
+ ) -> Iterator[tuple[str, tuple[Module, str]]]:
278
+ """Find the classes `spelled_classes` yields, from the index itself.
279
+
280
+ Yields:
281
+ Each spelling, and the module defining the class and its name there.
282
+
283
+ """
284
+ local: str
285
+ origin: Origin
286
+ for local, origin in resolved(catalog, named[0]):
287
+ spelled: list[tuple[str, Origin]] = []
288
+ if origin[1] is not None and origin[0] != target.name:
289
+ spelled = [(local, origin)]
290
+ generics.update([local] * is_generic(catalog.modules, origin))
291
+ elif origin[1] is None:
292
+ package: Module | None = catalog.modules.get(origin[0])
293
+ spelled = [
294
+ *classes_under(catalog, local, origin[0], CLASS),
295
+ *(
296
+ (f"{local}.{name}", where)
297
+ for name, where in (() if package is None else exported(package).items())
298
+ if (named[1] is None or name in named[1])
299
+ and where[1] is not None
300
+ and where[0] != origin[0]
301
+ ),
302
+ ]
303
+ generics.update(_reexported_generics(catalog.modules, local, origin[0]))
304
+ key: str
305
+ where: Origin
306
+ for key, where in spelled:
307
+ defined: tuple[Module, str] | None
308
+ if (defined := definition(catalog.modules, where, CLASS)) is not None:
309
+ yield key, defined
310
+
311
+
312
+ def _reexported_generics(modules: Mapping[str, Module], local: str, name: str) -> Iterator[str]:
313
+ """Spell the generic classes module `name` re-exports (`from ._c import OrderedSet`) as `local.C`.
314
+
315
+ Only its own names, not its submodules': a package's `__init__` is where they're re-exported,
316
+ for type checking alone too, where it imports them only when they're asked for (pydantic's).
317
+
318
+ Yields:
319
+ Each spelling.
320
+
321
+ """
322
+ module: Module | None = modules.get(name)
323
+ reexported: str
324
+ origin: Origin
325
+ for reexported, origin in ({} if module is None else {**module.guarded, **module.names}).items():
326
+ if origin[1] is not None and origin[0] != name and is_generic(modules, origin):
327
+ yield f"{local}.{reexported}"
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Returns under decorators another module defines: kept where it gives the function back.
3
+
4
+ A module alone vouches for the standard library's such decorators and its own (see
5
+ `constricter.rules.decorators`); with the index, for those it imports from a checked file or an
6
+ installed package too (`@set_module("pandas")`, declared `-> Callable[[F], F]`), once the type
7
+ variables their signatures name are found to be ones. A declared return is kept, and an unannotated
8
+ function's `return`s may type its calls (`unannotated`), as an undecorated one's do.
9
+ """
10
+
11
+ from collections.abc import Iterator, Mapping
12
+ from dataclasses import replace
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from constricter.fix.index import project
17
+ from constricter.fix.index.modules import SUFFIX, Index, Module, module_name
18
+ from constricter.fix.index.project import DECORATOR
19
+
20
+ if TYPE_CHECKING:
21
+ from constricter.fix.core.known import Origin
22
+ from constricter.rules.decorators import Pass
23
+
24
+
25
+ def passed(catalog: Index) -> Index:
26
+ """Add to each module's functions' declared returns those under decorators the index vouches for.
27
+
28
+ Its classes' classmethods' and staticmethods' too; and its unannotated functions under such
29
+ decorators (`decorated`) join those a `return` could type (`unannotated`).
30
+
31
+ Returns:
32
+ The index, with them among each module's `returns`, `sides` and `unannotated`, and named in
33
+ its `vouched` and `vouched_sides`.
34
+
35
+ """
36
+ found: dict[str, Module] = {}
37
+ module: Module
38
+ for module in catalog.modules.values():
39
+ if not (module.held or module.held_sides or module.decorated):
40
+ continue
41
+ trusted: frozenset[str] = frozenset(_vouched(catalog, module))
42
+ more: dict[str, str] = {
43
+ name: held.returns for name, held in module.held.items() if trusted.issuperset(held.decorators)
44
+ }
45
+ sides: dict[tuple[str, str], str] = {
46
+ (owner, name): held.returns
47
+ for owner, methods in module.held_sides.items()
48
+ for name, held in methods.items()
49
+ if trusted.issuperset(held.decorators)
50
+ }
51
+ freed: frozenset[str] = frozenset(
52
+ name for name, spellings in module.decorated.items() if trusted.issuperset(spellings)
53
+ )
54
+ if more or sides or freed:
55
+ found[module.name] = replace(
56
+ module,
57
+ returns={**module.returns, **more},
58
+ vouched=frozenset(more),
59
+ sides=_with_sides(module.sides, sides),
60
+ vouched_sides=frozenset(sides),
61
+ unannotated=module.unannotated | freed,
62
+ )
63
+ return catalog._replace(modules={**catalog.modules, **found}) if found else catalog
64
+
65
+
66
+ def vouched(catalog: Index, path: Path) -> frozenset[str]:
67
+ """Spell the decorators the file at `path` uses that give a function back, by the index.
68
+
69
+ Returns:
70
+ Each, as `decorators.spelled` reads its use; nothing for a file `catalog` doesn't have.
71
+
72
+ """
73
+ module: Module | None
74
+ if path.suffix != SUFFIX or (module := catalog.modules.get(module_name(path))) is None:
75
+ return frozenset()
76
+ return frozenset(_vouched(catalog, module))
77
+
78
+
79
+ def _with_sides(
80
+ sides: Mapping[str, Mapping[str, str]],
81
+ more: Mapping[tuple[str, str], str],
82
+ ) -> dict[str, dict[str, str]]:
83
+ """Add methods' returns (`more`, by class and name) to classes' (`sides`).
84
+
85
+ Returns:
86
+ Each class's, with them.
87
+
88
+ """
89
+ found: dict[str, dict[str, str]] = {owner: dict(methods) for owner, methods in sides.items()}
90
+ owner: str
91
+ name: str
92
+ annotation: str
93
+ for (owner, name), annotation in more.items():
94
+ found.setdefault(owner, {})[name] = annotation
95
+ return found
96
+
97
+
98
+ def _vouched(catalog: Index, module: Module) -> Iterator[str]:
99
+ """Spell the decorators `module` uses that give a function back, by the index: its own, and imported.
100
+
101
+ Yields:
102
+ Each, as `decorators.spelled` reads its use.
103
+
104
+ """
105
+ name: str
106
+ read: Pass
107
+ for name, read in module.passes.items():
108
+ if all(project.is_type_var(catalog.modules, module, word) for word in read.type_vars):
109
+ yield read.spelled(name)
110
+ key: str
111
+ origin: Origin
112
+ for key, origin in project.spellings(catalog, module, DECORATOR):
113
+ defined: tuple[Module, str] | None
114
+ if (defined := project.definition(catalog.modules, origin, DECORATOR)) is not None:
115
+ read = defined[0].passes[defined[1]]
116
+ if all(project.is_type_var(catalog.modules, defined[0], word) for word in read.type_vars):
117
+ yield read.spelled(key)
118
+
119
+
120
+ def own(catalog: Index, path: Path) -> dict[str, str]:
121
+ """Return, for the file at `path`, its own functions' returns the index vouched for (see `passed`).
122
+
123
+ Its classes' classmethods' and staticmethods' too, as called on the class (`Box.make`), where
124
+ the class's name is the class throughout the file.
125
+
126
+ Returns:
127
+ Each one's name and declared return; nothing for a file `catalog` doesn't have.
128
+
129
+ """
130
+ module: Module | None
131
+ if path.suffix != SUFFIX or (module := catalog.modules.get(module_name(path))) is None:
132
+ return {}
133
+ return {
134
+ **{name: module.returns[name] for name in module.vouched},
135
+ **{
136
+ f"{owner}.{name}": module.sides[owner][name]
137
+ for owner, name in module.vouched_sides
138
+ if module.names.get(owner) == (module.name, owner) and owner not in module.shadowed
139
+ },
140
+ }
@@ -0,0 +1,176 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Pytest fixtures' values, for the tests that take them as parameters.
3
+
4
+ A test (or a fixture) names the fixtures it wants as its parameters, which no call passes: pytest
5
+ finds each in the test's module, then in the `conftest.py` of each package above it, the nearest
6
+ first. A fixture's value is what its function returns, or yields: its declared return, or the one
7
+ its `return`s give (see `constricter.fix.values.returned`). A parameter so typed is a guess
8
+ (`fixture`): a plugin's fixture, or one a `conftest.py` out of the checked files defines, may be
9
+ the one pytest takes. A `conftest.py` in a directory that isn't a package is looked in next, by
10
+ where it is: the test's directory's, or its top package's, then each one's above it. Pytest's own
11
+ come last (`capsys`, `monkeypatch`, `caplog`), read from its installed modules where a checked file
12
+ imports `pytest`.
13
+ """
14
+
15
+ import ast
16
+ from collections.abc import Mapping
17
+ from pathlib import Path
18
+ from typing import Final
19
+
20
+ from constricter.fix.core.known import Guarded, Passed
21
+ from constricter.fix.index import project
22
+ from constricter.fix.index.modules import CONFTEST, SUFFIX, Index, Module
23
+
24
+ KIND: Final = "fixture" # the guessing mechanism (`FIX_KINDS`)
25
+ _DOT: Final = "."
26
+ _PYTEST: Final = "_pytest" # the package pytest defines its own fixtures in
27
+ # pytest's own fixtures whose value is a standard-library class's instance, each with its class:
28
+ # typed without the index, by an import the module has or one added (see `rules.calls`).
29
+ STDLIB_OWN: Final = {"tmp_path": "pathlib.Path"}
30
+ # What a generator fixture declares it returns: its value is the first argument.
31
+ _YIELDING: Final = frozenset({"Iterator", "Generator", "Iterable", "AsyncIterator", "AsyncGenerator"})
32
+
33
+
34
+ def providers(catalog: Index, name: str) -> list[Module]:
35
+ """Find the modules whose fixtures module `name`'s tests can take, the nearest first.
36
+
37
+ Returns:
38
+ It, then its packages' `conftest`s, those `catalog` has, then those outside any package (see
39
+ `_above`), then pytest's own modules.
40
+
41
+ """
42
+ parts: list[str] = name.split(_DOT)
43
+ names: list[str] = [
44
+ name,
45
+ *(_DOT.join([*parts[:depth], CONFTEST]) for depth in range(len(parts) - 1, 0, -1)),
46
+ ]
47
+ found: list[Module] = [catalog.modules[each] for each in dict.fromkeys(names) if each in catalog.modules]
48
+ target: Module | None = catalog.modules.get(name)
49
+ return [*found, *(each for each in _above(catalog, name) if each is not target), *_own(catalog)]
50
+
51
+
52
+ def _above(catalog: Index, name: str) -> list[Module]:
53
+ """Find the `conftest.py`s outside any package that module `name`'s tests take fixtures from.
54
+
55
+ Returns:
56
+ Those in the directory of `name`'s file, or of its top package, and in each one above it, the
57
+ nearest first.
58
+
59
+ """
60
+ target: Module | None = catalog.modules.get(name)
61
+ folder: str = "" if target is None else target.folder
62
+ top: Path = Path(folder).joinpath(*[".."] * name.count(_DOT)).resolve()
63
+ folders: list[str] = [str(each) for each in (top, *top.parents)] if folder else []
64
+ flat: Module | None = catalog.modules.get(CONFTEST)
65
+ if flat is not None and CONFTEST not in catalog.repeated: # the index's: it has what it returns
66
+ return [flat] if flat.folder in folders else []
67
+ return [catalog.conftests[each] for each in folders if each in catalog.conftests]
68
+
69
+
70
+ def _own(catalog: Index) -> list[Module]:
71
+ """Find pytest's modules that define its own fixtures, among the installed ones `catalog` has.
72
+
73
+ Returns:
74
+ Them, by name.
75
+
76
+ """
77
+ return [
78
+ module
79
+ for name, module in sorted(catalog.modules.items())
80
+ if module.installed and module.fixtures and name.partition(_DOT)[0] == _PYTEST
81
+ ]
82
+
83
+
84
+ def needed(catalog: Index, name: str) -> set[str]:
85
+ """Name the other modules whose fixtures module `name` can take: checked before it, their types are known.
86
+
87
+ Returns:
88
+ Them.
89
+
90
+ """
91
+ return {module.name for module in providers(catalog, name) if module.fixtures} - {name}
92
+
93
+
94
+ def visible(catalog: Index, path: Path, guarded: dict[str, Guarded]) -> dict[str, Passed]:
95
+ """Type the fixtures the file at `path` can take, as it can write each type (see `project.spelled_in`).
96
+
97
+ `guarded` records the names those types need imported for type checking.
98
+
99
+ Returns:
100
+ Each typed fixture's value's type, and what it rests on, by its name; nothing for a file
101
+ `catalog` doesn't have.
102
+
103
+ """
104
+ target: Module | None
105
+ if path.suffix != SUFFIX or (target := catalog.modules.get(project.module_name(path))) is None:
106
+ return {}
107
+ if target.name in catalog.repeated: # another file's module, maybe: its `conftest.py`s aren't this one's
108
+ return {}
109
+ found: dict[str, Passed | None] = {}
110
+ module: Module
111
+ for module in providers(catalog, target.name):
112
+ fixture: str
113
+ # A nearer one of the name is the one taken; pytest's of a standard-library class is `STDLIB_OWN`'s.
114
+ skipped: set[str] = found.keys() | (STDLIB_OWN.keys() if module.installed else set())
115
+ for fixture in module.fixtures.keys() - skipped:
116
+ found[fixture] = _typed(catalog, target, module, fixture, guarded)
117
+ return {fixture: typed for fixture, typed in found.items() if typed is not None}
118
+
119
+
120
+ def attributes(seeds: Mapping[str, Passed]) -> frozenset[str]:
121
+ """Name the attributes fixtures' types are written with: `MonkeyPatch`, in `pytest.MonkeyPatch`.
122
+
123
+ Returns:
124
+ Them.
125
+
126
+ """
127
+ return frozenset(
128
+ node.attr
129
+ for typed, _ in seeds.values()
130
+ for node in ast.walk(ast.parse(typed, mode="eval"))
131
+ if isinstance(node, ast.Attribute)
132
+ )
133
+
134
+
135
+ def _typed(
136
+ catalog: Index,
137
+ target: Module,
138
+ module: Module,
139
+ fixture: str,
140
+ guarded: dict[str, Guarded],
141
+ ) -> Passed | None:
142
+ """Type one of `module`'s fixtures' value, as `target` can write it.
143
+
144
+ Returns:
145
+ The type, and what it rests on; or `None`.
146
+
147
+ """
148
+ declared: str | None = module.held[fixture].returns if fixture in module.held else None
149
+ annotation: str | None = declared or module.returned.calls.get(fixture)
150
+ if annotation is not None and module.fixtures[fixture]:
151
+ annotation = _yielded(annotation)
152
+ written: str | None = (
153
+ None if annotation is None else project.spelled_in(catalog, target, module, annotation, guarded)
154
+ )
155
+ if written is None:
156
+ return None
157
+ rests: frozenset[str] = frozenset() if declared else module.returned.guesses.get(fixture, frozenset())
158
+ return written, frozenset({KIND}) | rests
159
+
160
+
161
+ def _yielded(annotation: str) -> str | None:
162
+ """Read what a generator's declared type says it yields: `Frame` of an `Iterator[Frame]`.
163
+
164
+ Returns:
165
+ It, or `None` for anything else.
166
+
167
+ """
168
+ head: ast.expr
169
+ inner: ast.expr
170
+ match ast.parse(annotation, mode="eval").body:
171
+ case ast.Subscript(value=ast.Name() | ast.Attribute() as head, slice=inner) if (
172
+ ast.unparse(head).rpartition(_DOT)[2] in _YIELDING
173
+ ):
174
+ return ast.unparse(inner.elts[0] if isinstance(inner, ast.Tuple) else inner)
175
+ case _:
176
+ return None