wrapture 1.0.0.dev2__tar.gz → 1.0.0.dev3__tar.gz
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-1.0.0.dev2/src/wrapture.egg-info → wrapture-1.0.0.dev3}/PKG-INFO +20 -22
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/README.md +17 -19
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/pyproject.toml +15 -3
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/__init__.py +76 -2
- wrapture-1.0.0.dev3/src/wrapture/__main__.py +148 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/attributes.py +53 -9
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/bindings.py +404 -58
- wrapture-1.0.0.dev3/src/wrapture/bootstrap.py +86 -0
- wrapture-1.0.0.dev3/src/wrapture/config.py +1113 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/events.py +49 -11
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/exceptions.py +18 -0
- wrapture-1.0.0.dev3/src/wrapture/export.py +298 -0
- wrapture-1.0.0.dev3/src/wrapture/observed.py +422 -0
- wrapture-1.0.0.dev3/src/wrapture/sinks.py +1006 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/stacks.py +49 -7
- wrapture-1.0.0.dev3/src/wrapture/timeline.py +566 -0
- wrapture-1.0.0.dev3/src/wrapture/tools/__init__.py +8 -0
- wrapture-1.0.0.dev3/src/wrapture/tools/__main__.py +51 -0
- wrapture-1.0.0.dev3/src/wrapture/tools/convert.py +150 -0
- wrapture-1.0.0.dev3/src/wrapture/wsgi.py +633 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3/src/wrapture.egg-info}/PKG-INFO +20 -22
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/SOURCES.txt +23 -1
- wrapture-1.0.0.dev3/src/wrapture.egg-info/entry_points.txt +2 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_attribute_events.py +14 -2
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_bindings.py +173 -1
- wrapture-1.0.0.dev3/tests/test_bootstrap.py +147 -0
- wrapture-1.0.0.dev3/tests/test_config.py +1084 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_events.py +8 -9
- wrapture-1.0.0.dev3/tests/test_export.py +253 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_generator_events.py +9 -4
- wrapture-1.0.0.dev3/tests/test_jsonlines.py +307 -0
- wrapture-1.0.0.dev3/tests/test_observed.py +264 -0
- wrapture-1.0.0.dev3/tests/test_predicates.py +231 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_recording.py +44 -7
- wrapture-1.0.0.dev3/tests/test_runner.py +213 -0
- wrapture-1.0.0.dev3/tests/test_sink_library.py +360 -0
- wrapture-1.0.0.dev3/tests/test_sinks.py +454 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_stacks.py +56 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_threads.py +67 -2
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_timeline.py +154 -31
- wrapture-1.0.0.dev3/tests/test_tools.py +235 -0
- wrapture-1.0.0.dev3/tests/test_wsgi.py +668 -0
- wrapture-1.0.0.dev2/src/wrapture/timeline.py +0 -364
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/LICENSE +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/setup.cfg +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/behaviours.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/capture.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/eventlogs.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/iterators.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/py.typed +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/pytest_plugin.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/dependency_links.txt +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/not-zip-safe +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/requires.txt +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/top_level.txt +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_attributes.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_behaviours.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_capture.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_eventlogs.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_iterators.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_pytest_plugin.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_targets.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_testing_patterns.py +0 -0
- {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_version.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wrapture
|
|
3
|
-
Version: 1.0.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.0.0.dev3
|
|
4
|
+
Summary: Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt.
|
|
5
5
|
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
7
7
|
Project-URL: Homepage, https://github.com/GrahamDumpleton/wrapture
|
|
8
8
|
Project-URL: Documentation, https://wrapture.readthedocs.io
|
|
9
9
|
Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture/issues/
|
|
10
|
-
Keywords: wrapper,monkey patching,tracing,testing
|
|
10
|
+
Keywords: wrapper,monkey patching,tracing,testing
|
|
11
11
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -44,10 +44,10 @@ It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
|
|
|
44
44
|
and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
|
|
45
45
|
safe monkey-patching machinery wrapt provides.
|
|
46
46
|
|
|
47
|
-
> **Status: early development.** The monkey patching
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
47
|
+
> **Status: early development.** The monkey patching, unit testing and
|
|
48
|
+
> ad-hoc tracing layers are implemented, including WSGI request tracing.
|
|
49
|
+
> Development previews are published to PyPI; the API may still shift
|
|
50
|
+
> before 1.0.0.
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
@@ -98,7 +98,7 @@ failure, or transform one argument while the real code keeps running.
|
|
|
98
98
|
|
|
99
99
|
## What it does
|
|
100
100
|
|
|
101
|
-
One mechanism,
|
|
101
|
+
One mechanism, three uses, in increasing order of machinery:
|
|
102
102
|
|
|
103
103
|
1. **Monkey patching.** A clean lifecycle and behaviour vocabulary over
|
|
104
104
|
wrapt's `wrap_object()`. Point at a method by name and stub it, fail it,
|
|
@@ -119,16 +119,17 @@ One mechanism, four uses, in increasing order of machinery:
|
|
|
119
119
|
3. **Ad-hoc tracing.** Attach bindings to a running application, including
|
|
120
120
|
one you cannot modify or redeploy, and emit a structured, nested trace to
|
|
121
121
|
process or chart elsewhere. Name a handful of methods and a call tree
|
|
122
|
-
appears; no code changes required.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
appears; no code changes required: with a `wrapture.toml` naming the
|
|
123
|
+
methods and a sink, `python -m wrapture manage.py runserver` traces the
|
|
124
|
+
application untouched. With [autowrapt](https://github.com/GrahamDumpleton/autowrapt)
|
|
125
|
+
installed, not even the launcher is needed: `AUTOWRAPT_BOOTSTRAP=wrapture`
|
|
126
|
+
in the environment applies the same config at interpreter startup, so the
|
|
127
|
+
program starts with plain `python`.
|
|
128
|
+
|
|
129
|
+
The distinction that matters: most tracing tools either need the code to
|
|
130
|
+
have been written with them in mind, or can only be switched on for the
|
|
131
|
+
whole program at once. wrapture needs neither: you point at a method by
|
|
132
|
+
name and a trace appears.
|
|
132
133
|
|
|
133
134
|
## Why
|
|
134
135
|
|
|
@@ -142,8 +143,7 @@ nested trace, assert on it or export it, in tests or in production":
|
|
|
142
143
|
`InMemorySpanExporter`) require the code to already be instrumented.
|
|
143
144
|
- `sys.settrace` tools (`hunter`, `snoop`) give a firehose with no assertion
|
|
144
145
|
API.
|
|
145
|
-
-
|
|
146
|
-
are all-or-nothing products rather than a toolkit.
|
|
146
|
+
- APM agents are all-or-nothing products rather than a toolkit.
|
|
147
147
|
|
|
148
148
|
wrapture fills that gap: a targeted call tree with normalized arguments and
|
|
149
149
|
return values, produced by naming the methods you care about, usable as a
|
|
@@ -153,8 +153,6 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
153
153
|
|
|
154
154
|
- **Not a replacement for `unittest.mock`.** It complements mocking where
|
|
155
155
|
code has seams; it exists for the code that doesn't.
|
|
156
|
-
- **Not a sampling profiler.** `py-spy` and `austin` do that better and
|
|
157
|
-
without distortion.
|
|
158
156
|
- **Not a production APM.** It is a toolkit that APM-like things could be
|
|
159
157
|
built on.
|
|
160
158
|
- **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
|
|
@@ -13,10 +13,10 @@ It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
|
|
|
13
13
|
and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
|
|
14
14
|
safe monkey-patching machinery wrapt provides.
|
|
15
15
|
|
|
16
|
-
> **Status: early development.** The monkey patching
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
16
|
+
> **Status: early development.** The monkey patching, unit testing and
|
|
17
|
+
> ad-hoc tracing layers are implemented, including WSGI request tracing.
|
|
18
|
+
> Development previews are published to PyPI; the API may still shift
|
|
19
|
+
> before 1.0.0.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -67,7 +67,7 @@ failure, or transform one argument while the real code keeps running.
|
|
|
67
67
|
|
|
68
68
|
## What it does
|
|
69
69
|
|
|
70
|
-
One mechanism,
|
|
70
|
+
One mechanism, three uses, in increasing order of machinery:
|
|
71
71
|
|
|
72
72
|
1. **Monkey patching.** A clean lifecycle and behaviour vocabulary over
|
|
73
73
|
wrapt's `wrap_object()`. Point at a method by name and stub it, fail it,
|
|
@@ -88,16 +88,17 @@ One mechanism, four uses, in increasing order of machinery:
|
|
|
88
88
|
3. **Ad-hoc tracing.** Attach bindings to a running application, including
|
|
89
89
|
one you cannot modify or redeploy, and emit a structured, nested trace to
|
|
90
90
|
process or chart elsewhere. Name a handful of methods and a call tree
|
|
91
|
-
appears; no code changes required.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
appears; no code changes required: with a `wrapture.toml` naming the
|
|
92
|
+
methods and a sink, `python -m wrapture manage.py runserver` traces the
|
|
93
|
+
application untouched. With [autowrapt](https://github.com/GrahamDumpleton/autowrapt)
|
|
94
|
+
installed, not even the launcher is needed: `AUTOWRAPT_BOOTSTRAP=wrapture`
|
|
95
|
+
in the environment applies the same config at interpreter startup, so the
|
|
96
|
+
program starts with plain `python`.
|
|
97
|
+
|
|
98
|
+
The distinction that matters: most tracing tools either need the code to
|
|
99
|
+
have been written with them in mind, or can only be switched on for the
|
|
100
|
+
whole program at once. wrapture needs neither: you point at a method by
|
|
101
|
+
name and a trace appears.
|
|
101
102
|
|
|
102
103
|
## Why
|
|
103
104
|
|
|
@@ -111,8 +112,7 @@ nested trace, assert on it or export it, in tests or in production":
|
|
|
111
112
|
`InMemorySpanExporter`) require the code to already be instrumented.
|
|
112
113
|
- `sys.settrace` tools (`hunter`, `snoop`) give a firehose with no assertion
|
|
113
114
|
API.
|
|
114
|
-
-
|
|
115
|
-
are all-or-nothing products rather than a toolkit.
|
|
115
|
+
- APM agents are all-or-nothing products rather than a toolkit.
|
|
116
116
|
|
|
117
117
|
wrapture fills that gap: a targeted call tree with normalized arguments and
|
|
118
118
|
return values, produced by naming the methods you care about, usable as a
|
|
@@ -122,8 +122,6 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
122
122
|
|
|
123
123
|
- **Not a replacement for `unittest.mock`.** It complements mocking where
|
|
124
124
|
code has seams; it exists for the code that doesn't.
|
|
125
|
-
- **Not a sampling profiler.** `py-spy` and `austin` do that better and
|
|
126
|
-
without distortion.
|
|
127
125
|
- **Not a production APM.** It is a toolkit that APM-like things could be
|
|
128
126
|
built on.
|
|
129
127
|
- **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "wrapture"
|
|
9
9
|
dynamic = ["version"]
|
|
10
|
-
description = "
|
|
10
|
+
description = "Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = "BSD-2-Clause"
|
|
13
13
|
license-files = ["LICENSE"]
|
|
@@ -15,7 +15,7 @@ authors = [
|
|
|
15
15
|
{name = "Graham Dumpleton", email = "Graham.Dumpleton@gmail.com"}
|
|
16
16
|
]
|
|
17
17
|
requires-python = ">=3.12"
|
|
18
|
-
keywords = ["wrapper", "monkey patching", "tracing", "testing"
|
|
18
|
+
keywords = ["wrapper", "monkey patching", "tracing", "testing"]
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Development Status :: 2 - Pre-Alpha",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
@@ -33,6 +33,18 @@ Homepage = "https://github.com/GrahamDumpleton/wrapture"
|
|
|
33
33
|
Documentation = "https://wrapture.readthedocs.io"
|
|
34
34
|
"Bug Tracker" = "https://github.com/GrahamDumpleton/wrapture/issues/"
|
|
35
35
|
|
|
36
|
+
# The zero-code injection trigger, in the shape autowrapt consumes:
|
|
37
|
+
# AUTOWRAPT_BOOTSTRAP=wrapture names this entry point group, which
|
|
38
|
+
# autowrapt hands to wrapt.discover_post_import_hooks() at interpreter
|
|
39
|
+
# startup. Each entry maps a trigger module to a callback; hooking os,
|
|
40
|
+
# which is always already imported by then, makes the callback fire
|
|
41
|
+
# immediately at startup. Inert metadata unless autowrapt is installed
|
|
42
|
+
# and the environment variable is set; wrapture itself has no
|
|
43
|
+
# dependency on autowrapt and never imports it.
|
|
44
|
+
|
|
45
|
+
[project.entry-points.wrapture]
|
|
46
|
+
os = "wrapture.bootstrap:bootstrap"
|
|
47
|
+
|
|
36
48
|
[project.optional-dependencies]
|
|
37
49
|
dev = [
|
|
38
50
|
"mypy",
|
|
@@ -49,7 +61,7 @@ docs = [
|
|
|
49
61
|
[tool.setuptools]
|
|
50
62
|
package-dir = {"" = "src"}
|
|
51
63
|
zip-safe = false
|
|
52
|
-
packages = ["wrapture"]
|
|
64
|
+
packages = ["wrapture", "wrapture.tools"]
|
|
53
65
|
|
|
54
66
|
[tool.setuptools.package-data]
|
|
55
67
|
wrapture = ["py.typed"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Wrapture is a library for attaching bindings to arbitrary Python call sites,
|
|
3
3
|
without modifying the code being observed, for use in monkey patching,
|
|
4
|
-
testing
|
|
4
|
+
testing and tracing.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ def _format_version(parts: tuple[str, ...]) -> str:
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
__version_info__ = ("1", "0", "0", "
|
|
20
|
+
__version_info__ = ("1", "0", "0", "dev3")
|
|
21
21
|
__version__ = _format_version(__version_info__)
|
|
22
22
|
|
|
23
23
|
from wrapt import MISSING
|
|
@@ -33,10 +33,19 @@ from .bindings import (
|
|
|
33
33
|
BindingGroup,
|
|
34
34
|
binding,
|
|
35
35
|
bindings,
|
|
36
|
+
discover,
|
|
36
37
|
)
|
|
37
38
|
from .capture import (
|
|
38
39
|
redact,
|
|
39
40
|
)
|
|
41
|
+
from .config import (
|
|
42
|
+
AppliedConfig,
|
|
43
|
+
Config,
|
|
44
|
+
ObserveEntry,
|
|
45
|
+
SetupEntry,
|
|
46
|
+
find_config,
|
|
47
|
+
load_config,
|
|
48
|
+
)
|
|
40
49
|
from .eventlogs import (
|
|
41
50
|
EventLog,
|
|
42
51
|
)
|
|
@@ -45,13 +54,22 @@ from .events import (
|
|
|
45
54
|
)
|
|
46
55
|
from .exceptions import (
|
|
47
56
|
AlreadyAppliedError,
|
|
57
|
+
ConfigError,
|
|
58
|
+
ConfigWarning,
|
|
48
59
|
DeferredTargetError,
|
|
49
60
|
ExpectationNotMetError,
|
|
50
61
|
NeverAppliedError,
|
|
51
62
|
NotImplementedYetError,
|
|
52
63
|
RecordingGapWarning,
|
|
64
|
+
SinkErrorWarning,
|
|
53
65
|
WrongModeError,
|
|
54
66
|
)
|
|
67
|
+
from .export import (
|
|
68
|
+
canonical,
|
|
69
|
+
chrome_trace,
|
|
70
|
+
load_events,
|
|
71
|
+
mermaid,
|
|
72
|
+
)
|
|
55
73
|
from .iterators import (
|
|
56
74
|
IteratorAbandonBehaviour,
|
|
57
75
|
IteratorErrorBehaviour,
|
|
@@ -60,8 +78,28 @@ from .iterators import (
|
|
|
60
78
|
IteratorProxy,
|
|
61
79
|
iterator,
|
|
62
80
|
)
|
|
81
|
+
from .observed import (
|
|
82
|
+
ObservedCallable,
|
|
83
|
+
observed,
|
|
84
|
+
)
|
|
85
|
+
from .sinks import (
|
|
86
|
+
Aggregate,
|
|
87
|
+
Counter,
|
|
88
|
+
Depth,
|
|
89
|
+
Fanout,
|
|
90
|
+
Filter,
|
|
91
|
+
JSONLines,
|
|
92
|
+
PathStats,
|
|
93
|
+
Printer,
|
|
94
|
+
Sample,
|
|
95
|
+
Sink,
|
|
96
|
+
add_sink,
|
|
97
|
+
flush_sinks,
|
|
98
|
+
remove_sink,
|
|
99
|
+
)
|
|
63
100
|
from .stacks import (
|
|
64
101
|
StackFrame,
|
|
102
|
+
clear_stacks,
|
|
65
103
|
stack_frames,
|
|
66
104
|
)
|
|
67
105
|
from .timeline import (
|
|
@@ -69,40 +107,76 @@ from .timeline import (
|
|
|
69
107
|
Timeline,
|
|
70
108
|
annotate,
|
|
71
109
|
current_event,
|
|
110
|
+
propagate,
|
|
72
111
|
timeline,
|
|
73
112
|
)
|
|
113
|
+
from .wsgi import (
|
|
114
|
+
WSGIMiddleware,
|
|
115
|
+
)
|
|
74
116
|
|
|
75
117
|
__all__ = [
|
|
76
118
|
"MISSING",
|
|
119
|
+
"Aggregate",
|
|
77
120
|
"AlreadyAppliedError",
|
|
121
|
+
"AppliedConfig",
|
|
78
122
|
"Binding",
|
|
79
123
|
"BindingGroup",
|
|
80
124
|
"CallBehaviour",
|
|
125
|
+
"Config",
|
|
126
|
+
"ConfigError",
|
|
127
|
+
"ConfigWarning",
|
|
128
|
+
"Counter",
|
|
81
129
|
"DeferredTargetError",
|
|
82
130
|
"DeleteBehaviour",
|
|
131
|
+
"Depth",
|
|
83
132
|
"Event",
|
|
84
133
|
"EventLog",
|
|
85
134
|
"ExpectationNotMetError",
|
|
135
|
+
"Fanout",
|
|
136
|
+
"Filter",
|
|
86
137
|
"GetBehaviour",
|
|
87
138
|
"IteratorAbandonBehaviour",
|
|
88
139
|
"IteratorErrorBehaviour",
|
|
89
140
|
"IteratorFinishBehaviour",
|
|
90
141
|
"IteratorItemBehaviour",
|
|
91
142
|
"IteratorProxy",
|
|
143
|
+
"JSONLines",
|
|
92
144
|
"NeverAppliedError",
|
|
93
145
|
"NotImplementedYetError",
|
|
146
|
+
"ObserveEntry",
|
|
147
|
+
"ObservedCallable",
|
|
148
|
+
"PathStats",
|
|
149
|
+
"Printer",
|
|
94
150
|
"RecordingGapWarning",
|
|
151
|
+
"Sample",
|
|
95
152
|
"SetBehaviour",
|
|
153
|
+
"SetupEntry",
|
|
154
|
+
"Sink",
|
|
155
|
+
"SinkErrorWarning",
|
|
96
156
|
"StackFrame",
|
|
97
157
|
"Tape",
|
|
98
158
|
"Timeline",
|
|
159
|
+
"WSGIMiddleware",
|
|
99
160
|
"WrongModeError",
|
|
161
|
+
"add_sink",
|
|
100
162
|
"annotate",
|
|
101
163
|
"binding",
|
|
102
164
|
"bindings",
|
|
165
|
+
"canonical",
|
|
166
|
+
"chrome_trace",
|
|
167
|
+
"clear_stacks",
|
|
103
168
|
"current_event",
|
|
169
|
+
"discover",
|
|
170
|
+
"find_config",
|
|
171
|
+
"flush_sinks",
|
|
104
172
|
"iterator",
|
|
173
|
+
"load_config",
|
|
174
|
+
"load_events",
|
|
175
|
+
"mermaid",
|
|
176
|
+
"observed",
|
|
177
|
+
"propagate",
|
|
105
178
|
"redact",
|
|
179
|
+
"remove_sink",
|
|
106
180
|
"stack_frames",
|
|
107
181
|
"timeline",
|
|
108
182
|
]
|
|
@@ -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()
|
|
@@ -22,22 +22,29 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
import inspect
|
|
24
24
|
import sys
|
|
25
|
+
import time
|
|
25
26
|
from collections.abc import Callable
|
|
26
27
|
from typing import TYPE_CHECKING, Any
|
|
27
28
|
|
|
28
29
|
import wrapt
|
|
29
30
|
from wrapt import MISSING, BaseObjectProxy, apply_patch
|
|
30
31
|
|
|
31
|
-
from .capture import
|
|
32
|
+
from .capture import _capture_value, _level_of
|
|
32
33
|
from .events import Event, EventKind
|
|
33
34
|
from .exceptions import NotImplementedYetError
|
|
35
|
+
from .sinks import (
|
|
36
|
+
_active_sinks,
|
|
37
|
+
_in_recorder,
|
|
38
|
+
_notify_error,
|
|
39
|
+
_notify_exit,
|
|
40
|
+
_record_event,
|
|
41
|
+
_required_policy,
|
|
42
|
+
)
|
|
34
43
|
from .stacks import _capture as _capture_stack
|
|
35
44
|
from .timeline import (
|
|
36
45
|
_capture_result,
|
|
37
|
-
_in_recorder,
|
|
38
46
|
_pop,
|
|
39
47
|
_push,
|
|
40
|
-
_tape,
|
|
41
48
|
_timelines_active,
|
|
42
49
|
)
|
|
43
50
|
|
|
@@ -121,20 +128,43 @@ def _record(
|
|
|
121
128
|
on the in-progress stack so anything it triggers nests under it.
|
|
122
129
|
"""
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
# when=False is a behaviour-only binding: it never records, counts
|
|
132
|
+
# nothing, and takes no part in gap detection.
|
|
133
|
+
|
|
134
|
+
if binding._when is False:
|
|
135
|
+
return operate()
|
|
136
|
+
|
|
137
|
+
active = _active_sinks()
|
|
138
|
+
if not active or _in_recorder.get():
|
|
139
|
+
if not active and not _in_recorder.get() and _timelines_active():
|
|
127
140
|
binding._note_missed_call()
|
|
128
141
|
|
|
129
142
|
return operate()
|
|
130
143
|
|
|
144
|
+
# The per-access predicate, mapped onto call shape the same way
|
|
145
|
+
# behaviour stages are: a set passes the written value as the one
|
|
146
|
+
# positional argument, a get or delete passes empty args.
|
|
147
|
+
|
|
148
|
+
if binding._when is not None:
|
|
149
|
+
call_args = (value,) if value is not MISSING else ()
|
|
150
|
+
|
|
151
|
+
guard = _in_recorder.set(True)
|
|
152
|
+
try:
|
|
153
|
+
wanted = binding._when(instance, call_args, {})
|
|
154
|
+
finally:
|
|
155
|
+
_in_recorder.reset(guard)
|
|
156
|
+
|
|
157
|
+
if not wanted:
|
|
158
|
+
binding._filtered_calls += 1
|
|
159
|
+
return operate()
|
|
160
|
+
|
|
131
161
|
# The written value and the prior value are inbound data, so they
|
|
132
162
|
# capture on the arguments axis, under the attribute's name so a
|
|
133
163
|
# by-name policy such as redact() applies to writes too.
|
|
134
164
|
|
|
135
165
|
policy = binding._capture_args
|
|
136
166
|
if policy is None:
|
|
137
|
-
policy =
|
|
167
|
+
policy = _required_policy(active, "capture_args")
|
|
138
168
|
|
|
139
169
|
guard = _in_recorder.set(True)
|
|
140
170
|
try:
|
|
@@ -163,28 +193,42 @@ def _record(
|
|
|
163
193
|
if previous is not MISSING:
|
|
164
194
|
event.previous = _capture_value(policy, attribute, previous)
|
|
165
195
|
|
|
166
|
-
tape.record(event)
|
|
167
196
|
finally:
|
|
168
197
|
_in_recorder.reset(guard)
|
|
169
198
|
|
|
199
|
+
# Position before delivery: pushed first, so sinks hearing
|
|
200
|
+
# on_enter see the event's final depth and parent link. Timing
|
|
201
|
+
# starts after the bookkeeping, so its overhead is not charged to
|
|
202
|
+
# the observed operation.
|
|
203
|
+
|
|
170
204
|
token = _push(event)
|
|
205
|
+
_record_event(event, active)
|
|
206
|
+
|
|
207
|
+
started = time.perf_counter()
|
|
208
|
+
event.started = started
|
|
209
|
+
|
|
171
210
|
try:
|
|
172
211
|
outcome = operate()
|
|
173
212
|
except BaseException as exc:
|
|
213
|
+
event.duration = time.perf_counter() - started
|
|
174
214
|
event.exception = exc
|
|
215
|
+
_notify_error(event, active)
|
|
175
216
|
raise
|
|
176
217
|
finally:
|
|
177
218
|
_pop(token)
|
|
178
219
|
|
|
220
|
+
event.duration = time.perf_counter() - started
|
|
221
|
+
|
|
179
222
|
# The value a read produced is its outcome, so it captures on the
|
|
180
223
|
# result axis, exactly as a call's return value does.
|
|
181
224
|
|
|
182
225
|
if kind == "get":
|
|
183
226
|
result_policy = binding._capture_result
|
|
184
227
|
if result_policy is None:
|
|
185
|
-
result_policy =
|
|
228
|
+
result_policy = _required_policy(active, "capture_result")
|
|
186
229
|
_capture_result(event, outcome, result_policy)
|
|
187
230
|
|
|
231
|
+
_notify_exit(event, active)
|
|
188
232
|
return outcome
|
|
189
233
|
|
|
190
234
|
|