Skip to content

Commit bf9b28d

Browse files
committed
refactor: improve weakref handling in store listeners and event handlers, remove manual weakref handling in SideEffectRunner, optimize Autorun checks and subscription logic
1 parent 1548a3d commit bf9b28d

File tree

9 files changed

+29
-45
lines changed

9 files changed

+29
-45
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: use custom value `NOT_SET = object()` instead of `None` to signal the absence of a value for the `default_value` parameter in `AutorunOptions` and internally in `Autorun` class for properties storing last selector result and last call result
66
- build: switch versioning source to `version.py`, support Python 3.14
7+
- refactor: improve weakref handling in store listeners and event handlers, remove manual `weakref` handling in `SideEffectRunner`, optimize `Autorun` checks and subscription logic
78

89
## Version 0.24.0
910

demo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ def inverse_reducer(
112112

113113
reducer, reducer_id = combine_reducers(
114114
state_type=StateType,
115-
action_type=ActionType, # pyright: ignore [reportArgumentType]
116-
event_type=SleepEvent | PrintEvent, # pyright: ignore [reportArgumentType]
115+
action_type=ActionType, # type: ignore [reportArgumentType]
116+
event_type=SleepEvent | PrintEvent, # type: ignore [reportArgumentType]
117117
straight=straight_reducer,
118118
base10=base10_reducer,
119119
)

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ profile = "black"
9292

9393
[tool.pyright]
9494
exclude = ['typings', '.venv']
95-
filterwarnings = 'error'
9695

9796
[tool.pytest.ini_options]
9897
log_cli = true

redux/autorun.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def __init__( # noqa: C901, PLR0912
128128
else:
129129
self._func = weakref.ref(func, self.unsubscribe)
130130
self._is_coroutine = (
131-
asyncio.coroutines._is_coroutine # pyright: ignore [reportAttributeAccessIssue] # noqa: SLF001
131+
asyncio.coroutines._is_coroutine # type: ignore [reportAttributeAccessIssue] # noqa: SLF001
132132
if asyncio.iscoroutinefunction(func) and options.auto_await is False
133133
else None
134134
)
@@ -231,11 +231,9 @@ def check(
231231
Args,
232232
ReturnType,
233233
],
234-
state: State | None,
234+
state: State,
235235
) -> bool:
236236
"""Check if the autorun should be called based on the current state."""
237-
if state is None:
238-
return False
239237
try:
240238
selector_result = self._selector(state)
241239
except AttributeError:
@@ -317,7 +315,7 @@ def __call__(
317315
if self._should_be_called or args or kwargs or not self._options.memoization:
318316
self._should_be_called = False
319317
self.call(*args, **kwargs)
320-
return cast('ReturnType', self._latest_value)
318+
return self._latest_value
321319

322320
def __repr__(
323321
self: Autorun[
@@ -349,7 +347,7 @@ def value(
349347
],
350348
) -> ReturnType:
351349
"""Get the latest value of the autorun function."""
352-
return cast('ReturnType', self._latest_value)
350+
return self._latest_value
353351

354352
def subscribe(
355353
self: Autorun[
@@ -379,7 +377,7 @@ def subscribe(
379377
callback_ref = weakref.ref(callback)
380378
self._subscriptions.add(callback_ref)
381379

382-
if initial_run:
380+
if initial_run and self.value is not NOT_SET:
383381
callback(self.value)
384382

385383
def unsubscribe() -> None:

redux/main.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import contextlib
67
import inspect
78
import queue
89
import time
@@ -120,19 +121,11 @@ def __init__(
120121

121122
def _call_listeners(self: Store[State, Action, Event], state: State) -> None:
122123
for listener_ in self._listeners.copy():
123-
if isinstance(listener_, weakref.ref):
124-
listener = listener_()
125-
if listener is None:
126-
msg = (
127-
'Listener has been garbage collected. '
128-
'Consider using `keep_ref=True` if it suits your use case.'
129-
)
130-
raise RuntimeError(msg)
131-
else:
132-
listener = listener_
133-
result = listener(state)
134-
if asyncio.iscoroutine(result) and self.store_options.task_creator:
135-
self.store_options.task_creator(result)
124+
listener = listener_() if isinstance(listener_, weakref.ref) else listener_
125+
if listener is not None:
126+
result = listener(state)
127+
if asyncio.iscoroutine(result) and self.store_options.task_creator:
128+
self.store_options.task_creator(result)
136129

137130
def _run_actions(self: Store[State, Action, Event]) -> None:
138131
while len(self._actions) > 0:
@@ -248,7 +241,8 @@ def _subscribe(
248241
"""Subscribe to state changes."""
249242

250243
def unsubscribe(_: weakref.ref | None = None) -> None:
251-
return self._listeners.remove(listener_ref)
244+
with contextlib.suppress(KeyError):
245+
self._listeners.remove(listener_ref)
252246

253247
if keep_ref:
254248
listener_ref = listener
@@ -269,18 +263,19 @@ def subscribe_event(
269263
keep_ref: bool = True,
270264
) -> SubscribeEventCleanup:
271265
"""Subscribe to events."""
266+
267+
def unsubscribe(_: weakref.ref | None = None) -> None:
268+
self._event_handlers[cast('Event', event_type)].discard(handler_ref)
269+
272270
if keep_ref:
273271
handler_ref = handler
274272
elif inspect.ismethod(handler):
275-
handler_ref = weakref.WeakMethod(handler)
273+
handler_ref = weakref.WeakMethod(handler, unsubscribe)
276274
else:
277-
handler_ref = weakref.ref(handler)
275+
handler_ref = weakref.ref(handler, unsubscribe)
278276

279277
self._event_handlers[cast('Event', event_type)].add(handler_ref)
280278

281-
def unsubscribe() -> None:
282-
self._event_handlers[cast('Event', event_type)].discard(handler_ref)
283-
284279
return SubscribeEventCleanup(unsubscribe=unsubscribe, handler=handler)
285280

286281
def _wait_for_store_to_finish(self: Store[State, Action, Event]) -> None:
@@ -438,7 +433,7 @@ def wrapper(*args: Args.args, **kwargs: Args.kwargs) -> ReturnType:
438433

439434
signature = signature_without_selector(func)
440435
wrapped = wraps(cast('Any', func))(wrapper)
441-
wrapped.__signature__ = signature # pyright: ignore [reportAttributeAccessIssue]
436+
wrapped.__signature__ = signature # type: ignore [reportAttributeAccessIssue]
442437

443438
return wrapped
444439

redux/side_effect_runner.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import contextlib
66
import inspect
77
import threading
8-
import weakref
98
from asyncio import Handle, iscoroutine
109
from typing import TYPE_CHECKING, Any, Generic, cast
1110

@@ -41,13 +40,7 @@ def run(self: SideEffectRunner[Event]) -> None:
4140
try:
4241
if task is None:
4342
break
44-
event_handler_, event = task
45-
if isinstance(event_handler_, weakref.ref):
46-
event_handler = event_handler_()
47-
if event_handler is None:
48-
continue
49-
else:
50-
event_handler = event_handler_
43+
event_handler, event = task
5144
parameters = 1
5245
with contextlib.suppress(Exception):
5346
parameters = len(

redux_pytest/fixtures/snapshot.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
if TYPE_CHECKING:
1818
from collections.abc import Callable
1919

20-
from _pytest.fixtures import SubRequest # pyright: ignore[reportPrivateImportUsage]
20+
from _pytest.fixtures import SubRequest # type: ignore[reportPrivateImportUsage]
2121

2222
from redux.main import Store
2323

@@ -102,8 +102,6 @@ def take(
102102
)
103103
raise RuntimeError(msg)
104104

105-
from pathlib import Path
106-
107105
filename = self.get_filename(title)
108106
path = Path(self.results_dir / filename)
109107
json_path = path.with_suffix('.jsonc')

tests/test_features.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ def inverse_reducer(
124124
def reducer() -> Reducer:
125125
return combine_reducers(
126126
state_type=StateType,
127-
action_type=ActionType, # pyright: ignore [reportArgumentType]
128-
event_type=SleepEvent | PrintEvent, # pyright: ignore [reportArgumentType]
127+
action_type=ActionType, # type: ignore [reportArgumentType]
128+
event_type=SleepEvent | PrintEvent, # type: ignore [reportArgumentType]
129129
straight=straight_reducer,
130130
base10=base10_reducer,
131131
)

tests/test_weakref.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def method_with_keep_ref(self: AutorunClass, value: int) -> int:
6565
self.store_snapshot.take(title='autorun_method_with_keep_ref')
6666
return value
6767

68-
def method_without_keep_ref(self: AutorunClass, _: int) -> int:
68+
def method_without_keep_ref(self: AutorunClass, _: int) -> None:
6969
pytest.fail('This should never be called')
7070

7171

@@ -128,7 +128,7 @@ def render_with_keep_ref(value: int) -> int:
128128
lambda state: state.value,
129129
options=AutorunOptions(keep_ref=False, initial_call=False),
130130
)
131-
def render_without_keep_ref(_: int) -> int:
131+
def render_without_keep_ref(_: int) -> None:
132132
pytest.fail('This should never be called')
133133

134134
ref = weakref.ref(render_with_keep_ref)

0 commit comments

Comments
 (0)