Skip to content

Commit 521bb85

Browse files
committed
feat: add keep_ref parameter to subscriptions and autoruns, defaulting to True, if set to False, the subscription/autorun will not keep a reference to the callback
refacotr: general housekeeping
1 parent cdfaccf commit 521bb85

File tree

6 files changed

+146
-71
lines changed

6 files changed

+146
-71
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.11.0
4+
5+
- feat: add `keep_ref` parameter to subscriptions and autoruns, defaulting to `True`,
6+
if set to `False`, the subscription/autorun will not keep a reference to the callback
7+
- refacotr: general housekeeping
8+
39
## Version 0.10.7
410

511
- fix: autorun now correctly updates its value when the store is updated

poetry.lock

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

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-redux"
3-
version = "0.10.7"
3+
version = "0.11.0"
44
description = "Redux implementation for Python"
55
authors = ["Sassan Haradji <[email protected]>"]
66
license = "Apache-2.0"
@@ -17,8 +17,8 @@ optional = true
1717

1818
[tool.poetry.group.dev.dependencies]
1919
poethepoet = "^0.24.4"
20-
pyright = "^1.1.350"
21-
ruff = "^0.2.2"
20+
pyright = "^1.1.354"
21+
ruff = "^0.3.2"
2222

2323
[build-system]
2424
requires = ["poetry-core"]
@@ -29,7 +29,7 @@ demo = "demo:main"
2929
todo_demo = "todo_demo:main"
3030

3131
[tool.poe.tasks]
32-
lint = "ruff . --unsafe-fixes"
32+
lint = "ruff check . --unsafe-fixes"
3333
typecheck = "pyright -p pyproject.toml ."
3434
sanity = ["typecheck", "lint"]
3535

redux/autorun.py

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

4+
import inspect
45
import weakref
6+
from asyncio import iscoroutinefunction
57
from inspect import signature
6-
from types import MethodType
7-
from typing import TYPE_CHECKING, Any, Callable, Generic, cast
8+
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, cast
89

910
from redux.basic_types import (
1011
Action,
@@ -17,6 +18,8 @@
1718
)
1819

1920
if TYPE_CHECKING:
21+
from types import MethodType
22+
2023
from redux.main import Store
2124

2225

@@ -43,30 +46,104 @@ def __init__( # noqa: PLR0913
4346
self._store = store
4447
self._selector = selector
4548
self._comparator = comparator
46-
self._func = func
49+
if options.keep_ref:
50+
self._func = func
51+
elif inspect.ismethod(func):
52+
self._func = weakref.WeakMethod(func)
53+
else:
54+
self._func = weakref.ref(func)
4755
self._options = options
4856

4957
self._last_selector_result: SelectorOutput | None = None
5058
self._last_comparator_result: ComparatorOutput = cast(
5159
ComparatorOutput,
5260
object(),
5361
)
54-
self._latest_value: AutorunOriginalReturnType | None = options.default_value
62+
self._latest_value: AutorunOriginalReturnType = options.default_value
5563
self._subscriptions: set[
5664
Callable[[AutorunOriginalReturnType], Any]
5765
| weakref.ref[Callable[[AutorunOriginalReturnType], Any]]
5866
] = set()
67+
self._immediate_run = (
68+
not iscoroutinefunction(func)
69+
if options.subscribers_immediate_run is None
70+
else options.subscribers_immediate_run
71+
)
5972

6073
if self._options.initial_run and store._state is not None: # noqa: SLF001
61-
self.check_and_call(store._state) # noqa: SLF001
74+
self._check_and_call(store._state) # noqa: SLF001
6275

63-
store.subscribe(self.check_and_call)
76+
store.subscribe(self._check_and_call)
6477

65-
def check_and_call(self: Autorun, state: State) -> None:
78+
def inform_subscribers(
79+
self: Autorun[
80+
State,
81+
Action,
82+
Event,
83+
SelectorOutput,
84+
ComparatorOutput,
85+
AutorunOriginalReturnType,
86+
],
87+
) -> None:
88+
for subscriber_ in self._subscriptions.copy():
89+
if isinstance(subscriber_, weakref.ref):
90+
subscriber = subscriber_()
91+
if subscriber is None:
92+
self._subscriptions.discard(subscriber_)
93+
continue
94+
else:
95+
subscriber = subscriber_
96+
subscriber(self._latest_value)
97+
98+
def call_func(
99+
self: Autorun[
100+
State,
101+
Action,
102+
Event,
103+
SelectorOutput,
104+
ComparatorOutput,
105+
AutorunOriginalReturnType,
106+
],
107+
selector_result: SelectorOutput,
108+
previous_result: SelectorOutput | None,
109+
func: Callable[
110+
[SelectorOutput, SelectorOutput],
111+
AutorunOriginalReturnType,
112+
]
113+
| Callable[[SelectorOutput], AutorunOriginalReturnType]
114+
| MethodType,
115+
) -> AutorunOriginalReturnType:
116+
if len(signature(func).parameters) == 1:
117+
return cast(
118+
Callable[[SelectorOutput], AutorunOriginalReturnType],
119+
func,
120+
)(selector_result)
121+
return cast(
122+
Callable[
123+
[SelectorOutput, SelectorOutput | None],
124+
AutorunOriginalReturnType,
125+
],
126+
func,
127+
)(selector_result, previous_result)
128+
129+
def _check_and_call(
130+
self: Autorun[
131+
State,
132+
Action,
133+
Event,
134+
SelectorOutput,
135+
ComparatorOutput,
136+
AutorunOriginalReturnType,
137+
],
138+
state: State,
139+
) -> None:
66140
try:
67141
selector_result = self._selector(state)
68142
except AttributeError:
69143
return
144+
func = self._func() if isinstance(self._func, weakref.ref) else self._func
145+
if func is None:
146+
return
70147
if self._comparator is None:
71148
comparator_result = cast(ComparatorOutput, selector_result)
72149
else:
@@ -75,31 +152,11 @@ def check_and_call(self: Autorun, state: State) -> None:
75152
previous_result = self._last_selector_result
76153
self._last_selector_result = selector_result
77154
self._last_comparator_result = comparator_result
78-
if len(signature(self._func).parameters) == 1:
79-
self._latest_value = cast(
80-
Callable[[SelectorOutput], AutorunOriginalReturnType],
81-
self._func,
82-
)(selector_result)
155+
self._latest_value = self.call_func(selector_result, previous_result, func)
156+
if self._immediate_run:
157+
self.inform_subscribers()
83158
else:
84-
self._latest_value = cast(
85-
Callable[
86-
[SelectorOutput, SelectorOutput | None],
87-
AutorunOriginalReturnType,
88-
],
89-
self._func,
90-
)(
91-
selector_result,
92-
previous_result,
93-
)
94-
for subscriber_ in self._subscriptions.copy():
95-
if isinstance(subscriber_, weakref.ref):
96-
subscriber = subscriber_()
97-
if subscriber is None:
98-
self._subscriptions.discard(subscriber_)
99-
continue
100-
else:
101-
subscriber = subscriber_
102-
subscriber(self._latest_value)
159+
self._store._create_task(cast(Coroutine, self._latest_value)) # noqa: SLF001
103160

104161
def __call__(
105162
self: Autorun[
@@ -112,7 +169,7 @@ def __call__(
112169
],
113170
) -> AutorunOriginalReturnType:
114171
if self._store._state is not None: # noqa: SLF001
115-
self.check_and_call(self._store._state) # noqa: SLF001
172+
self._check_and_call(self._store._state) # noqa: SLF001
116173
return cast(AutorunOriginalReturnType, self._latest_value)
117174

118175
def __repr__(
@@ -161,7 +218,7 @@ def subscribe(
161218
keep_ref = self._options.subscribers_keep_ref
162219
if keep_ref:
163220
callback_ref = callback
164-
elif isinstance(callback, MethodType):
221+
elif inspect.ismethod(callback):
165222
callback_ref = weakref.WeakMethod(callback)
166223
else:
167224
callback_ref = weakref.ref(callback)

redux/basic_types.py

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

4-
from typing import Any, Callable, Generic, Protocol, TypeAlias, TypeGuard
4+
from typing import Any, Callable, Coroutine, Generic, Protocol, TypeAlias, TypeGuard
55

66
from immutable import Immutable
77
from typing_extensions import TypeVar
@@ -16,7 +16,7 @@ class BaseEvent(Immutable):
1616

1717

1818
class EventSubscriptionOptions(Immutable):
19-
run_async: bool = True
19+
immediate_run: bool = False
2020
keep_ref: bool = True
2121

2222

@@ -85,12 +85,14 @@ class CreateStoreOptions(Immutable):
8585
scheduler: Scheduler | None = None
8686
action_middleware: Callable[[BaseAction], Any] | None = None
8787
event_middleware: Callable[[BaseEvent], Any] | None = None
88+
task_creator: Callable[[Coroutine], Any] | None = None
8889

8990

9091
class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]):
9192
default_value: AutorunOriginalReturnType | None = None
9293
initial_run: bool = True
93-
subscribers_immediate_run: bool = True
94+
keep_ref: bool = True
95+
subscribers_immediate_run: bool | None = None
9496
subscribers_keep_ref: bool = True
9597

9698

0 commit comments

Comments
 (0)