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 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