Skip to content

Commit 9de215e

Browse files
committed
refactor: move store serializer from test framework to code Store class
feat: add ability to set custom serializer for store snapshots
1 parent a17c652 commit 9de215e

File tree

5 files changed

+67
-48
lines changed

5 files changed

+67
-48
lines changed

CHANGELOG.md

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

3+
## Version 0.12.1
4+
5+
- refactor: move store serializer from test framework to code `Store` class
6+
- feat: add ability to set custom serializer for store snapshots
7+
38
## Version 0.12.0
49

510
- refactor: improve creating new state classes in `combine_reducers` upon registering/unregistering

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-redux"
3-
version = "0.12.0"
3+
version = "0.12.1"
44
description = "Redux implementation for Python"
55
authors = ["Sassan Haradji <[email protected]>"]
66
license = "Apache-2.0"

redux/basic_types.py

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

4+
from types import NoneType
45
from typing import Any, Callable, Coroutine, Generic, Protocol, TypeAlias, TypeGuard
56

67
from immutable import Immutable
@@ -191,3 +192,14 @@ class CombineReducerRegisterAction(CombineReducerAction):
191192

192193
class CombineReducerUnregisterAction(CombineReducerAction):
193194
key: str
195+
196+
197+
SnapshotAtom = (
198+
int
199+
| float
200+
| str
201+
| bool
202+
| NoneType
203+
| dict[str, 'SnapshotAtom']
204+
| list['SnapshotAtom']
205+
)

redux/main.py

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

4+
import dataclasses
45
import inspect
56
import queue
67
import threading
78
import weakref
89
from asyncio import create_task, iscoroutine
910
from collections import defaultdict
11+
from enum import IntEnum, StrEnum
1012
from inspect import signature
1113
from threading import Lock
14+
from types import NoneType
1215
from typing import Any, Callable, Coroutine, Generic, cast
1316

17+
from immutable import Immutable, is_immutable
18+
1419
from redux.autorun import Autorun
1520
from redux.basic_types import (
1621
Action,
@@ -32,6 +37,7 @@
3237
InitAction,
3338
ReducerType,
3439
SelectorOutput,
40+
SnapshotAtom,
3541
State,
3642
is_complete_reducer_result,
3743
is_state_reducer_result,
@@ -68,6 +74,8 @@ def run(self: _SideEffectRunnerThread[Event]) -> None:
6874

6975

7076
class Store(Generic[State, Action, Event]):
77+
custom_serializer = None
78+
7179
def __init__(
7280
self: Store[State, Action, Event],
7381
reducer: ReducerType[State, Action, Event],
@@ -276,3 +284,42 @@ def decorator(
276284
)
277285

278286
return decorator
287+
288+
def set_custom_serializer(
289+
self: Store,
290+
serializer: Callable[[object | type], SnapshotAtom],
291+
) -> None:
292+
"""Set a custom serializer for the store snapshot."""
293+
self.custom_serializer = serializer
294+
295+
@property
296+
def snapshot(self: Store[State, Action, Event]) -> SnapshotAtom:
297+
return self._serialize_value(self._state)
298+
299+
def _serialize_value(self: Store, obj: object | type) -> SnapshotAtom:
300+
if self.custom_serializer:
301+
return self.custom_serializer(obj)
302+
if is_immutable(obj):
303+
return self._serialize_dataclass_to_dict(obj)
304+
if isinstance(obj, (list, tuple)):
305+
return [self._serialize_value(i) for i in obj]
306+
if callable(obj):
307+
return self._serialize_value(obj())
308+
if isinstance(obj, StrEnum):
309+
return str(obj)
310+
if isinstance(obj, IntEnum):
311+
return int(obj)
312+
if isinstance(obj, (int, float, str, bool, NoneType)):
313+
return obj
314+
msg = f'Unable to serialize object with type {type(obj)}.'
315+
raise ValueError(msg)
316+
317+
def _serialize_dataclass_to_dict(
318+
self: Store,
319+
obj: Immutable,
320+
) -> dict[str, Any]:
321+
result = {}
322+
for field in dataclasses.fields(obj):
323+
value = self._serialize_value(getattr(obj, field.name))
324+
result[field.name] = value
325+
return result

redux/test.py

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22
"""Let the test check snapshots of the window during execution."""
33
from __future__ import annotations
44

5-
import dataclasses
65
import json
76
import os
8-
from enum import IntEnum, StrEnum
9-
from types import NoneType
10-
from typing import TYPE_CHECKING, Any
7+
from typing import TYPE_CHECKING
118

129
import pytest
13-
from immutable import Immutable, is_immutable
1410

1511
if TYPE_CHECKING:
1612
from logging import Logger
@@ -24,9 +20,6 @@
2420
override_store_snapshots = os.environ.get('REDUX_TEST_OVERRIDE_SNAPSHOTS', '0') == '1'
2521

2622

27-
Atom = int | float | str | bool | NoneType | dict[str, 'Atom'] | list['Atom']
28-
29-
3023
class StoreSnapshotContext:
3124
"""Context object for tests taking snapshots of the store."""
3225

@@ -43,44 +36,6 @@ def __init__(
4336
self.logger = logger
4437
self.results_dir.mkdir(exist_ok=True)
4538

46-
def _convert_value(self: StoreSnapshotContext, obj: object | type) -> Atom:
47-
import sys
48-
from pathlib import Path
49-
50-
if is_immutable(obj):
51-
return self._convert_dataclass_to_dict(obj)
52-
if isinstance(obj, (list, tuple)):
53-
return [self._convert_value(i) for i in obj]
54-
if isinstance(obj, type):
55-
file_path = sys.modules[obj.__module__].__file__
56-
if file_path:
57-
return f"""{Path(file_path).relative_to(Path().absolute()).as_posix()}:{
58-
obj.__name__}"""
59-
return f'{obj.__module__}:{obj.__name__}'
60-
if callable(obj):
61-
return self._convert_value(obj())
62-
if isinstance(obj, StrEnum):
63-
return str(obj)
64-
if isinstance(obj, IntEnum):
65-
return int(obj)
66-
if isinstance(obj, (int, float, str, bool, NoneType)):
67-
return obj
68-
self.logger.warning(
69-
'Unable to serialize',
70-
extra={'type': type(obj), 'value': obj},
71-
)
72-
return None
73-
74-
def _convert_dataclass_to_dict(
75-
self: StoreSnapshotContext,
76-
obj: Immutable,
77-
) -> dict[str, Any]:
78-
result = {}
79-
for field in dataclasses.fields(obj):
80-
value = self._convert_value(getattr(obj, field.name))
81-
result[field.name] = value
82-
return result
83-
8439
def set_store(self: StoreSnapshotContext, store: Store) -> None:
8540
"""Set the store to take snapshots of."""
8641
self.store = store
@@ -89,7 +44,7 @@ def set_store(self: StoreSnapshotContext, store: Store) -> None:
8944
def snapshot(self: StoreSnapshotContext) -> str:
9045
"""Return the snapshot of the current state of the store."""
9146
return (
92-
json.dumps(self._convert_value(self.store._state), indent=2) # noqa: SLF001
47+
json.dumps(self.store.snapshot, indent=2)
9348
if self.store._state # noqa: SLF001
9449
else ''
9550
)

0 commit comments

Comments
 (0)