wrapture 1.0.0.dev1__py3-none-any.whl
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/__init__.py +119 -0
- wrapture/attributes.py +344 -0
- wrapture/behaviours.py +428 -0
- wrapture/bindings.py +1110 -0
- wrapture/capture.py +165 -0
- wrapture/eventlogs.py +232 -0
- wrapture/events.py +201 -0
- wrapture/exceptions.py +45 -0
- wrapture/iterators.py +451 -0
- wrapture/py.typed +0 -0
- wrapture/pytest_plugin.py +119 -0
- wrapture/stacks.py +93 -0
- wrapture/timeline.py +364 -0
- wrapture-1.0.0.dev1.dist-info/METADATA +155 -0
- wrapture-1.0.0.dev1.dist-info/RECORD +18 -0
- wrapture-1.0.0.dev1.dist-info/WHEEL +5 -0
- wrapture-1.0.0.dev1.dist-info/licenses/LICENSE +24 -0
- wrapture-1.0.0.dev1.dist-info/top_level.txt +1 -0
wrapture/__init__.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wrapture is a library for attaching bindings to arbitrary Python call sites,
|
|
3
|
+
without modifying the code being observed, for use in monkey patching,
|
|
4
|
+
testing, tracing and profiling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _format_version(parts: tuple[str, ...]) -> str:
|
|
9
|
+
base = ".".join(parts[:3])
|
|
10
|
+
|
|
11
|
+
if len(parts) == 3:
|
|
12
|
+
return base
|
|
13
|
+
|
|
14
|
+
suffix = parts[3]
|
|
15
|
+
return (
|
|
16
|
+
f"{base}.{suffix}" if suffix.startswith(("dev", "post")) else f"{base}{suffix}"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__version_info__ = ("1", "0", "0", "dev1")
|
|
21
|
+
__version__ = _format_version(__version_info__)
|
|
22
|
+
|
|
23
|
+
from .behaviours import (
|
|
24
|
+
CallBehaviour,
|
|
25
|
+
DeleteBehaviour,
|
|
26
|
+
GetBehaviour,
|
|
27
|
+
SetBehaviour,
|
|
28
|
+
)
|
|
29
|
+
from .bindings import (
|
|
30
|
+
Binding,
|
|
31
|
+
BindingGroup,
|
|
32
|
+
binding,
|
|
33
|
+
bindings,
|
|
34
|
+
)
|
|
35
|
+
from .capture import (
|
|
36
|
+
NONE,
|
|
37
|
+
REFERENCE,
|
|
38
|
+
SNAPSHOT,
|
|
39
|
+
SUMMARY,
|
|
40
|
+
TYPES,
|
|
41
|
+
redact,
|
|
42
|
+
)
|
|
43
|
+
from .eventlogs import (
|
|
44
|
+
EventLog,
|
|
45
|
+
)
|
|
46
|
+
from .events import (
|
|
47
|
+
Event,
|
|
48
|
+
)
|
|
49
|
+
from .exceptions import (
|
|
50
|
+
AlreadyAppliedError,
|
|
51
|
+
DeferredTargetError,
|
|
52
|
+
ExpectationNotMetError,
|
|
53
|
+
NeverAppliedError,
|
|
54
|
+
NotImplementedYetError,
|
|
55
|
+
RecordingGapWarning,
|
|
56
|
+
WrongModeError,
|
|
57
|
+
)
|
|
58
|
+
from .iterators import (
|
|
59
|
+
AbandonBehaviour,
|
|
60
|
+
ErrorBehaviour,
|
|
61
|
+
FinishBehaviour,
|
|
62
|
+
ItemBehaviour,
|
|
63
|
+
IteratorProxy,
|
|
64
|
+
iterator,
|
|
65
|
+
)
|
|
66
|
+
from .stacks import (
|
|
67
|
+
StackFrame,
|
|
68
|
+
caller,
|
|
69
|
+
full,
|
|
70
|
+
stack_frames,
|
|
71
|
+
)
|
|
72
|
+
from .timeline import (
|
|
73
|
+
Tape,
|
|
74
|
+
Timeline,
|
|
75
|
+
annotate,
|
|
76
|
+
current_event,
|
|
77
|
+
timeline,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"NONE",
|
|
82
|
+
"REFERENCE",
|
|
83
|
+
"SNAPSHOT",
|
|
84
|
+
"SUMMARY",
|
|
85
|
+
"TYPES",
|
|
86
|
+
"AbandonBehaviour",
|
|
87
|
+
"AlreadyAppliedError",
|
|
88
|
+
"Binding",
|
|
89
|
+
"BindingGroup",
|
|
90
|
+
"CallBehaviour",
|
|
91
|
+
"DeferredTargetError",
|
|
92
|
+
"DeleteBehaviour",
|
|
93
|
+
"ErrorBehaviour",
|
|
94
|
+
"Event",
|
|
95
|
+
"EventLog",
|
|
96
|
+
"ExpectationNotMetError",
|
|
97
|
+
"FinishBehaviour",
|
|
98
|
+
"GetBehaviour",
|
|
99
|
+
"ItemBehaviour",
|
|
100
|
+
"IteratorProxy",
|
|
101
|
+
"NeverAppliedError",
|
|
102
|
+
"NotImplementedYetError",
|
|
103
|
+
"RecordingGapWarning",
|
|
104
|
+
"SetBehaviour",
|
|
105
|
+
"StackFrame",
|
|
106
|
+
"Tape",
|
|
107
|
+
"Timeline",
|
|
108
|
+
"WrongModeError",
|
|
109
|
+
"annotate",
|
|
110
|
+
"binding",
|
|
111
|
+
"bindings",
|
|
112
|
+
"caller",
|
|
113
|
+
"current_event",
|
|
114
|
+
"full",
|
|
115
|
+
"iterator",
|
|
116
|
+
"redact",
|
|
117
|
+
"stack_frames",
|
|
118
|
+
"timeline",
|
|
119
|
+
]
|
wrapture/attributes.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""The descriptor behind attribute-mode bindings.
|
|
2
|
+
|
|
3
|
+
An attribute binding installs a data descriptor on the class, wrapping
|
|
4
|
+
whatever previously occupied the class attribute: another descriptor such
|
|
5
|
+
as a property, a plain class default, or wrapt's MISSING sentinel when
|
|
6
|
+
nothing was defined. Reads, writes and deletes hook the binding's
|
|
7
|
+
behaviour first and then perform the real operation, honouring a prior
|
|
8
|
+
descriptor's own logic beneath the interception.
|
|
9
|
+
|
|
10
|
+
The descriptor derives from wrapt's BaseObjectProxy and wraps the prior
|
|
11
|
+
definition, so wrapt's unwrap_object() can traverse and splice the
|
|
12
|
+
wrapper chain, and two attribute bindings on one name compose rather
|
|
13
|
+
than clobber. The read precedence and the write and delete delegation
|
|
14
|
+
mirror wrapt's own AttributeWrapper, which only hooks reads.
|
|
15
|
+
|
|
16
|
+
Inside a timeline, each operation additionally records an event of kind
|
|
17
|
+
"get", "set" or "delete" onto the ambient tape, mirroring the callable
|
|
18
|
+
mode's recording path.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import inspect
|
|
24
|
+
import sys
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from typing import TYPE_CHECKING, Any
|
|
27
|
+
|
|
28
|
+
import wrapt
|
|
29
|
+
from wrapt import MISSING, BaseObjectProxy, apply_patch
|
|
30
|
+
|
|
31
|
+
from .capture import REFERENCE, _capture_value, _level_of
|
|
32
|
+
from .events import Event, EventKind
|
|
33
|
+
from .exceptions import NotImplementedYetError
|
|
34
|
+
from .stacks import _capture as _capture_stack
|
|
35
|
+
from .timeline import (
|
|
36
|
+
_capture_result,
|
|
37
|
+
_in_recorder,
|
|
38
|
+
_pop,
|
|
39
|
+
_push,
|
|
40
|
+
_tape,
|
|
41
|
+
_timelines_active,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from .bindings import Binding
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read(prior: Any, attribute: str, instance: Any, owner: Any) -> Any:
|
|
49
|
+
"""Perform the real read, as the unwrapped attribute would.
|
|
50
|
+
|
|
51
|
+
Standard lookup precedence applies: a data descriptor prior takes
|
|
52
|
+
precedence over the instance dictionary, a non-data descriptor prior
|
|
53
|
+
yields to it, and a plain class default is the fallback when no
|
|
54
|
+
instance value exists. Only when the prior is the MISSING sentinel,
|
|
55
|
+
meaning no definition of any sort existed, is AttributeError raised.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
prior_type = type(prior)
|
|
59
|
+
|
|
60
|
+
if hasattr(prior_type, "__get__") and (
|
|
61
|
+
hasattr(prior_type, "__set__") or hasattr(prior_type, "__delete__")
|
|
62
|
+
):
|
|
63
|
+
return prior.__get__(instance, owner)
|
|
64
|
+
|
|
65
|
+
if attribute in instance.__dict__:
|
|
66
|
+
return instance.__dict__[attribute]
|
|
67
|
+
|
|
68
|
+
if hasattr(prior_type, "__get__"):
|
|
69
|
+
return prior.__get__(instance, owner)
|
|
70
|
+
|
|
71
|
+
if prior is not MISSING:
|
|
72
|
+
return prior
|
|
73
|
+
|
|
74
|
+
raise AttributeError(
|
|
75
|
+
f"{type(instance).__name__!r} object has no attribute {attribute!r}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _write(prior: Any, attribute: str, instance: Any, value: Any) -> None:
|
|
80
|
+
"""Perform the real write, delegating to a prior descriptor which
|
|
81
|
+
implements __set__ so its validation and storage are honoured, and
|
|
82
|
+
otherwise storing into the instance dictionary."""
|
|
83
|
+
|
|
84
|
+
if hasattr(type(prior), "__set__"):
|
|
85
|
+
prior.__set__(instance, value)
|
|
86
|
+
else:
|
|
87
|
+
instance.__dict__[attribute] = value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _delete(prior: Any, attribute: str, instance: Any) -> None:
|
|
91
|
+
"""Perform the real delete, delegating to a prior descriptor which
|
|
92
|
+
implements __delete__, and otherwise removing from the instance
|
|
93
|
+
dictionary, raising AttributeError rather than KeyError when there is
|
|
94
|
+
nothing to remove."""
|
|
95
|
+
|
|
96
|
+
if hasattr(type(prior), "__delete__"):
|
|
97
|
+
prior.__delete__(instance)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
del instance.__dict__[attribute]
|
|
102
|
+
except KeyError:
|
|
103
|
+
raise AttributeError(
|
|
104
|
+
f"{type(instance).__name__!r} object has no attribute {attribute!r}"
|
|
105
|
+
) from None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _record(
|
|
109
|
+
binding: Binding,
|
|
110
|
+
kind: EventKind,
|
|
111
|
+
instance: Any,
|
|
112
|
+
attribute: str,
|
|
113
|
+
operate: Callable[[], Any],
|
|
114
|
+
value: Any = MISSING,
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""Run one attribute operation, recording it onto the ambient tape.
|
|
117
|
+
|
|
118
|
+
Mirrors the callable-mode recording path: no ambient tape (or the
|
|
119
|
+
recorder's own reentrancy guard) means the operation just runs, and
|
|
120
|
+
otherwise an event is recorded around it, with the operation pushed
|
|
121
|
+
on the in-progress stack so anything it triggers nests under it.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
tape = _tape.get()
|
|
125
|
+
if tape is None or _in_recorder.get():
|
|
126
|
+
if tape is None and not _in_recorder.get() and _timelines_active():
|
|
127
|
+
binding._note_missed_call()
|
|
128
|
+
|
|
129
|
+
return operate()
|
|
130
|
+
|
|
131
|
+
# The written value and the prior value are inbound data, so they
|
|
132
|
+
# capture on the arguments axis, under the attribute's name so a
|
|
133
|
+
# by-name policy such as redact() applies to writes too.
|
|
134
|
+
|
|
135
|
+
policy = binding._capture_args
|
|
136
|
+
if policy is None:
|
|
137
|
+
policy = getattr(tape, "capture_args", REFERENCE)
|
|
138
|
+
|
|
139
|
+
guard = _in_recorder.set(True)
|
|
140
|
+
try:
|
|
141
|
+
event = Event(
|
|
142
|
+
kind,
|
|
143
|
+
binding._path,
|
|
144
|
+
label=binding._label,
|
|
145
|
+
instance=instance,
|
|
146
|
+
binding=binding,
|
|
147
|
+
capture=_level_of(policy),
|
|
148
|
+
injected=binding._injects.get(kind, False),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if binding._stack_depth is not None:
|
|
152
|
+
event.stack = _capture_stack(binding._stack_depth)
|
|
153
|
+
|
|
154
|
+
if value is not MISSING:
|
|
155
|
+
event.value = _capture_value(policy, attribute, value)
|
|
156
|
+
|
|
157
|
+
# The prior value, when cheaply available: only what already
|
|
158
|
+
# sits in the instance dictionary. A prior held by a descriptor
|
|
159
|
+
# would take running user code to read, so it is not recorded.
|
|
160
|
+
|
|
161
|
+
if kind in ("set", "delete"):
|
|
162
|
+
previous = getattr(instance, "__dict__", {}).get(attribute, MISSING)
|
|
163
|
+
if previous is not MISSING:
|
|
164
|
+
event.previous = _capture_value(policy, attribute, previous)
|
|
165
|
+
|
|
166
|
+
tape.record(event)
|
|
167
|
+
finally:
|
|
168
|
+
_in_recorder.reset(guard)
|
|
169
|
+
|
|
170
|
+
token = _push(event)
|
|
171
|
+
try:
|
|
172
|
+
outcome = operate()
|
|
173
|
+
except BaseException as exc:
|
|
174
|
+
event.exception = exc
|
|
175
|
+
raise
|
|
176
|
+
finally:
|
|
177
|
+
_pop(token)
|
|
178
|
+
|
|
179
|
+
# The value a read produced is its outcome, so it captures on the
|
|
180
|
+
# result axis, exactly as a call's return value does.
|
|
181
|
+
|
|
182
|
+
if kind == "get":
|
|
183
|
+
result_policy = binding._capture_result
|
|
184
|
+
if result_policy is None:
|
|
185
|
+
result_policy = getattr(tape, "capture_result", REFERENCE)
|
|
186
|
+
_capture_result(event, outcome, result_policy)
|
|
187
|
+
|
|
188
|
+
return outcome
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AttributeDescriptor(BaseObjectProxy[Any]):
|
|
192
|
+
"""The data descriptor an attribute binding installs on the class.
|
|
193
|
+
|
|
194
|
+
Wraps the prior definition of the attribute, or MISSING when there
|
|
195
|
+
was none. Each operation consults the owning binding: suspended means
|
|
196
|
+
the operation passes straight through, and otherwise the binding's
|
|
197
|
+
behaviour pipeline for the operation runs around the real operation.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, prior: Any, attribute: str, binding: Binding) -> None:
|
|
201
|
+
super().__init__(prior)
|
|
202
|
+
self._self_attribute = attribute
|
|
203
|
+
self._self_wrapture_binding = binding
|
|
204
|
+
|
|
205
|
+
def __get__(self, instance: Any, owner: Any = None) -> Any:
|
|
206
|
+
# Class-level access returns the descriptor itself. Being a
|
|
207
|
+
# transparent proxy, introspection of the prior definition then
|
|
208
|
+
# works through delegation.
|
|
209
|
+
|
|
210
|
+
if instance is None:
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
binding = self._self_wrapture_binding
|
|
214
|
+
prior = self.__wrapped__
|
|
215
|
+
attribute = self._self_attribute
|
|
216
|
+
|
|
217
|
+
if binding._suspended:
|
|
218
|
+
binding._suspended_calls += 1
|
|
219
|
+
return _read(prior, attribute, instance, owner)
|
|
220
|
+
|
|
221
|
+
behaviour = binding._behaviour("get")
|
|
222
|
+
|
|
223
|
+
def read() -> Any:
|
|
224
|
+
return _read(prior, attribute, instance, owner)
|
|
225
|
+
|
|
226
|
+
def operate() -> Any:
|
|
227
|
+
if behaviour is None:
|
|
228
|
+
return read()
|
|
229
|
+
return behaviour(read, instance, (), {})
|
|
230
|
+
|
|
231
|
+
return _record(binding, "get", instance, attribute, operate)
|
|
232
|
+
|
|
233
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
234
|
+
binding = self._self_wrapture_binding
|
|
235
|
+
prior = self.__wrapped__
|
|
236
|
+
attribute = self._self_attribute
|
|
237
|
+
|
|
238
|
+
if binding._suspended:
|
|
239
|
+
binding._suspended_calls += 1
|
|
240
|
+
_write(prior, attribute, instance, value)
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
behaviour = binding._behaviour("set")
|
|
244
|
+
|
|
245
|
+
def write(new_value: Any) -> None:
|
|
246
|
+
_write(prior, attribute, instance, new_value)
|
|
247
|
+
|
|
248
|
+
def operate() -> Any:
|
|
249
|
+
if behaviour is None:
|
|
250
|
+
write(value)
|
|
251
|
+
return None
|
|
252
|
+
return behaviour(write, instance, (value,), {})
|
|
253
|
+
|
|
254
|
+
_record(binding, "set", instance, attribute, operate, value=value)
|
|
255
|
+
|
|
256
|
+
def __delete__(self, instance: Any) -> None:
|
|
257
|
+
binding = self._self_wrapture_binding
|
|
258
|
+
prior = self.__wrapped__
|
|
259
|
+
attribute = self._self_attribute
|
|
260
|
+
|
|
261
|
+
if binding._suspended:
|
|
262
|
+
binding._suspended_calls += 1
|
|
263
|
+
_delete(prior, attribute, instance)
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
behaviour = binding._behaviour("delete")
|
|
267
|
+
|
|
268
|
+
def erase() -> None:
|
|
269
|
+
_delete(prior, attribute, instance)
|
|
270
|
+
|
|
271
|
+
def operate() -> Any:
|
|
272
|
+
if behaviour is None:
|
|
273
|
+
erase()
|
|
274
|
+
return None
|
|
275
|
+
return behaviour(erase, instance, (), {})
|
|
276
|
+
|
|
277
|
+
_record(binding, "delete", instance, attribute, operate)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _resolve_parent(target: Any, name: str) -> tuple[Any, str]:
|
|
281
|
+
"""Resolve the object holding the final attribute of a dotted name."""
|
|
282
|
+
|
|
283
|
+
if "." in name:
|
|
284
|
+
prefix, attribute = name.rsplit(".", 1)
|
|
285
|
+
parent = wrapt.resolve_path(target, prefix)[2]
|
|
286
|
+
return parent, attribute
|
|
287
|
+
|
|
288
|
+
if isinstance(target, str):
|
|
289
|
+
__import__(target)
|
|
290
|
+
return sys.modules[target], name
|
|
291
|
+
|
|
292
|
+
return target, name
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def install(binding: Binding, target: Any, name: str) -> AttributeDescriptor:
|
|
296
|
+
"""Install an AttributeDescriptor for a binding, returning the handle.
|
|
297
|
+
|
|
298
|
+
The descriptor is installed on the class the name resolves to, with
|
|
299
|
+
the prior definition found through the MRO so an inherited default
|
|
300
|
+
keeps working beneath the interception. Whether installation created
|
|
301
|
+
the attribute slot is recorded on the descriptor the same way
|
|
302
|
+
wrapt.wrap_object() records it, so wrapt.unwrap_object() removes the
|
|
303
|
+
slot rather than leaving a shadowing copy where appropriate.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
parent, attribute = _resolve_parent(target, name)
|
|
307
|
+
|
|
308
|
+
if inspect.ismodule(parent):
|
|
309
|
+
raise NotImplementedYetError(
|
|
310
|
+
f"{binding._label}: attribute bindings on a module are not"
|
|
311
|
+
f" supported; module attribute access does not go through"
|
|
312
|
+
f" class descriptors"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if not inspect.isclass(parent):
|
|
316
|
+
raise TypeError(
|
|
317
|
+
f"{binding._label}: an attribute binding installs a descriptor"
|
|
318
|
+
f" on the class, so the target must be a class, not an instance"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
prior: Any = MISSING
|
|
322
|
+
for cls in inspect.getmro(parent):
|
|
323
|
+
if attribute in vars(cls):
|
|
324
|
+
prior = vars(cls)[attribute]
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
# An absent attribute is only bindable when the binding was created
|
|
328
|
+
# with missing_ok=True; with detection skipped by an explicit mode=,
|
|
329
|
+
# this is where a misspelled name surfaces.
|
|
330
|
+
|
|
331
|
+
if prior is MISSING and not binding._missing_ok:
|
|
332
|
+
raise AttributeError(
|
|
333
|
+
f"{binding._label}: attribute {attribute!r} is not defined on"
|
|
334
|
+
f" {parent.__name__!r}; pass missing_ok=True to bind a name"
|
|
335
|
+
f" that is assigned only on instances"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
created = attribute not in vars(parent)
|
|
339
|
+
|
|
340
|
+
descriptor = AttributeDescriptor(prior, attribute, binding)
|
|
341
|
+
descriptor.__self_setattr__("__wrapt_wrap_object_created_slot__", created)
|
|
342
|
+
apply_patch(parent, attribute, descriptor)
|
|
343
|
+
|
|
344
|
+
return descriptor
|