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.
Files changed (41) hide show
  1. {wrapture-1.0.0.dev1/src/wrapture.egg-info → wrapture-1.0.0.dev2}/PKG-INFO +19 -3
  2. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/README.md +18 -2
  3. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/__init__.py +12 -23
  4. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/bindings.py +26 -18
  5. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/capture.py +31 -1
  6. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/iterators.py +12 -12
  7. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/stacks.py +19 -2
  8. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2/src/wrapture.egg-info}/PKG-INFO +19 -3
  9. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_attribute_events.py +2 -2
  10. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_capture.py +25 -10
  11. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_events.py +9 -0
  12. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_stacks.py +11 -8
  13. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/LICENSE +0 -0
  14. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/pyproject.toml +0 -0
  15. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/setup.cfg +0 -0
  16. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/attributes.py +0 -0
  17. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/behaviours.py +0 -0
  18. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/eventlogs.py +0 -0
  19. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/events.py +0 -0
  20. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/exceptions.py +0 -0
  21. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/py.typed +0 -0
  22. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/pytest_plugin.py +0 -0
  23. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture/timeline.py +0 -0
  24. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/SOURCES.txt +0 -0
  25. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/dependency_links.txt +0 -0
  26. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/not-zip-safe +0 -0
  27. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/requires.txt +0 -0
  28. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/src/wrapture.egg-info/top_level.txt +0 -0
  29. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_attributes.py +0 -0
  30. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_behaviours.py +0 -0
  31. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_bindings.py +0 -0
  32. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_eventlogs.py +0 -0
  33. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_generator_events.py +0 -0
  34. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_iterators.py +0 -0
  35. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_pytest_plugin.py +0 -0
  36. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_recording.py +0 -0
  37. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_targets.py +0 -0
  38. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_testing_patterns.py +0 -0
  39. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_threads.py +0 -0
  40. {wrapture-1.0.0.dev1 → wrapture-1.0.0.dev2}/tests/test_timeline.py +0 -0
  41. {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.dev1
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. Nothing is published to PyPI yet.
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 [LICENSE](LICENSE).
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. Nothing is published to PyPI yet.
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 [LICENSE](LICENSE).
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", "dev1")
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
- AbandonBehaviour,
60
- ErrorBehaviour,
61
- FinishBehaviour,
62
- ItemBehaviour,
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
- "NONE",
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
- "ItemBehaviour",
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, wrapture.caller, wrapture.full or a"
372
- f" positive frame count, got {stack!r}"
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 = capture_args if capture_args is not None else capture
394
- self._capture_result = capture_result if capture_result is not None else capture
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 (a level such as SUMMARY, or a fn(name, value) callable),
1075
- with `capture_args=` and `capture_result=` controlling the two axes
1076
- separately and winning over the shorthand. Left unset, the binding
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
- wrapture.caller for just the calling frame, a frame count, or
1081
- wrapture.full for the whole stack. The default None captures
1082
- nothing and costs 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, level: CapturePolicy = REFERENCE, marker: str = "<redacted>"
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 ItemBehaviour(_IteratorBehaviour):
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 FinishBehaviour(_IteratorBehaviour):
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 ErrorBehaviour(_IteratorBehaviour):
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 AbandonBehaviour(_IteratorBehaviour):
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) -> ItemBehaviour:
346
+ def on_item(self) -> IteratorItemBehaviour:
347
347
  """The behaviour namespace for items."""
348
348
 
349
- return ItemBehaviour(self)
349
+ return IteratorItemBehaviour(self)
350
350
 
351
351
  @property
352
- def on_finish(self) -> FinishBehaviour:
352
+ def on_finish(self) -> IteratorFinishBehaviour:
353
353
  """The behaviour namespace for normal exhaustion."""
354
354
 
355
- return FinishBehaviour(self)
355
+ return IteratorFinishBehaviour(self)
356
356
 
357
357
  @property
358
- def on_error(self) -> ErrorBehaviour:
358
+ def on_error(self) -> IteratorErrorBehaviour:
359
359
  """The behaviour namespace for a failed iteration."""
360
360
 
361
- return ErrorBehaviour(self)
361
+ return IteratorErrorBehaviour(self)
362
362
 
363
363
  @property
364
- def on_abandon(self) -> AbandonBehaviour:
364
+ def on_abandon(self) -> IteratorAbandonBehaviour:
365
365
  """The behaviour namespace for an abandoned iteration."""
366
366
 
367
- return AbandonBehaviour(self)
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(): capture just the calling frame, or
38
- # every frame. Any other positive integer captures that many frames.
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.dev1
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. Nothing is published to PyPI yet.
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 [LICENSE](LICENSE).
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 SUMMARY, annotate, binding, redact, timeline
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=SUMMARY)
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=SUMMARY)
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=TYPES)
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=NONE)
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=SNAPSHOT)
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=NONE, capture_result=SUMMARY)
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, caller, full, stack_frames, timeline
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