Skip to content

Commit 286dbba

Browse files
committed
test(middleware): add middleware tests
1 parent ff25e91 commit 286dbba

File tree

4 files changed

+199
-23
lines changed

4 files changed

+199
-23
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+
## Version 0.14.5
4+
5+
- test(middleware): add middleware tests
6+
37
## Version 0.14.4
48

59
- refactor(test): add the counter id of the failed snapshot to the error message

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-redux"
3-
version = "0.14.4"
3+
version = "0.14.5"
44
description = "Redux implementation for Python"
55
authors = ["Sassan Haradji <[email protected]>"]
66
license = "Apache-2.0"
@@ -50,7 +50,7 @@ inline-quotes = "single"
5050
multiline-quotes = "double"
5151

5252
[tool.ruff.lint.per-file-ignores]
53-
"tests/*" = ["S101", "PLR0915"]
53+
"tests/*" = ["S101", "PLR0915", "PLR2004"]
5454

5555
[tool.ruff.format]
5656
quote-style = 'single'

redux/main.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,33 +109,27 @@ def _call_listeners(self: Store[State, Action, Event], state: State) -> None:
109109
self._create_task(result)
110110

111111
def _run_actions(self: Store[State, Action, Event]) -> None:
112-
while True:
113-
if len(self._actions) == 0:
114-
return
112+
while len(self._actions) > 0:
115113
action = self._actions.pop(0)
116114
if action is not None:
117-
break
118-
result = self.reducer(self._state, action)
119-
if is_complete_reducer_result(result):
120-
self._state = result.state
121-
self._call_listeners(self._state)
122-
self.dispatch([*(result.actions or []), *(result.events or [])])
123-
elif is_state_reducer_result(result):
124-
self._state = result
125-
self._call_listeners(self._state)
126-
127-
if isinstance(action, FinishAction):
128-
self.dispatch(cast(Event, FinishEvent()))
115+
result = self.reducer(self._state, action)
116+
if is_complete_reducer_result(result):
117+
self._state = result.state
118+
self._call_listeners(self._state)
119+
self.dispatch([*(result.actions or []), *(result.events or [])])
120+
elif is_state_reducer_result(result):
121+
self._state = result
122+
self._call_listeners(self._state)
123+
124+
if isinstance(action, FinishAction):
125+
self.dispatch(cast(Event, FinishEvent()))
129126

130127
def _run_event_handlers(self: Store[State, Action, Event]) -> None:
131-
while True:
132-
if len(self._events) == 0:
133-
return
128+
while len(self._events) > 0:
134129
event = self._events.pop(0)
135130
if event is not None:
136-
break
137-
for event_handler in self._event_handlers[type(event)].copy():
138-
self._event_handlers_queue.put_nowait((event_handler, event))
131+
for event_handler in self._event_handlers[type(event)].copy():
132+
self._event_handlers_queue.put_nowait((event_handler, event))
139133

140134
def run(self: Store[State, Action, Event]) -> None:
141135
"""Run the store."""

tests/test_middleware.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# ruff: noqa: D100, D101, D102, D103, D104, D107
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import replace
6+
7+
import pytest
8+
from immutable import Immutable
9+
10+
from redux.basic_types import (
11+
BaseAction,
12+
BaseEvent,
13+
CompleteReducerResult,
14+
CreateStoreOptions,
15+
FinishAction,
16+
FinishEvent,
17+
InitAction,
18+
InitializationActionError,
19+
)
20+
from redux.main import Store
21+
22+
23+
class StateType(Immutable):
24+
value: int
25+
26+
27+
class IncrementAction(BaseAction): ...
28+
29+
30+
class DecrementAction(BaseAction): ...
31+
32+
33+
class SomeEvent(BaseEvent): ...
34+
35+
36+
Action = IncrementAction | DecrementAction | InitAction | FinishAction
37+
38+
39+
def reducer(
40+
state: StateType | None,
41+
action: Action,
42+
) -> StateType | CompleteReducerResult[StateType, Action, SomeEvent | FinishEvent]:
43+
if state is None:
44+
if isinstance(action, InitAction):
45+
return StateType(value=0)
46+
raise InitializationActionError(action)
47+
48+
if isinstance(action, IncrementAction):
49+
return replace(state, value=state.value + 1)
50+
51+
if isinstance(action, DecrementAction):
52+
return replace(state, value=state.value - 1)
53+
54+
return state
55+
56+
57+
class StoreType(Store[StateType, Action, FinishEvent | SomeEvent]):
58+
@property
59+
def state(self: StoreType) -> StateType | None:
60+
return self._state
61+
62+
63+
@pytest.fixture()
64+
def store() -> StoreType:
65+
return StoreType(reducer, options=CreateStoreOptions(auto_init=True))
66+
67+
68+
def test_identity_action_middleware(store: StoreType) -> None:
69+
calls = []
70+
71+
def middleware(action: Action) -> Action:
72+
calls.append(action)
73+
if isinstance(action, IncrementAction):
74+
return DecrementAction()
75+
return action
76+
77+
store.register_action_middleware(middleware)
78+
79+
actions = [
80+
IncrementAction(),
81+
IncrementAction(),
82+
FinishAction(),
83+
]
84+
85+
def check() -> None:
86+
assert calls == actions
87+
assert store.state
88+
assert store.state.value == -2
89+
90+
store.subscribe_event(FinishEvent, check)
91+
92+
for action in actions:
93+
store.dispatch(action)
94+
95+
96+
def test_cancelling_action_middleware(store: StoreType) -> None:
97+
calls = []
98+
99+
def middleware(action: Action) -> Action | None:
100+
calls.append(action)
101+
if len(calls) == 1:
102+
return None
103+
return action
104+
105+
store.register_action_middleware(middleware)
106+
107+
actions = [
108+
IncrementAction(),
109+
IncrementAction(),
110+
FinishAction(),
111+
]
112+
113+
def check() -> None:
114+
assert store.state
115+
assert store.state.value == 1
116+
117+
store.subscribe_event(FinishEvent, check)
118+
119+
for action in actions:
120+
store.dispatch(action)
121+
122+
123+
def test_identity_event_middlewares(store: StoreType) -> None:
124+
calls = []
125+
126+
def middleware(event: SomeEvent) -> SomeEvent | FinishEvent:
127+
calls.append(event)
128+
if len(calls) == 2:
129+
return FinishEvent()
130+
return event
131+
132+
store.register_event_middleware(middleware)
133+
134+
events = [
135+
SomeEvent(),
136+
SomeEvent(),
137+
SomeEvent(),
138+
]
139+
140+
def check() -> None:
141+
assert calls == events
142+
143+
store.subscribe_event(FinishEvent, check)
144+
145+
for event in events:
146+
store.dispatch(event)
147+
148+
149+
def test_cancelling_event_middlewares(store: StoreType) -> None:
150+
calls = []
151+
152+
def middleware(event: SomeEvent | FinishEvent) -> SomeEvent | FinishEvent | None:
153+
calls.append(event)
154+
if len(calls) == 1 and isinstance(event, SomeEvent):
155+
return None
156+
return event
157+
158+
side_effect_calls = []
159+
160+
def some_side_effect(event: SomeEvent) -> None:
161+
side_effect_calls.append(event)
162+
163+
store.register_event_middleware(middleware)
164+
165+
events = [
166+
SomeEvent(),
167+
SomeEvent(),
168+
]
169+
170+
def check() -> None:
171+
assert side_effect_calls == events[1:2]
172+
173+
store.subscribe_event(SomeEvent, some_side_effect)
174+
store.subscribe_event(FinishEvent, check)
175+
176+
for event in events:
177+
store.dispatch(event)
178+
store.dispatch(FinishAction())

0 commit comments

Comments
 (0)