python-redux 0.25.2__cp314-cp314-win_arm64.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.
Binary file
redux/autorun.py ADDED
@@ -0,0 +1,410 @@
1
+ """Redux autorun module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ import inspect
8
+ import weakref
9
+ from asyncio import iscoroutine, iscoroutinefunction
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Any,
13
+ Concatenate,
14
+ Generic,
15
+ Literal,
16
+ cast,
17
+ )
18
+
19
+ from redux.basic_types import (
20
+ NOT_SET,
21
+ Action,
22
+ Args,
23
+ AutoAwait,
24
+ AutorunOptionsType,
25
+ ComparatorOutput,
26
+ Event,
27
+ ReturnType,
28
+ SelectorOutput,
29
+ State,
30
+ T,
31
+ )
32
+ from redux.utils import call_func, signature_without_selector
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Callable, Coroutine, Generator
36
+
37
+ from redux.main import Store
38
+
39
+
40
+ class AwaitableWrapper(Generic[T]):
41
+ """A wrapper for a coroutine to track if it has been awaited."""
42
+
43
+ _unawaited = object()
44
+ value: tuple[Literal[False], None] | tuple[Literal[True], T]
45
+
46
+ def __init__(self, coro: Coroutine[None, None, T]) -> None:
47
+ """Initialize the AwaitableWrapper with a coroutine."""
48
+ self.coro = coro
49
+ self.value = (False, None)
50
+
51
+ def __await__(self) -> Generator[None, None, T]:
52
+ """Await the coroutine and set the awaited flag to True."""
53
+ return self._wrap().__await__()
54
+
55
+ async def _wrap(self) -> T:
56
+ """Wrap the coroutine and set the awaited flag to True."""
57
+ if self.value[0] is True:
58
+ return self.value[1]
59
+ self.value = (True, await self.coro)
60
+ return self.value[1]
61
+
62
+ def close(self) -> None:
63
+ """Close the coroutine if it has not been awaited."""
64
+ self.coro.close()
65
+
66
+ @property
67
+ def awaited(self) -> bool:
68
+ """Check if the coroutine has been awaited."""
69
+ return self.value[0] is True
70
+
71
+ def __repr__(self) -> str:
72
+ """Return a string representation of the AwaitableWrapper."""
73
+ return f'AwaitableWrapper({self.coro}, awaited={self.awaited})'
74
+
75
+
76
+ class Autorun(
77
+ Generic[
78
+ State,
79
+ Action,
80
+ Event,
81
+ SelectorOutput,
82
+ ComparatorOutput,
83
+ Args,
84
+ ReturnType,
85
+ ],
86
+ ):
87
+ """Run a wrapped function in response to specific state changes in the store."""
88
+
89
+ def __init__( # noqa: C901, PLR0912
90
+ self: Autorun,
91
+ *,
92
+ store: Store[State, Action, Event],
93
+ selector: Callable[[State], SelectorOutput],
94
+ comparator: Callable[[State], Any] | None,
95
+ func: Callable[
96
+ Concatenate[SelectorOutput, Args],
97
+ ReturnType,
98
+ ],
99
+ options: AutorunOptionsType[ReturnType, AutoAwait],
100
+ ) -> None:
101
+ """Initialize the Autorun instance."""
102
+ if hasattr(func, '__name__'):
103
+ self.__name__ = f'Autorun:{func.__name__}'
104
+ else:
105
+ self.__name__ = f'Autorun:{func}'
106
+ if hasattr(func, '__qualname__'):
107
+ self.__qualname__ = f'Autorun:{func.__qualname__}'
108
+ else:
109
+ self.__qualname__ = f'Autorun:{func}'
110
+ self.__signature__ = signature_without_selector(func)
111
+ self.__module__ = func.__module__
112
+ if (annotations := getattr(func, '__annotations__', None)) is not None:
113
+ self.__annotations__ = annotations
114
+ if (defaults := getattr(func, '__defaults__', None)) is not None:
115
+ self.__defaults__ = defaults
116
+ if (kwdefaults := getattr(func, '__kwdefaults__', None)) is not None:
117
+ self.__kwdefaults__ = kwdefaults
118
+
119
+ self._store = store
120
+ self._selector = selector
121
+ self._comparator = comparator
122
+ self._should_be_called = False
123
+
124
+ if options.keep_ref:
125
+ self._func = func
126
+ elif inspect.ismethod(func):
127
+ self._func = weakref.WeakMethod(func, self.unsubscribe)
128
+ else:
129
+ self._func = weakref.ref(func, self.unsubscribe)
130
+ self._is_coroutine = (
131
+ asyncio.coroutines._is_coroutine # type: ignore [reportAttributeAccessIssue] # noqa: SLF001
132
+ if asyncio.iscoroutinefunction(func) and options.auto_await is False
133
+ else None
134
+ )
135
+ self._options = options
136
+
137
+ self._last_selector_result: SelectorOutput | None = NOT_SET
138
+ self._last_comparator_result: ComparatorOutput = cast(
139
+ 'ComparatorOutput',
140
+ object(),
141
+ )
142
+ if iscoroutinefunction(func):
143
+
144
+ async def default_value_wrapper() -> ReturnType | None:
145
+ return options.default_value
146
+
147
+ default_value = default_value_wrapper()
148
+
149
+ self._create_task(default_value)
150
+ self._latest_value: ReturnType = default_value
151
+ else:
152
+ self._latest_value: ReturnType = options.default_value
153
+ self._subscriptions: set[
154
+ Callable[[ReturnType], Any] | weakref.ref[Callable[[ReturnType], Any]]
155
+ ] = set()
156
+
157
+ if (
158
+ store.with_state(lambda state: state, ignore_uninitialized_store=True)(
159
+ self.check,
160
+ )()
161
+ and self._options.initial_call
162
+ ):
163
+ self._should_be_called = False
164
+ self.call()
165
+
166
+ if self._options.reactive:
167
+ self._unsubscribe = store._subscribe(self.react) # noqa: SLF001
168
+ else:
169
+ self._unsubscribe = None
170
+
171
+ def _create_task(self: Autorun, coro: Coroutine[None, None, Any]) -> None:
172
+ """Create a task for the coroutine."""
173
+ if self._store.store_options.task_creator:
174
+ self._store.store_options.task_creator(coro)
175
+
176
+ def react(
177
+ self: Autorun,
178
+ state: State,
179
+ ) -> None:
180
+ """React to state changes in the store."""
181
+ if self._options.reactive and self.check(state):
182
+ self._should_be_called = False
183
+ self.call()
184
+
185
+ def unsubscribe(
186
+ self: Autorun[
187
+ State,
188
+ Action,
189
+ Event,
190
+ SelectorOutput,
191
+ ComparatorOutput,
192
+ Args,
193
+ ReturnType,
194
+ ],
195
+ _: weakref.ref | None = None,
196
+ ) -> None:
197
+ """Unsubscribe the autorun from the store and clean up resources."""
198
+ if self._unsubscribe:
199
+ self._unsubscribe()
200
+ self._unsubscribe = None
201
+
202
+ def inform_subscribers(
203
+ self: Autorun[
204
+ State,
205
+ Action,
206
+ Event,
207
+ SelectorOutput,
208
+ ComparatorOutput,
209
+ Args,
210
+ ReturnType,
211
+ ],
212
+ ) -> None:
213
+ """Inform all subscribers about the latest value."""
214
+ for subscriber_ in self._subscriptions.copy():
215
+ if isinstance(subscriber_, weakref.ref):
216
+ subscriber = subscriber_()
217
+ if subscriber is None:
218
+ self._subscriptions.discard(subscriber_)
219
+ continue
220
+ else:
221
+ subscriber = subscriber_
222
+ subscriber(self._latest_value)
223
+
224
+ def check(
225
+ self: Autorun[
226
+ State,
227
+ Action,
228
+ Event,
229
+ SelectorOutput,
230
+ ComparatorOutput,
231
+ Args,
232
+ ReturnType,
233
+ ],
234
+ state: State,
235
+ ) -> bool:
236
+ """Check if the autorun should be called based on the current state."""
237
+ try:
238
+ selector_result = self._selector(state)
239
+ except AttributeError:
240
+ return False
241
+ if self._comparator is None:
242
+ comparator_result = cast('ComparatorOutput', selector_result)
243
+ else:
244
+ try:
245
+ comparator_result = self._comparator(state)
246
+ except AttributeError:
247
+ return False
248
+ self._should_be_called = (
249
+ self._should_be_called or comparator_result != self._last_comparator_result
250
+ )
251
+ self._last_selector_result = selector_result
252
+ self._last_comparator_result = comparator_result
253
+ return self._should_be_called
254
+
255
+ def call(
256
+ self: Autorun[
257
+ State,
258
+ Action,
259
+ Event,
260
+ SelectorOutput,
261
+ ComparatorOutput,
262
+ Args,
263
+ ReturnType,
264
+ ],
265
+ *args: Args.args,
266
+ **kwargs: Args.kwargs,
267
+ ) -> None:
268
+ """Call the wrapped function with the current state of the store."""
269
+ func = self._func() if isinstance(self._func, weakref.ref) else self._func
270
+ if func and self._last_selector_result is not NOT_SET:
271
+ value: ReturnType = call_func(
272
+ func,
273
+ [self._last_selector_result],
274
+ *args,
275
+ **kwargs,
276
+ )
277
+ previous_value = self._latest_value
278
+ if iscoroutine(value):
279
+ if (
280
+ self._options.auto_await
281
+ is False # only explicit `False` disables auto-await, not `None`
282
+ ):
283
+ if (
284
+ self._latest_value is not NOT_SET
285
+ and isinstance(self._latest_value, AwaitableWrapper)
286
+ and not self._latest_value.awaited
287
+ ):
288
+ self._latest_value.close()
289
+ self._latest_value = cast('ReturnType', AwaitableWrapper(value))
290
+ else:
291
+ self._latest_value = cast('ReturnType', None)
292
+ self._create_task(value)
293
+ else:
294
+ self._latest_value = value
295
+ if self._latest_value is not previous_value:
296
+ self.inform_subscribers()
297
+
298
+ def __call__(
299
+ self: Autorun[
300
+ State,
301
+ Action,
302
+ Event,
303
+ SelectorOutput,
304
+ ComparatorOutput,
305
+ Args,
306
+ ReturnType,
307
+ ],
308
+ *args: Args.args,
309
+ **kwargs: Args.kwargs,
310
+ ) -> ReturnType:
311
+ """Call the wrapped function with the current state of the store."""
312
+ self._store.with_state(lambda state: state, ignore_uninitialized_store=True)(
313
+ self.check,
314
+ )()
315
+ if self._should_be_called or args or kwargs or not self._options.memoization:
316
+ self._should_be_called = False
317
+ self.call(*args, **kwargs)
318
+ return self._latest_value
319
+
320
+ def __repr__(
321
+ self: Autorun[
322
+ State,
323
+ Action,
324
+ Event,
325
+ SelectorOutput,
326
+ ComparatorOutput,
327
+ Args,
328
+ ReturnType,
329
+ ],
330
+ ) -> str:
331
+ """Return a string representation of the Autorun instance."""
332
+ return (
333
+ super().__repr__()
334
+ + f'(func: {self._func}, last_value: {self._latest_value})'
335
+ )
336
+
337
+ @property
338
+ def value(
339
+ self: Autorun[
340
+ State,
341
+ Action,
342
+ Event,
343
+ SelectorOutput,
344
+ ComparatorOutput,
345
+ Args,
346
+ ReturnType,
347
+ ],
348
+ ) -> ReturnType:
349
+ """Get the latest value of the autorun function."""
350
+ return self._latest_value
351
+
352
+ def subscribe(
353
+ self: Autorun[
354
+ State,
355
+ Action,
356
+ Event,
357
+ SelectorOutput,
358
+ ComparatorOutput,
359
+ Args,
360
+ ReturnType,
361
+ ],
362
+ callback: Callable[[ReturnType], Any],
363
+ *,
364
+ initial_run: bool | None = None,
365
+ keep_ref: bool | None = None,
366
+ ) -> Callable[[], None]:
367
+ """Subscribe to the autorun to be notified of changes in the state."""
368
+ if initial_run is None:
369
+ initial_run = self._options.subscribers_initial_run
370
+ if keep_ref is None:
371
+ keep_ref = self._options.subscribers_keep_ref
372
+ if keep_ref:
373
+ callback_ref = callback
374
+ elif inspect.ismethod(callback):
375
+ callback_ref = weakref.WeakMethod(callback)
376
+ else:
377
+ callback_ref = weakref.ref(callback)
378
+ self._subscriptions.add(callback_ref)
379
+
380
+ if initial_run and self.value is not NOT_SET:
381
+ callback(self.value)
382
+
383
+ def unsubscribe() -> None:
384
+ self._subscriptions.discard(callback_ref)
385
+
386
+ return unsubscribe
387
+
388
+ def __get__(
389
+ self: Autorun[
390
+ State,
391
+ Action,
392
+ Event,
393
+ SelectorOutput,
394
+ ComparatorOutput,
395
+ Args,
396
+ ReturnType,
397
+ ],
398
+ obj: object | None,
399
+ _: type | None = None,
400
+ ) -> Autorun[
401
+ State,
402
+ Action,
403
+ Event,
404
+ SelectorOutput,
405
+ ComparatorOutput,
406
+ Args,
407
+ ReturnType,
408
+ ]:
409
+ """Get the autorun instance."""
410
+ return cast('Autorun', functools.partial(self, obj))