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.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp311-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-win32.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp311-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-win32.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp311-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-win32.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- 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())
|
|
Binary file
|