Skip to content

Commit debf12d

Browse files
committed
feat(core): add view method to Store to allow computing a derived value from the state only when it is accessed and caching the result until the relevant parts of the state change
1 parent 58343ce commit debf12d

File tree

8 files changed

+215
-146
lines changed

8 files changed

+215
-146
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Version 0.15.1
4+
5+
- feat(core): add `view` method to `Store` to allow computing a derived value from
6+
the state only when it is accessed and caching the result until the relevant parts
7+
of the state change
8+
39
## Version 0.15.0
410

511
- refactor(autorun)!: setting `initial_run` option of autorun to `False` used to

poetry.lock

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ optional = true
1616

1717
[tool.poetry.group.dev.dependencies]
1818
poethepoet = "^0.24.4"
19-
pyright = "^1.1.357"
20-
ruff = "^0.3.5"
19+
pyright = "^1.1.361"
20+
ruff = "^0.4.3"
2121
pytest = "^8.1.1"
2222
pytest-cov = "^4.1.0"
2323
pytest-timeout = "^2.3.1"

redux/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
AutorunDecorator,
55
AutorunOptions,
66
AutorunReturnType,
7-
AutorunType,
87
BaseAction,
98
BaseCombineReducerState,
109
BaseEvent,
@@ -24,6 +23,8 @@
2423
ReducerResult,
2524
ReducerType,
2625
Scheduler,
26+
ViewDecorator,
27+
ViewReturnType,
2728
is_complete_reducer_result,
2829
is_state_reducer_result,
2930
)
@@ -34,9 +35,13 @@
3435
'AutorunDecorator',
3536
'AutorunOptions',
3637
'AutorunReturnType',
37-
'AutorunType',
3838
'BaseAction',
39+
'BaseCombineReducerState',
3940
'BaseEvent',
41+
'CombineReducerAction',
42+
'CombineReducerInitAction',
43+
'CombineReducerRegisterAction',
44+
'CombineReducerUnregisterAction',
4045
'CompleteReducerResult',
4146
'CreateStoreOptions',
4247
'Dispatch',
@@ -49,13 +54,10 @@
4954
'ReducerResult',
5055
'ReducerType',
5156
'Scheduler',
57+
'ViewDecorator',
58+
'ViewReturnType',
5259
'is_complete_reducer_result',
5360
'is_state_reducer_result',
54-
'BaseCombineReducerState',
55-
'CombineReducerAction',
56-
'CombineReducerInitAction',
57-
'CombineReducerRegisterAction',
58-
'CombineReducerUnregisterAction',
5961
'combine_reducers',
6062
'Store',
6163
)

redux/autorun.py

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

4-
import functools
54
import inspect
65
import weakref
76
from asyncio import Task, iscoroutine
8-
from inspect import signature
9-
from typing import TYPE_CHECKING, Any, Callable, Generic, cast
7+
from typing import TYPE_CHECKING, Any, Callable, Concatenate, Generic, cast
108

119
from redux.basic_types import (
1210
Action,
11+
AutorunArgs,
1312
AutorunOptions,
1413
AutorunOriginalReturnType,
1514
ComparatorOutput,
@@ -19,8 +18,6 @@
1918
)
2019

2120
if TYPE_CHECKING:
22-
from types import MethodType
23-
2421
from redux.main import Store
2522

2623

@@ -32,6 +29,7 @@ class Autorun(
3229
SelectorOutput,
3330
ComparatorOutput,
3431
AutorunOriginalReturnType,
32+
AutorunArgs,
3533
],
3634
):
3735
def __init__( # noqa: PLR0913
@@ -40,8 +38,10 @@ def __init__( # noqa: PLR0913
4038
store: Store[State, Action, Event],
4139
selector: Callable[[State], SelectorOutput],
4240
comparator: Callable[[State], Any] | None,
43-
func: Callable[[SelectorOutput], AutorunOriginalReturnType]
44-
| Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType],
41+
func: Callable[
42+
Concatenate[SelectorOutput, AutorunArgs],
43+
AutorunOriginalReturnType,
44+
],
4545
options: AutorunOptions[AutorunOriginalReturnType],
4646
) -> None:
4747
if not options.reactive and options.auto_call:
@@ -54,9 +54,9 @@ def __init__( # noqa: PLR0913
5454
if options.keep_ref:
5555
self._func = func
5656
elif inspect.ismethod(func):
57-
self._func = weakref.WeakMethod(func)
57+
self._func = weakref.WeakMethod(func, self.unsubscribe)
5858
else:
59-
self._func = weakref.ref(func, lambda _: self.unsubscribe())
59+
self._func = weakref.ref(func, self.unsubscribe)
6060
self._options = options
6161

6262
self._last_selector_result: SelectorOutput | None = None
@@ -70,12 +70,19 @@ def __init__( # noqa: PLR0913
7070
| weakref.ref[Callable[[AutorunOriginalReturnType], Any]]
7171
] = set()
7272

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

7575
if self._options.reactive:
76-
self.unsubscribe = store.subscribe(
77-
functools.partial(self._check_and_call, call=self._options.auto_call),
76+
self._unsubscribe = store.subscribe(
77+
lambda state: self._check_and_call(state, self._options.auto_call),
7878
)
79+
else:
80+
self._unsubscribe = None
81+
82+
def unsubscribe(self: Autorun, _: weakref.ref | None = None) -> None:
83+
if self._unsubscribe:
84+
self._unsubscribe()
85+
self._unsubscribe = None
7986

8087
def inform_subscribers(
8188
self: Autorun[
@@ -85,6 +92,7 @@ def inform_subscribers(
8592
SelectorOutput,
8693
ComparatorOutput,
8794
AutorunOriginalReturnType,
95+
AutorunArgs,
8896
],
8997
) -> None:
9098
for subscriber_ in self._subscriptions.copy():
@@ -97,37 +105,6 @@ def inform_subscribers(
97105
subscriber = subscriber_
98106
subscriber(self._latest_value)
99107

100-
def call_func(
101-
self: Autorun[
102-
State,
103-
Action,
104-
Event,
105-
SelectorOutput,
106-
ComparatorOutput,
107-
AutorunOriginalReturnType,
108-
],
109-
selector_result: SelectorOutput,
110-
previous_result: SelectorOutput | None,
111-
func: Callable[
112-
[SelectorOutput, SelectorOutput],
113-
AutorunOriginalReturnType,
114-
]
115-
| Callable[[SelectorOutput], AutorunOriginalReturnType]
116-
| MethodType,
117-
) -> AutorunOriginalReturnType:
118-
if len(signature(func).parameters) == 1:
119-
return cast(
120-
Callable[[SelectorOutput], AutorunOriginalReturnType],
121-
func,
122-
)(selector_result)
123-
return cast(
124-
Callable[
125-
[SelectorOutput, SelectorOutput | None],
126-
AutorunOriginalReturnType,
127-
],
128-
func,
129-
)(selector_result, previous_result)
130-
131108
def _task_callback(
132109
self: Autorun[
133110
State,
@@ -136,6 +113,7 @@ def _task_callback(
136113
SelectorOutput,
137114
ComparatorOutput,
138115
AutorunOriginalReturnType,
116+
AutorunArgs,
139117
],
140118
task: Task,
141119
) -> None:
@@ -150,10 +128,12 @@ def _check_and_call(
150128
SelectorOutput,
151129
ComparatorOutput,
152130
AutorunOriginalReturnType,
131+
AutorunArgs,
153132
],
154133
state: State,
155-
*,
156-
call: bool = True,
134+
_call: bool, # noqa: FBT001
135+
*args: AutorunArgs.args,
136+
**kwargs: AutorunArgs.kwargs,
157137
) -> None:
158138
try:
159139
selector_result = self._selector(state)
@@ -167,26 +147,19 @@ def _check_and_call(
167147
except AttributeError:
168148
return
169149
if self._should_be_called or comparator_result != self._last_comparator_result:
170-
self._should_be_called = False
171-
previous_result = self._last_selector_result
172150
self._last_selector_result = selector_result
173151
self._last_comparator_result = comparator_result
174-
func = self._func() if isinstance(self._func, weakref.ref) else self._func
175-
if func:
176-
if call:
177-
self._latest_value = self.call_func(
178-
selector_result,
179-
previous_result,
180-
func,
181-
)
152+
self._should_be_called = not _call
153+
if _call:
154+
func = (
155+
self._func() if isinstance(self._func, weakref.ref) else self._func
156+
)
157+
if func:
158+
self._latest_value = func(selector_result, *args, **kwargs)
182159
create_task = self._store._create_task # noqa: SLF001
183160
if iscoroutine(self._latest_value) and create_task:
184161
create_task(self._latest_value, callback=self._task_callback)
185162
self.inform_subscribers()
186-
else:
187-
self._should_be_called = True
188-
else:
189-
self.unsubscribe()
190163

191164
def __call__(
192165
self: Autorun[
@@ -196,11 +169,14 @@ def __call__(
196169
SelectorOutput,
197170
ComparatorOutput,
198171
AutorunOriginalReturnType,
172+
AutorunArgs,
199173
],
174+
*args: AutorunArgs.args,
175+
**kwargs: AutorunArgs.kwargs,
200176
) -> AutorunOriginalReturnType:
201177
state = self._store._state # noqa: SLF001
202178
if state is not None:
203-
self._check_and_call(state, call=True)
179+
self._check_and_call(state, True, *args, **kwargs) # noqa: FBT003
204180
return cast(AutorunOriginalReturnType, self._latest_value)
205181

206182
def __repr__(
@@ -211,6 +187,7 @@ def __repr__(
211187
SelectorOutput,
212188
ComparatorOutput,
213189
AutorunOriginalReturnType,
190+
AutorunArgs,
214191
],
215192
) -> str:
216193
return f"""{super().__repr__()}(func: {self._func}, last_value: {
@@ -225,6 +202,7 @@ def value(
225202
SelectorOutput,
226203
ComparatorOutput,
227204
AutorunOriginalReturnType,
205+
AutorunArgs,
228206
],
229207
) -> AutorunOriginalReturnType:
230208
return cast(AutorunOriginalReturnType, self._latest_value)
@@ -237,6 +215,7 @@ def subscribe(
237215
SelectorOutput,
238216
ComparatorOutput,
239217
AutorunOriginalReturnType,
218+
AutorunArgs,
240219
],
241220
callback: Callable[[AutorunOriginalReturnType], Any],
242221
*,

0 commit comments

Comments
 (0)