python-constricter 0.4.1__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp311-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp311-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp311-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
constricter/trace.py ADDED
@@ -0,0 +1,374 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Record the types a run binds its functions' locals to, for `--fix --infer-from` to read.
3
+
4
+ python -m constricter.trace [--output FILE] [--root DIR] (-m MODULE | SCRIPT) [ARG ...]
5
+
6
+ Runs the module or script as `python` would (`-m pytest`), and each time a statement of a function
7
+ defined in a file under DIR (default: the working directory; nothing installed there) ends, notes
8
+ the type of each name it binds (see `constricter.recording`). At exit it writes them to FILE
9
+ (default: `constricter-trace.json`).
10
+
11
+ Lines are followed by `sys.monitoring`, or `sys.settrace` on a Python without it (before 3.12). A
12
+ pytest process the run starts (pytest-xdist's workers) records itself the same way: the run names
13
+ this module in `PYTEST_PLUGINS`, and its trace has what each wrote as its session ended. No other
14
+ child process is recorded.
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import runpy
21
+ import sys
22
+ import tempfile
23
+ import threading
24
+ from collections.abc import Callable, Mapping, Sequence
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from types import CodeType, FrameType
28
+ from typing import Final, Protocol, TypeAlias, cast
29
+
30
+ from constricter.recording import Found, Recorder, merged, write
31
+
32
+ DEFAULT: Final = "constricter-trace.json"
33
+ # What tells a pytest process a traced run started it: the run's process, its root, and where to write.
34
+ ENVIRONMENT: Final = "CONSTRICTER_TRACE"
35
+ _PLUGINS: Final = "PYTEST_PLUGINS" # the modules every pytest process loads, by name
36
+ _PLUGIN: Final = "constricter.trace" # this one: `pytest_configure` and `pytest_unconfigure`
37
+ _PARTS: Final = "*.json" # what those processes write
38
+ _LINE: Final = "line" # `sys.settrace`'s events
39
+ _RETURN: Final = "return"
40
+ _EXCEPTION: Final = "exception"
41
+ _TOOL: Final = "constricter" # to `sys.monitoring`
42
+ # Its ids no kind of tool has: this takes the first free. A profiler's is `cProfile`'s, which a run may start.
43
+ _TOOLS: Final = (4, 3)
44
+ _GETFRAME: Final = "_getframe" # `sys`'s, which `inspect.currentframe` asks for each time
45
+ _OPTIONS: Final = frozenset({"--output", "--root"})
46
+ _MODULE: Final = "-m"
47
+ _MAIN: Final = "__main__"
48
+ _Trace: TypeAlias = Callable[[FrameType, str, object], "_Trace | None"] # a `sys.settrace` function
49
+ _Callback: TypeAlias = Callable[..., object] # what `sys.monitoring` reports an event to
50
+ _Sentinel: TypeAlias = object # what a callback answers to have an event reported no more
51
+
52
+
53
+ class Events(Protocol): # pylint: disable=too-few-public-methods
54
+ """`sys.monitoring.events`: the ones followed."""
55
+
56
+ LINE: int
57
+ PY_RETURN: int
58
+ PY_START: int
59
+ RAISE: int
60
+
61
+
62
+ class Monitoring(Protocol):
63
+ """`sys.monitoring` (Python 3.12), as it's used here."""
64
+
65
+ DISABLE: _Sentinel
66
+ events: Events
67
+
68
+ def get_tool(self, tool_id: int, /) -> str | None:
69
+ """Name the tool that has an id, if one has."""
70
+
71
+ def use_tool_id(self, tool_id: int, name: str, /) -> None:
72
+ """Claim a tool's id, under a name."""
73
+
74
+ def free_tool_id(self, tool_id: int, /) -> None:
75
+ """Give a tool's id up."""
76
+
77
+ def register_callback(self, tool_id: int, event: int, func: _Callback | None, /) -> object:
78
+ """Set what one event is reported to, or nothing."""
79
+
80
+ def set_events(self, tool_id: int, event_set: int, /) -> None:
81
+ """Choose the events reported of every function."""
82
+
83
+ def set_local_events(self, tool_id: int, code: CodeType, event_set: int, /) -> None:
84
+ """Choose the events reported of one function besides."""
85
+
86
+
87
+ MONITORING: Final = cast("Monitoring | None", getattr(sys, "monitoring", None))
88
+
89
+
90
+ # What finds a frame, held as this module is loaded: a run may take it away (a test of code without it).
91
+ _frame: Final = cast("Callable[[int], FrameType]", getattr(sys, _GETFRAME))
92
+
93
+
94
+ class Monitor:
95
+ """`sys.monitoring`'s callbacks: each recorded function's lines and returns, and every exception."""
96
+
97
+ def __init__(self, recorder: Recorder, monitoring: Monitoring) -> None:
98
+ """Tell `recorder` what `monitoring` reports."""
99
+ self.recorder: Recorder = recorder
100
+ self.monitoring: Monitoring = monitoring
101
+ self.codes: list[CodeType] = [] # each recorded function's, its lines and returns reported
102
+ self.tool: int = _TOOLS[0] # its id, once started
103
+
104
+ def start(self) -> None:
105
+ """Have every function's start reported, and every exception."""
106
+ events: Events = self.monitoring.events
107
+ self.tool = next((tool for tool in _TOOLS if self.monitoring.get_tool(tool) is None), self.tool)
108
+ self.monitoring.use_tool_id(self.tool, _TOOL)
109
+ self._register(self.started, self.line, self.returned, self.raised)
110
+ self.monitoring.set_events(self.tool, events.PY_START | events.RAISE)
111
+
112
+ def stop(self) -> None:
113
+ """Have nothing reported any more."""
114
+ self.monitoring.set_events(self.tool, 0)
115
+ code: CodeType
116
+ for code in self.codes:
117
+ self.monitoring.set_local_events(self.tool, code, 0)
118
+ self._register(None, None, None, None)
119
+ self.monitoring.free_tool_id(self.tool)
120
+
121
+ def _register(self, *callbacks: _Callback | None) -> None:
122
+ """Set the callbacks for a start, a line, a return and an exception."""
123
+ events: Events = self.monitoring.events
124
+ event: int
125
+ callback: _Callback | None
126
+ for event, callback in zip(
127
+ (events.PY_START, events.LINE, events.PY_RETURN, events.RAISE),
128
+ callbacks,
129
+ strict=True,
130
+ ):
131
+ _ = self.monitoring.register_callback(self.tool, event, callback)
132
+
133
+ def started(self, code: CodeType, _offset: int) -> object:
134
+ """Have a recorded function's lines and returns reported, the first time it starts.
135
+
136
+ Returns:
137
+ `DISABLE`: no start of it is reported again.
138
+
139
+ """
140
+ if self.recorder.wanted(_frame(1)):
141
+ self.codes.append(code)
142
+ events: Events = self.monitoring.events
143
+ self.monitoring.set_local_events(
144
+ self.tool,
145
+ code,
146
+ events.LINE | events.PY_RETURN,
147
+ )
148
+ return self.monitoring.DISABLE
149
+
150
+ def line(self, _code: CodeType, line: int) -> object:
151
+ """Tell the recorder its caller has arrived at a line.
152
+
153
+ Returns:
154
+ `DISABLE` for a function no longer recorded: that line of it isn't reported again.
155
+
156
+ """
157
+ return None if self.recorder.line(_frame(1), line) else self.monitoring.DISABLE
158
+
159
+ def returned(self, _code: CodeType, _offset: int, _value: object) -> object:
160
+ """Tell the recorder its caller returns.
161
+
162
+ Returns:
163
+ `DISABLE` for a function no longer recorded.
164
+
165
+ """
166
+ return None if self.recorder.left(_frame(1), finished=True) else self.monitoring.DISABLE
167
+
168
+ def raised(self, _code: CodeType, _offset: int, _error: BaseException) -> None:
169
+ """Tell the recorder an exception is raised in its caller, or through it."""
170
+ self.recorder.raised(_frame(1))
171
+
172
+
173
+ class Tracer:
174
+ """`sys.settrace`'s functions: each recorded function's lines, returns and exceptions."""
175
+
176
+ def __init__(self, recorder: Recorder) -> None:
177
+ """Tell `recorder` what the interpreter reports."""
178
+ self.recorder: Recorder = recorder
179
+ self.previous: tuple[_Trace | None, _Trace | None] = (
180
+ cast("_Trace | None", sys.gettrace()),
181
+ cast("_Trace | None", threading.gettrace()),
182
+ )
183
+
184
+ def start(self) -> None:
185
+ """Trace this thread, and those `threading` starts."""
186
+ sys.settrace(self.entered)
187
+ threading.settrace(self.entered)
188
+
189
+ def stop(self) -> None:
190
+ """Trace as before."""
191
+ sys.settrace(self.previous[0])
192
+ threading.settrace(cast("_Trace", self.previous[1]))
193
+
194
+ def entered(self, frame: FrameType, _event: str, _arg: object) -> _Trace | None:
195
+ """Choose what traces a function that starts, or resumes.
196
+
197
+ Returns:
198
+ `within` for one to record, or `None`.
199
+
200
+ """
201
+ return self.within if self.recorder.wanted(frame) else None
202
+
203
+ def within(self, frame: FrameType, event: str, _arg: object) -> _Trace | None:
204
+ """Tell the recorder what a recorded function's frame does.
205
+
206
+ Returns:
207
+ Itself: what traces the frame next.
208
+
209
+ """
210
+ if event == _LINE:
211
+ _ = self.recorder.line(frame)
212
+ elif event == _RETURN:
213
+ _ = self.recorder.left(frame)
214
+ elif event == _EXCEPTION:
215
+ self.recorder.raised(frame)
216
+ return self.within
217
+
218
+
219
+ def following(recorder: Recorder) -> Monitor | Tracer:
220
+ """Choose what tells `recorder` of the run's lines: `sys.monitoring` where there is one.
221
+
222
+ Returns:
223
+ It, not started.
224
+
225
+ """
226
+ return Tracer(recorder) if MONITORING is None else Monitor(recorder, MONITORING)
227
+
228
+
229
+ @dataclass
230
+ class _Worker:
231
+ """A pytest process a traced run started: what records it, and where it writes, while it has a session."""
232
+
233
+ sessions: int = 0
234
+ recorder: Recorder | None = None
235
+ follower: Monitor | Tracer | None = None
236
+ output: Path = Path()
237
+
238
+
239
+ _WORKER: Final = _Worker()
240
+
241
+
242
+ def pytest_configure() -> None:
243
+ """Record a pytest process a traced run started, from its first session: not the run's own."""
244
+ told: list[str] = cast("list[str]", json.loads(os.environ.get(ENVIRONMENT, "[]")))
245
+ if not told or told[0] == str(os.getpid()):
246
+ return
247
+ _WORKER.sessions += 1
248
+ if _WORKER.recorder is None:
249
+ _WORKER.recorder = Recorder(Path(told[1]))
250
+ _WORKER.follower = following(_WORKER.recorder)
251
+ _WORKER.output = Path(told[2]) / f"{os.getpid()}.json"
252
+ _WORKER.follower.start()
253
+
254
+
255
+ def pytest_unconfigure() -> None:
256
+ """Write what such a process recorded where the run reads it, as its last session ends."""
257
+ if _WORKER.recorder is None or _WORKER.follower is None:
258
+ return
259
+ _WORKER.sessions -= 1
260
+ if not _WORKER.sessions:
261
+ _WORKER.follower.stop()
262
+ _WORKER.recorder.write(_WORKER.output)
263
+ _WORKER.recorder = _WORKER.follower = None
264
+
265
+
266
+ def _announced(root: Path, parts: Path) -> dict[str, str | None]:
267
+ """Tell the pytest processes this one starts to record themselves, and write to `parts`.
268
+
269
+ Returns:
270
+ The environment variables that say so, as they were.
271
+
272
+ """
273
+ before: dict[str, str | None] = {name: os.environ.get(name) for name in (ENVIRONMENT, _PLUGINS)}
274
+ os.environ[ENVIRONMENT] = json.dumps([str(os.getpid()), str(root.resolve()), str(parts)])
275
+ os.environ[_PLUGINS] = f"{before[_PLUGINS]},{_PLUGIN}" if before[_PLUGINS] else _PLUGIN
276
+ return before
277
+
278
+
279
+ def _gathered(parts: Path, before: Mapping[str, str | None]) -> list[Found]:
280
+ """Read what the processes this one started wrote to `parts`, the environment as it was `before`.
281
+
282
+ Returns:
283
+ Each one's trace: not one that can't be read (a process killed as it wrote).
284
+
285
+ """
286
+ name: str
287
+ value: str | None
288
+ for name, value in before.items():
289
+ if value is None:
290
+ del os.environ[name]
291
+ else:
292
+ os.environ[name] = value
293
+ found: list[Found] = []
294
+ path: Path
295
+ for path in sorted(parts.glob(_PARTS)):
296
+ try:
297
+ found.append(cast("Found", json.loads(path.read_text(encoding="utf-8"))))
298
+ except ValueError:
299
+ continue
300
+ return found
301
+
302
+
303
+ def _split(args: Sequence[str]) -> tuple[list[str], list[str]]:
304
+ """Split the command line where what's to run starts: at `-m`, or the first word that's no option's.
305
+
306
+ Returns:
307
+ This command's options, and the module or script with its arguments.
308
+
309
+ """
310
+ valued: bool = False
311
+ at: int
312
+ arg: str
313
+ for at, arg in enumerate(args):
314
+ if valued or arg in _OPTIONS:
315
+ valued = not valued
316
+ elif arg == _MODULE or not arg.startswith("-"):
317
+ return list(args[:at]), list(args[at:])
318
+ return list(args), []
319
+
320
+
321
+ def _run(name: str, *, module: bool) -> None:
322
+ """Run the module or the script `name`, as `python` would."""
323
+ if module:
324
+ _ = runpy.run_module(name, run_name=_MAIN, alter_sys=True)
325
+ else:
326
+ sys.path[0] = str(Path(name).resolve().parent)
327
+ _ = runpy.run_path(name, run_name=_MAIN)
328
+
329
+
330
+ def main(argv: Sequence[str] | None = None) -> int:
331
+ """Run the module or script named on the command line, recording its functions' locals' types.
332
+
333
+ Returns:
334
+ 0: the exit status, where what ran doesn't exit itself.
335
+
336
+ """
337
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(
338
+ prog="python -m constricter.trace",
339
+ usage="%(prog)s [--output FILE] [--root DIR] (-m MODULE | SCRIPT) [ARG ...]",
340
+ description="Record the types a run binds its functions' locals to, for `constricter --infer-from`.",
341
+ )
342
+ _ = parser.add_argument(
343
+ "--output",
344
+ type=Path,
345
+ default=Path(DEFAULT),
346
+ metavar="FILE",
347
+ help="where to write",
348
+ )
349
+ _ = parser.add_argument("--root", type=Path, default=Path(), metavar="DIR", help="whose files to record")
350
+ own: list[str]
351
+ target: list[str]
352
+ own, target = _split(sys.argv[1:] if argv is None else argv)
353
+ args: argparse.Namespace = parser.parse_args(own)
354
+ module: int = int(target[:1] == [_MODULE])
355
+ if len(target) <= module:
356
+ parser.error("nothing to run: name a script, or a module after -m")
357
+ name: str = target[module]
358
+ sys.argv = [name, *target[module:][1:]]
359
+ recorder: Recorder = Recorder(cast("Path", args.root))
360
+ follower: Monitor | Tracer = following(recorder)
361
+ parts: str
362
+ with tempfile.TemporaryDirectory(prefix=f"{_TOOL}-trace-") as parts:
363
+ before: dict[str, str | None] = _announced(recorder.root, Path(parts))
364
+ follower.start()
365
+ try:
366
+ _run(name, module=bool(module))
367
+ finally:
368
+ follower.stop()
369
+ write(cast("Path", args.output), merged([recorder.found(), *_gathered(Path(parts), before)]))
370
+ return 0
371
+
372
+
373
+ if __name__ == "__main__":
374
+ sys.exit(main())