Skip to content

Commit 5eee67c

Browse files
committed
refactor: add WithState class to be the return value of the store.with_state so that it can report correct signature of its __call__ method
1 parent 49f45d3 commit 5eee67c

File tree

4 files changed

+101
-12
lines changed

4 files changed

+101
-12
lines changed

CHANGELOG.md

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

3+
## Upcoming
4+
5+
- refactor: add `WithState` class to be the return value of the `store.with_state` so that it can report correct signature of its `__call__` method
6+
37
## Version 0.20.2
48

59
- chore(lint): update `ruff` to `0.10.0` and fix linting issues, make `store.subscribe` private

redux/main.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
)
6060
from redux.serialization_mixin import SerializationMixin
6161
from redux.side_effect_runner import SideEffectRunnerThread
62+
from redux.with_state import WithState
6263

6364
if TYPE_CHECKING:
6465
from collections.abc import Awaitable, Callable
@@ -439,15 +440,7 @@ def with_state_decorator(
439440
ReturnType,
440441
],
441442
) -> Callable[Args, ReturnType]:
442-
def wrapper(*args: Args.args, **kwargs: Args.kwargs) -> ReturnType:
443-
if self._state is None:
444-
msg = 'Store has not been initialized yet.'
445-
raise RuntimeError(msg)
446-
return func(selector(self._state), *args, **kwargs)
447-
448-
wrapper.__name__ = f'with_state:{func.__name__}'
449-
450-
return wrapper
443+
return WithState(store=self, selector=selector, func=cast('Callable', func))
451444

452445
return with_state_decorator
453446

redux/with_state.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""A wrapper for functions that require the current state of the store."""
2+
3+
from __future__ import annotations
4+
5+
import inspect
6+
from typing import TYPE_CHECKING, Concatenate, Generic
7+
8+
from redux.basic_types import Action, Args, Event, ReturnType, SelectorOutput, State
9+
10+
if TYPE_CHECKING:
11+
from collections.abc import Callable
12+
13+
from redux.main import Store
14+
15+
16+
class WithState(Generic[State, Action, Event, SelectorOutput, ReturnType, Args]):
17+
"""A wrapper for functions that require the current state of the store."""
18+
19+
def __init__(
20+
self: WithState,
21+
*,
22+
store: Store[State, Action, Event],
23+
selector: Callable[[State], SelectorOutput],
24+
func: Callable[
25+
Concatenate[SelectorOutput, Args],
26+
ReturnType,
27+
],
28+
) -> None:
29+
"""Initialize the WithState instance."""
30+
self._store = store
31+
self._selector = selector
32+
self._func = func
33+
signature = inspect.signature(func)
34+
parameters = list(signature.parameters.values())[1:] # Skip the first parameter
35+
if parameters and parameters[0].kind in [
36+
inspect.Parameter.POSITIONAL_ONLY,
37+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
38+
]:
39+
parameters = parameters[1:]
40+
self._signature = signature.replace(parameters=parameters)
41+
42+
def __call__(
43+
self: WithState[
44+
State,
45+
Action,
46+
Event,
47+
SelectorOutput,
48+
ReturnType,
49+
Args,
50+
],
51+
*args: Args.args,
52+
**kwargs: Args.kwargs,
53+
) -> ReturnType:
54+
"""Call the wrapped function with the current state of the store."""
55+
if self._store._state is None: # noqa: SLF001
56+
msg = 'Store has not been initialized yet.'
57+
raise RuntimeError(msg)
58+
return self._func(self._selector(self._store._state), *args, **kwargs) # noqa: SLF001
59+
60+
def __repr__(
61+
self: WithState[
62+
State,
63+
Action,
64+
Event,
65+
SelectorOutput,
66+
ReturnType,
67+
Args,
68+
],
69+
) -> str:
70+
"""Return the string representation of the WithState instance."""
71+
return super().__repr__() + f'(func: {self._func})'
72+
73+
@property
74+
def __signature__(
75+
self: WithState[
76+
State,
77+
Action,
78+
Event,
79+
SelectorOutput,
80+
ReturnType,
81+
Args,
82+
],
83+
) -> inspect.Signature:
84+
"""Get the signature of the wrapped function."""
85+
return self._signature

tests/test_with_state.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,15 @@ def test_with_state_for_uninitialized_store(
8282
mocker: MockerFixture,
8383
) -> None:
8484
"""Test `with_state` decorator for uninitialized store."""
85-
_check = mocker.stub()
86-
check = store.with_state(lambda state: state.value)(_check)
85+
86+
class X:
87+
def check(self: X, value: int) -> None:
88+
assert value == 0
89+
90+
instance = X()
91+
92+
check_spy = mocker.spy(instance, 'check')
93+
check = store.with_state(lambda state: state.value)(instance.check)
8794

8895
with pytest.raises(RuntimeError, match=r'^Store has not been initialized yet.$'):
8996
check()
@@ -94,4 +101,4 @@ def test_with_state_for_uninitialized_store(
94101

95102
store.dispatch(FinishAction())
96103

97-
_check.assert_called_once_with(0)
104+
check_spy.assert_called_once_with(0)

0 commit comments

Comments
 (0)