Skip to content

Commit 58343ce

Browse files
committed
feat(autorun): add auto_call and reactive options to autorun to control whether the autorun should call the function automatically when the comparator's value changes and whether it shouldn't automatically call it but yet register a change so that when it is manually called the next time, it will call the function.
1 parent 8187b6b commit 58343ce

File tree

8 files changed

+150
-12
lines changed

8 files changed

+150
-12
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
- refactor(autorun)!: setting `initial_run` option of autorun to `False` used to
66
make the autorun simply not call the function on initialization, now it makes
77
sure the function is not called until the selector's value actually changes
8+
- feat(autorun): add `auto_call` and `reactive` options to autorun to control whether
9+
the autorun should call the function automatically when the comparator's value
10+
changes and whether it shouldn't automatically call it but yet register a change
11+
so that when it is manually called the next time, it will call the function.
812

913
## Version 0.14.5
1014

redux/autorun.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ruff: noqa: D100, D101, D102, D103, D104, D105, D107
22
from __future__ import annotations
33

4+
import functools
45
import inspect
56
import weakref
67
from asyncio import Task, iscoroutine
@@ -43,9 +44,13 @@ def __init__( # noqa: PLR0913
4344
| Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType],
4445
options: AutorunOptions[AutorunOriginalReturnType],
4546
) -> None:
47+
if not options.reactive and options.auto_call:
48+
msg = '`reactive` must be `True` if `auto_call` is `True`'
49+
raise ValueError(msg)
4650
self._store = store
4751
self._selector = selector
4852
self._comparator = comparator
53+
self._should_be_called = False
4954
if options.keep_ref:
5055
self._func = func
5156
elif inspect.ismethod(func):
@@ -65,9 +70,12 @@ def __init__( # noqa: PLR0913
6570
| weakref.ref[Callable[[AutorunOriginalReturnType], Any]]
6671
] = set()
6772

68-
self._check_and_call(store._state, call=self._options.initial_run) # noqa: SLF001
73+
self._check_and_call(store._state, call=self._options.initial_call) # noqa: SLF001
6974

70-
self.unsubscribe = store.subscribe(self._check_and_call)
75+
if self._options.reactive:
76+
self.unsubscribe = store.subscribe(
77+
functools.partial(self._check_and_call, call=self._options.auto_call),
78+
)
7179

7280
def inform_subscribers(
7381
self: Autorun[
@@ -158,7 +166,8 @@ def _check_and_call(
158166
comparator_result = self._comparator(state)
159167
except AttributeError:
160168
return
161-
if comparator_result != self._last_comparator_result:
169+
if self._should_be_called or comparator_result != self._last_comparator_result:
170+
self._should_be_called = False
162171
previous_result = self._last_selector_result
163172
self._last_selector_result = selector_result
164173
self._last_comparator_result = comparator_result
@@ -174,6 +183,8 @@ def _check_and_call(
174183
if iscoroutine(self._latest_value) and create_task:
175184
create_task(self._latest_value, callback=self._task_callback)
176185
self.inform_subscribers()
186+
else:
187+
self._should_be_called = True
177188
else:
178189
self.unsubscribe()
179190

@@ -189,7 +200,7 @@ def __call__(
189200
) -> AutorunOriginalReturnType:
190201
state = self._store._state # noqa: SLF001
191202
if state is not None:
192-
self._check_and_call(state)
203+
self._check_and_call(state, call=True)
193204
return cast(AutorunOriginalReturnType, self._latest_value)
194205

195206
def __repr__(

redux/basic_types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ class CreateStoreOptions(Immutable, Generic[Action, Event]):
117117

118118
class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]):
119119
default_value: AutorunOriginalReturnType | None = None
120-
initial_run: bool = True
120+
initial_call: bool = True
121+
auto_call: bool = True
122+
reactive: bool = True
121123
keep_ref: bool = True
122124
subscribers_initial_run: bool = True
123125
subscribers_keep_ref: bool = True

redux/serialization_mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def serialize_value(
2929
return [cls.serialize_value(i) for i in obj]
3030
if is_immutable(obj):
3131
return cls._serialize_dataclass_to_dict(obj)
32-
msg = f'Unable to serialize object with type `{type(obj)}`.'
32+
msg = f'Unable to serialize object with type `{type(obj)}`'
3333
raise TypeError(msg)
3434

3535
@classmethod

tests/test_autorun.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import re
55
from dataclasses import replace
66
from typing import TYPE_CHECKING, Generator
7+
from unittest.mock import call
78

89
import pytest
910
from immutable import Immutable
1011

1112
from redux.basic_types import (
13+
AutorunOptions,
1214
BaseAction,
1315
CompleteReducerResult,
1416
CreateStoreOptions,
@@ -20,6 +22,8 @@
2022
from redux.main import Store
2123

2224
if TYPE_CHECKING:
25+
from pytest_mock import MockerFixture
26+
2327
from redux_pytest.fixtures import StoreSnapshot
2428

2529

@@ -30,10 +34,15 @@ class StateType(Immutable):
3034
class IncrementAction(BaseAction): ...
3135

3236

37+
class DecrementAction(BaseAction): ...
38+
39+
3340
class IncrementByTwoAction(BaseAction): ...
3441

3542

36-
Action = IncrementAction | IncrementByTwoAction | InitAction | FinishAction
43+
Action = (
44+
IncrementAction | DecrementAction | IncrementByTwoAction | InitAction | FinishAction
45+
)
3746

3847

3948
def reducer(
@@ -48,6 +57,9 @@ def reducer(
4857
if isinstance(action, IncrementAction):
4958
return replace(state, value=state.value + 1)
5059

60+
if isinstance(action, DecrementAction):
61+
return replace(state, value=state.value - 1)
62+
5163
if isinstance(action, IncrementByTwoAction):
5264
return replace(state, value=state.value + 2)
5365

@@ -189,3 +201,112 @@ def render(value: int) -> int:
189201
r'.*\(func: <function test_repr\.<locals>\.render at .*>, last_value: 1\)$',
190202
repr(render),
191203
)
204+
205+
206+
def test_auto_call_without_reactive(store: StoreType) -> None:
207+
with pytest.raises(
208+
ValueError,
209+
match='^`reactive` must be `True` if `auto_call` is `True`$',
210+
):
211+
212+
@store.autorun(
213+
lambda state: state.value,
214+
options=AutorunOptions(reactive=False, auto_call=True),
215+
)
216+
def _(_: int) -> int:
217+
pytest.fail('This should never be called')
218+
219+
220+
call_sequence = [
221+
# 0
222+
[
223+
(IncrementAction()),
224+
],
225+
# 1
226+
[
227+
(IncrementAction()),
228+
(DecrementAction()),
229+
(IncrementByTwoAction()),
230+
(DecrementAction()),
231+
(IncrementAction()),
232+
],
233+
# 3
234+
[
235+
(DecrementAction()),
236+
(DecrementAction()),
237+
],
238+
# 1
239+
]
240+
241+
242+
def test_no_auto_call_with_initial_call_and_reactive_set(
243+
store: StoreType,
244+
mocker: MockerFixture,
245+
) -> None:
246+
def render(_: int) -> None: ...
247+
248+
render = mocker.create_autospec(render)
249+
250+
render_autorun = store.autorun(
251+
lambda state: state.value,
252+
options=AutorunOptions(reactive=True, auto_call=False, initial_call=True),
253+
)(render)
254+
255+
for actions in call_sequence:
256+
for action in actions:
257+
store.dispatch(action)
258+
render_autorun()
259+
260+
assert render.mock_calls == [call(0), call(1), call(3), call(1)]
261+
262+
263+
def test_no_auto_call_and_no_initial_call_with_reactive_set(
264+
store: StoreType,
265+
mocker: MockerFixture,
266+
) -> None:
267+
def render(_: int) -> None: ...
268+
269+
render = mocker.create_autospec(render)
270+
271+
render_autorun = store.autorun(
272+
lambda state: state.value,
273+
options=AutorunOptions(reactive=True, auto_call=False, initial_call=False),
274+
)(render)
275+
276+
for actions in call_sequence:
277+
for action in actions:
278+
store.dispatch(action)
279+
render_autorun()
280+
281+
assert render.mock_calls == [call(1), call(3), call(1)]
282+
283+
284+
def test_with_auto_call_and_initial_call_and_reactive_set(
285+
store: StoreType,
286+
mocker: MockerFixture,
287+
) -> None:
288+
def render(_: int) -> None: ...
289+
290+
render = mocker.create_autospec(render)
291+
292+
render_autorun = store.autorun(
293+
lambda state: state.value,
294+
options=AutorunOptions(reactive=True, auto_call=True, initial_call=True),
295+
)(render)
296+
297+
for actions in call_sequence:
298+
for action in actions:
299+
store.dispatch(action)
300+
render_autorun()
301+
302+
assert render.mock_calls == [
303+
call(0),
304+
call(1),
305+
call(2),
306+
call(1),
307+
call(3),
308+
call(2),
309+
call(3),
310+
call(2),
311+
call(1),
312+
]

tests/test_monitor_fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ def is_closed() -> None:
118118
assert store_snapshot._is_closed # noqa: SLF001
119119
with pytest.raises(
120120
RuntimeError,
121-
match='Snapshot context is closed, make sure you are not calling `take` '
122-
'after `FinishEvent` is dispatched.',
121+
match='^Snapshot context is closed, make sure you are not calling `take` '
122+
'after `FinishEvent` is dispatched.$',
123123
):
124124
store_snapshot.take()
125125

tests/test_serialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class InvalidType: ...
4646
def test_invalid() -> None:
4747
with pytest.raises(
4848
TypeError,
49-
match=f'Unable to serialize object with type `{InvalidType}`',
49+
match=f'^Unable to serialize object with type `{InvalidType}`$',
5050
):
5151
Store.serialize_value(InvalidType())
5252

tests/test_weakref.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def render_with_keep_ref(value: int) -> int:
117117

118118
@store.autorun(
119119
lambda state: state.value,
120-
options=AutorunOptions(keep_ref=False, initial_run=False),
120+
options=AutorunOptions(keep_ref=False, initial_call=False),
121121
)
122122
def render_without_keep_ref(_: int) -> int:
123123
pytest.fail('This should never be called')
@@ -155,7 +155,7 @@ def test_autorun_method(
155155
instance_without_keep_ref = AutorunClass(store_snapshot)
156156
store.autorun(
157157
lambda state: state.value,
158-
options=AutorunOptions(keep_ref=False, initial_run=False),
158+
options=AutorunOptions(keep_ref=False, initial_call=False),
159159
)(
160160
instance_without_keep_ref.method_without_keep_ref,
161161
)

0 commit comments

Comments
 (0)