wrapture 1.0.0a1__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.0a1/LICENSE +24 -0
- wrapture-1.0.0a1/PKG-INFO +193 -0
- wrapture-1.0.0a1/README.md +162 -0
- wrapture-1.0.0a1/pyproject.toml +111 -0
- wrapture-1.0.0a1/setup.cfg +4 -0
- wrapture-1.0.0a1/src/wrapture/__init__.py +232 -0
- wrapture-1.0.0a1/src/wrapture/__main__.py +148 -0
- wrapture-1.0.0a1/src/wrapture/_wrappermixins.py +438 -0
- wrapture-1.0.0a1/src/wrapture/asgi.py +620 -0
- wrapture-1.0.0a1/src/wrapture/attributes.py +667 -0
- wrapture-1.0.0a1/src/wrapture/behaviours.py +947 -0
- wrapture-1.0.0a1/src/wrapture/bindings.py +2533 -0
- wrapture-1.0.0a1/src/wrapture/bootstrap.py +86 -0
- wrapture-1.0.0a1/src/wrapture/capture.py +195 -0
- wrapture-1.0.0a1/src/wrapture/collectors.py +377 -0
- wrapture-1.0.0a1/src/wrapture/config.py +1413 -0
- wrapture-1.0.0a1/src/wrapture/decorators.py +702 -0
- wrapture-1.0.0a1/src/wrapture/doubles.py +452 -0
- wrapture-1.0.0a1/src/wrapture/eventlogs.py +297 -0
- wrapture-1.0.0a1/src/wrapture/events.py +294 -0
- wrapture-1.0.0a1/src/wrapture/exceptions.py +74 -0
- wrapture-1.0.0a1/src/wrapture/export.py +299 -0
- wrapture-1.0.0a1/src/wrapture/iterators.py +485 -0
- wrapture-1.0.0a1/src/wrapture/lifecycle.py +84 -0
- wrapture-1.0.0a1/src/wrapture/observed.py +506 -0
- wrapture-1.0.0a1/src/wrapture/outputs.py +219 -0
- wrapture-1.0.0a1/src/wrapture/py.typed +0 -0
- wrapture-1.0.0a1/src/wrapture/pytest_plugin.py +119 -0
- wrapture-1.0.0a1/src/wrapture/scheduler.py +297 -0
- wrapture-1.0.0a1/src/wrapture/sinks.py +1176 -0
- wrapture-1.0.0a1/src/wrapture/stacks.py +152 -0
- wrapture-1.0.0a1/src/wrapture/timeline.py +670 -0
- wrapture-1.0.0a1/src/wrapture/tools/__init__.py +8 -0
- wrapture-1.0.0a1/src/wrapture/tools/__main__.py +51 -0
- wrapture-1.0.0a1/src/wrapture/tools/convert.py +150 -0
- wrapture-1.0.0a1/src/wrapture/values.py +163 -0
- wrapture-1.0.0a1/src/wrapture/windows.py +988 -0
- wrapture-1.0.0a1/src/wrapture/wsgi.py +637 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/PKG-INFO +193 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/SOURCES.txt +84 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/dependency_links.txt +1 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/entry_points.txt +2 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/not-zip-safe +1 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/requires.txt +12 -0
- wrapture-1.0.0a1/src/wrapture.egg-info/top_level.txt +1 -0
- wrapture-1.0.0a1/tests/test_asgi.py +728 -0
- wrapture-1.0.0a1/tests/test_async_stubs.py +450 -0
- wrapture-1.0.0a1/tests/test_attribute_events.py +303 -0
- wrapture-1.0.0a1/tests/test_attributes.py +586 -0
- wrapture-1.0.0a1/tests/test_behaviours.py +403 -0
- wrapture-1.0.0a1/tests/test_bindings.py +653 -0
- wrapture-1.0.0a1/tests/test_bootstrap.py +147 -0
- wrapture-1.0.0a1/tests/test_capture.py +315 -0
- wrapture-1.0.0a1/tests/test_collectors.py +214 -0
- wrapture-1.0.0a1/tests/test_config.py +1309 -0
- wrapture-1.0.0a1/tests/test_decorators.py +647 -0
- wrapture-1.0.0a1/tests/test_doubles.py +678 -0
- wrapture-1.0.0a1/tests/test_eventlogs.py +519 -0
- wrapture-1.0.0a1/tests/test_events.py +215 -0
- wrapture-1.0.0a1/tests/test_export.py +253 -0
- wrapture-1.0.0a1/tests/test_generator_events.py +441 -0
- wrapture-1.0.0a1/tests/test_iterators.py +563 -0
- wrapture-1.0.0a1/tests/test_jsonlines.py +440 -0
- wrapture-1.0.0a1/tests/test_mapping.py +383 -0
- wrapture-1.0.0a1/tests/test_module_attributes.py +389 -0
- wrapture-1.0.0a1/tests/test_observed.py +371 -0
- wrapture-1.0.0a1/tests/test_outputs.py +196 -0
- wrapture-1.0.0a1/tests/test_phases.py +920 -0
- wrapture-1.0.0a1/tests/test_predicates.py +231 -0
- wrapture-1.0.0a1/tests/test_pytest_plugin.py +198 -0
- wrapture-1.0.0a1/tests/test_recording.py +492 -0
- wrapture-1.0.0a1/tests/test_runner.py +213 -0
- wrapture-1.0.0a1/tests/test_sink_library.py +360 -0
- wrapture-1.0.0a1/tests/test_sinks.py +603 -0
- wrapture-1.0.0a1/tests/test_stacks.py +249 -0
- wrapture-1.0.0a1/tests/test_strict.py +228 -0
- wrapture-1.0.0a1/tests/test_targets.py +153 -0
- wrapture-1.0.0a1/tests/test_testing_patterns.py +136 -0
- wrapture-1.0.0a1/tests/test_threads.py +324 -0
- wrapture-1.0.0a1/tests/test_timeline.py +752 -0
- wrapture-1.0.0a1/tests/test_tools.py +235 -0
- wrapture-1.0.0a1/tests/test_values.py +535 -0
- wrapture-1.0.0a1/tests/test_version.py +33 -0
- wrapture-1.0.0a1/tests/test_windows.py +931 -0
- wrapture-1.0.0a1/tests/test_wrappermixins.py +214 -0
- wrapture-1.0.0a1/tests/test_wsgi.py +668 -0
wrapture-1.0.0a1/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Copyright (c) 2026, Graham Dumpleton
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
16
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
17
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
18
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
19
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
20
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
21
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
22
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
23
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
24
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wrapture
|
|
3
|
+
Version: 1.0.0a1
|
|
4
|
+
Summary: Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt.
|
|
5
|
+
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrahamDumpleton/wrapture
|
|
8
|
+
Project-URL: Documentation, https://wrapture.readthedocs.io
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture/issues/
|
|
10
|
+
Keywords: wrapper,monkey patching,tracing,testing
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: wrapt>=2.4.0rc5
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
26
|
+
Provides-Extra: docs
|
|
27
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
28
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
29
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# wrapture
|
|
33
|
+
|
|
34
|
+
**Wrap anything, capture everything, change nothing.**
|
|
35
|
+
|
|
36
|
+
[](https://github.com/GrahamDumpleton/wrapture/actions/workflows/build-test-release.yml)
|
|
37
|
+
[](https://wrapture.readthedocs.io)
|
|
38
|
+
|
|
39
|
+
wrapture (`wrapt` + `capture`) is a Python library for attaching bindings to
|
|
40
|
+
arbitrary call sites, without modifying the code being observed, and doing
|
|
41
|
+
something useful with what flows through them.
|
|
42
|
+
|
|
43
|
+
It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
|
|
44
|
+
and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
|
|
45
|
+
safe monkey-patching machinery wrapt provides.
|
|
46
|
+
|
|
47
|
+
> **Status: stabilising ahead of 1.0.0.** The feature set is complete for
|
|
48
|
+
> a first release and pre-releases are published to PyPI. Until 1.0.0 is
|
|
49
|
+
> final, a plain `pip install wrapture` picks up the latest pre-release
|
|
50
|
+
> automatically, so there is no need to pin a specific version. The API
|
|
51
|
+
> is being exercised and tidied rather than extended, so small changes are
|
|
52
|
+
> still possible before 1.0.0.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
wrapture is on [PyPI](https://pypi.org/project/wrapture/):
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
$ pip install wrapture
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or with uv:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
$ uv add wrapture
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
Full documentation is at [wrapture.readthedocs.io](https://wrapture.readthedocs.io).
|
|
71
|
+
Start with the [getting started](https://wrapture.readthedocs.io/en/latest/getting-started.html)
|
|
72
|
+
page: everything on it can be pasted into a Python interpreter. Coming
|
|
73
|
+
from `unittest.mock`? There is a
|
|
74
|
+
[comparison page](https://wrapture.readthedocs.io/en/latest/coming-from-mock.html)
|
|
75
|
+
mapping each mock idiom to its wrapture counterpart. After that, the
|
|
76
|
+
worked examples, starting with
|
|
77
|
+
[testing code that calls external services](https://wrapture.readthedocs.io/en/latest/example-external-services.html),
|
|
78
|
+
each take one question you might arrive with and answer it end to end.
|
|
79
|
+
|
|
80
|
+
## At a glance
|
|
81
|
+
|
|
82
|
+
None of the classes below import wrapture or know they are observed:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
place = wrapture.binding(OrderService, "place")
|
|
86
|
+
charge = wrapture.binding(Gateway, "charge")
|
|
87
|
+
record = wrapture.binding(Ledger, "record")
|
|
88
|
+
|
|
89
|
+
with wrapture.timeline(place, charge, record) as tape:
|
|
90
|
+
OrderService().place(500)
|
|
91
|
+
|
|
92
|
+
print(tape.tree())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
OrderService.place(amount=500) -> {'id': 'ch_500', 'amount': 500}
|
|
97
|
+
Gateway.charge(amount=500, currency='USD') -> {'id': 'ch_500', 'amount': 500}
|
|
98
|
+
Ledger.record(entry={'id': 'ch_500', 'amount': 500}) -> 'led_ch_500'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The same bindings intervene as well as observe: stub a result, inject a
|
|
102
|
+
failure, or transform one argument while the real code keeps running.
|
|
103
|
+
|
|
104
|
+
## What it does
|
|
105
|
+
|
|
106
|
+
One mechanism, three uses, in increasing order of machinery:
|
|
107
|
+
|
|
108
|
+
1. **Monkey patching.** A clean lifecycle and behaviour vocabulary over
|
|
109
|
+
wrapt's `wrap_object()`. Point at a method by name and stub it, fail it,
|
|
110
|
+
transform its arguments or result, or wrap it with a decorator, then
|
|
111
|
+
remove it again, with honest reporting if something else displaced the
|
|
112
|
+
patch in the meantime. Useful entirely on its own, with nothing else
|
|
113
|
+
switched on.
|
|
114
|
+
|
|
115
|
+
2. **Unit testing.** Observe and assert on how calls actually flowed through a
|
|
116
|
+
*real* call graph (nesting, ordering, arguments and return values) and
|
|
117
|
+
optionally intervene (stub, transform, fail-inject). Unlike a
|
|
118
|
+
`unittest.mock` `Mock`, which fabricates values and cannot see calls an object makes to itself,
|
|
119
|
+
wrapture watches the real code run, and when a test must supply a
|
|
120
|
+
stand-in it provides strict, recorded ones: `stub()` for a callable,
|
|
121
|
+
spec-required `mock()` for a collaborator. This makes it possible to test code
|
|
122
|
+
with no injectable seams at all, and to assert on what *didn't* happen on
|
|
123
|
+
an error path: inject a gateway timeout, then verify the ledger was not
|
|
124
|
+
written, the receipt was not sent, and the compensating refund was issued.
|
|
125
|
+
|
|
126
|
+
3. **Ad-hoc tracing.** Attach bindings to a running application, including
|
|
127
|
+
one you cannot modify or redeploy, and emit a structured, nested trace to
|
|
128
|
+
process or chart elsewhere. Name a handful of methods and a call tree
|
|
129
|
+
appears; no code changes required: with a `wrapture.toml` naming the
|
|
130
|
+
methods and a sink, `python -m wrapture manage.py runserver` traces the
|
|
131
|
+
application untouched. With [autowrapt](https://github.com/GrahamDumpleton/autowrapt)
|
|
132
|
+
installed, not even the launcher is needed: `AUTOWRAPT_BOOTSTRAP=wrapture`
|
|
133
|
+
in the environment applies the same config at interpreter startup, so the
|
|
134
|
+
program starts with plain `python`.
|
|
135
|
+
|
|
136
|
+
The distinction that matters: most tracing tools either need the code to
|
|
137
|
+
have been written with them in mind, or can only be switched on for the
|
|
138
|
+
whole program at once. wrapture needs neither: you point at a method by
|
|
139
|
+
name and a trace appears.
|
|
140
|
+
|
|
141
|
+
## Why
|
|
142
|
+
|
|
143
|
+
No single existing tool covers "point at arbitrary methods, get a structured
|
|
144
|
+
nested trace, assert on it or export it, in tests or in production":
|
|
145
|
+
|
|
146
|
+
- `unittest.mock` records a flat call list, with no nesting and no return
|
|
147
|
+
values, and a patched call returns a fabricated `MagicMock` rather than
|
|
148
|
+
running the real code.
|
|
149
|
+
- Span-assertion tools (`logfire.testing`, OpenTelemetry's
|
|
150
|
+
`InMemorySpanExporter`) require the code to already be instrumented.
|
|
151
|
+
- `sys.settrace` tools (`hunter`, `snoop`) give a firehose with no assertion
|
|
152
|
+
API.
|
|
153
|
+
- APM agents are all-or-nothing products rather than a toolkit.
|
|
154
|
+
|
|
155
|
+
wrapture fills that gap: a targeted call tree with normalized arguments and
|
|
156
|
+
return values, produced by naming the methods you care about, usable as a
|
|
157
|
+
testing assertion library, a tracing tool, or both at once.
|
|
158
|
+
|
|
159
|
+
## What it is not
|
|
160
|
+
|
|
161
|
+
- **Not a fabrication tool.** There is no spec-less `Mock()` here by
|
|
162
|
+
design; stand-ins are strict and built from named specs, and
|
|
163
|
+
`unittest.mock` remains the tool for invented objects.
|
|
164
|
+
- **Not a production APM.** It is a toolkit that APM-like things could be
|
|
165
|
+
built on.
|
|
166
|
+
- **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
|
|
167
|
+
it.
|
|
168
|
+
|
|
169
|
+
## How it was built
|
|
170
|
+
|
|
171
|
+
wrapture's code and documentation were written by an AI assistant under
|
|
172
|
+
the direction of Graham Dumpleton, the author of wrapt, through a long
|
|
173
|
+
process of specification, layered implementation, and validation against
|
|
174
|
+
real-world test suites.
|
|
175
|
+
[How wrapture was built](https://wrapture.readthedocs.io/en/latest/how-wrapture-was-built.html)
|
|
176
|
+
explains the process and the thinking.
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- Python 3.12+
|
|
181
|
+
- [wrapt](https://github.com/GrahamDumpleton/wrapt) 2.4.0+
|
|
182
|
+
|
|
183
|
+
## Issues
|
|
184
|
+
|
|
185
|
+
Bug reports and feature requests go to the
|
|
186
|
+
[issue tracker](https://github.com/GrahamDumpleton/wrapture/issues).
|
|
187
|
+
Include the wrapture and Python versions and, where you can, a small
|
|
188
|
+
binding that reproduces the problem.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
BSD 2-Clause. See
|
|
193
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture/blob/develop/LICENSE).
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# wrapture
|
|
2
|
+
|
|
3
|
+
**Wrap anything, capture everything, change nothing.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/GrahamDumpleton/wrapture/actions/workflows/build-test-release.yml)
|
|
6
|
+
[](https://wrapture.readthedocs.io)
|
|
7
|
+
|
|
8
|
+
wrapture (`wrapt` + `capture`) is a Python library for attaching bindings to
|
|
9
|
+
arbitrary call sites, without modifying the code being observed, and doing
|
|
10
|
+
something useful with what flows through them.
|
|
11
|
+
|
|
12
|
+
It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
|
|
13
|
+
and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
|
|
14
|
+
safe monkey-patching machinery wrapt provides.
|
|
15
|
+
|
|
16
|
+
> **Status: stabilising ahead of 1.0.0.** The feature set is complete for
|
|
17
|
+
> a first release and pre-releases are published to PyPI. Until 1.0.0 is
|
|
18
|
+
> final, a plain `pip install wrapture` picks up the latest pre-release
|
|
19
|
+
> automatically, so there is no need to pin a specific version. The API
|
|
20
|
+
> is being exercised and tidied rather than extended, so small changes are
|
|
21
|
+
> still possible before 1.0.0.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
wrapture is on [PyPI](https://pypi.org/project/wrapture/):
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ pip install wrapture
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or with uv:
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ uv add wrapture
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
Full documentation is at [wrapture.readthedocs.io](https://wrapture.readthedocs.io).
|
|
40
|
+
Start with the [getting started](https://wrapture.readthedocs.io/en/latest/getting-started.html)
|
|
41
|
+
page: everything on it can be pasted into a Python interpreter. Coming
|
|
42
|
+
from `unittest.mock`? There is a
|
|
43
|
+
[comparison page](https://wrapture.readthedocs.io/en/latest/coming-from-mock.html)
|
|
44
|
+
mapping each mock idiom to its wrapture counterpart. After that, the
|
|
45
|
+
worked examples, starting with
|
|
46
|
+
[testing code that calls external services](https://wrapture.readthedocs.io/en/latest/example-external-services.html),
|
|
47
|
+
each take one question you might arrive with and answer it end to end.
|
|
48
|
+
|
|
49
|
+
## At a glance
|
|
50
|
+
|
|
51
|
+
None of the classes below import wrapture or know they are observed:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
place = wrapture.binding(OrderService, "place")
|
|
55
|
+
charge = wrapture.binding(Gateway, "charge")
|
|
56
|
+
record = wrapture.binding(Ledger, "record")
|
|
57
|
+
|
|
58
|
+
with wrapture.timeline(place, charge, record) as tape:
|
|
59
|
+
OrderService().place(500)
|
|
60
|
+
|
|
61
|
+
print(tape.tree())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
OrderService.place(amount=500) -> {'id': 'ch_500', 'amount': 500}
|
|
66
|
+
Gateway.charge(amount=500, currency='USD') -> {'id': 'ch_500', 'amount': 500}
|
|
67
|
+
Ledger.record(entry={'id': 'ch_500', 'amount': 500}) -> 'led_ch_500'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The same bindings intervene as well as observe: stub a result, inject a
|
|
71
|
+
failure, or transform one argument while the real code keeps running.
|
|
72
|
+
|
|
73
|
+
## What it does
|
|
74
|
+
|
|
75
|
+
One mechanism, three uses, in increasing order of machinery:
|
|
76
|
+
|
|
77
|
+
1. **Monkey patching.** A clean lifecycle and behaviour vocabulary over
|
|
78
|
+
wrapt's `wrap_object()`. Point at a method by name and stub it, fail it,
|
|
79
|
+
transform its arguments or result, or wrap it with a decorator, then
|
|
80
|
+
remove it again, with honest reporting if something else displaced the
|
|
81
|
+
patch in the meantime. Useful entirely on its own, with nothing else
|
|
82
|
+
switched on.
|
|
83
|
+
|
|
84
|
+
2. **Unit testing.** Observe and assert on how calls actually flowed through a
|
|
85
|
+
*real* call graph (nesting, ordering, arguments and return values) and
|
|
86
|
+
optionally intervene (stub, transform, fail-inject). Unlike a
|
|
87
|
+
`unittest.mock` `Mock`, which fabricates values and cannot see calls an object makes to itself,
|
|
88
|
+
wrapture watches the real code run, and when a test must supply a
|
|
89
|
+
stand-in it provides strict, recorded ones: `stub()` for a callable,
|
|
90
|
+
spec-required `mock()` for a collaborator. This makes it possible to test code
|
|
91
|
+
with no injectable seams at all, and to assert on what *didn't* happen on
|
|
92
|
+
an error path: inject a gateway timeout, then verify the ledger was not
|
|
93
|
+
written, the receipt was not sent, and the compensating refund was issued.
|
|
94
|
+
|
|
95
|
+
3. **Ad-hoc tracing.** Attach bindings to a running application, including
|
|
96
|
+
one you cannot modify or redeploy, and emit a structured, nested trace to
|
|
97
|
+
process or chart elsewhere. Name a handful of methods and a call tree
|
|
98
|
+
appears; no code changes required: with a `wrapture.toml` naming the
|
|
99
|
+
methods and a sink, `python -m wrapture manage.py runserver` traces the
|
|
100
|
+
application untouched. With [autowrapt](https://github.com/GrahamDumpleton/autowrapt)
|
|
101
|
+
installed, not even the launcher is needed: `AUTOWRAPT_BOOTSTRAP=wrapture`
|
|
102
|
+
in the environment applies the same config at interpreter startup, so the
|
|
103
|
+
program starts with plain `python`.
|
|
104
|
+
|
|
105
|
+
The distinction that matters: most tracing tools either need the code to
|
|
106
|
+
have been written with them in mind, or can only be switched on for the
|
|
107
|
+
whole program at once. wrapture needs neither: you point at a method by
|
|
108
|
+
name and a trace appears.
|
|
109
|
+
|
|
110
|
+
## Why
|
|
111
|
+
|
|
112
|
+
No single existing tool covers "point at arbitrary methods, get a structured
|
|
113
|
+
nested trace, assert on it or export it, in tests or in production":
|
|
114
|
+
|
|
115
|
+
- `unittest.mock` records a flat call list, with no nesting and no return
|
|
116
|
+
values, and a patched call returns a fabricated `MagicMock` rather than
|
|
117
|
+
running the real code.
|
|
118
|
+
- Span-assertion tools (`logfire.testing`, OpenTelemetry's
|
|
119
|
+
`InMemorySpanExporter`) require the code to already be instrumented.
|
|
120
|
+
- `sys.settrace` tools (`hunter`, `snoop`) give a firehose with no assertion
|
|
121
|
+
API.
|
|
122
|
+
- APM agents are all-or-nothing products rather than a toolkit.
|
|
123
|
+
|
|
124
|
+
wrapture fills that gap: a targeted call tree with normalized arguments and
|
|
125
|
+
return values, produced by naming the methods you care about, usable as a
|
|
126
|
+
testing assertion library, a tracing tool, or both at once.
|
|
127
|
+
|
|
128
|
+
## What it is not
|
|
129
|
+
|
|
130
|
+
- **Not a fabrication tool.** There is no spec-less `Mock()` here by
|
|
131
|
+
design; stand-ins are strict and built from named specs, and
|
|
132
|
+
`unittest.mock` remains the tool for invented objects.
|
|
133
|
+
- **Not a production APM.** It is a toolkit that APM-like things could be
|
|
134
|
+
built on.
|
|
135
|
+
- **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
|
|
136
|
+
it.
|
|
137
|
+
|
|
138
|
+
## How it was built
|
|
139
|
+
|
|
140
|
+
wrapture's code and documentation were written by an AI assistant under
|
|
141
|
+
the direction of Graham Dumpleton, the author of wrapt, through a long
|
|
142
|
+
process of specification, layered implementation, and validation against
|
|
143
|
+
real-world test suites.
|
|
144
|
+
[How wrapture was built](https://wrapture.readthedocs.io/en/latest/how-wrapture-was-built.html)
|
|
145
|
+
explains the process and the thinking.
|
|
146
|
+
|
|
147
|
+
## Requirements
|
|
148
|
+
|
|
149
|
+
- Python 3.12+
|
|
150
|
+
- [wrapt](https://github.com/GrahamDumpleton/wrapt) 2.4.0+
|
|
151
|
+
|
|
152
|
+
## Issues
|
|
153
|
+
|
|
154
|
+
Bug reports and feature requests go to the
|
|
155
|
+
[issue tracker](https://github.com/GrahamDumpleton/wrapture/issues).
|
|
156
|
+
Include the wrapture and Python versions and, where you can, a small
|
|
157
|
+
binding that reproduces the problem.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
BSD 2-Clause. See
|
|
162
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture/blob/develop/LICENSE).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# wrapt is needed at build time because reading the dynamic version imports
|
|
3
|
+
# the package, and the package imports wrapt at the top level.
|
|
4
|
+
requires = ["setuptools>=62.0", "wheel", "wrapt>=2.4.0rc5"]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "wrapture"
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
description = "Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = "BSD-2-Clause"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
authors = [
|
|
15
|
+
{name = "Graham Dumpleton", email = "Graham.Dumpleton@gmail.com"}
|
|
16
|
+
]
|
|
17
|
+
requires-python = ">=3.12"
|
|
18
|
+
keywords = ["wrapper", "monkey patching", "tracing", "testing"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"wrapt>=2.4.0rc5",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/GrahamDumpleton/wrapture"
|
|
33
|
+
Documentation = "https://wrapture.readthedocs.io"
|
|
34
|
+
"Bug Tracker" = "https://github.com/GrahamDumpleton/wrapture/issues/"
|
|
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
|
+
|
|
48
|
+
[project.optional-dependencies]
|
|
49
|
+
dev = [
|
|
50
|
+
"mypy",
|
|
51
|
+
"pytest",
|
|
52
|
+
"ruff",
|
|
53
|
+
"setuptools",
|
|
54
|
+
]
|
|
55
|
+
docs = [
|
|
56
|
+
"myst-parser",
|
|
57
|
+
"sphinx",
|
|
58
|
+
"sphinx-rtd-theme",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.setuptools]
|
|
62
|
+
package-dir = {"" = "src"}
|
|
63
|
+
zip-safe = false
|
|
64
|
+
packages = ["wrapture", "wrapture.tools"]
|
|
65
|
+
|
|
66
|
+
[tool.setuptools.package-data]
|
|
67
|
+
wrapture = ["py.typed"]
|
|
68
|
+
|
|
69
|
+
[tool.setuptools.dynamic]
|
|
70
|
+
version = {attr = "wrapture.__version__"}
|
|
71
|
+
|
|
72
|
+
[dependency-groups]
|
|
73
|
+
test = [
|
|
74
|
+
"pytest",
|
|
75
|
+
]
|
|
76
|
+
dev = [
|
|
77
|
+
{include-group = "test"},
|
|
78
|
+
"mypy",
|
|
79
|
+
"ruff",
|
|
80
|
+
"setuptools",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.pytest.ini_options]
|
|
84
|
+
# The docs are on the test paths so the interpreter transcripts in the
|
|
85
|
+
# getting started page run as doctests and cannot rot.
|
|
86
|
+
testpaths = ["tests", "docs"]
|
|
87
|
+
addopts = "--doctest-glob=*.md"
|
|
88
|
+
# ELLIPSIS lets a transcript elide values that vary between runs, such
|
|
89
|
+
# as the elapsed time on a printer's closing line.
|
|
90
|
+
doctest_optionflags = ["ELLIPSIS"]
|
|
91
|
+
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
python_version = "3.12"
|
|
94
|
+
strict = true
|
|
95
|
+
files = ["src/wrapture", "tests"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff]
|
|
98
|
+
line-length = 88
|
|
99
|
+
target-version = "py312"
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint]
|
|
102
|
+
# Version information is defined before the imports in __init__.py.
|
|
103
|
+
per-file-ignores = {"src/wrapture/__init__.py" = ["E402"]}
|
|
104
|
+
select = [
|
|
105
|
+
"E", # pycodestyle errors
|
|
106
|
+
"W", # pycodestyle warnings
|
|
107
|
+
"F", # pyflakes
|
|
108
|
+
"I", # isort
|
|
109
|
+
"UP", # pyupgrade
|
|
110
|
+
"B", # flake8-bugbear
|
|
111
|
+
]
|