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/main.py ADDED
@@ -0,0 +1,473 @@
1
+ """Redux store for managing state and side effects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import inspect
8
+ import queue
9
+ import time
10
+ import weakref
11
+ from collections import defaultdict
12
+ from collections.abc import Awaitable, Iterable, Sequence
13
+ from functools import wraps
14
+ from threading import Lock, Thread
15
+ from typing import (
16
+ TYPE_CHECKING,
17
+ Any,
18
+ Concatenate,
19
+ Generic,
20
+ cast,
21
+ overload,
22
+ )
23
+
24
+ from redux.basic_types import (
25
+ Action,
26
+ ActionMiddleware,
27
+ Args,
28
+ AutoAwait,
29
+ AutorunDecorator,
30
+ AutorunOptions,
31
+ AutorunOptionsType,
32
+ AutorunReturnType,
33
+ AwaitableOrNot,
34
+ BaseAction,
35
+ BaseEvent,
36
+ ComparatorOutput,
37
+ DispatchParameters,
38
+ Event,
39
+ EventHandler,
40
+ EventMiddleware,
41
+ FinishAction,
42
+ FinishEvent,
43
+ InitAction,
44
+ MethodSelf,
45
+ ReducerType,
46
+ ReturnType,
47
+ SelectorOutput,
48
+ SnapshotAtom,
49
+ State,
50
+ StoreOptions,
51
+ StrictEvent,
52
+ SubscribeEventCleanup,
53
+ ViewDecorator,
54
+ ViewOptions,
55
+ ViewReturnType,
56
+ WithStateDecorator,
57
+ is_complete_reducer_result,
58
+ is_state_reducer_result,
59
+ )
60
+ from redux.serialization_mixin import SerializationMixin
61
+ from redux.utils import call_func, signature_without_selector
62
+
63
+ if TYPE_CHECKING:
64
+ from collections.abc import Callable
65
+
66
+
67
+ class Store(SerializationMixin, Generic[State, Action, Event]):
68
+ """Redux store for managing state and side effects."""
69
+
70
+ def __init__(
71
+ self,
72
+ reducer: ReducerType[State, Action | InitAction, Event | None],
73
+ options: StoreOptions[Action, Event] | None = None,
74
+ ) -> None:
75
+ """Create a new store."""
76
+ self.store_options = options or StoreOptions()
77
+ self.reducer = reducer
78
+
79
+ self._action_middlewares = list(self.store_options.action_middlewares)
80
+ self._event_middlewares = list(self.store_options.event_middlewares)
81
+
82
+ self._state: State | None = None
83
+ self._listeners: set[
84
+ Callable[[State], AwaitableOrNot[None]]
85
+ | weakref.ref[Callable[[State], AwaitableOrNot[None]]]
86
+ ] = set()
87
+ self._event_handlers: defaultdict[
88
+ type[Event | FinishEvent],
89
+ set[EventHandler | weakref.ref[EventHandler]],
90
+ ] = defaultdict(set)
91
+
92
+ self._actions: list[Action | InitAction] = []
93
+ self._events: list[Event | FinishEvent] = []
94
+
95
+ self._event_handlers_queue = queue.Queue[
96
+ tuple[EventHandler[Event | FinishEvent], Event | FinishEvent] | None
97
+ ]()
98
+ self._workers = [
99
+ self.store_options.side_effect_runner_class(
100
+ task_queue=self._event_handlers_queue,
101
+ create_task=self.store_options.task_creator,
102
+ )
103
+ for _ in range(self.store_options.side_effect_threads)
104
+ ]
105
+ for worker in self._workers:
106
+ worker.start()
107
+
108
+ self._is_running = Lock()
109
+
110
+ if self.store_options.auto_init:
111
+ if self.store_options.scheduler:
112
+ self.store_options.scheduler(
113
+ lambda: self.dispatch(InitAction()),
114
+ interval=False,
115
+ )
116
+ else:
117
+ self.dispatch(InitAction())
118
+
119
+ if self.store_options.scheduler:
120
+ self.store_options.scheduler(self.run, interval=True)
121
+
122
+ def _call_listeners(self: Store[State, Action, Event], state: State) -> None:
123
+ for listener_ in self._listeners.copy():
124
+ listener = listener_() if isinstance(listener_, weakref.ref) else listener_
125
+ if listener is not None:
126
+ result = listener(state)
127
+ if asyncio.iscoroutine(result) and self.store_options.task_creator:
128
+ self.store_options.task_creator(result)
129
+
130
+ def _run_actions(self: Store[State, Action, Event]) -> None:
131
+ while len(self._actions) > 0:
132
+ action = self._actions.pop(0)
133
+ if action is not None:
134
+ result = self.reducer(self._state, action)
135
+ if is_complete_reducer_result(result):
136
+ self._state = result.state
137
+ self._call_listeners(self._state)
138
+ self._dispatch([*(result.actions or []), *(result.events or [])])
139
+ elif is_state_reducer_result(result):
140
+ self._state = result
141
+ self._call_listeners(self._state)
142
+
143
+ if isinstance(action, FinishAction):
144
+ self._dispatch([FinishEvent()])
145
+
146
+ def _run_event_handlers(self: Store[State, Action, Event]) -> None:
147
+ while len(self._events) > 0:
148
+ event = self._events.pop(0)
149
+ if event is not None:
150
+ if isinstance(event, FinishEvent):
151
+ self._handle_finish_event()
152
+ for event_handler in self._event_handlers[type(event)].copy():
153
+ self._event_handlers_queue.put_nowait((event_handler, event))
154
+
155
+ def run(self: Store[State, Action, Event]) -> None:
156
+ """Run the store."""
157
+ with self._is_running:
158
+ while len(self._actions) > 0 or len(self._events) > 0:
159
+ if len(self._actions) > 0:
160
+ self._run_actions()
161
+
162
+ if len(self._events) > 0:
163
+ self._run_event_handlers()
164
+
165
+ def clean_up(self: Store[State, Action, Event]) -> None:
166
+ """Clean up the store."""
167
+ self.wait_for_event_handlers()
168
+ for _ in range(self.store_options.side_effect_threads):
169
+ self._event_handlers_queue.put_nowait(None)
170
+ self.wait_for_event_handlers()
171
+ for worker in self._workers:
172
+ worker.join()
173
+ self._workers.clear()
174
+ self._listeners.clear()
175
+ self._event_handlers.clear()
176
+
177
+ def wait_for_event_handlers(self: Store[State, Action, Event]) -> None:
178
+ """Wait for the event handlers to finish."""
179
+ self._event_handlers_queue.join()
180
+
181
+ @overload
182
+ def dispatch(
183
+ self: Store[State, Action, Event],
184
+ *parameters: DispatchParameters[Action],
185
+ ) -> None: ...
186
+ @overload
187
+ def dispatch(
188
+ self: Store[State, Action, Event],
189
+ *,
190
+ with_state: Callable[[State | None], DispatchParameters[Action]] | None = None,
191
+ ) -> None: ...
192
+ def dispatch(
193
+ self: Store[State, Action, Event],
194
+ *parameters: DispatchParameters[Action],
195
+ with_state: Callable[[State | None], DispatchParameters[Action]] | None = None,
196
+ ) -> None:
197
+ """Dispatch actions."""
198
+ if with_state is not None:
199
+ self.dispatch(with_state(self._state))
200
+
201
+ actions = [
202
+ action
203
+ for actions in parameters
204
+ for action in (actions if isinstance(actions, Iterable) else [actions])
205
+ ]
206
+ self._dispatch(actions)
207
+
208
+ def _dispatch(
209
+ self: Store[State, Action, Event],
210
+ items: Sequence[Action | Event | InitAction | FinishEvent | None],
211
+ ) -> None:
212
+ for item in items:
213
+ if isinstance(item, BaseAction):
214
+ action = item
215
+ for action_middleware in self._action_middlewares:
216
+ action_ = action_middleware(action)
217
+ if action_ is None:
218
+ break
219
+ action = action_
220
+ else:
221
+ self._actions.append(action)
222
+ if isinstance(item, BaseEvent):
223
+ event = item
224
+ for event_middleware in self._event_middlewares:
225
+ event_ = event_middleware(event)
226
+ if event_ is None:
227
+ break
228
+ event = event_
229
+ else:
230
+ self._events.append(event)
231
+
232
+ if self.store_options.scheduler is None and not self._is_running.locked():
233
+ self.run()
234
+
235
+ def _subscribe(
236
+ self: Store[State, Action, Event],
237
+ listener: Callable[[State], Any],
238
+ *,
239
+ keep_ref: bool = True,
240
+ ) -> Callable[[], None]:
241
+ """Subscribe to state changes."""
242
+
243
+ def unsubscribe(_: weakref.ref | None = None) -> None:
244
+ with contextlib.suppress(KeyError):
245
+ self._listeners.remove(listener_ref)
246
+
247
+ if keep_ref:
248
+ listener_ref = listener
249
+ elif inspect.ismethod(listener):
250
+ listener_ref = weakref.WeakMethod(listener, unsubscribe)
251
+ else:
252
+ listener_ref = weakref.ref(listener, unsubscribe)
253
+
254
+ self._listeners.add(listener_ref)
255
+
256
+ return unsubscribe
257
+
258
+ def subscribe_event(
259
+ self: Store[State, Action, Event],
260
+ event_type: type[StrictEvent],
261
+ handler: EventHandler[StrictEvent],
262
+ *,
263
+ keep_ref: bool = True,
264
+ ) -> SubscribeEventCleanup:
265
+ """Subscribe to events."""
266
+
267
+ def unsubscribe(_: weakref.ref | None = None) -> None:
268
+ self._event_handlers[cast('Event', event_type)].discard(handler_ref)
269
+
270
+ if keep_ref:
271
+ handler_ref = handler
272
+ elif inspect.ismethod(handler):
273
+ handler_ref = weakref.WeakMethod(handler, unsubscribe)
274
+ else:
275
+ handler_ref = weakref.ref(handler, unsubscribe)
276
+
277
+ self._event_handlers[cast('Event', event_type)].add(handler_ref)
278
+
279
+ return SubscribeEventCleanup(unsubscribe=unsubscribe, handler=handler)
280
+
281
+ def _wait_for_store_to_finish(self: Store[State, Action, Event]) -> None:
282
+ """Wait for the store to finish."""
283
+ while True:
284
+ if (
285
+ self._actions == []
286
+ and self._events == []
287
+ and self._event_handlers_queue.qsize() == 0
288
+ ):
289
+ time.sleep(self.store_options.grace_time_in_seconds)
290
+ self.clean_up()
291
+ if self.store_options.on_finish:
292
+ self.store_options.on_finish()
293
+ break
294
+
295
+ def _handle_finish_event(self: Store[State, Action, Event]) -> None:
296
+ Thread(target=self._wait_for_store_to_finish).start()
297
+
298
+ def autorun(
299
+ self: Store[State, Action, Event],
300
+ selector: Callable[[State], SelectorOutput],
301
+ comparator: Callable[[State], ComparatorOutput] | None = None,
302
+ *,
303
+ options: AutorunOptionsType[ReturnType, AutoAwait] | None = None,
304
+ ) -> AutorunDecorator[ReturnType, SelectorOutput, AutoAwait]:
305
+ """Create a new autorun, reflecting on state changes."""
306
+
307
+ def autorun_decorator(
308
+ func: Callable[
309
+ Concatenate[SelectorOutput, Args],
310
+ AwaitableOrNot[ReturnType],
311
+ ],
312
+ ) -> AutorunReturnType[Args, ReturnType]:
313
+ return self.store_options.autorun_class(
314
+ store=self,
315
+ selector=selector,
316
+ comparator=comparator,
317
+ func=cast('Callable', func),
318
+ options=options or AutorunOptions(),
319
+ )
320
+
321
+ return cast('AutorunDecorator', autorun_decorator)
322
+
323
+ def view(
324
+ self: Store[State, Action, Event],
325
+ selector: Callable[[State], SelectorOutput],
326
+ *,
327
+ options: ViewOptions[ReturnType] | None = None,
328
+ ) -> ViewDecorator[ReturnType, SelectorOutput]:
329
+ """Create a new view, throttling calls for unchanged selector results."""
330
+
331
+ @overload
332
+ def view_decorator(
333
+ func: Callable[
334
+ Concatenate[SelectorOutput, Args],
335
+ ReturnType,
336
+ ]
337
+ | Callable[
338
+ Concatenate[MethodSelf, SelectorOutput, Args],
339
+ ReturnType,
340
+ ],
341
+ ) -> ViewReturnType[ReturnType, Args]: ...
342
+ @overload
343
+ def view_decorator(
344
+ func: Callable[
345
+ Concatenate[SelectorOutput, Args],
346
+ Awaitable[ReturnType],
347
+ ]
348
+ | Callable[
349
+ Concatenate[MethodSelf, SelectorOutput, Args],
350
+ Awaitable[ReturnType],
351
+ ],
352
+ ) -> ViewReturnType[Awaitable[ReturnType], Args]: ...
353
+ def view_decorator(
354
+ func: Callable[
355
+ Concatenate[SelectorOutput, Args],
356
+ AwaitableOrNot[ReturnType],
357
+ ]
358
+ | Callable[
359
+ Concatenate[MethodSelf, SelectorOutput, Args],
360
+ AwaitableOrNot[ReturnType],
361
+ ],
362
+ ) -> ViewReturnType[AwaitableOrNot[ReturnType], Args]:
363
+ _options = options or ViewOptions()
364
+ return self.store_options.autorun_class(
365
+ store=self,
366
+ selector=selector,
367
+ comparator=None,
368
+ func=cast('Callable', func),
369
+ options=AutorunOptions(
370
+ default_value=_options.default_value,
371
+ auto_await=False,
372
+ initial_call=False,
373
+ reactive=False,
374
+ memoization=_options.memoization,
375
+ keep_ref=_options.keep_ref,
376
+ subscribers_initial_run=_options.subscribers_initial_run,
377
+ subscribers_keep_ref=_options.subscribers_keep_ref,
378
+ ),
379
+ )
380
+
381
+ return view_decorator
382
+
383
+ def with_state(
384
+ self: Store[State, Action, Event],
385
+ selector: Callable[[State], SelectorOutput],
386
+ *,
387
+ ignore_uninitialized_store: bool = False,
388
+ ) -> WithStateDecorator[SelectorOutput]:
389
+ """Wrap a function, passing the result of the selector as its first argument.
390
+
391
+ Note that it has zero reactivity, it just runs the function with the result
392
+ of the selector whenever it is called explicitly. To make it reactive, use
393
+ `autorun` instead.
394
+
395
+ This is mostly for improving code oragnization and readability. Directly Using
396
+ `store._state` is also possible.
397
+ """
398
+
399
+ @overload
400
+ def with_state_decorator(
401
+ func: Callable[
402
+ Concatenate[SelectorOutput, Args],
403
+ ReturnType,
404
+ ],
405
+ ) -> Callable[Args, ReturnType]: ...
406
+ @overload
407
+ def with_state_decorator(
408
+ func: Callable[
409
+ Concatenate[MethodSelf, SelectorOutput, Args],
410
+ ReturnType,
411
+ ],
412
+ ) -> Callable[Concatenate[MethodSelf, Args], ReturnType]: ...
413
+ def with_state_decorator(
414
+ func: Callable[
415
+ Concatenate[SelectorOutput, Args],
416
+ ReturnType,
417
+ ]
418
+ | Callable[
419
+ Concatenate[MethodSelf, SelectorOutput, Args],
420
+ ReturnType,
421
+ ],
422
+ ) -> (
423
+ Callable[Args, ReturnType]
424
+ | Callable[Concatenate[MethodSelf, Args], ReturnType]
425
+ ):
426
+ def wrapper(*args: Args.args, **kwargs: Args.kwargs) -> ReturnType:
427
+ if self._state is None:
428
+ if ignore_uninitialized_store:
429
+ return cast('ReturnType', None)
430
+ msg = 'Store has not been initialized yet.'
431
+ raise RuntimeError(msg)
432
+ return call_func(func, [selector(self._state)], *args, **kwargs)
433
+
434
+ signature = signature_without_selector(func)
435
+ wrapped = wraps(cast('Any', func))(wrapper)
436
+ wrapped.__signature__ = signature # type: ignore [reportAttributeAccessIssue]
437
+
438
+ return wrapped
439
+
440
+ return with_state_decorator
441
+
442
+ @property
443
+ def snapshot(self: Store[State, Action, Event]) -> SnapshotAtom:
444
+ """Return a snapshot of the current state of the store."""
445
+ return self.serialize_value(self._state)
446
+
447
+ def register_action_middleware(
448
+ self: Store[State, Action, Event],
449
+ action_middleware: ActionMiddleware,
450
+ ) -> None:
451
+ """Register an action dispatch middleware."""
452
+ self._action_middlewares.append(action_middleware)
453
+
454
+ def register_event_middleware(
455
+ self: Store[State, Action, Event],
456
+ event_middleware: EventMiddleware,
457
+ ) -> None:
458
+ """Register an action dispatch middleware."""
459
+ self._event_middlewares.append(event_middleware)
460
+
461
+ def unregister_action_middleware(
462
+ self: Store[State, Action, Event],
463
+ action_middleware: ActionMiddleware,
464
+ ) -> None:
465
+ """Unregister an action dispatch middleware."""
466
+ self._action_middlewares.remove(action_middleware)
467
+
468
+ def unregister_event_middleware(
469
+ self: Store[State, Action, Event],
470
+ event_middleware: EventMiddleware,
471
+ ) -> None:
472
+ """Unregister an action dispatch middleware."""
473
+ self._event_middlewares.remove(event_middleware)
redux/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561. The redux package uses inline types.