Skip to content

Commit 220af95

Browse files
committed
refactor: add autorun_class and side_effect_runner_class to improve extensibility
1 parent 8aea613 commit 220af95

26 files changed

+190
-114
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- refactor: remove `WithState` as it wasn't doing anything beyond `functools.wraps`
66
- refactor: autorun doesn't inform subscribers when the output value is not changed
7+
- refactor: add `autorun_class` and `side_effect_runner_class` to improve extensibility
78

89
## Version 0.22.2
910

demo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
FinishAction,
2323
ReducerResult,
2424
)
25-
from redux.main import CreateStoreOptions
25+
from redux.main import StoreOptions
2626

2727

2828
class CountAction(BaseAction): ...
@@ -124,7 +124,7 @@ def main() -> None:
124124
# Initialization <
125125
store = Store(
126126
reducer,
127-
CreateStoreOptions(auto_init=True, threads=2),
127+
StoreOptions(auto_init=True, side_effect_threads=2),
128128
)
129129

130130
def event_handler(event: SleepEvent) -> None:

redux/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
CombineReducerRegisterAction,
1313
CombineReducerUnregisterAction,
1414
CompleteReducerResult,
15-
CreateStoreOptions,
1615
Dispatch,
1716
DispatchParameters,
1817
EventSubscriber,
@@ -23,6 +22,7 @@
2322
ReducerResult,
2423
ReducerType,
2524
Scheduler,
25+
StoreOptions,
2626
ViewDecorator,
2727
ViewOptions,
2828
ViewReturnType,
@@ -44,7 +44,6 @@
4444
'CombineReducerRegisterAction',
4545
'CombineReducerUnregisterAction',
4646
'CompleteReducerResult',
47-
'CreateStoreOptions',
4847
'Dispatch',
4948
'DispatchParameters',
5049
'EventSubscriber',
@@ -56,6 +55,7 @@
5655
'ReducerType',
5756
'Scheduler',
5857
'Store',
58+
'StoreOptions',
5959
'ViewDecorator',
6060
'ViewOptions',
6161
'ViewReturnType',

redux/autorun.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ def __init__( # noqa: C901, PLR0912
140140
Callable[[ReturnType], Any] | weakref.ref[Callable[[ReturnType], Any]]
141141
] = set()
142142

143-
if self._check(store._state) and self._options.initial_call: # noqa: SLF001
144-
self._call()
143+
if self.check(store._state) and self._options.initial_call: # noqa: SLF001
144+
self._should_be_called = False
145+
self.call()
145146

146147
if self._options.reactive:
147148
self._unsubscribe = store._subscribe(self._react) # noqa: SLF001
@@ -153,8 +154,9 @@ def _react(
153154
state: State,
154155
) -> None:
155156
"""React to state changes in the store."""
156-
if self._options.reactive and self._check(state):
157-
self._call()
157+
if self._options.reactive and self.check(state):
158+
self._should_be_called = False
159+
self.call()
158160

159161
def unsubscribe(
160162
self: Autorun[
@@ -216,7 +218,7 @@ def _task_callback(
216218
),
217219
)
218220

219-
def _check(
221+
def check(
220222
self: Autorun[
221223
State,
222224
Action,
@@ -228,6 +230,7 @@ def _check(
228230
],
229231
state: State | None,
230232
) -> bool:
233+
"""Check if the autorun should be called based on the current state."""
231234
if state is None:
232235
return False
233236
try:
@@ -248,7 +251,7 @@ def _check(
248251
self._last_comparator_result = comparator_result
249252
return self._should_be_called
250253

251-
def _call(
254+
def call(
252255
self: Autorun[
253256
State,
254257
Action,
@@ -261,7 +264,7 @@ def _call(
261264
*args: Args.args,
262265
**kwargs: Args.kwargs,
263266
) -> None:
264-
self._should_be_called = False
267+
"""Call the wrapped function with the current state of the store."""
265268
func = self._func() if isinstance(self._func, weakref.ref) else self._func
266269
if func and self._last_selector_result is not None:
267270
value: ReturnType = func(
@@ -313,9 +316,10 @@ def __call__(
313316
) -> ReturnType:
314317
"""Call the wrapped function with the current state of the store."""
315318
state = self._store._state # noqa: SLF001
316-
self._check(state)
319+
self.check(state)
317320
if self._should_be_called or args or kwargs or not self._options.memoization:
318-
self._call(*args, **kwargs)
321+
self._should_be_called = False
322+
self.call(*args, **kwargs)
319323
return cast('ReturnType', self._latest_value)
320324

321325
def __repr__(

redux/basic_types.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
if TYPE_CHECKING:
2424
from asyncio import Task
2525

26+
from redux.autorun import Autorun
27+
from redux.side_effect_runner import SideEffectRunner
2628

2729
T = TypeVar('T')
2830

@@ -113,15 +115,31 @@ class EventMiddleware(Protocol, Generic[Event]):
113115
def __call__(self: EventMiddleware, event: Event) -> Event | None: ...
114116

115117

116-
class CreateStoreOptions(Immutable, Generic[Action, Event]):
118+
def default_autorun() -> type[Autorun]:
119+
from redux.autorun import Autorun
120+
121+
return Autorun
122+
123+
124+
def default_side_effect_runner() -> type[SideEffectRunner]:
125+
from redux.side_effect_runner import SideEffectRunner
126+
127+
return SideEffectRunner
128+
129+
130+
class StoreOptions(Immutable, Generic[Action, Event]):
117131
auto_init: bool = False
118-
threads: int = 5
132+
side_effect_threads: int = 1
119133
scheduler: Scheduler | None = None
120134
action_middlewares: Sequence[ActionMiddleware[Action]] = field(default_factory=list)
121135
event_middlewares: Sequence[EventMiddleware[Event]] = field(default_factory=list)
122136
task_creator: TaskCreator | None = None
123137
on_finish: Callable[[], Any] | None = None
124138
grace_time_in_seconds: float = 1
139+
autorun_class: type[Autorun] = field(default_factory=default_autorun)
140+
side_effect_runner_class: type[SideEffectRunner] = field(
141+
default_factory=default_side_effect_runner,
142+
)
125143

126144

127145
# Autorun

redux/combine_reducers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import operator
77
import uuid
88
from dataclasses import fields
9-
from typing import TYPE_CHECKING, Any, TypeVar, cast
9+
from typing import TYPE_CHECKING, TypeVar
1010

1111
from immutable import make_immutable
1212

@@ -104,7 +104,7 @@ def combined_reducer(
104104
del fields_copy[key]
105105
del annotations_copy[key]
106106
state_class = make_immutable(state_type.__name__, annotations_copy)
107-
cast('Any', state_class).__dataclass_fields__ = fields_copy
107+
state_class.__dataclass_fields__ = fields_copy
108108

109109
state = state_class(
110110
_id=state._id, # noqa: SLF001

redux/main.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
overload,
1919
)
2020

21-
from redux.autorun import Autorun
2221
from redux.basic_types import (
2322
Action,
2423
ActionMiddleware,
@@ -32,7 +31,6 @@
3231
BaseAction,
3332
BaseEvent,
3433
ComparatorOutput,
35-
CreateStoreOptions,
3634
DispatchParameters,
3735
Event,
3836
EventHandler,
@@ -45,6 +43,7 @@
4543
SelectorOutput,
4644
SnapshotAtom,
4745
State,
46+
StoreOptions,
4847
StrictEvent,
4948
SubscribeEventCleanup,
5049
UnknownAutorunDecorator,
@@ -59,7 +58,6 @@
5958
is_state_reducer_result,
6059
)
6160
from redux.serialization_mixin import SerializationMixin
62-
from redux.side_effect_runner import SideEffectRunnerThread
6361

6462
if TYPE_CHECKING:
6563
from collections.abc import Awaitable, Callable
@@ -69,12 +67,12 @@ class Store(Generic[State, Action, Event], SerializationMixin):
6967
"""Redux store for managing state and side effects."""
7068

7169
def __init__(
72-
self: Store,
70+
self,
7371
reducer: ReducerType[State, Action, Event],
74-
options: CreateStoreOptions[Action, Event] | None = None,
72+
options: StoreOptions[Action, Event] | None = None,
7573
) -> None:
7674
"""Create a new store."""
77-
self.store_options = options or CreateStoreOptions()
75+
self.store_options = options or StoreOptions()
7876
self.reducer = reducer
7977
self._create_task = self.store_options.task_creator
8078

@@ -97,11 +95,11 @@ def __init__(
9795
tuple[EventHandler[Event], Event] | None
9896
]()
9997
self._workers = [
100-
SideEffectRunnerThread(
98+
self.store_options.side_effect_runner_class(
10199
task_queue=self._event_handlers_queue,
102100
create_task=self._create_task,
103101
)
104-
for _ in range(self.store_options.threads)
102+
for _ in range(self.store_options.side_effect_threads)
105103
]
106104
for worker in self._workers:
107105
worker.start()
@@ -169,7 +167,7 @@ def run(self: Store[State, Action, Event]) -> None:
169167
def clean_up(self: Store[State, Action, Event]) -> None:
170168
"""Clean up the store."""
171169
self.wait_for_event_handlers()
172-
for _ in range(self.store_options.threads):
170+
for _ in range(self.store_options.side_effect_threads):
173171
self._event_handlers_queue.put_nowait(None)
174172
self.wait_for_event_handlers()
175173
for worker in self._workers:
@@ -347,7 +345,7 @@ def autorun_decorator(
347345
AwaitableOrNot[ReturnType],
348346
],
349347
) -> AutorunReturnType[AwaitableOrNot[ReturnType], Args]:
350-
return Autorun(
348+
return self.store_options.autorun_class(
351349
store=self,
352350
selector=selector,
353351
comparator=comparator,
@@ -403,7 +401,7 @@ def view_decorator(
403401
],
404402
) -> ViewReturnType[AwaitableOrNot[ReturnType], Args]:
405403
_options = options or ViewOptions()
406-
return Autorun(
404+
return self.store_options.autorun_class(
407405
store=self,
408406
selector=selector,
409407
comparator=None,

redux/side_effect_runner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
from collections.abc import Callable
1818

1919

20-
class SideEffectRunnerThread(threading.Thread, Generic[Event]):
20+
class SideEffectRunner(threading.Thread, Generic[Event]):
2121
"""Thread for running side effects."""
2222

2323
def __init__(
24-
self: SideEffectRunnerThread,
24+
self: SideEffectRunner,
2525
*,
2626
task_queue: queue.Queue[tuple[EventHandler[Event], Event] | None],
2727
create_task: TaskCreator | None,
@@ -34,7 +34,7 @@ def __init__(
3434
self._handles: set[Handle] = set()
3535
self.create_task = create_task
3636

37-
def run(self: SideEffectRunnerThread[Event]) -> None:
37+
def run(self: SideEffectRunner[Event]) -> None:
3838
"""Run the side effect runner thread."""
3939
while True:
4040
task = self.task_queue.get()
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
// store-000
22
{
3-
"_type": "ToDoState",
3+
"_type": "TodoState",
44
"items": [
55
{
6-
"_type": "ToDoItem",
6+
"_type": "TodoItem",
77
"content": "Initial Item",
88
"id": "e3e70682c2094cac629f6fbed82c07cd",
99
"timestamp": 1672531200.0
10+
},
11+
{
12+
"_type": "TodoItem",
13+
"content": "New Item",
14+
"id": "f728b4fa42485e3a0a5d2f346baa9455",
15+
"timestamp": 1672531200.0
1016
}
1117
]
1218
}

tests/results/test_todo/todo/store-001.jsonc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// store-001
22
{
3-
"_type": "ToDoState",
3+
"_type": "TodoState",
44
"items": [
55
{
6-
"_type": "ToDoItem",
6+
"_type": "TodoItem",
77
"content": "Initial Item",
88
"id": "e3e70682c2094cac629f6fbed82c07cd",
99
"timestamp": 1672531200.0
1010
},
1111
{
12-
"_type": "ToDoItem",
12+
"_type": "TodoItem",
1313
"content": "New Item",
1414
"id": "f728b4fa42485e3a0a5d2f346baa9455",
1515
"timestamp": 1672531200.0

0 commit comments

Comments
 (0)