wrapture-instrumentation 1.0.0.dev1__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_instrumentation-1.0.0.dev1/CHANGES.md +14 -0
- wrapture_instrumentation-1.0.0.dev1/LICENSE +24 -0
- wrapture_instrumentation-1.0.0.dev1/MANIFEST.in +7 -0
- wrapture_instrumentation-1.0.0.dev1/PKG-INFO +134 -0
- wrapture_instrumentation-1.0.0.dev1/README.md +112 -0
- wrapture_instrumentation-1.0.0.dev1/TESTING.md +183 -0
- wrapture_instrumentation-1.0.0.dev1/demo/__init__.py +17 -0
- wrapture_instrumentation-1.0.0.dev1/demo/framework_flask.py +117 -0
- wrapture_instrumentation-1.0.0.dev1/pyproject.toml +96 -0
- wrapture_instrumentation-1.0.0.dev1/setup.cfg +4 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/__init__.py +26 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/framework_flask/__init__.py +36 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/framework_flask/hooks.py +115 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation/py.typed +0 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/PKG-INFO +134 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/SOURCES.txt +30 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/dependency_links.txt +1 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/entry_points.txt +2 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/not-zip-safe +1 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/requires.txt +1 -0
- wrapture_instrumentation-1.0.0.dev1/src/wrapture_instrumentation.egg-info/top_level.txt +1 -0
- wrapture_instrumentation-1.0.0.dev1/tests/__init__.py +0 -0
- wrapture_instrumentation-1.0.0.dev1/tests/conftest.py +88 -0
- wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/__init__.py +0 -0
- wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/shop.py +72 -0
- wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_apply_remove.py +169 -0
- wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_class.py +68 -0
- wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_registration.py +78 -0
- wrapture_instrumentation-1.0.0.dev1/tests/framework_flask/test_tracing.py +148 -0
- wrapture_instrumentation-1.0.0.dev1/tests/test_package.py +149 -0
- wrapture_instrumentation-1.0.0.dev1/tests/test_wsgi.py +379 -0
- wrapture_instrumentation-1.0.0.dev1/tests/wsgi.py +258 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changes
|
|
2
|
+
|
|
3
|
+
## Version 1.0.0
|
|
4
|
+
|
|
5
|
+
In development.
|
|
6
|
+
|
|
7
|
+
- Project skeleton: the `wrapture_instrumentation` package with its
|
|
8
|
+
version, the test-side WSGI driver, and the package-level tests
|
|
9
|
+
that keep every registered instrumentation import-light.
|
|
10
|
+
|
|
11
|
+
- Flask (`flask`, Flask 3.x): every application's `wsgi_app` wrapped in
|
|
12
|
+
the recording WSGI middleware at construction, every view function
|
|
13
|
+
observed as it registers, and the exception `Flask.handle_exception`
|
|
14
|
+
receives noted against the request event. No settings yet.
|
|
@@ -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,7 @@
|
|
|
1
|
+
# setuptools includes test_*.py files by default but not the helpers
|
|
2
|
+
# they import (conftest.py, wsgi.py, the package __init__.py), so the
|
|
3
|
+
# tests directory is taken whole, with the repository's other notes.
|
|
4
|
+
graft tests
|
|
5
|
+
graft demo
|
|
6
|
+
include CHANGES.md TESTING.md
|
|
7
|
+
global-exclude __pycache__ *.py[cod]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wrapture-instrumentation
|
|
3
|
+
Version: 1.0.0.dev1
|
|
4
|
+
Summary: Instrumentation for common Python packages, applied through wrapture.
|
|
5
|
+
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrahamDumpleton/wrapture-instrumentation
|
|
8
|
+
Project-URL: Documentation, https://wrapture.readthedocs.io
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture-instrumentation/issues/
|
|
10
|
+
Keywords: wrapture,instrumentation,monkey patching,tracing
|
|
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: wrapture>=1.0.0a4
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# wrapture-instrumentation
|
|
24
|
+
|
|
25
|
+
Instrumentation for common Python packages, applied through
|
|
26
|
+
[wrapture](https://github.com/GrahamDumpleton/wrapture).
|
|
27
|
+
|
|
28
|
+
wrapture attaches bindings to arbitrary Python call sites without
|
|
29
|
+
modifying the code being observed, and its config layer can switch on
|
|
30
|
+
packaged instrumentation for a third-party package by name. This
|
|
31
|
+
project is the collection of that packaged instrumentation: one
|
|
32
|
+
`wrapture.Instrumentation` class per target package (Flask first,
|
|
33
|
+
more to follow), each registered under the bare target name, so that
|
|
34
|
+
tracing a framework is one config entry and no code.
|
|
35
|
+
|
|
36
|
+
> **Status: pre-alpha.** The package is being built target by target
|
|
37
|
+
> against wrapture's alpha series; nothing is published to PyPI yet.
|
|
38
|
+
> Flask is the first target and covers its basics; see the table below.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```console
|
|
43
|
+
$ pip install wrapture-instrumentation
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Installing it brings wrapture and nothing else. No target package is
|
|
47
|
+
a dependency: the instrumentation for a package you do not have is
|
|
48
|
+
inert, and wrapture checks the installed version of each target
|
|
49
|
+
against the range the instrumentation supports at apply time.
|
|
50
|
+
|
|
51
|
+
## Using it
|
|
52
|
+
|
|
53
|
+
An `[[instrument]]` entry in `wrapture.toml` names a target:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[[instrument]]
|
|
57
|
+
name = "flask"
|
|
58
|
+
|
|
59
|
+
[[sink]]
|
|
60
|
+
type = "printer"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
and the runner applies it before the application starts, so the
|
|
64
|
+
patches are in place before the framework is imported:
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
$ python -m wrapture -m myapp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The same config works through
|
|
71
|
+
[autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
|
|
72
|
+
(`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`) and, in a test,
|
|
73
|
+
through `wrapture.instrumentation("flask")` scoping the
|
|
74
|
+
instrumentation to a block. The
|
|
75
|
+
[ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
|
|
76
|
+
covers the config file itself.
|
|
77
|
+
|
|
78
|
+
To see what is installed, what each instrumentation supports in the
|
|
79
|
+
current environment, and what settings it takes:
|
|
80
|
+
|
|
81
|
+
```console
|
|
82
|
+
$ python -m wrapture.tools instrumentation --verbose
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
and to generate the `[[instrument]]` entries to paste into a config,
|
|
86
|
+
every one disabled and every setting commented out at its default:
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
$ python -m wrapture.tools instrumentation --toml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Provided instrumentation
|
|
93
|
+
|
|
94
|
+
| Target | Supported versions | Records | Settings |
|
|
95
|
+
| ------ | ------------------ | ------- | -------- |
|
|
96
|
+
| `flask` | Flask 3.x | Every request as one tree (the recording WSGI middleware installed on each application at construction), every view function beneath its request (observed as routes register, blueprints and `MethodView`s included), and the exception Flask catches on its way to a 500 noted on the request. | none yet |
|
|
97
|
+
|
|
98
|
+
The entry point name is the config's `name`; the table says what each
|
|
99
|
+
instrumentation does in the current cut. Settings, further choke
|
|
100
|
+
points and wider version ranges are being added target by target.
|
|
101
|
+
|
|
102
|
+
## Adding a target
|
|
103
|
+
|
|
104
|
+
Each target lives in its own subpackage under
|
|
105
|
+
`src/wrapture_instrumentation/`, named `<category>_<target>`:
|
|
106
|
+
`framework_flask`, `external_requests`, `database_sqlite3`. The
|
|
107
|
+
category says what kind of thing the target is and, with it, which
|
|
108
|
+
part of wrapture the instrumentation mostly uses:
|
|
109
|
+
|
|
110
|
+
- `framework_`: web frameworks, and their extensions as compound
|
|
111
|
+
names (`framework_flask_restful`).
|
|
112
|
+
- `external_`: outbound HTTP and RPC clients and service SDKs.
|
|
113
|
+
- `database_`: DB-API drivers and SQL toolkits.
|
|
114
|
+
- `datastore_`: other stores and caches.
|
|
115
|
+
- `task_`: task queues. `messaging_`: brokers and their clients.
|
|
116
|
+
- `server_`: WSGI and ASGI servers. `template_`: template engines.
|
|
117
|
+
|
|
118
|
+
A new category is added when a target fits none of these. The
|
|
119
|
+
directory name is internal; the entry point name, and so the name a
|
|
120
|
+
config uses, is always the bare target.
|
|
121
|
+
|
|
122
|
+
The subpackage's `__init__.py` holds one `wrapture.Instrumentation`
|
|
123
|
+
subclass and imports only wrapture; everything that touches the
|
|
124
|
+
target lives in `hooks.py`, reached by an import inside `apply()`.
|
|
125
|
+
The class is registered in `pyproject.toml` under
|
|
126
|
+
`[project.entry-points."wrapture.instrumentation"]`, and gets its own
|
|
127
|
+
test suite under `tests/<category>_<target>/`. The
|
|
128
|
+
[instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
|
|
129
|
+
page of the wrapture documentation is the full contract; TESTING.md
|
|
130
|
+
here covers the tests.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
BSD-2-Clause, as wrapture.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# wrapture-instrumentation
|
|
2
|
+
|
|
3
|
+
Instrumentation for common Python packages, applied through
|
|
4
|
+
[wrapture](https://github.com/GrahamDumpleton/wrapture).
|
|
5
|
+
|
|
6
|
+
wrapture attaches bindings to arbitrary Python call sites without
|
|
7
|
+
modifying the code being observed, and its config layer can switch on
|
|
8
|
+
packaged instrumentation for a third-party package by name. This
|
|
9
|
+
project is the collection of that packaged instrumentation: one
|
|
10
|
+
`wrapture.Instrumentation` class per target package (Flask first,
|
|
11
|
+
more to follow), each registered under the bare target name, so that
|
|
12
|
+
tracing a framework is one config entry and no code.
|
|
13
|
+
|
|
14
|
+
> **Status: pre-alpha.** The package is being built target by target
|
|
15
|
+
> against wrapture's alpha series; nothing is published to PyPI yet.
|
|
16
|
+
> Flask is the first target and covers its basics; see the table below.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
$ pip install wrapture-instrumentation
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Installing it brings wrapture and nothing else. No target package is
|
|
25
|
+
a dependency: the instrumentation for a package you do not have is
|
|
26
|
+
inert, and wrapture checks the installed version of each target
|
|
27
|
+
against the range the instrumentation supports at apply time.
|
|
28
|
+
|
|
29
|
+
## Using it
|
|
30
|
+
|
|
31
|
+
An `[[instrument]]` entry in `wrapture.toml` names a target:
|
|
32
|
+
|
|
33
|
+
```toml
|
|
34
|
+
[[instrument]]
|
|
35
|
+
name = "flask"
|
|
36
|
+
|
|
37
|
+
[[sink]]
|
|
38
|
+
type = "printer"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
and the runner applies it before the application starts, so the
|
|
42
|
+
patches are in place before the framework is imported:
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ python -m wrapture -m myapp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The same config works through
|
|
49
|
+
[autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
|
|
50
|
+
(`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`) and, in a test,
|
|
51
|
+
through `wrapture.instrumentation("flask")` scoping the
|
|
52
|
+
instrumentation to a block. The
|
|
53
|
+
[ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
|
|
54
|
+
covers the config file itself.
|
|
55
|
+
|
|
56
|
+
To see what is installed, what each instrumentation supports in the
|
|
57
|
+
current environment, and what settings it takes:
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ python -m wrapture.tools instrumentation --verbose
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
and to generate the `[[instrument]]` entries to paste into a config,
|
|
64
|
+
every one disabled and every setting commented out at its default:
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
$ python -m wrapture.tools instrumentation --toml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Provided instrumentation
|
|
71
|
+
|
|
72
|
+
| Target | Supported versions | Records | Settings |
|
|
73
|
+
| ------ | ------------------ | ------- | -------- |
|
|
74
|
+
| `flask` | Flask 3.x | Every request as one tree (the recording WSGI middleware installed on each application at construction), every view function beneath its request (observed as routes register, blueprints and `MethodView`s included), and the exception Flask catches on its way to a 500 noted on the request. | none yet |
|
|
75
|
+
|
|
76
|
+
The entry point name is the config's `name`; the table says what each
|
|
77
|
+
instrumentation does in the current cut. Settings, further choke
|
|
78
|
+
points and wider version ranges are being added target by target.
|
|
79
|
+
|
|
80
|
+
## Adding a target
|
|
81
|
+
|
|
82
|
+
Each target lives in its own subpackage under
|
|
83
|
+
`src/wrapture_instrumentation/`, named `<category>_<target>`:
|
|
84
|
+
`framework_flask`, `external_requests`, `database_sqlite3`. The
|
|
85
|
+
category says what kind of thing the target is and, with it, which
|
|
86
|
+
part of wrapture the instrumentation mostly uses:
|
|
87
|
+
|
|
88
|
+
- `framework_`: web frameworks, and their extensions as compound
|
|
89
|
+
names (`framework_flask_restful`).
|
|
90
|
+
- `external_`: outbound HTTP and RPC clients and service SDKs.
|
|
91
|
+
- `database_`: DB-API drivers and SQL toolkits.
|
|
92
|
+
- `datastore_`: other stores and caches.
|
|
93
|
+
- `task_`: task queues. `messaging_`: brokers and their clients.
|
|
94
|
+
- `server_`: WSGI and ASGI servers. `template_`: template engines.
|
|
95
|
+
|
|
96
|
+
A new category is added when a target fits none of these. The
|
|
97
|
+
directory name is internal; the entry point name, and so the name a
|
|
98
|
+
config uses, is always the bare target.
|
|
99
|
+
|
|
100
|
+
The subpackage's `__init__.py` holds one `wrapture.Instrumentation`
|
|
101
|
+
subclass and imports only wrapture; everything that touches the
|
|
102
|
+
target lives in `hooks.py`, reached by an import inside `apply()`.
|
|
103
|
+
The class is registered in `pyproject.toml` under
|
|
104
|
+
`[project.entry-points."wrapture.instrumentation"]`, and gets its own
|
|
105
|
+
test suite under `tests/<category>_<target>/`. The
|
|
106
|
+
[instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
|
|
107
|
+
page of the wrapture documentation is the full contract; TESTING.md
|
|
108
|
+
here covers the tests.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
BSD-2-Clause, as wrapture.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## Where the tests are
|
|
4
|
+
|
|
5
|
+
Tests live in the [tests/](tests/) directory at the top of the
|
|
6
|
+
repository, separate from the package code in
|
|
7
|
+
src/wrapture_instrumentation/. Test files are named `test_*.py` and
|
|
8
|
+
are discovered by pytest, which is configured via the
|
|
9
|
+
`[tool.pytest.ini_options]` section of [pyproject.toml](pyproject.toml).
|
|
10
|
+
|
|
11
|
+
The directory has two levels:
|
|
12
|
+
|
|
13
|
+
- Package-level tests directly under tests/: the version, the rule
|
|
14
|
+
that importing the package or loading any registered class never
|
|
15
|
+
imports a target, the listing tool reporting every entry cleanly,
|
|
16
|
+
and the test-side WSGI driver's own tests.
|
|
17
|
+
- One subdirectory per target, `tests/<category>_<target>/`
|
|
18
|
+
(`tests/framework_flask/`), holding that instrumentation's suite:
|
|
19
|
+
settings validation, applying and removing the class directly, the
|
|
20
|
+
whole path through `wrapture.instrumentation()` with a timeline
|
|
21
|
+
recording what the bindings observe, resolving the entry point by
|
|
22
|
+
name, and a check that the installed target satisfies the class's
|
|
23
|
+
`supports` range.
|
|
24
|
+
|
|
25
|
+
Shared helpers live in [tests/conftest.py](tests/conftest.py) and
|
|
26
|
+
[tests/wsgi.py](tests/wsgi.py).
|
|
27
|
+
|
|
28
|
+
## The WSGI driver
|
|
29
|
+
|
|
30
|
+
WSGI applications are driven in process by `tests/wsgi.py`, which
|
|
31
|
+
plays the server's side of PEP 3333 exactly: it builds a complete
|
|
32
|
+
environ, supplies a `start_response` that honours the `exc_info`
|
|
33
|
+
re-invocation rule and returns a working `write` callable, iterates
|
|
34
|
+
the application's result, and always calls its `close()`, including
|
|
35
|
+
when iteration raises or is abandoned. It is used instead of a
|
|
36
|
+
framework's test client because a request event's closing line is
|
|
37
|
+
tied to the moment the response iterable is consumed and closed, and
|
|
38
|
+
the driver makes that moment explicit: `request(app, "GET", "/path")`
|
|
39
|
+
reads and closes for the common case, and `consume=False` hands back
|
|
40
|
+
the response with its body unconsumed so a test can check the
|
|
41
|
+
request is still open, then `read()` and `close()` it. The same
|
|
42
|
+
driver serves every WSGI target.
|
|
43
|
+
|
|
44
|
+
## Running the tests
|
|
45
|
+
|
|
46
|
+
All tooling in this project goes through [uv](https://docs.astral.sh/uv/),
|
|
47
|
+
which manages the project environment and installs the package, its
|
|
48
|
+
development dependencies (including pytest) and the target packages
|
|
49
|
+
the tests need.
|
|
50
|
+
|
|
51
|
+
The simplest way to run the test suite is via the Justfile target:
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
just test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Extra arguments are passed through to pytest, for example:
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
just test -v
|
|
61
|
+
just test tests/test_wsgi.py
|
|
62
|
+
just test -k version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
One target's suite alone:
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
just test-target framework_flask
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Equivalently, run pytest directly with uv:
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
uv run pytest
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Watching what the tests record
|
|
78
|
+
|
|
79
|
+
The suites assert on tapes rather than printing anything, but the
|
|
80
|
+
recorded events can be watched live for visual verification. Setting
|
|
81
|
+
WRAPTURE_PRINTER in the environment installs a process-wide
|
|
82
|
+
wrapture.Printer sink for the session, streaming one line to stderr
|
|
83
|
+
as each operation begins and a closing line with its outcome and
|
|
84
|
+
timing; pytest captures stderr, so add -s to see it:
|
|
85
|
+
|
|
86
|
+
```console
|
|
87
|
+
WRAPTURE_PRINTER=1 just test tests/framework_flask -s
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The sink is consulted alongside the tests' own scoped tapes, so the
|
|
91
|
+
stream shows exactly what each tape hears without disturbing any
|
|
92
|
+
assertion.
|
|
93
|
+
|
|
94
|
+
For a purpose-built run rather than the tests' traffic, each target
|
|
95
|
+
also has a demo module under demo/ that applies its instrumentation,
|
|
96
|
+
drives the test application through the WSGI driver, and prints both
|
|
97
|
+
the live stream and the reconstructed tree with timings:
|
|
98
|
+
|
|
99
|
+
```console
|
|
100
|
+
just demo-flask
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
With --otel the same events also export as OpenTelemetry spans over
|
|
104
|
+
OTLP (to http://localhost:4318, or wherever
|
|
105
|
+
OTEL_EXPORTER_OTLP_ENDPOINT points), for verifying the spans in a
|
|
106
|
+
local backend such as Jaeger; the Justfile target overlays the
|
|
107
|
+
wrapture[otel] dependencies for the run:
|
|
108
|
+
|
|
109
|
+
```console
|
|
110
|
+
just demo-flask --otel
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Testing across Python versions
|
|
114
|
+
|
|
115
|
+
The project supports multiple Python versions, including the free
|
|
116
|
+
threaded builds of 3.13, 3.14 and 3.15. The supported list is defined
|
|
117
|
+
at the top of the [Justfile](Justfile). The default version used by
|
|
118
|
+
plain `just test` is pinned in [.python-version](.python-version).
|
|
119
|
+
|
|
120
|
+
Run the test suite on every supported version:
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
just test-all
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run the test suite on one nominated version:
|
|
127
|
+
|
|
128
|
+
```console
|
|
129
|
+
just test-python 3.12
|
|
130
|
+
just test-python 3.14t
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Extra arguments are passed through to pytest for these targets too.
|
|
134
|
+
uv downloads any Python version it does not already have, and each
|
|
135
|
+
version gets its own environment (.venv-VERSION) so the default .venv
|
|
136
|
+
is left untouched.
|
|
137
|
+
|
|
138
|
+
## Testing across target versions
|
|
139
|
+
|
|
140
|
+
The `test` dependency group installs each target at whatever version
|
|
141
|
+
the lock resolves. An instrumentation's `supports` range is kept
|
|
142
|
+
honest by running its suite against other versions of the target,
|
|
143
|
+
overlaid on the project environment for that run. Each target has a
|
|
144
|
+
recipe for it in the Justfile (`just test-flask 3.0.3`) and a list of
|
|
145
|
+
versions that `just test-flask-all` loops over; the CI workflow runs
|
|
146
|
+
the same matrix. A test in each target's suite asserts the installed
|
|
147
|
+
target satisfies `supports`, so a matrix entry outside the range
|
|
148
|
+
fails loudly rather than passing vacuously.
|
|
149
|
+
|
|
150
|
+
## Testing against unreleased wrapture
|
|
151
|
+
|
|
152
|
+
The package depends on a released wrapture. To run the tests against
|
|
153
|
+
a checkout of wrapture in the sibling directory ../wrapture, without
|
|
154
|
+
editing pyproject.toml:
|
|
155
|
+
|
|
156
|
+
```console
|
|
157
|
+
just test-dev
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
which overlays that checkout as an editable install for the run.
|
|
161
|
+
|
|
162
|
+
## Writing tests
|
|
163
|
+
|
|
164
|
+
- Put new test files in tests/ (package level) or
|
|
165
|
+
tests/<category>_<target>/ (for one instrumentation) and name them
|
|
166
|
+
`test_*.py`.
|
|
167
|
+
- Import the package under test as `wrapture_instrumentation`, and
|
|
168
|
+
the instrumentation classes from their subpackages. The project is
|
|
169
|
+
installed into the uv-managed environment, so no path manipulation
|
|
170
|
+
is needed.
|
|
171
|
+
- Validate behaviour with wrapture's own unit testing layer:
|
|
172
|
+
`wrapture.timeline()` to record what the instrumentation's bindings
|
|
173
|
+
observe and the tape's tree and queries to assert on it, bindings
|
|
174
|
+
with `when=` and behaviours where a test needs to see a target
|
|
175
|
+
internal being called or control what it does, and
|
|
176
|
+
`wrapture.instrumentation()` to scope an application of a class to
|
|
177
|
+
a block. Do not use `unittest.mock`. If something cannot be
|
|
178
|
+
expressed that way, write it plainly with a comment naming the gap,
|
|
179
|
+
and say so when summarising the work.
|
|
180
|
+
- Drive WSGI applications through `tests/wsgi.py`, not a framework's
|
|
181
|
+
test client.
|
|
182
|
+
- Tests should not depend on anything in the scratch/ directory,
|
|
183
|
+
which is not part of the repository.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Demonstration scripts, one per instrumented target.
|
|
2
|
+
|
|
3
|
+
Each module drives a small application with its target's
|
|
4
|
+
instrumentation applied, streaming every recorded event live and
|
|
5
|
+
printing the reconstructed tree at the end, so the instrumentation
|
|
6
|
+
can be verified by eye rather than by assertion. Run from the
|
|
7
|
+
repository root as a module, so the tests package (whose application
|
|
8
|
+
and WSGI driver the demos reuse) imports cleanly:
|
|
9
|
+
|
|
10
|
+
uv run python -m demo.framework_flask
|
|
11
|
+
|
|
12
|
+
or through the Justfile target, which also carries the OpenTelemetry
|
|
13
|
+
dependencies for the --otel flag:
|
|
14
|
+
|
|
15
|
+
just demo-flask
|
|
16
|
+
just demo-flask --otel
|
|
17
|
+
"""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Drive the shop application with the Flask instrumentation applied.
|
|
2
|
+
|
|
3
|
+
The instrumentation is resolved by its entry point name, the way a
|
|
4
|
+
config file finds it, and the application is the tests' own shop,
|
|
5
|
+
built only after the instrumentation applies, the order the runner
|
|
6
|
+
guarantees in real use. A handful of requests then cover every shape
|
|
7
|
+
the instrumentation handles: plain views, a streaming response, a
|
|
8
|
+
class-based view, a blueprint, and a view that raises (Flask answers
|
|
9
|
+
500 and the exception is noted against the request event).
|
|
10
|
+
|
|
11
|
+
Two views of the run always print: the live stream, one line as each
|
|
12
|
+
operation begins and a closing line with its outcome, and the tidy
|
|
13
|
+
tree reconstructed afterwards with timings. With --otel the same
|
|
14
|
+
events also export as OpenTelemetry spans to a local OTLP endpoint
|
|
15
|
+
(http://localhost:4318 unless OTEL_EXPORTER_OTLP_ENDPOINT says
|
|
16
|
+
otherwise), for verifying the spans in a backend such as Jaeger:
|
|
17
|
+
|
|
18
|
+
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
import wrapture
|
|
28
|
+
|
|
29
|
+
# Every route the shop registers, the failing request last so the
|
|
30
|
+
# stream ends on the interesting case.
|
|
31
|
+
|
|
32
|
+
REQUESTS: tuple[tuple[str, str], ...] = (
|
|
33
|
+
("GET", "/"),
|
|
34
|
+
("GET", "/quote/widget"),
|
|
35
|
+
("GET", "/export"),
|
|
36
|
+
("GET", "/catalog"),
|
|
37
|
+
("GET", "/reports/summary"),
|
|
38
|
+
("GET", "/quote/missing"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def add_otel_sink() -> None:
|
|
43
|
+
"""Register the OpenTelemetry sink, exporting the run's events as
|
|
44
|
+
spans; exits with guidance when the optional dependencies are
|
|
45
|
+
missing."""
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import wrapture.otel
|
|
49
|
+
except ImportError as error:
|
|
50
|
+
raise SystemExit(
|
|
51
|
+
"the OpenTelemetry dependencies are not installed; run the"
|
|
52
|
+
" demo through `just demo-flask --otel`, which overlays"
|
|
53
|
+
" wrapture[otel] for the run"
|
|
54
|
+
) from error
|
|
55
|
+
|
|
56
|
+
wrapture.add_sink(wrapture.otel.sink(service_name="wrapture-flask-demo"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(arguments: list[str] | None = None) -> None:
|
|
60
|
+
"""Run the demo: apply the instrumentation, drive the requests,
|
|
61
|
+
print the live stream and the tree, and flush any exporters."""
|
|
62
|
+
|
|
63
|
+
parser = argparse.ArgumentParser(
|
|
64
|
+
prog="demo.framework_flask",
|
|
65
|
+
description="Drive the shop application with the Flask"
|
|
66
|
+
" instrumentation applied, printing the live stream and the"
|
|
67
|
+
" tree.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--otel",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="also export the events as OpenTelemetry spans over OTLP",
|
|
73
|
+
)
|
|
74
|
+
options = parser.parse_args(arguments)
|
|
75
|
+
|
|
76
|
+
# The exporting sink registers first, mirroring the config loader,
|
|
77
|
+
# which always puts the [otel] table's sink ahead of the [[sink]]
|
|
78
|
+
# list; the printer writes to stdout so the stream and the demo's
|
|
79
|
+
# own headings interleave in order.
|
|
80
|
+
|
|
81
|
+
if options.otel:
|
|
82
|
+
add_otel_sink()
|
|
83
|
+
|
|
84
|
+
wrapture.add_sink(wrapture.Printer(stream=sys.stdout))
|
|
85
|
+
|
|
86
|
+
print("== live stream ==")
|
|
87
|
+
|
|
88
|
+
with wrapture.instrumentation("flask"), wrapture.timeline() as tape:
|
|
89
|
+
from tests.framework_flask.shop import make_app
|
|
90
|
+
from tests.wsgi import request
|
|
91
|
+
|
|
92
|
+
app = make_app()
|
|
93
|
+
|
|
94
|
+
for method, path in REQUESTS:
|
|
95
|
+
request(app, method, path)
|
|
96
|
+
|
|
97
|
+
print()
|
|
98
|
+
print("== tree ==")
|
|
99
|
+
print(tape.tree(times=True))
|
|
100
|
+
|
|
101
|
+
# Deliver everything owed: batched spans push to the exporter here
|
|
102
|
+
# rather than at interpreter exit, so the closing hint below is
|
|
103
|
+
# true by the time it prints.
|
|
104
|
+
|
|
105
|
+
wrapture.shutdown()
|
|
106
|
+
|
|
107
|
+
if options.otel:
|
|
108
|
+
endpoint = os.environ.get(
|
|
109
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"
|
|
110
|
+
)
|
|
111
|
+
print()
|
|
112
|
+
print("== otel ==")
|
|
113
|
+
print(f"spans flushed to {endpoint} as service wrapture-flask-demo")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|