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/behaviours.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Behaviour namespaces for bindings.
|
|
2
|
+
|
|
3
|
+
Behaviour is what a binding does to the operation it intercepts:
|
|
4
|
+
substitute a result, raise an exception, transform arguments or results,
|
|
5
|
+
validate them in flight, or wrap the whole operation with a decorator.
|
|
6
|
+
|
|
7
|
+
Behaviour is scoped by operation. A callable binding exposes on_call; an
|
|
8
|
+
attribute binding exposes on_get, on_set and on_delete. Configured
|
|
9
|
+
behaviour forms a pipeline per operation: composing stages (transforms_*
|
|
10
|
+
and validates_*) wrap around what follows and accumulate in the order
|
|
11
|
+
added, while a terminal (returns / raises / decorates / rejects) decides
|
|
12
|
+
what happens at the centre and replaces any previous terminal.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import inspect
|
|
18
|
+
from collections.abc import Callable, Sequence
|
|
19
|
+
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .bindings import Binding
|
|
23
|
+
|
|
24
|
+
# The signature wrapt uses for wrappers and decorators:
|
|
25
|
+
# fn(wrapped, instance, args, kwargs).
|
|
26
|
+
|
|
27
|
+
WrappedFunction = Callable[..., Any]
|
|
28
|
+
WrapperFunction = Callable[[WrappedFunction, Any, tuple[Any, ...], dict[str, Any]], Any]
|
|
29
|
+
|
|
30
|
+
# A composing stage has the same shape as a wrapper, except that its first
|
|
31
|
+
# argument is a forward() callable that invokes the rest of the pipeline.
|
|
32
|
+
|
|
33
|
+
StageFunction = WrapperFunction
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _then(outcome: Any, fn: Callable[[Any], Any]) -> Any:
|
|
37
|
+
"""Apply `fn` to `outcome`, awaiting first if it is awaitable.
|
|
38
|
+
|
|
39
|
+
Result-side pipeline stages use this so that on an async target the
|
|
40
|
+
stage applies to the awaited value rather than to the coroutine.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if inspect.isawaitable(outcome):
|
|
44
|
+
|
|
45
|
+
async def resolve() -> Any:
|
|
46
|
+
return fn(await outcome)
|
|
47
|
+
|
|
48
|
+
return resolve()
|
|
49
|
+
return fn(outcome)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _compose(
|
|
53
|
+
pipeline: Sequence[StageFunction], terminal: WrapperFunction | None
|
|
54
|
+
) -> WrapperFunction:
|
|
55
|
+
"""Build one callable(wrapped, instance, args, kwargs) from the stages.
|
|
56
|
+
|
|
57
|
+
Each composing stage is called as stage(forward, instance, args, kwargs)
|
|
58
|
+
where forward(*args, **kwargs) invokes the rest of the chain, so a stage
|
|
59
|
+
can alter what goes in, what comes back, or both. The first stage added
|
|
60
|
+
is outermost. A terminal of None means "perform the real operation".
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def innermost(
|
|
64
|
+
wrapped: WrappedFunction,
|
|
65
|
+
instance: Any,
|
|
66
|
+
args: tuple[Any, ...],
|
|
67
|
+
kwargs: dict[str, Any],
|
|
68
|
+
) -> Any:
|
|
69
|
+
if terminal is None:
|
|
70
|
+
return wrapped(*args, **kwargs)
|
|
71
|
+
return terminal(wrapped, instance, args, kwargs)
|
|
72
|
+
|
|
73
|
+
chain: WrapperFunction = innermost
|
|
74
|
+
for stage in reversed(pipeline):
|
|
75
|
+
chain = _stage_wrapper(stage, chain)
|
|
76
|
+
|
|
77
|
+
return chain
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _stage_wrapper(stage: StageFunction, nxt: WrapperFunction) -> WrapperFunction:
|
|
81
|
+
def call(
|
|
82
|
+
wrapped: WrappedFunction,
|
|
83
|
+
instance: Any,
|
|
84
|
+
args: tuple[Any, ...],
|
|
85
|
+
kwargs: dict[str, Any],
|
|
86
|
+
) -> Any:
|
|
87
|
+
def forward(*a: Any, **k: Any) -> Any:
|
|
88
|
+
return nxt(wrapped, instance, a, k)
|
|
89
|
+
|
|
90
|
+
return stage(forward, instance, args, kwargs)
|
|
91
|
+
|
|
92
|
+
return call
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _Behaviour:
|
|
96
|
+
"""Base for the per-operation behaviour namespaces."""
|
|
97
|
+
|
|
98
|
+
__slots__ = ("_binding",)
|
|
99
|
+
|
|
100
|
+
_operation: ClassVar[str]
|
|
101
|
+
|
|
102
|
+
def __init__(self, bnd: Binding) -> None:
|
|
103
|
+
self._binding = bnd
|
|
104
|
+
|
|
105
|
+
def _terminal(self, fn: WrapperFunction, *, injected: bool = False) -> Binding:
|
|
106
|
+
self._binding._set_terminal(self._operation, fn, injected=injected)
|
|
107
|
+
return self._binding
|
|
108
|
+
|
|
109
|
+
def _stage(self, fn: StageFunction) -> Binding:
|
|
110
|
+
self._binding._add_stage(self._operation, fn)
|
|
111
|
+
return self._binding
|
|
112
|
+
|
|
113
|
+
def raises(self, exc: BaseException | type[BaseException]) -> Binding:
|
|
114
|
+
"""Raise `exc` instead of performing the operation. Terminal."""
|
|
115
|
+
|
|
116
|
+
def boom(
|
|
117
|
+
nxt: WrappedFunction,
|
|
118
|
+
instance: Any,
|
|
119
|
+
args: tuple[Any, ...],
|
|
120
|
+
kwargs: dict[str, Any],
|
|
121
|
+
) -> NoReturn:
|
|
122
|
+
raise exc
|
|
123
|
+
|
|
124
|
+
return self._terminal(boom, injected=True)
|
|
125
|
+
|
|
126
|
+
def passes_through(self) -> Binding:
|
|
127
|
+
"""Drop all configured behaviour for this operation: both the
|
|
128
|
+
terminal and every composing stage."""
|
|
129
|
+
|
|
130
|
+
self._binding._clear_behaviour(self._operation)
|
|
131
|
+
return self._binding
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CallBehaviour(_Behaviour):
|
|
135
|
+
"""`binding.on_call`: behaviour for calls to a wrapped callable."""
|
|
136
|
+
|
|
137
|
+
__slots__ = ()
|
|
138
|
+
|
|
139
|
+
_operation = "call"
|
|
140
|
+
|
|
141
|
+
# -- terminal ---------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def returns(self, value: Any) -> Binding:
|
|
144
|
+
"""Return `value`; the real callable is never invoked. Terminal."""
|
|
145
|
+
|
|
146
|
+
return self._terminal(lambda nxt, i, a, k: value, injected=True)
|
|
147
|
+
|
|
148
|
+
def decorates(self, fn: WrapperFunction) -> Binding:
|
|
149
|
+
"""Wrap the real callable: fn(wrapped, instance, args, kwargs).
|
|
150
|
+
|
|
151
|
+
This is wrapt's own wrapper signature, so the function you would
|
|
152
|
+
apply @wrapt.decorator to moves here unedited; pass that
|
|
153
|
+
function, not the result of decorating it. Terminal: it decides
|
|
154
|
+
whether and how the real callable is invoked.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
return self._terminal(fn)
|
|
158
|
+
|
|
159
|
+
# -- composing --------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def transforms_args(
|
|
162
|
+
self,
|
|
163
|
+
fn: Callable[
|
|
164
|
+
[tuple[Any, ...], dict[str, Any]],
|
|
165
|
+
tuple[tuple[Any, ...], dict[str, Any]],
|
|
166
|
+
],
|
|
167
|
+
) -> Binding:
|
|
168
|
+
"""fn(args, kwargs) -> (args, kwargs), rewriting the inbound call."""
|
|
169
|
+
|
|
170
|
+
def stage(
|
|
171
|
+
nxt: WrappedFunction,
|
|
172
|
+
instance: Any,
|
|
173
|
+
args: tuple[Any, ...],
|
|
174
|
+
kwargs: dict[str, Any],
|
|
175
|
+
) -> Any:
|
|
176
|
+
new_args, new_kwargs = fn(args, kwargs)
|
|
177
|
+
return nxt(*new_args, **new_kwargs)
|
|
178
|
+
|
|
179
|
+
return self._stage(stage)
|
|
180
|
+
|
|
181
|
+
def transforms_result(self, fn: Callable[[Any], Any]) -> Binding:
|
|
182
|
+
"""fn(result) -> result, rewriting what came back.
|
|
183
|
+
|
|
184
|
+
Await-aware: when the target is async, the transform is applied to
|
|
185
|
+
the awaited value rather than to the coroutine.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def stage(
|
|
189
|
+
nxt: WrappedFunction,
|
|
190
|
+
instance: Any,
|
|
191
|
+
args: tuple[Any, ...],
|
|
192
|
+
kwargs: dict[str, Any],
|
|
193
|
+
) -> Any:
|
|
194
|
+
return _then(nxt(*args, **kwargs), fn)
|
|
195
|
+
|
|
196
|
+
return self._stage(stage)
|
|
197
|
+
|
|
198
|
+
def validates_args(self, check: Callable[..., Any]) -> Binding:
|
|
199
|
+
"""check(*args, **kwargs); the call passes through unchanged.
|
|
200
|
+
|
|
201
|
+
The check fails the call only by raising; its return value is
|
|
202
|
+
ignored, so returning False fails nothing.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def stage(
|
|
206
|
+
nxt: WrappedFunction,
|
|
207
|
+
instance: Any,
|
|
208
|
+
args: tuple[Any, ...],
|
|
209
|
+
kwargs: dict[str, Any],
|
|
210
|
+
) -> Any:
|
|
211
|
+
check(*args, **kwargs)
|
|
212
|
+
return nxt(*args, **kwargs)
|
|
213
|
+
|
|
214
|
+
return self._stage(stage)
|
|
215
|
+
|
|
216
|
+
def validates_result(self, check: Callable[[Any], Any]) -> Binding:
|
|
217
|
+
"""check(result); the result passes through unchanged.
|
|
218
|
+
|
|
219
|
+
The check fails the call only by raising; its return value is
|
|
220
|
+
ignored. Await-aware: when the target is async, the check sees
|
|
221
|
+
the awaited value rather than the coroutine.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def stage(
|
|
225
|
+
nxt: WrappedFunction,
|
|
226
|
+
instance: Any,
|
|
227
|
+
args: tuple[Any, ...],
|
|
228
|
+
kwargs: dict[str, Any],
|
|
229
|
+
) -> Any:
|
|
230
|
+
def verify(result: Any) -> Any:
|
|
231
|
+
check(result)
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
return _then(nxt(*args, **kwargs), verify)
|
|
235
|
+
|
|
236
|
+
return self._stage(stage)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class GetBehaviour(_Behaviour):
|
|
240
|
+
"""`binding.on_get`: behaviour for attribute reads.
|
|
241
|
+
|
|
242
|
+
The real read is a zero-argument operation producing the value.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
__slots__ = ()
|
|
246
|
+
|
|
247
|
+
_operation = "get"
|
|
248
|
+
|
|
249
|
+
def returns(self, value: Any) -> Binding:
|
|
250
|
+
"""Reading gives `value`; the real read never happens. Terminal."""
|
|
251
|
+
|
|
252
|
+
return self._terminal(lambda nxt, i, a, k: value, injected=True)
|
|
253
|
+
|
|
254
|
+
def decorates(self, fn: Callable[[Callable[[], Any], Any], Any]) -> Binding:
|
|
255
|
+
"""Wrap the real read: fn(read, instance) -> value, where read()
|
|
256
|
+
performs the read. Terminal."""
|
|
257
|
+
|
|
258
|
+
def terminal(
|
|
259
|
+
wrapped: WrappedFunction,
|
|
260
|
+
instance: Any,
|
|
261
|
+
args: tuple[Any, ...],
|
|
262
|
+
kwargs: dict[str, Any],
|
|
263
|
+
) -> Any:
|
|
264
|
+
return fn(wrapped, instance)
|
|
265
|
+
|
|
266
|
+
return self._terminal(terminal)
|
|
267
|
+
|
|
268
|
+
def transforms(self, fn: Callable[[Any], Any]) -> Binding:
|
|
269
|
+
"""fn(value) -> value, rewriting the value read."""
|
|
270
|
+
|
|
271
|
+
def stage(
|
|
272
|
+
nxt: WrappedFunction,
|
|
273
|
+
instance: Any,
|
|
274
|
+
args: tuple[Any, ...],
|
|
275
|
+
kwargs: dict[str, Any],
|
|
276
|
+
) -> Any:
|
|
277
|
+
return fn(nxt())
|
|
278
|
+
|
|
279
|
+
return self._stage(stage)
|
|
280
|
+
|
|
281
|
+
def validates(self, check: Callable[[Any], Any]) -> Binding:
|
|
282
|
+
"""check(value); the read passes through unchanged.
|
|
283
|
+
|
|
284
|
+
The check fails the read only by raising; its return value is
|
|
285
|
+
ignored.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def stage(
|
|
289
|
+
nxt: WrappedFunction,
|
|
290
|
+
instance: Any,
|
|
291
|
+
args: tuple[Any, ...],
|
|
292
|
+
kwargs: dict[str, Any],
|
|
293
|
+
) -> Any:
|
|
294
|
+
value = nxt()
|
|
295
|
+
check(value)
|
|
296
|
+
return value
|
|
297
|
+
|
|
298
|
+
return self._stage(stage)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class SetBehaviour(_Behaviour):
|
|
302
|
+
"""`binding.on_set`: behaviour for attribute writes.
|
|
303
|
+
|
|
304
|
+
The real write takes the value and produces nothing, so there is no
|
|
305
|
+
returns().
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
__slots__ = ()
|
|
309
|
+
|
|
310
|
+
_operation = "set"
|
|
311
|
+
|
|
312
|
+
def rejects(self) -> Binding:
|
|
313
|
+
"""Raise AttributeError instead of writing. Terminal."""
|
|
314
|
+
|
|
315
|
+
binding = self._binding
|
|
316
|
+
|
|
317
|
+
def terminal(
|
|
318
|
+
wrapped: WrappedFunction,
|
|
319
|
+
instance: Any,
|
|
320
|
+
args: tuple[Any, ...],
|
|
321
|
+
kwargs: dict[str, Any],
|
|
322
|
+
) -> NoReturn:
|
|
323
|
+
raise AttributeError(f"can't set attribute {binding._name!r}")
|
|
324
|
+
|
|
325
|
+
return self._terminal(terminal, injected=True)
|
|
326
|
+
|
|
327
|
+
def decorates(self, fn: Callable[[Callable[[Any], Any], Any, Any], Any]) -> Binding:
|
|
328
|
+
"""Wrap the real write: fn(write, instance, value), where
|
|
329
|
+
write(value) performs the write. Terminal."""
|
|
330
|
+
|
|
331
|
+
def terminal(
|
|
332
|
+
wrapped: WrappedFunction,
|
|
333
|
+
instance: Any,
|
|
334
|
+
args: tuple[Any, ...],
|
|
335
|
+
kwargs: dict[str, Any],
|
|
336
|
+
) -> Any:
|
|
337
|
+
return fn(wrapped, instance, args[0])
|
|
338
|
+
|
|
339
|
+
return self._terminal(terminal)
|
|
340
|
+
|
|
341
|
+
def transforms(self, fn: Callable[[Any], Any]) -> Binding:
|
|
342
|
+
"""fn(value) -> value, rewriting the value actually written."""
|
|
343
|
+
|
|
344
|
+
def stage(
|
|
345
|
+
nxt: WrappedFunction,
|
|
346
|
+
instance: Any,
|
|
347
|
+
args: tuple[Any, ...],
|
|
348
|
+
kwargs: dict[str, Any],
|
|
349
|
+
) -> Any:
|
|
350
|
+
return nxt(fn(args[0]))
|
|
351
|
+
|
|
352
|
+
return self._stage(stage)
|
|
353
|
+
|
|
354
|
+
def validates(self, check: Callable[[Any], Any]) -> Binding:
|
|
355
|
+
"""check(value); the write passes through unchanged.
|
|
356
|
+
|
|
357
|
+
The check fails the write only by raising; its return value is
|
|
358
|
+
ignored.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def stage(
|
|
362
|
+
nxt: WrappedFunction,
|
|
363
|
+
instance: Any,
|
|
364
|
+
args: tuple[Any, ...],
|
|
365
|
+
kwargs: dict[str, Any],
|
|
366
|
+
) -> Any:
|
|
367
|
+
check(args[0])
|
|
368
|
+
return nxt(args[0])
|
|
369
|
+
|
|
370
|
+
return self._stage(stage)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class DeleteBehaviour(_Behaviour):
|
|
374
|
+
"""`binding.on_delete`: behaviour for attribute deletes.
|
|
375
|
+
|
|
376
|
+
The real delete takes nothing and produces nothing.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
__slots__ = ()
|
|
380
|
+
|
|
381
|
+
_operation = "delete"
|
|
382
|
+
|
|
383
|
+
def rejects(self) -> Binding:
|
|
384
|
+
"""Raise AttributeError instead of deleting. Terminal."""
|
|
385
|
+
|
|
386
|
+
binding = self._binding
|
|
387
|
+
|
|
388
|
+
def terminal(
|
|
389
|
+
wrapped: WrappedFunction,
|
|
390
|
+
instance: Any,
|
|
391
|
+
args: tuple[Any, ...],
|
|
392
|
+
kwargs: dict[str, Any],
|
|
393
|
+
) -> NoReturn:
|
|
394
|
+
raise AttributeError(f"can't delete attribute {binding._name!r}")
|
|
395
|
+
|
|
396
|
+
return self._terminal(terminal, injected=True)
|
|
397
|
+
|
|
398
|
+
def decorates(self, fn: Callable[[Callable[[], Any], Any], Any]) -> Binding:
|
|
399
|
+
"""Wrap the real delete: fn(erase, instance), where erase()
|
|
400
|
+
performs the delete. Terminal."""
|
|
401
|
+
|
|
402
|
+
def terminal(
|
|
403
|
+
wrapped: WrappedFunction,
|
|
404
|
+
instance: Any,
|
|
405
|
+
args: tuple[Any, ...],
|
|
406
|
+
kwargs: dict[str, Any],
|
|
407
|
+
) -> Any:
|
|
408
|
+
return fn(wrapped, instance)
|
|
409
|
+
|
|
410
|
+
return self._terminal(terminal)
|
|
411
|
+
|
|
412
|
+
def validates(self, check: Callable[[Any], Any]) -> Binding:
|
|
413
|
+
"""check(instance); the delete passes through unchanged.
|
|
414
|
+
|
|
415
|
+
The check fails the delete only by raising; its return value is
|
|
416
|
+
ignored.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
def stage(
|
|
420
|
+
nxt: WrappedFunction,
|
|
421
|
+
instance: Any,
|
|
422
|
+
args: tuple[Any, ...],
|
|
423
|
+
kwargs: dict[str, Any],
|
|
424
|
+
) -> Any:
|
|
425
|
+
check(instance)
|
|
426
|
+
return nxt()
|
|
427
|
+
|
|
428
|
+
return self._stage(stage)
|