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