Skip to content

Commit e278776

Browse files
committed
feat: allow customizing the async task creator of the Autorun instance by overriding its _create_task method
1 parent a8573a7 commit e278776

File tree

4 files changed

+41
-25
lines changed

4 files changed

+41
-25
lines changed

.github/workflows/integration_delivery.yml

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,10 @@ jobs:
9393
- uses: actions/checkout@v4
9494
name: Checkout
9595

96-
- name: Setup Python
97-
uses: actions/setup-python@v5
98-
with:
99-
python-version: ${{ env.PYTHON_VERSION }}
100-
10196
- name: Install uv
10297
uses: astral-sh/setup-uv@v3
10398
with:
99+
python-version: ${{ env.PYTHON_VERSION }}
104100
enable-cache: true
105101

106102
- name: Create virtualenv
@@ -159,9 +155,9 @@ jobs:
159155
run: |
160156
git fetch --prune --unshallow
161157
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
162-
echo "VERSION=$(uvx hatch version)" >> "$GITHUB_OUTPUT"
158+
echo "VERSION=$(uvx hatch version)" >>"$GITHUB_OUTPUT"
163159
echo "VERSION=$(uvx hatch version)"
164-
echo "NAME=$(uvx hatch project metadata | jq -r .name)" >> "$GITHUB_OUTPUT"
160+
echo "NAME=$(uvx hatch project metadata | jq -r .name)" >>"$GITHUB_OUTPUT"
165161
echo "NAME=$(uvx hatch project metadata | jq -r .name)"
166162
167163
- name: Extract Version from CHANGELOG.md
@@ -275,12 +271,12 @@ jobs:
275271
- name: Extract Changelog
276272
id: changelog
277273
run: |
278-
perl -0777 -ne 'while (/## Version ${{ needs.build.outputs.version }}\n(\s*\n)*(.*?)(\s*\n)*## Version \d+\.\d+\.\d+\n/sg) {print "$2\n"}' CHANGELOG.md > CURRENT_CHANGELOG.md
274+
perl -0777 -ne 'while (/## Version ${{ needs.build.outputs.version }}\n(\s*\n)*(.*?)(\s*\n)*## Version \d+\.\d+\.\d+\n/sg) {print "$2\n"}' CHANGELOG.md >CURRENT_CHANGELOG.md
279275
{
280276
echo "CONTENT<<EOF"
281277
cat CURRENT_CHANGELOG.md
282278
echo "EOF"
283-
} >> "$GITHUB_OUTPUT"
279+
} >>"$GITHUB_OUTPUT"
284280
285281
- name: Release
286282
uses: softprops/action-gh-release@v2

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- feat: support `with_state` to be applied to methods of classes, not just functions
88
- feat: support `view` to be applied to methods of classes, not just functions, it works for `autorun` too, but only when it is being called directly like a view
99
- refactor: rename `_id` field of combine reducer state to `combine_reducers_id`
10+
- feat: allow customizing the async task creator of the `Autorun` instance by overriding its `_create_task` method
1011

1112
## Version 0.23.0
1213

redux/autorun.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,22 @@ def __init__( # noqa: C901, PLR0912
143143
async def default_value_wrapper() -> ReturnType | None:
144144
return options.default_value
145145

146-
create_task = self._store._create_task # noqa: SLF001
147146
default_value = default_value_wrapper()
148147

149-
if create_task:
150-
create_task(default_value)
148+
self._create_task(default_value)
151149
self._latest_value: ReturnType = default_value
152150
else:
153151
self._latest_value: ReturnType = options.default_value
154152
self._subscriptions: set[
155153
Callable[[ReturnType], Any] | weakref.ref[Callable[[ReturnType], Any]]
156154
] = set()
157155

158-
if self.check(store._state) and self._options.initial_call: # noqa: SLF001
156+
if (
157+
store.with_state(lambda state: state, ignore_uninitialized_store=True)(
158+
self.check,
159+
)()
160+
and self._options.initial_call
161+
):
159162
self._should_be_called = False
160163
self.call()
161164

@@ -164,6 +167,11 @@ async def default_value_wrapper() -> ReturnType | None:
164167
else:
165168
self._unsubscribe = None
166169

170+
def _create_task(self: Autorun, coro: Coroutine[None, None, Any]) -> None:
171+
"""Create a task for the coroutine."""
172+
if self._store.store_options.task_creator:
173+
self._store.store_options.task_creator(coro)
174+
167175
def react(
168176
self: Autorun,
169177
state: State,
@@ -267,10 +275,12 @@ def call(
267275
*args,
268276
**kwargs,
269277
)
270-
create_task = self._store._create_task # noqa: SLF001
271278
previous_value = self._latest_value
272-
if iscoroutine(value) and create_task:
273-
if self._options.auto_await is False:
279+
if iscoroutine(value):
280+
if (
281+
self._options.auto_await
282+
is False # only explicit `False` disables auto-await, not `None`
283+
):
274284
if (
275285
self._latest_value is not None
276286
and isinstance(self._latest_value, AwaitableWrapper)
@@ -280,7 +290,7 @@ def call(
280290
self._latest_value = cast('ReturnType', AwaitableWrapper(value))
281291
else:
282292
self._latest_value = cast('ReturnType', None)
283-
create_task(value)
293+
self._create_task(value)
284294
else:
285295
self._latest_value = value
286296
if self._latest_value is not previous_value:
@@ -300,8 +310,9 @@ def __call__(
300310
**kwargs: Args.kwargs,
301311
) -> ReturnType:
302312
"""Call the wrapped function with the current state of the store."""
303-
state = self._store._state # noqa: SLF001
304-
self.check(state)
313+
self._store.with_state(lambda state: state, ignore_uninitialized_store=True)(
314+
self.check,
315+
)()
305316
if self._should_be_called or args or kwargs or not self._options.memoization:
306317
self._should_be_called = False
307318
self.call(*args, **kwargs)
@@ -386,7 +397,7 @@ def __get__(
386397
ReturnType,
387398
],
388399
obj: object | None,
389-
type_: type | None = None,
400+
_: type | None = None,
390401
) -> Autorun[
391402
State,
392403
Action,

redux/main.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ def __init__(
7373
"""Create a new store."""
7474
self.store_options = options or StoreOptions()
7575
self.reducer = reducer
76-
self._create_task = self.store_options.task_creator
7776

7877
self._action_middlewares = list(self.store_options.action_middlewares)
7978
self._event_middlewares = list(self.store_options.event_middlewares)
@@ -97,7 +96,7 @@ def __init__(
9796
self._workers = [
9897
self.store_options.side_effect_runner_class(
9998
task_queue=self._event_handlers_queue,
100-
create_task=self._create_task,
99+
create_task=self.store_options.task_creator,
101100
)
102101
for _ in range(self.store_options.side_effect_threads)
103102
]
@@ -122,12 +121,17 @@ def _call_listeners(self: Store[State, Action, Event], state: State) -> None:
122121
for listener_ in self._listeners.copy():
123122
if isinstance(listener_, weakref.ref):
124123
listener = listener_()
125-
assert listener is not None # noqa: S101
124+
if listener is None:
125+
msg = (
126+
'Listener has been garbage collected. '
127+
'Consider using `keep_ref=True` if it suits your use case.'
128+
)
129+
raise RuntimeError(msg)
126130
else:
127131
listener = listener_
128132
result = listener(state)
129-
if asyncio.iscoroutine(result) and self._create_task:
130-
self._create_task(result)
133+
if asyncio.iscoroutine(result) and self.store_options.task_creator:
134+
self.store_options.task_creator(result)
131135

132136
def _run_actions(self: Store[State, Action, Event]) -> None:
133137
while len(self._actions) > 0:
@@ -385,6 +389,8 @@ def view_decorator(
385389
def with_state(
386390
self: Store[State, Action, Event],
387391
selector: Callable[[State], SelectorOutput],
392+
*,
393+
ignore_uninitialized_store: bool = False,
388394
) -> WithStateDecorator[SelectorOutput]:
389395
"""Wrap a function, passing the result of the selector as its first argument.
390396
@@ -425,6 +431,8 @@ def with_state_decorator(
425431
):
426432
def wrapper(*args: Args.args, **kwargs: Args.kwargs) -> ReturnType:
427433
if self._state is None:
434+
if ignore_uninitialized_store:
435+
return cast('ReturnType', None)
428436
msg = 'Store has not been initialized yet.'
429437
raise RuntimeError(msg)
430438
return call_func(func, [selector(self._state)], *args, **kwargs)

0 commit comments

Comments
 (0)