wrapture 1.0.0.dev1__tar.gz → 1.0.0.dev2__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.dev1/src/wrapture.egg-info → wrapture-1.0.0.dev2}/PKG-INFO +19 -3
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/README.md +18 -2
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/__init__.py +12 -23
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/bindings.py +26 -18
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/capture.py +31 -1
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/iterators.py +12 -12
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/stacks.py +19 -2
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2/src/wrapture.egg-info}/PKG-INFO +19 -3
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_attribute_events.py +2 -2
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_capture.py +25 -10
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_events.py +9 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_stacks.py +11 -8
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/LICENSE +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/pyproject.toml +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/setup.cfg +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/attributes.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/behaviours.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/eventlogs.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/events.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/exceptions.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/py.typed +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/pytest_plugin.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/timeline.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/SOURCES.txt +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/dependency_links.txt +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/not-zip-safe +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/requires.txt +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/top_level.txt +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_attributes.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_behaviours.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_bindings.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_eventlogs.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_generator_events.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_iterators.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_pytest_plugin.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_recording.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_targets.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_testing_patterns.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_threads.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_timeline.py +0 -0
- {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wrapture
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev2
|
|
4
4
|
Summary: Library for attaching bindings to arbitrary Python call sites for monkey patching, testing, tracing and profiling.
|
|
5
5
|
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -46,7 +46,22 @@ safe monkey-patching machinery wrapt provides.
|
|
|
46
46
|
|
|
47
47
|
> **Status: early development.** The monkey patching and unit testing layers
|
|
48
48
|
> are implemented; the tracing and profiling layers are designed but not
|
|
49
|
-
> built.
|
|
49
|
+
> built. Development previews are published to PyPI; the API may still
|
|
50
|
+
> shift before 1.0.0.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
wrapture is on [PyPI](https://pypi.org/project/wrapture/):
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
$ pip install wrapture
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
or with uv:
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
$ uv add wrapture
|
|
64
|
+
```
|
|
50
65
|
|
|
51
66
|
## Documentation
|
|
52
67
|
|
|
@@ -152,4 +167,5 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
152
167
|
|
|
153
168
|
## License
|
|
154
169
|
|
|
155
|
-
BSD 2-Clause. See
|
|
170
|
+
BSD 2-Clause. See
|
|
171
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture/blob/develop/LICENSE).
|
|
@@ -15,7 +15,22 @@ safe monkey-patching machinery wrapt provides.
|
|
|
15
15
|
|
|
16
16
|
> **Status: early development.** The monkey patching and unit testing layers
|
|
17
17
|
> are implemented; the tracing and profiling layers are designed but not
|
|
18
|
-
> built.
|
|
18
|
+
> built. Development previews are published to PyPI; the API may still
|
|
19
|
+
> shift before 1.0.0.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
wrapture is on [PyPI](https://pypi.org/project/wrapture/):
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ pip install wrapture
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
or with uv:
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
$ uv add wrapture
|
|
33
|
+
```
|
|
19
34
|
|
|
20
35
|
## Documentation
|
|
21
36
|
|
|
@@ -121,4 +136,5 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
121
136
|
|
|
122
137
|
## License
|
|
123
138
|
|
|
124
|
-
BSD 2-Clause. See
|
|
139
|
+
BSD 2-Clause. See
|
|
140
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture/blob/develop/LICENSE).
|
|
@@ -17,9 +17,11 @@ 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", "dev2")
|
|
21
21
|
__version__ = _format_version(__version_info__)
|
|
22
22
|
|
|
23
|
+
from wrapt import MISSING
|
|
24
|
+
|
|
23
25
|
from .behaviours import (
|
|
24
26
|
CallBehaviour,
|
|
25
27
|
DeleteBehaviour,
|
|
@@ -33,11 +35,6 @@ from .bindings import (
|
|
|
33
35
|
bindings,
|
|
34
36
|
)
|
|
35
37
|
from .capture import (
|
|
36
|
-
NONE,
|
|
37
|
-
REFERENCE,
|
|
38
|
-
SNAPSHOT,
|
|
39
|
-
SUMMARY,
|
|
40
|
-
TYPES,
|
|
41
38
|
redact,
|
|
42
39
|
)
|
|
43
40
|
from .eventlogs import (
|
|
@@ -56,17 +53,15 @@ from .exceptions import (
|
|
|
56
53
|
WrongModeError,
|
|
57
54
|
)
|
|
58
55
|
from .iterators import (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
IteratorAbandonBehaviour,
|
|
57
|
+
IteratorErrorBehaviour,
|
|
58
|
+
IteratorFinishBehaviour,
|
|
59
|
+
IteratorItemBehaviour,
|
|
63
60
|
IteratorProxy,
|
|
64
61
|
iterator,
|
|
65
62
|
)
|
|
66
63
|
from .stacks import (
|
|
67
64
|
StackFrame,
|
|
68
|
-
caller,
|
|
69
|
-
full,
|
|
70
65
|
stack_frames,
|
|
71
66
|
)
|
|
72
67
|
from .timeline import (
|
|
@@ -78,25 +73,21 @@ from .timeline import (
|
|
|
78
73
|
)
|
|
79
74
|
|
|
80
75
|
__all__ = [
|
|
81
|
-
"
|
|
82
|
-
"REFERENCE",
|
|
83
|
-
"SNAPSHOT",
|
|
84
|
-
"SUMMARY",
|
|
85
|
-
"TYPES",
|
|
86
|
-
"AbandonBehaviour",
|
|
76
|
+
"MISSING",
|
|
87
77
|
"AlreadyAppliedError",
|
|
88
78
|
"Binding",
|
|
89
79
|
"BindingGroup",
|
|
90
80
|
"CallBehaviour",
|
|
91
81
|
"DeferredTargetError",
|
|
92
82
|
"DeleteBehaviour",
|
|
93
|
-
"ErrorBehaviour",
|
|
94
83
|
"Event",
|
|
95
84
|
"EventLog",
|
|
96
85
|
"ExpectationNotMetError",
|
|
97
|
-
"FinishBehaviour",
|
|
98
86
|
"GetBehaviour",
|
|
99
|
-
"
|
|
87
|
+
"IteratorAbandonBehaviour",
|
|
88
|
+
"IteratorErrorBehaviour",
|
|
89
|
+
"IteratorFinishBehaviour",
|
|
90
|
+
"IteratorItemBehaviour",
|
|
100
91
|
"IteratorProxy",
|
|
101
92
|
"NeverAppliedError",
|
|
102
93
|
"NotImplementedYetError",
|
|
@@ -109,9 +100,7 @@ __all__ = [
|
|
|
109
100
|
"annotate",
|
|
110
101
|
"binding",
|
|
111
102
|
"bindings",
|
|
112
|
-
"caller",
|
|
113
103
|
"current_event",
|
|
114
|
-
"full",
|
|
115
104
|
"iterator",
|
|
116
105
|
"redact",
|
|
117
106
|
"stack_frames",
|
|
@@ -37,6 +37,7 @@ from .capture import (
|
|
|
37
37
|
CapturePolicy,
|
|
38
38
|
_capture_value,
|
|
39
39
|
_level_of,
|
|
40
|
+
_resolve_policy,
|
|
40
41
|
)
|
|
41
42
|
from .eventlogs import EventLog
|
|
42
43
|
from .events import Event, normalized_arguments
|
|
@@ -49,6 +50,7 @@ from .exceptions import (
|
|
|
49
50
|
WrongModeError,
|
|
50
51
|
)
|
|
51
52
|
from .stacks import _capture as _capture_stack
|
|
53
|
+
from .stacks import _resolve_depth
|
|
52
54
|
from .timeline import (
|
|
53
55
|
Tape,
|
|
54
56
|
_capture_result,
|
|
@@ -356,20 +358,21 @@ class Binding:
|
|
|
356
358
|
label: str | None = None,
|
|
357
359
|
mode: str | None = None,
|
|
358
360
|
missing_ok: bool = False,
|
|
359
|
-
capture: CapturePolicy | None = None,
|
|
360
|
-
capture_args: CapturePolicy | None = None,
|
|
361
|
-
capture_result: CapturePolicy | None = None,
|
|
362
|
-
stack: int | None = None,
|
|
361
|
+
capture: CapturePolicy | str | None = None,
|
|
362
|
+
capture_args: CapturePolicy | str | None = None,
|
|
363
|
+
capture_result: CapturePolicy | str | None = None,
|
|
364
|
+
stack: int | str | None = None,
|
|
363
365
|
) -> None:
|
|
364
366
|
# Validate the target and settle the mode before anything is
|
|
365
367
|
# stored, so a bad binding fails on the line that created it.
|
|
366
368
|
|
|
367
369
|
_reject_deferred(target)
|
|
368
370
|
|
|
371
|
+
stack = _resolve_depth(stack)
|
|
369
372
|
if stack is not None and stack < 1:
|
|
370
373
|
raise ValueError(
|
|
371
|
-
f"stack must be None,
|
|
372
|
-
f"
|
|
374
|
+
f"stack must be None, 'caller', 'full' or a positive"
|
|
375
|
+
f" frame count, got {stack!r}"
|
|
373
376
|
)
|
|
374
377
|
|
|
375
378
|
if mode is None:
|
|
@@ -390,8 +393,12 @@ class Binding:
|
|
|
390
393
|
# consuming the events declares; capture= is shorthand for both
|
|
391
394
|
# axes, with the specific parameters winning.
|
|
392
395
|
|
|
393
|
-
self._capture_args =
|
|
394
|
-
|
|
396
|
+
self._capture_args = _resolve_policy(
|
|
397
|
+
capture_args if capture_args is not None else capture
|
|
398
|
+
)
|
|
399
|
+
self._capture_result = _resolve_policy(
|
|
400
|
+
capture_result if capture_result is not None else capture
|
|
401
|
+
)
|
|
395
402
|
self._stack_depth = stack
|
|
396
403
|
|
|
397
404
|
# The behaviour pipelines, keyed by operation ("call", "get",
|
|
@@ -1052,10 +1059,10 @@ def binding(
|
|
|
1052
1059
|
label: str | None = None,
|
|
1053
1060
|
mode: str | None = None,
|
|
1054
1061
|
missing_ok: bool = False,
|
|
1055
|
-
capture: CapturePolicy | None = None,
|
|
1056
|
-
capture_args: CapturePolicy | None = None,
|
|
1057
|
-
capture_result: CapturePolicy | None = None,
|
|
1058
|
-
stack: int | None = None,
|
|
1062
|
+
capture: CapturePolicy | str | None = None,
|
|
1063
|
+
capture_args: CapturePolicy | str | None = None,
|
|
1064
|
+
capture_result: CapturePolicy | str | None = None,
|
|
1065
|
+
stack: int | str | None = None,
|
|
1059
1066
|
) -> Binding:
|
|
1060
1067
|
"""Create a binding for one target attribute.
|
|
1061
1068
|
|
|
@@ -1071,15 +1078,16 @@ def binding(
|
|
|
1071
1078
|
AttributeError, because it is indistinguishable from a typo.
|
|
1072
1079
|
|
|
1073
1080
|
`capture=` overrides how much of the recorded values this binding
|
|
1074
|
-
stores
|
|
1075
|
-
|
|
1076
|
-
|
|
1081
|
+
stores: a level named by string ("none", "types", "reference",
|
|
1082
|
+
"summary" or "snapshot"), or a fn(name, value) callable.
|
|
1083
|
+
`capture_args=` and `capture_result=` control the two axes
|
|
1084
|
+
separately and win over the shorthand. Left unset, the binding
|
|
1077
1085
|
follows what the sink consuming the events declares.
|
|
1078
1086
|
|
|
1079
1087
|
`stack=` captures how control reached each recorded event:
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
nothing
|
|
1088
|
+
"caller" for just the calling frame, a frame count, or "full" for
|
|
1089
|
+
the whole stack. The default None captures nothing and costs
|
|
1090
|
+
nothing.
|
|
1083
1091
|
|
|
1084
1092
|
Does NOT apply the wrapper; call apply() or use the binding as a
|
|
1085
1093
|
context manager.
|
|
@@ -35,6 +35,32 @@ SNAPSHOT = 4
|
|
|
35
35
|
|
|
36
36
|
CapturePolicy = int | Callable[[str | None, Any], Any]
|
|
37
37
|
|
|
38
|
+
# The user-facing spellings of the levels: binding() and redact() accept
|
|
39
|
+
# these strings and resolve them to the numeric levels above.
|
|
40
|
+
|
|
41
|
+
_LEVEL_NAMES = {
|
|
42
|
+
"none": NONE,
|
|
43
|
+
"types": TYPES,
|
|
44
|
+
"reference": REFERENCE,
|
|
45
|
+
"summary": SUMMARY,
|
|
46
|
+
"snapshot": SNAPSHOT,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_policy(policy: CapturePolicy | str | None) -> CapturePolicy | None:
|
|
51
|
+
if isinstance(policy, str):
|
|
52
|
+
try:
|
|
53
|
+
return _LEVEL_NAMES[policy]
|
|
54
|
+
except KeyError:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"capture level must be one of {sorted(_LEVEL_NAMES)}, a"
|
|
57
|
+
f" numeric level, or a fn(name, value) callable, got"
|
|
58
|
+
f" {policy!r}"
|
|
59
|
+
) from None
|
|
60
|
+
|
|
61
|
+
return policy
|
|
62
|
+
|
|
63
|
+
|
|
38
64
|
_ATOMIC = (bool, int, float, complex, type(None))
|
|
39
65
|
|
|
40
66
|
|
|
@@ -100,7 +126,9 @@ def summarize(value: Any, *, limit: int = 200, items: int = 10) -> Any:
|
|
|
100
126
|
|
|
101
127
|
|
|
102
128
|
def redact(
|
|
103
|
-
*names: str,
|
|
129
|
+
*names: str,
|
|
130
|
+
level: CapturePolicy | str = REFERENCE,
|
|
131
|
+
marker: str = "<redacted>",
|
|
104
132
|
) -> CapturePolicy:
|
|
105
133
|
"""A capture policy that replaces named parameters with a marker.
|
|
106
134
|
|
|
@@ -119,6 +147,8 @@ def redact(
|
|
|
119
147
|
"""
|
|
120
148
|
|
|
121
149
|
wanted = set(names)
|
|
150
|
+
resolved = _resolve_policy(level)
|
|
151
|
+
level = REFERENCE if resolved is None else resolved
|
|
122
152
|
|
|
123
153
|
def policy(name: str | None, value: Any) -> Any:
|
|
124
154
|
if name in wanted:
|
|
@@ -227,7 +227,7 @@ class _IteratorBehaviour:
|
|
|
227
227
|
self._factory = factory
|
|
228
228
|
|
|
229
229
|
|
|
230
|
-
class
|
|
230
|
+
class IteratorItemBehaviour(_IteratorBehaviour):
|
|
231
231
|
"""`iterator().on_item`: behaviour applied to each item.
|
|
232
232
|
|
|
233
233
|
Mirrors the composing half of a binding's on_call: stages accumulate
|
|
@@ -260,7 +260,7 @@ class ItemBehaviour(_IteratorBehaviour):
|
|
|
260
260
|
return self._factory._clear(self._factory._item_stages)
|
|
261
261
|
|
|
262
262
|
|
|
263
|
-
class
|
|
263
|
+
class IteratorFinishBehaviour(_IteratorBehaviour):
|
|
264
264
|
"""`iterator().on_finish`: behaviour for normal exhaustion.
|
|
265
265
|
|
|
266
266
|
Checks receive the wrapped generator's return value, or None for
|
|
@@ -280,7 +280,7 @@ class FinishBehaviour(_IteratorBehaviour):
|
|
|
280
280
|
return self._factory._clear(self._factory._finish_checks)
|
|
281
281
|
|
|
282
282
|
|
|
283
|
-
class
|
|
283
|
+
class IteratorErrorBehaviour(_IteratorBehaviour):
|
|
284
284
|
"""`iterator().on_error`: behaviour for a failed iteration.
|
|
285
285
|
|
|
286
286
|
Hooks receive the exception about to reach the consumer, whether it
|
|
@@ -302,7 +302,7 @@ class ErrorBehaviour(_IteratorBehaviour):
|
|
|
302
302
|
return self._factory._clear(self._factory._error_hooks)
|
|
303
303
|
|
|
304
304
|
|
|
305
|
-
class
|
|
305
|
+
class IteratorAbandonBehaviour(_IteratorBehaviour):
|
|
306
306
|
"""`iterator().on_abandon`: behaviour for an abandoned iteration.
|
|
307
307
|
|
|
308
308
|
Hooks fire when a started, unexhausted wrapped generator is closed,
|
|
@@ -343,28 +343,28 @@ class IteratorProxy:
|
|
|
343
343
|
self._abandon_hooks: list[AbandonFunction] = []
|
|
344
344
|
|
|
345
345
|
@property
|
|
346
|
-
def on_item(self) ->
|
|
346
|
+
def on_item(self) -> IteratorItemBehaviour:
|
|
347
347
|
"""The behaviour namespace for items."""
|
|
348
348
|
|
|
349
|
-
return
|
|
349
|
+
return IteratorItemBehaviour(self)
|
|
350
350
|
|
|
351
351
|
@property
|
|
352
|
-
def on_finish(self) ->
|
|
352
|
+
def on_finish(self) -> IteratorFinishBehaviour:
|
|
353
353
|
"""The behaviour namespace for normal exhaustion."""
|
|
354
354
|
|
|
355
|
-
return
|
|
355
|
+
return IteratorFinishBehaviour(self)
|
|
356
356
|
|
|
357
357
|
@property
|
|
358
|
-
def on_error(self) ->
|
|
358
|
+
def on_error(self) -> IteratorErrorBehaviour:
|
|
359
359
|
"""The behaviour namespace for a failed iteration."""
|
|
360
360
|
|
|
361
|
-
return
|
|
361
|
+
return IteratorErrorBehaviour(self)
|
|
362
362
|
|
|
363
363
|
@property
|
|
364
|
-
def on_abandon(self) ->
|
|
364
|
+
def on_abandon(self) -> IteratorAbandonBehaviour:
|
|
365
365
|
"""The behaviour namespace for an abandoned iteration."""
|
|
366
366
|
|
|
367
|
-
return
|
|
367
|
+
return IteratorAbandonBehaviour(self)
|
|
368
368
|
|
|
369
369
|
def _add(self, hooks: list[Any], fn: Any) -> Self:
|
|
370
370
|
hooks.append(fn)
|
|
@@ -34,12 +34,29 @@ class StackFrame(NamedTuple):
|
|
|
34
34
|
function: str
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
# The stack= argument to binding():
|
|
38
|
-
# every frame
|
|
37
|
+
# The depths behind the stack= argument to binding(): "caller" captures
|
|
38
|
+
# just the calling frame, "full" every frame, and any positive integer
|
|
39
|
+
# captures that many frames.
|
|
39
40
|
|
|
40
41
|
caller: Final[int] = 1
|
|
41
42
|
full: Final[int] = sys.maxsize
|
|
42
43
|
|
|
44
|
+
_DEPTH_NAMES = {"caller": caller, "full": full}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_depth(stack: int | str | None) -> int | None:
|
|
48
|
+
if isinstance(stack, str):
|
|
49
|
+
try:
|
|
50
|
+
return _DEPTH_NAMES[stack]
|
|
51
|
+
except KeyError:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"stack must be None, 'caller', 'full' or a positive"
|
|
54
|
+
f" frame count, got {stack!r}"
|
|
55
|
+
) from None
|
|
56
|
+
|
|
57
|
+
return stack
|
|
58
|
+
|
|
59
|
+
|
|
43
60
|
# Frames of the observation machinery itself are elided, so a captured
|
|
44
61
|
# stack starts at the code under observation.
|
|
45
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wrapture
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev2
|
|
4
4
|
Summary: Library for attaching bindings to arbitrary Python call sites for monkey patching, testing, tracing and profiling.
|
|
5
5
|
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -46,7 +46,22 @@ safe monkey-patching machinery wrapt provides.
|
|
|
46
46
|
|
|
47
47
|
> **Status: early development.** The monkey patching and unit testing layers
|
|
48
48
|
> are implemented; the tracing and profiling layers are designed but not
|
|
49
|
-
> built.
|
|
49
|
+
> built. Development previews are published to PyPI; the API may still
|
|
50
|
+
> shift before 1.0.0.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
wrapture is on [PyPI](https://pypi.org/project/wrapture/):
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
$ pip install wrapture
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
or with uv:
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
$ uv add wrapture
|
|
64
|
+
```
|
|
50
65
|
|
|
51
66
|
## Documentation
|
|
52
67
|
|
|
@@ -152,4 +167,5 @@ testing assertion library, a tracing tool, or both at once.
|
|
|
152
167
|
|
|
153
168
|
## License
|
|
154
169
|
|
|
155
|
-
BSD 2-Clause. See
|
|
170
|
+
BSD 2-Clause. See
|
|
171
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture/blob/develop/LICENSE).
|
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
import pytest
|
|
11
11
|
from wrapt import MISSING
|
|
12
12
|
|
|
13
|
-
from wrapture import
|
|
13
|
+
from wrapture import annotate, binding, redact, timeline
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Model:
|
|
@@ -216,7 +216,7 @@ def test_calls_triggered_by_a_property_read_nest_under_the_get_event() -> None:
|
|
|
216
216
|
|
|
217
217
|
|
|
218
218
|
def test_set_values_capture_at_the_argument_level() -> None:
|
|
219
|
-
status = binding(Model, "status", missing_ok=True, capture=
|
|
219
|
+
status = binding(Model, "status", missing_ok=True, capture="summary")
|
|
220
220
|
model = Model()
|
|
221
221
|
tags = ["a", "b"]
|
|
222
222
|
|
|
@@ -12,17 +12,13 @@ from typing import Any
|
|
|
12
12
|
from wrapt import MISSING
|
|
13
13
|
|
|
14
14
|
from wrapture import (
|
|
15
|
-
NONE,
|
|
16
|
-
SNAPSHOT,
|
|
17
|
-
SUMMARY,
|
|
18
|
-
TYPES,
|
|
19
15
|
annotate,
|
|
20
16
|
binding,
|
|
21
17
|
current_event,
|
|
22
18
|
redact,
|
|
23
19
|
timeline,
|
|
24
20
|
)
|
|
25
|
-
from wrapture.capture import summarize, type_name
|
|
21
|
+
from wrapture.capture import NONE, summarize, type_name
|
|
26
22
|
|
|
27
23
|
|
|
28
24
|
class Ledger:
|
|
@@ -59,7 +55,7 @@ def test_reference_default_stores_references() -> None:
|
|
|
59
55
|
|
|
60
56
|
|
|
61
57
|
def test_summary_snapshots_a_bounded_repr() -> None:
|
|
62
|
-
record = binding(Ledger, "record", capture=
|
|
58
|
+
record = binding(Ledger, "record", capture="summary")
|
|
63
59
|
entries = ["a", "b"]
|
|
64
60
|
|
|
65
61
|
with timeline(record):
|
|
@@ -75,7 +71,7 @@ def test_summary_snapshots_a_bounded_repr() -> None:
|
|
|
75
71
|
|
|
76
72
|
|
|
77
73
|
def test_types_records_type_names_only() -> None:
|
|
78
|
-
record = binding(Ledger, "record", capture=
|
|
74
|
+
record = binding(Ledger, "record", capture="types")
|
|
79
75
|
|
|
80
76
|
with timeline(record):
|
|
81
77
|
Ledger().record(["a"])
|
|
@@ -87,7 +83,7 @@ def test_types_records_type_names_only() -> None:
|
|
|
87
83
|
|
|
88
84
|
|
|
89
85
|
def test_none_keeps_the_event_but_no_values() -> None:
|
|
90
|
-
record = binding(Ledger, "record", capture=
|
|
86
|
+
record = binding(Ledger, "record", capture="none")
|
|
91
87
|
|
|
92
88
|
with timeline(record):
|
|
93
89
|
Ledger().record(["a"])
|
|
@@ -100,7 +96,7 @@ def test_none_keeps_the_event_but_no_values() -> None:
|
|
|
100
96
|
|
|
101
97
|
|
|
102
98
|
def test_snapshot_deepcopies_and_falls_back_where_it_cannot() -> None:
|
|
103
|
-
record = binding(Ledger, "record", capture=
|
|
99
|
+
record = binding(Ledger, "record", capture="snapshot")
|
|
104
100
|
entries = ["a", "b"]
|
|
105
101
|
|
|
106
102
|
with timeline(record):
|
|
@@ -121,7 +117,7 @@ def test_snapshot_deepcopies_and_falls_back_where_it_cannot() -> None:
|
|
|
121
117
|
|
|
122
118
|
|
|
123
119
|
def test_specific_axis_wins_over_the_shorthand() -> None:
|
|
124
|
-
record = binding(Ledger, "record", capture=
|
|
120
|
+
record = binding(Ledger, "record", capture="none", capture_result="summary")
|
|
125
121
|
|
|
126
122
|
with timeline(record):
|
|
127
123
|
Ledger().record(["a"])
|
|
@@ -154,6 +150,25 @@ def test_redact_matches_by_name_however_the_caller_passed_it() -> None:
|
|
|
154
150
|
assert vault.events.first.result == "opened:hunter2"
|
|
155
151
|
|
|
156
152
|
|
|
153
|
+
def test_a_misspelled_capture_level_raises_at_creation() -> None:
|
|
154
|
+
import pytest
|
|
155
|
+
|
|
156
|
+
with pytest.raises(ValueError, match="capture level must be"):
|
|
157
|
+
binding(Ledger, "record", capture="sumary")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_redact_accepts_a_level_name_for_everything_unnamed() -> None:
|
|
161
|
+
vault = binding(Vault, "open", capture_args=redact("secret", level="types"))
|
|
162
|
+
|
|
163
|
+
with timeline(vault):
|
|
164
|
+
Vault().open("hunter2", attempts=3)
|
|
165
|
+
|
|
166
|
+
event = vault.events.first
|
|
167
|
+
assert event.arguments is not None
|
|
168
|
+
assert event.arguments["secret"] == "<redacted>"
|
|
169
|
+
assert event.arguments["attempts"] == "<int>"
|
|
170
|
+
|
|
171
|
+
|
|
157
172
|
def test_a_custom_callable_is_a_policy() -> None:
|
|
158
173
|
def masked(name: str | None, value: Any) -> Any:
|
|
159
174
|
return f"{name}!{value}"
|
|
@@ -24,6 +24,15 @@ def charge(amount: int, currency: str = "USD", *, retries: int = 3) -> None:
|
|
|
24
24
|
# ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def test_missing_is_reexported_from_wrapture() -> None:
|
|
28
|
+
# Docs teach `event.result is MISSING`; the sentinel is reachable
|
|
29
|
+
# without knowing it comes from wrapt.
|
|
30
|
+
|
|
31
|
+
import wrapture
|
|
32
|
+
|
|
33
|
+
assert wrapture.MISSING is MISSING
|
|
34
|
+
|
|
35
|
+
|
|
27
36
|
def test_call_event_defaults() -> None:
|
|
28
37
|
event = Event("call", "Gateway.charge")
|
|
29
38
|
|
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
import pytest
|
|
12
12
|
|
|
13
|
-
from wrapture import binding,
|
|
13
|
+
from wrapture import binding, stack_frames, timeline
|
|
14
14
|
from wrapture.stacks import _stacks
|
|
15
15
|
|
|
16
16
|
|
|
@@ -44,7 +44,7 @@ def test_no_capture_by_default() -> None:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def test_caller_captures_exactly_the_calling_frame() -> None:
|
|
47
|
-
charge = binding(Gateway, "charge", stack=caller)
|
|
47
|
+
charge = binding(Gateway, "charge", stack="caller")
|
|
48
48
|
|
|
49
49
|
with timeline(charge):
|
|
50
50
|
place_order(Gateway())
|
|
@@ -75,7 +75,7 @@ def test_a_frame_count_walks_outward_from_the_caller() -> None:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def test_full_captures_more_than_caller() -> None:
|
|
78
|
-
charge = binding(Gateway, "charge", stack=full)
|
|
78
|
+
charge = binding(Gateway, "charge", stack="full")
|
|
79
79
|
|
|
80
80
|
with timeline(charge):
|
|
81
81
|
place_order(Gateway())
|
|
@@ -92,6 +92,9 @@ def test_an_invalid_depth_is_rejected_at_creation() -> None:
|
|
|
92
92
|
with pytest.raises(ValueError, match="stack must be"):
|
|
93
93
|
binding(Gateway, "charge", stack=0)
|
|
94
94
|
|
|
95
|
+
with pytest.raises(ValueError, match="stack must be"):
|
|
96
|
+
binding(Gateway, "charge", stack="fll")
|
|
97
|
+
|
|
95
98
|
|
|
96
99
|
# ---------------------------------------------------------------------------
|
|
97
100
|
# interning
|
|
@@ -99,7 +102,7 @@ def test_an_invalid_depth_is_rejected_at_creation() -> None:
|
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
def test_repeated_captures_from_one_site_intern_to_one_stack() -> None:
|
|
102
|
-
charge = binding(Gateway, "charge", stack=caller)
|
|
105
|
+
charge = binding(Gateway, "charge", stack="caller")
|
|
103
106
|
|
|
104
107
|
with timeline(charge):
|
|
105
108
|
gateway = Gateway()
|
|
@@ -114,7 +117,7 @@ def test_repeated_captures_from_one_site_intern_to_one_stack() -> None:
|
|
|
114
117
|
|
|
115
118
|
|
|
116
119
|
def test_different_call_sites_intern_to_different_stacks() -> None:
|
|
117
|
-
charge = binding(Gateway, "charge", stack=caller)
|
|
120
|
+
charge = binding(Gateway, "charge", stack="caller")
|
|
118
121
|
|
|
119
122
|
with timeline(charge):
|
|
120
123
|
gateway = Gateway()
|
|
@@ -134,7 +137,7 @@ def test_different_call_sites_intern_to_different_stacks() -> None:
|
|
|
134
137
|
|
|
135
138
|
|
|
136
139
|
def test_an_attribute_read_names_the_line_that_triggered_it() -> None:
|
|
137
|
-
total = binding(Model, "total", stack=caller)
|
|
140
|
+
total = binding(Model, "total", stack="caller")
|
|
138
141
|
|
|
139
142
|
def render(model: Model) -> str:
|
|
140
143
|
return f"total: {model.total}"
|
|
@@ -164,7 +167,7 @@ def test_no_wrapture_or_wrapt_frames_appear() -> None:
|
|
|
164
167
|
os.path.dirname(os.path.abspath(wrapt.__file__)),
|
|
165
168
|
)
|
|
166
169
|
|
|
167
|
-
charge = binding(Gateway, "charge", stack=full)
|
|
170
|
+
charge = binding(Gateway, "charge", stack="full")
|
|
168
171
|
|
|
169
172
|
with timeline(charge):
|
|
170
173
|
place_order(Gateway())
|
|
@@ -180,7 +183,7 @@ def test_capture_works_through_behaviour() -> None:
|
|
|
180
183
|
def note(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
|
181
184
|
return args, kwargs
|
|
182
185
|
|
|
183
|
-
charge = binding(Gateway, "charge", stack=caller).on_call.transforms_args(note)
|
|
186
|
+
charge = binding(Gateway, "charge", stack="caller").on_call.transforms_args(note)
|
|
184
187
|
|
|
185
188
|
with timeline(charge):
|
|
186
189
|
place_order(Gateway())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|