Skip to content

Commit 7d64e10

Browse files
committed
test: write tests for different features of the api
refactor: rename certain names in the api to better reflect their job refactor: store_snapshot now puts snapshot files in a hierarchical directory structure based on the test module and test name
1 parent fcc6d6e commit 7d64e10

File tree

89 files changed

+1394
-242
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1394
-242
lines changed

.github/workflows/integration_delivery.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
workflow_dispatch:
77

88
env:
9-
PYTHON_VERSION: '3.11'
9+
PYTHON_VERSION: "3.11"
1010

1111
jobs:
1212
dependencies:
@@ -121,7 +121,7 @@ jobs:
121121
key: poetry-${{ hashFiles('poetry.lock') }}
122122

123123
- name: Test
124-
run: poetry run poe test --cov-report=xml --cov-report=html
124+
run: poetry run poe test
125125

126126
- name: Prepare list of JSON files with mismatching pairs
127127
if: failure()

CHANGELOG.md

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

3+
## Version 0.12.3
4+
5+
- test: write tests for different features of the api
6+
- refactor: rename certain names in the api to better reflect their job
7+
- refactor: store_snapshot now puts snapshot files in a hierarchical directory structure
8+
based on the test module and test name
9+
- fix: sort JSON keys in `snapshot_store`'s `json_snapshot`
10+
- test: cover most features with tests
11+
312
## Version 0.12.2
413

514
- docs: update path of demos migrated to tests in `README.md`

poetry.lock

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

pyproject.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-redux"
3-
version = "0.12.2"
3+
version = "0.12.3"
44
description = "Redux implementation for Python"
55
authors = ["Sassan Haradji <[email protected]>"]
66
license = "Apache-2.0"
@@ -11,6 +11,7 @@ packages = [{ include = "redux" }]
1111
python = "^3.11"
1212
python-immutable = "^1.0.5"
1313
typing-extensions = "^4.9.0"
14+
pytest-timeout = "^2.3.1"
1415

1516
[tool.poetry.group.dev]
1617
optional = true
@@ -33,7 +34,7 @@ todo_demo = "todo_demo:main"
3334
[tool.poe.tasks]
3435
lint = "ruff check . --unsafe-fixes"
3536
typecheck = "pyright -p pyproject.toml ."
36-
test = "pytest --cov=redux --cov-report=term-missing"
37+
test = "pytest --cov=redux --cov-report=term-missing --cov-report=html --cov-report=xml"
3738
sanity = ["typecheck", "lint", "test"]
3839

3940
[tool.ruff]
@@ -48,7 +49,7 @@ inline-quotes = "single"
4849
multiline-quotes = "double"
4950

5051
[tool.ruff.lint.per-file-ignores]
51-
"tests/*" = ["S101"]
52+
"tests/*" = ["S101", "PLR0915"]
5253

5354
[tool.ruff.format]
5455
quote-style = 'single'
@@ -58,3 +59,14 @@ profile = "black"
5859

5960
[tool.pyright]
6061
exclude = ['typings']
62+
63+
[tool.pytest.ini_options]
64+
log_cli = 1
65+
log_cli_level = 'ERROR'
66+
timeout = 4
67+
68+
[tool.coverage.report]
69+
exclude_also = ["if TYPE_CHECKING:"]
70+
71+
[tool.coverage.run]
72+
omit = ['redux/test.py']

redux/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Redux-like state management for Python."""
2+
23
from .basic_types import (
34
AutorunDecorator,
45
AutorunOptions,

redux/autorun.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,11 @@ def __init__( # noqa: PLR0913
6464
Callable[[AutorunOriginalReturnType], Any]
6565
| weakref.ref[Callable[[AutorunOriginalReturnType], Any]]
6666
] = set()
67-
self._immediate_run = (
68-
not iscoroutinefunction(func)
69-
if options.subscribers_immediate_run is None
70-
else options.subscribers_immediate_run
71-
)
7267

7368
if self._options.initial_run and store._state is not None: # noqa: SLF001
7469
self._check_and_call(store._state) # noqa: SLF001
7570

76-
store.subscribe(self._check_and_call)
71+
store.subscribe(self._check_and_call, keep_ref=options.keep_ref)
7772

7873
def inform_subscribers(
7974
self: Autorun[
@@ -142,21 +137,31 @@ def _check_and_call(
142137
except AttributeError:
143138
return
144139
func = self._func() if isinstance(self._func, weakref.ref) else self._func
145-
if func is None:
146-
return
147-
if self._comparator is None:
148-
comparator_result = cast(ComparatorOutput, selector_result)
149-
else:
150-
comparator_result = self._comparator(state)
151-
if comparator_result != self._last_comparator_result:
152-
previous_result = self._last_selector_result
153-
self._last_selector_result = selector_result
154-
self._last_comparator_result = comparator_result
155-
self._latest_value = self.call_func(selector_result, previous_result, func)
156-
if self._immediate_run:
157-
self.inform_subscribers()
140+
if func:
141+
if self._comparator is None:
142+
comparator_result = cast(ComparatorOutput, selector_result)
158143
else:
159-
self._store._create_task(cast(Coroutine, self._latest_value)) # noqa: SLF001
144+
try:
145+
comparator_result = self._comparator(state)
146+
except AttributeError:
147+
return
148+
if comparator_result != self._last_comparator_result:
149+
previous_result = self._last_selector_result
150+
self._last_selector_result = selector_result
151+
self._last_comparator_result = comparator_result
152+
self._latest_value = self.call_func(
153+
selector_result,
154+
previous_result,
155+
func,
156+
)
157+
if iscoroutinefunction(func):
158+
task = self._store._async_loop.create_task( # noqa: SLF001
159+
cast(Coroutine, self._latest_value),
160+
)
161+
task.add_done_callback(lambda _: self.inform_subscribers())
162+
self._latest_value = cast(AutorunOriginalReturnType, task)
163+
else:
164+
self.inform_subscribers()
160165

161166
def __call__(
162167
self: Autorun[
@@ -168,8 +173,9 @@ def __call__(
168173
AutorunOriginalReturnType,
169174
],
170175
) -> AutorunOriginalReturnType:
171-
if self._store._state is not None: # noqa: SLF001
172-
self._check_and_call(self._store._state) # noqa: SLF001
176+
state = self._store._state # noqa: SLF001
177+
if state is not None:
178+
self._check_and_call(state)
173179
return cast(AutorunOriginalReturnType, self._latest_value)
174180

175181
def __repr__(
@@ -209,11 +215,11 @@ def subscribe(
209215
],
210216
callback: Callable[[AutorunOriginalReturnType], Any],
211217
*,
212-
immediate_run: bool | None = None,
218+
initial_run: bool | None = None,
213219
keep_ref: bool | None = None,
214220
) -> Callable[[], None]:
215-
if immediate_run is None:
216-
immediate_run = self._options.subscribers_immediate_run
221+
if initial_run is None:
222+
initial_run = self._options.subscribers_initial_run
217223
if keep_ref is None:
218224
keep_ref = self._options.subscribers_keep_ref
219225
if keep_ref:
@@ -224,10 +230,16 @@ def subscribe(
224230
callback_ref = weakref.ref(callback)
225231
self._subscriptions.add(callback_ref)
226232

227-
if immediate_run:
233+
if initial_run:
228234
callback(self.value)
229235

230236
def unsubscribe() -> None:
231-
self._subscriptions.discard(callback_ref)
237+
callback = (
238+
callback_ref()
239+
if isinstance(callback_ref, weakref.ref)
240+
else callback_ref
241+
)
242+
if callback is not None:
243+
self._subscriptions.discard(callback)
232244

233245
return unsubscribe

redux/basic_types.py

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
from __future__ import annotations
33

44
from types import NoneType
5-
from typing import Any, Callable, Coroutine, Generic, Protocol, TypeAlias, TypeGuard
5+
from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol, TypeAlias, TypeGuard
66

77
from immutable import Immutable
88
from typing_extensions import TypeVar
99

10+
if TYPE_CHECKING:
11+
import asyncio
1012

11-
class BaseAction(Immutable):
12-
...
1313

14+
class BaseAction(Immutable): ...
1415

15-
class BaseEvent(Immutable):
16-
...
16+
17+
class BaseEvent(Immutable): ...
1718

1819

1920
class EventSubscriptionOptions(Immutable):
@@ -51,16 +52,13 @@ def __init__(self: InitializationActionError, action: BaseAction) -> None:
5152
)
5253

5354

54-
class InitAction(BaseAction):
55-
...
55+
class InitAction(BaseAction): ...
5656

5757

58-
class FinishAction(BaseAction):
59-
...
58+
class FinishAction(BaseAction): ...
6059

6160

62-
class FinishEvent(BaseEvent):
63-
...
61+
class FinishEvent(BaseEvent): ...
6462

6563

6664
def is_complete_reducer_result(
@@ -76,8 +74,7 @@ def is_state_reducer_result(
7674

7775

7876
class Scheduler(Protocol):
79-
def __call__(self: Scheduler, callback: Callable, *, interval: bool) -> None:
80-
...
77+
def __call__(self: Scheduler, callback: Callable, *, interval: bool) -> None: ...
8178

8279

8380
class CreateStoreOptions(Immutable):
@@ -86,14 +83,14 @@ class CreateStoreOptions(Immutable):
8683
scheduler: Scheduler | None = None
8784
action_middleware: Callable[[BaseAction], Any] | None = None
8885
event_middleware: Callable[[BaseEvent], Any] | None = None
89-
task_creator: Callable[[Coroutine], Any] | None = None
86+
async_loop: asyncio.AbstractEventLoop | None = None
9087

9188

9289
class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]):
9390
default_value: AutorunOriginalReturnType | None = None
9491
initial_run: bool = True
9592
keep_ref: bool = True
96-
subscribers_immediate_run: bool | None = None
93+
subscribers_initial_run: bool = True
9794
subscribers_keep_ref: bool = True
9895

9996

@@ -108,8 +105,7 @@ def __call__(
108105
State,
109106
SelectorOutput,
110107
AutorunOriginalReturnType,
111-
]:
112-
...
108+
]: ...
113109

114110

115111
class AutorunDecorator(
@@ -124,29 +120,25 @@ def __call__(
124120
self: AutorunDecorator,
125121
func: Callable[[SelectorOutput], AutorunOriginalReturnType]
126122
| Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType],
127-
) -> AutorunReturnType[AutorunOriginalReturnType]:
128-
...
123+
) -> AutorunReturnType[AutorunOriginalReturnType]: ...
129124

130125

131126
class AutorunReturnType(
132127
Protocol,
133128
Generic[AutorunOriginalReturnType],
134129
):
135-
def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType:
136-
...
130+
def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType: ...
137131

138132
@property
139-
def value(self: AutorunReturnType) -> AutorunOriginalReturnType:
140-
...
133+
def value(self: AutorunReturnType) -> AutorunOriginalReturnType: ...
141134

142135
def subscribe(
143136
self: AutorunReturnType,
144137
callback: Callable[[AutorunOriginalReturnType], Any],
145138
*,
146-
immediate_run: bool | None = None,
139+
initial_run: bool | None = None,
147140
keep_ref: bool | None = None,
148-
) -> Callable[[], None]:
149-
...
141+
) -> Callable[[], None]: ...
150142

151143

152144
class EventSubscriber(Protocol):
@@ -156,8 +148,7 @@ def __call__(
156148
handler: EventHandler[Event],
157149
*,
158150
options: EventSubscriptionOptions | None = None,
159-
) -> Callable[[], None]:
160-
...
151+
) -> Callable[[], None]: ...
161152

162153

163154
DispatchParameters: TypeAlias = Action | Event | list[Action | Event]
@@ -169,8 +160,7 @@ def __call__(
169160
*items: Action | Event | list[Action | Event],
170161
with_state: Callable[[State | None], Action | Event | list[Action | Event]]
171162
| None = None,
172-
) -> None:
173-
...
163+
) -> None: ...
174164

175165

176166
class BaseCombineReducerState(Immutable):

0 commit comments

Comments
 (0)