wrapture 1.0.0a1__py3-none-any.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.
- wrapture/__init__.py +232 -0
- wrapture/__main__.py +148 -0
- wrapture/_wrappermixins.py +438 -0
- wrapture/asgi.py +620 -0
- wrapture/attributes.py +667 -0
- wrapture/behaviours.py +947 -0
- wrapture/bindings.py +2533 -0
- wrapture/bootstrap.py +86 -0
- wrapture/capture.py +195 -0
- wrapture/collectors.py +377 -0
- wrapture/config.py +1413 -0
- wrapture/decorators.py +702 -0
- wrapture/doubles.py +452 -0
- wrapture/eventlogs.py +297 -0
- wrapture/events.py +294 -0
- wrapture/exceptions.py +74 -0
- wrapture/export.py +299 -0
- wrapture/iterators.py +485 -0
- wrapture/lifecycle.py +84 -0
- wrapture/observed.py +506 -0
- wrapture/outputs.py +219 -0
- wrapture/py.typed +0 -0
- wrapture/pytest_plugin.py +119 -0
- wrapture/scheduler.py +297 -0
- wrapture/sinks.py +1176 -0
- wrapture/stacks.py +152 -0
- wrapture/timeline.py +670 -0
- wrapture/tools/__init__.py +8 -0
- wrapture/tools/__main__.py +51 -0
- wrapture/tools/convert.py +150 -0
- wrapture/values.py +163 -0
- wrapture/windows.py +988 -0
- wrapture/wsgi.py +637 -0
- wrapture-1.0.0a1.dist-info/METADATA +193 -0
- wrapture-1.0.0a1.dist-info/RECORD +39 -0
- wrapture-1.0.0a1.dist-info/WHEEL +5 -0
- wrapture-1.0.0a1.dist-info/entry_points.txt +2 -0
- wrapture-1.0.0a1.dist-info/licenses/LICENSE +24 -0
- wrapture-1.0.0a1.dist-info/top_level.txt +1 -0
wrapture/__init__.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wrapture is a library for attaching bindings to arbitrary Python call sites,
|
|
3
|
+
without modifying the code being observed, for use in monkey patching,
|
|
4
|
+
testing and tracing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _format_version(parts: tuple[str, ...]) -> str:
|
|
9
|
+
base = ".".join(parts[:3])
|
|
10
|
+
|
|
11
|
+
if len(parts) == 3:
|
|
12
|
+
return base
|
|
13
|
+
|
|
14
|
+
suffix = parts[3]
|
|
15
|
+
return (
|
|
16
|
+
f"{base}.{suffix}" if suffix.startswith(("dev", "post")) else f"{base}{suffix}"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__version_info__ = ("1", "0", "0", "a1")
|
|
21
|
+
__version__ = _format_version(__version_info__)
|
|
22
|
+
|
|
23
|
+
from wrapt import MISSING, register_post_import_hook, when_imported
|
|
24
|
+
|
|
25
|
+
from .asgi import (
|
|
26
|
+
ASGIMiddleware,
|
|
27
|
+
)
|
|
28
|
+
from .behaviours import (
|
|
29
|
+
CallBehaviour,
|
|
30
|
+
CallPhase,
|
|
31
|
+
DeleteBehaviour,
|
|
32
|
+
DeletePhase,
|
|
33
|
+
GetBehaviour,
|
|
34
|
+
GetPhase,
|
|
35
|
+
SetBehaviour,
|
|
36
|
+
SetPhase,
|
|
37
|
+
)
|
|
38
|
+
from .bindings import (
|
|
39
|
+
Binding,
|
|
40
|
+
BindingGroup,
|
|
41
|
+
binding,
|
|
42
|
+
bindings,
|
|
43
|
+
discover,
|
|
44
|
+
)
|
|
45
|
+
from .capture import (
|
|
46
|
+
redact,
|
|
47
|
+
)
|
|
48
|
+
from .collectors import (
|
|
49
|
+
Aggregate,
|
|
50
|
+
Counter,
|
|
51
|
+
PathStats,
|
|
52
|
+
)
|
|
53
|
+
from .config import (
|
|
54
|
+
AppliedConfig,
|
|
55
|
+
Config,
|
|
56
|
+
ObserveEntry,
|
|
57
|
+
SetupEntry,
|
|
58
|
+
find_config,
|
|
59
|
+
load_config,
|
|
60
|
+
)
|
|
61
|
+
from .decorators import (
|
|
62
|
+
BoundSpec,
|
|
63
|
+
bound,
|
|
64
|
+
taped,
|
|
65
|
+
)
|
|
66
|
+
from .doubles import (
|
|
67
|
+
StubCallable,
|
|
68
|
+
mock,
|
|
69
|
+
stub,
|
|
70
|
+
)
|
|
71
|
+
from .eventlogs import (
|
|
72
|
+
EventLog,
|
|
73
|
+
)
|
|
74
|
+
from .events import (
|
|
75
|
+
Event,
|
|
76
|
+
)
|
|
77
|
+
from .exceptions import (
|
|
78
|
+
AlreadyAppliedError,
|
|
79
|
+
ConfigError,
|
|
80
|
+
ConfigWarning,
|
|
81
|
+
DeferredTargetError,
|
|
82
|
+
ExpectationNotMetError,
|
|
83
|
+
NeverAppliedError,
|
|
84
|
+
NotImplementedYetError,
|
|
85
|
+
RecordingGapWarning,
|
|
86
|
+
SequenceExhaustedError,
|
|
87
|
+
SinkErrorWarning,
|
|
88
|
+
WrongModeError,
|
|
89
|
+
)
|
|
90
|
+
from .export import (
|
|
91
|
+
canonical,
|
|
92
|
+
chrome_trace,
|
|
93
|
+
load_events,
|
|
94
|
+
mermaid,
|
|
95
|
+
)
|
|
96
|
+
from .iterators import (
|
|
97
|
+
IteratorAbandonBehaviour,
|
|
98
|
+
IteratorErrorBehaviour,
|
|
99
|
+
IteratorFinishBehaviour,
|
|
100
|
+
IteratorItemBehaviour,
|
|
101
|
+
IteratorProxy,
|
|
102
|
+
iterator,
|
|
103
|
+
)
|
|
104
|
+
from .lifecycle import (
|
|
105
|
+
shutdown,
|
|
106
|
+
)
|
|
107
|
+
from .observed import (
|
|
108
|
+
ObservedCallable,
|
|
109
|
+
observed,
|
|
110
|
+
)
|
|
111
|
+
from .sinks import (
|
|
112
|
+
Depth,
|
|
113
|
+
Fanout,
|
|
114
|
+
Filter,
|
|
115
|
+
JSONLines,
|
|
116
|
+
Printer,
|
|
117
|
+
Sample,
|
|
118
|
+
Sink,
|
|
119
|
+
add_sink,
|
|
120
|
+
flush_sinks,
|
|
121
|
+
remove_sink,
|
|
122
|
+
)
|
|
123
|
+
from .stacks import (
|
|
124
|
+
StackFrame,
|
|
125
|
+
clear_stacks,
|
|
126
|
+
stack_frames,
|
|
127
|
+
)
|
|
128
|
+
from .timeline import (
|
|
129
|
+
Tape,
|
|
130
|
+
Timeline,
|
|
131
|
+
annotate,
|
|
132
|
+
current_event,
|
|
133
|
+
propagate,
|
|
134
|
+
timeline,
|
|
135
|
+
)
|
|
136
|
+
from .windows import (
|
|
137
|
+
Collector,
|
|
138
|
+
Report,
|
|
139
|
+
Run,
|
|
140
|
+
Window,
|
|
141
|
+
window,
|
|
142
|
+
)
|
|
143
|
+
from .wsgi import (
|
|
144
|
+
WSGIMiddleware,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
__all__ = [
|
|
148
|
+
"MISSING",
|
|
149
|
+
"register_post_import_hook",
|
|
150
|
+
"when_imported",
|
|
151
|
+
"ASGIMiddleware",
|
|
152
|
+
"Aggregate",
|
|
153
|
+
"AlreadyAppliedError",
|
|
154
|
+
"AppliedConfig",
|
|
155
|
+
"Binding",
|
|
156
|
+
"BoundSpec",
|
|
157
|
+
"Collector",
|
|
158
|
+
"Report",
|
|
159
|
+
"Run",
|
|
160
|
+
"Window",
|
|
161
|
+
"BindingGroup",
|
|
162
|
+
"CallBehaviour",
|
|
163
|
+
"CallPhase",
|
|
164
|
+
"Config",
|
|
165
|
+
"ConfigError",
|
|
166
|
+
"ConfigWarning",
|
|
167
|
+
"Counter",
|
|
168
|
+
"DeferredTargetError",
|
|
169
|
+
"DeleteBehaviour",
|
|
170
|
+
"DeletePhase",
|
|
171
|
+
"Depth",
|
|
172
|
+
"Event",
|
|
173
|
+
"EventLog",
|
|
174
|
+
"ExpectationNotMetError",
|
|
175
|
+
"Fanout",
|
|
176
|
+
"Filter",
|
|
177
|
+
"GetBehaviour",
|
|
178
|
+
"GetPhase",
|
|
179
|
+
"IteratorAbandonBehaviour",
|
|
180
|
+
"IteratorErrorBehaviour",
|
|
181
|
+
"IteratorFinishBehaviour",
|
|
182
|
+
"IteratorItemBehaviour",
|
|
183
|
+
"IteratorProxy",
|
|
184
|
+
"JSONLines",
|
|
185
|
+
"NeverAppliedError",
|
|
186
|
+
"NotImplementedYetError",
|
|
187
|
+
"ObserveEntry",
|
|
188
|
+
"ObservedCallable",
|
|
189
|
+
"PathStats",
|
|
190
|
+
"Printer",
|
|
191
|
+
"RecordingGapWarning",
|
|
192
|
+
"Sample",
|
|
193
|
+
"SequenceExhaustedError",
|
|
194
|
+
"SetBehaviour",
|
|
195
|
+
"SetPhase",
|
|
196
|
+
"SetupEntry",
|
|
197
|
+
"Sink",
|
|
198
|
+
"SinkErrorWarning",
|
|
199
|
+
"StackFrame",
|
|
200
|
+
"StubCallable",
|
|
201
|
+
"Tape",
|
|
202
|
+
"Timeline",
|
|
203
|
+
"WSGIMiddleware",
|
|
204
|
+
"WrongModeError",
|
|
205
|
+
"add_sink",
|
|
206
|
+
"annotate",
|
|
207
|
+
"binding",
|
|
208
|
+
"bindings",
|
|
209
|
+
"bound",
|
|
210
|
+
"canonical",
|
|
211
|
+
"chrome_trace",
|
|
212
|
+
"clear_stacks",
|
|
213
|
+
"current_event",
|
|
214
|
+
"discover",
|
|
215
|
+
"find_config",
|
|
216
|
+
"flush_sinks",
|
|
217
|
+
"iterator",
|
|
218
|
+
"load_config",
|
|
219
|
+
"load_events",
|
|
220
|
+
"mermaid",
|
|
221
|
+
"mock",
|
|
222
|
+
"observed",
|
|
223
|
+
"propagate",
|
|
224
|
+
"redact",
|
|
225
|
+
"remove_sink",
|
|
226
|
+
"shutdown",
|
|
227
|
+
"stack_frames",
|
|
228
|
+
"stub",
|
|
229
|
+
"taped",
|
|
230
|
+
"timeline",
|
|
231
|
+
"window",
|
|
232
|
+
]
|
wrapture/__main__.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Run a Python program with a wrapture config applied first.
|
|
2
|
+
|
|
3
|
+
python -m wrapture [--config PATH] (-m MODULE | SCRIPT) [ARGS...]
|
|
4
|
+
|
|
5
|
+
The config is resolved, loaded and applied before the target runs, so
|
|
6
|
+
patches land before the target module imports anything: the same
|
|
7
|
+
ordering guarantee the injection path gives. The target then runs as
|
|
8
|
+
__main__ with sys.argv rebuilt to the target and its arguments,
|
|
9
|
+
exactly as python itself would have run it.
|
|
10
|
+
|
|
11
|
+
This module is the runner's entry point only, the same private -m
|
|
12
|
+
convention as pdb, cProfile and coverage: run it, do not import it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import runpy
|
|
19
|
+
import sys
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import NoReturn
|
|
23
|
+
|
|
24
|
+
from .config import find_config, load_config
|
|
25
|
+
from .exceptions import ConfigError
|
|
26
|
+
|
|
27
|
+
_USAGE = """\
|
|
28
|
+
usage: python -m wrapture [--config PATH] (-m MODULE | SCRIPT) [ARGS...]
|
|
29
|
+
|
|
30
|
+
Apply a wrapture config, then run the target as __main__ with ARGS as
|
|
31
|
+
its arguments. With --config the named TOML file is used; without it
|
|
32
|
+
the standard precedence chain locates one: a path in WRAPTURE_CONFIG,
|
|
33
|
+
wrapture.toml in the current directory, then a [tool.wrapture] table
|
|
34
|
+
in pyproject.toml. Everything after the target belongs to the target,
|
|
35
|
+
so wrapture's own options must come before it.
|
|
36
|
+
|
|
37
|
+
Trace files a run produces can be rendered in other formats with
|
|
38
|
+
python -m wrapture.tools convert.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class _Invocation:
|
|
44
|
+
config: str | None
|
|
45
|
+
module: str | None
|
|
46
|
+
script: str | None
|
|
47
|
+
arguments: tuple[str, ...]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _usage_error(message: str) -> NoReturn:
|
|
51
|
+
print(f"wrapture: {message}", file=sys.stderr)
|
|
52
|
+
print(_USAGE, file=sys.stderr, end="")
|
|
53
|
+
raise SystemExit(2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse(argv: Sequence[str]) -> _Invocation:
|
|
57
|
+
"""Split the command line into wrapture's own options and the
|
|
58
|
+
target with its arguments.
|
|
59
|
+
|
|
60
|
+
Parsing stops at the first target: everything after -m MODULE or
|
|
61
|
+
the script path is the target's own, however option-like it
|
|
62
|
+
looks, matching how python itself treats a command line.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
config: str | None = None
|
|
66
|
+
pending = list(argv)
|
|
67
|
+
|
|
68
|
+
while pending:
|
|
69
|
+
argument = pending[0]
|
|
70
|
+
|
|
71
|
+
if argument in ("-h", "--help"):
|
|
72
|
+
print(_USAGE, end="")
|
|
73
|
+
raise SystemExit(0)
|
|
74
|
+
|
|
75
|
+
if argument == "--config":
|
|
76
|
+
if len(pending) < 2:
|
|
77
|
+
_usage_error("--config requires a path")
|
|
78
|
+
config = pending[1]
|
|
79
|
+
del pending[:2]
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if argument.startswith("--config="):
|
|
83
|
+
config = argument.partition("=")[2]
|
|
84
|
+
if not config:
|
|
85
|
+
_usage_error("--config requires a path")
|
|
86
|
+
del pending[0]
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if argument == "-m":
|
|
90
|
+
if len(pending) < 2:
|
|
91
|
+
_usage_error("-m requires a module name")
|
|
92
|
+
return _Invocation(config, pending[1], None, tuple(pending[2:]))
|
|
93
|
+
|
|
94
|
+
if argument.startswith("-"):
|
|
95
|
+
_usage_error(f"unknown option {argument!r}")
|
|
96
|
+
|
|
97
|
+
return _Invocation(config, None, argument, tuple(pending[1:]))
|
|
98
|
+
|
|
99
|
+
_usage_error("a target is required: -m MODULE or a script path")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
103
|
+
"""Parse the command line, apply the config, and run the target."""
|
|
104
|
+
|
|
105
|
+
invocation = _parse(sys.argv[1:] if argv is None else argv)
|
|
106
|
+
|
|
107
|
+
# Resolve and apply the config before anything else happens, so
|
|
108
|
+
# patches and setup hooks are in place before the target module
|
|
109
|
+
# imports anything. Finding no config is an error rather than a
|
|
110
|
+
# silent untraced run.
|
|
111
|
+
|
|
112
|
+
source = invocation.config if invocation.config is not None else find_config()
|
|
113
|
+
|
|
114
|
+
if source is None:
|
|
115
|
+
print(
|
|
116
|
+
"wrapture: no config found: pass --config PATH, set"
|
|
117
|
+
" WRAPTURE_CONFIG, or provide wrapture.toml or a"
|
|
118
|
+
" [tool.wrapture] table in pyproject.toml in the current"
|
|
119
|
+
" directory",
|
|
120
|
+
file=sys.stderr,
|
|
121
|
+
)
|
|
122
|
+
raise SystemExit(1)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
load_config(source).apply()
|
|
126
|
+
except ConfigError as exc:
|
|
127
|
+
print(f"wrapture: {exc}", file=sys.stderr)
|
|
128
|
+
raise SystemExit(1) from None
|
|
129
|
+
|
|
130
|
+
# Hand over to the target as python itself would have run it. For
|
|
131
|
+
# a module target, alter_sys replaces sys.argv[0] with the
|
|
132
|
+
# module's own file, matching python -m; for a script, its
|
|
133
|
+
# directory joins the front of sys.path, matching python script.py.
|
|
134
|
+
|
|
135
|
+
if invocation.module is not None:
|
|
136
|
+
sys.argv[:] = [invocation.module, *invocation.arguments]
|
|
137
|
+
runpy.run_module(invocation.module, run_name="__main__", alter_sys=True)
|
|
138
|
+
else:
|
|
139
|
+
script = invocation.script
|
|
140
|
+
assert script is not None
|
|
141
|
+
|
|
142
|
+
sys.argv[:] = [script, *invocation.arguments]
|
|
143
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(script)))
|
|
144
|
+
runpy.run_path(script, run_name="__main__")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|