From 7d64e1002b5fa7a3a55f9a56cbfb3f2c1de01b93 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Sun, 17 Mar 2024 12:42:54 +0400 Subject: [PATCH] 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 --- .github/workflows/integration_delivery.yml | 4 +- CHANGELOG.md | 9 + poetry.lock | 16 +- pyproject.toml | 18 +- redux/__init__.py | 1 + redux/autorun.py | 66 ++-- redux/basic_types.py | 50 ++- redux/main.py | 55 +-- redux/test.py | 106 ++++-- tests/conftest.py | 11 +- tests/results/store:test_general-013.json | 9 - tests/results/store:test_general-014.json | 9 - .../test_autorun/callability/store:000.jsonc | 4 + .../test_autorun/callability/store:001.jsonc | 4 + .../test_autorun/callability/store:002.jsonc | 4 + .../test_autorun/callability/store:003.jsonc | 4 + .../test_autorun/general/store:000.jsonc | 4 + .../test_autorun/general/store:001.jsonc | 4 + .../test_autorun/general/store:002.jsonc | 4 + .../test_autorun/general/store:003.jsonc | 4 + .../test_autorun/subscription/store:000.jsonc | 4 + .../test_autorun/subscription/store:001.jsonc | 4 + .../test_autorun/subscription/store:002.jsonc | 4 + .../test_autorun/subscription/store:003.jsonc | 4 + .../value_property/store:000.jsonc | 4 + .../value_property/store:001.jsonc | 4 + .../value_property/store:002.jsonc | 4 + .../value_property/store:003.jsonc | 4 + .../with_comparator/store:000.jsonc | 4 + .../with_comparator/store:001.jsonc | 4 + .../with_comparator/store:002.jsonc | 4 + .../store:000.jsonc | 4 + .../store:001.jsonc | 4 + .../store:002.jsonc | 4 + .../with_old_value/store:000.jsonc | 4 + .../with_old_value/store:001.jsonc | 4 + .../with_old_value/store:002.jsonc | 4 + .../with_old_value/store:003.jsonc | 4 + .../general/store:000.jsonc} | 9 +- .../general/store:001.jsonc} | 9 +- .../general/store:002.jsonc} | 9 +- .../general/store:003.jsonc} | 3 +- .../test_features/general/store:004.jsonc | 10 + .../general/store:005.jsonc} | 7 +- .../general/store:autorun:000.jsonc} | 9 +- .../general/store:autorun:001.jsonc} | 9 +- .../general/store:autorun:002.jsonc} | 9 +- .../general/store:autorun:003.jsonc | 10 + .../general/store:autorun:004.jsonc} | 7 +- .../store:autorun_subscription:000.jsonc | 10 + .../store:autorun_subscription:001.jsonc | 10 + .../store:autorun_subscription:002.jsonc | 13 + .../store:autorun_subscription:003.jsonc | 10 + .../store:autorun_subscription:004.jsonc | 10 + .../general/store:initialization:000.jsonc | 2 + .../general/store:subscription:000.jsonc} | 9 +- .../general/store:subscription:001.jsonc} | 9 +- .../general/store:subscription:002.jsonc} | 9 +- .../general/store:subscription:003.jsonc} | 3 +- .../general/store:subscription:004.jsonc | 10 + .../general/store:subscription:005.jsonc | 10 + .../general/store:subscription:006.jsonc | 10 + .../todo/store:000.jsonc} | 5 +- .../todo/store:001.jsonc} | 7 +- .../todo/store:002.jsonc} | 7 +- .../test_weakref/autorun/store:000.jsonc | 4 + .../test_weakref/autorun/store:001.jsonc | 4 + ...ore:autorun_method_with_keep_ref:000.jsonc | 4 + ...ore:autorun_method_with_keep_ref:001.jsonc | 4 + .../store:000.jsonc | 4 + .../store:001.jsonc | 4 + ...ore:autorun_method_with_keep_ref:000.jsonc | 4 + ...ore:autorun_method_with_keep_ref:001.jsonc | 4 + .../autorun_subscription/store:000.jsonc | 4 + .../autorun_subscription/store:001.jsonc | 4 + .../store:autorun_subscription:000.jsonc | 4 + .../store:autorun_subscription:001.jsonc | 4 + .../event_subscription/store:000.jsonc | 4 + ...ubscription_method_with_keep_ref:000.jsonc | 4 + .../test_weakref/subscription/store:000.jsonc | 4 + .../test_weakref/subscription/store:001.jsonc | 4 + ...ubscription_method_with_keep_ref:000.jsonc | 4 + ...ubscription_method_with_keep_ref:001.jsonc | 4 + tests/test_async.py | 116 +++++++ tests/test_autorun.py | 237 +++++++++++++ tests/test_features.py | 104 +++--- tests/test_serialization.py | 84 +++++ tests/test_todo.py | 12 +- tests/test_weakref.py | 328 ++++++++++++++++++ 89 files changed, 1394 insertions(+), 242 deletions(-) delete mode 100644 tests/results/store:test_general-013.json delete mode 100644 tests/results/store:test_general-014.json create mode 100644 tests/results/test_autorun/callability/store:000.jsonc create mode 100644 tests/results/test_autorun/callability/store:001.jsonc create mode 100644 tests/results/test_autorun/callability/store:002.jsonc create mode 100644 tests/results/test_autorun/callability/store:003.jsonc create mode 100644 tests/results/test_autorun/general/store:000.jsonc create mode 100644 tests/results/test_autorun/general/store:001.jsonc create mode 100644 tests/results/test_autorun/general/store:002.jsonc create mode 100644 tests/results/test_autorun/general/store:003.jsonc create mode 100644 tests/results/test_autorun/subscription/store:000.jsonc create mode 100644 tests/results/test_autorun/subscription/store:001.jsonc create mode 100644 tests/results/test_autorun/subscription/store:002.jsonc create mode 100644 tests/results/test_autorun/subscription/store:003.jsonc create mode 100644 tests/results/test_autorun/value_property/store:000.jsonc create mode 100644 tests/results/test_autorun/value_property/store:001.jsonc create mode 100644 tests/results/test_autorun/value_property/store:002.jsonc create mode 100644 tests/results/test_autorun/value_property/store:003.jsonc create mode 100644 tests/results/test_autorun/with_comparator/store:000.jsonc create mode 100644 tests/results/test_autorun/with_comparator/store:001.jsonc create mode 100644 tests/results/test_autorun/with_comparator/store:002.jsonc create mode 100644 tests/results/test_autorun/with_comparator_and_old_value/store:000.jsonc create mode 100644 tests/results/test_autorun/with_comparator_and_old_value/store:001.jsonc create mode 100644 tests/results/test_autorun/with_comparator_and_old_value/store:002.jsonc create mode 100644 tests/results/test_autorun/with_old_value/store:000.jsonc create mode 100644 tests/results/test_autorun/with_old_value/store:001.jsonc create mode 100644 tests/results/test_autorun/with_old_value/store:002.jsonc create mode 100644 tests/results/test_autorun/with_old_value/store:003.jsonc rename tests/results/{store:test_general-000.json => test_features/general/store:000.jsonc} (88%) rename tests/results/{store:test_general-004.json => test_features/general/store:001.jsonc} (88%) rename tests/results/{store:test_general-006.json => test_features/general/store:002.jsonc} (91%) rename tests/results/{store:test_general-009.json => test_features/general/store:003.jsonc} (88%) create mode 100644 tests/results/test_features/general/store:004.jsonc rename tests/results/{store:test_general-012.json => test_features/general/store:005.jsonc} (65%) rename tests/results/{store:test_general-001.json => test_features/general/store:autorun:000.jsonc} (83%) rename tests/results/{store:test_general-002.json => test_features/general/store:autorun:001.jsonc} (83%) rename tests/results/{store:test_general-007.json => test_features/general/store:autorun:002.jsonc} (86%) create mode 100644 tests/results/test_features/general/store:autorun:003.jsonc rename tests/results/{store:test_general-011.json => test_features/general/store:autorun:004.jsonc} (61%) create mode 100644 tests/results/test_features/general/store:autorun_subscription:000.jsonc create mode 100644 tests/results/test_features/general/store:autorun_subscription:001.jsonc create mode 100644 tests/results/test_features/general/store:autorun_subscription:002.jsonc create mode 100644 tests/results/test_features/general/store:autorun_subscription:003.jsonc create mode 100644 tests/results/test_features/general/store:autorun_subscription:004.jsonc create mode 100644 tests/results/test_features/general/store:initialization:000.jsonc rename tests/results/{store:test_general-003.json => test_features/general/store:subscription:000.jsonc} (80%) rename tests/results/{store:test_general-005.json => test_features/general/store:subscription:001.jsonc} (84%) rename tests/results/{store:test_general-008.json => test_features/general/store:subscription:002.jsonc} (84%) rename tests/results/{store:test_general-010.json => test_features/general/store:subscription:003.jsonc} (80%) create mode 100644 tests/results/test_features/general/store:subscription:004.jsonc create mode 100644 tests/results/test_features/general/store:subscription:005.jsonc create mode 100644 tests/results/test_features/general/store:subscription:006.jsonc rename tests/results/{store:test_todo-000.json => test_todo/todo/store:000.jsonc} (90%) rename tests/results/{store:test_todo-001.json => test_todo/todo/store:001.jsonc} (94%) rename tests/results/{store:test_todo-002.json => test_todo/todo/store:002.jsonc} (94%) create mode 100644 tests/results/test_weakref/autorun/store:000.jsonc create mode 100644 tests/results/test_weakref/autorun/store:001.jsonc create mode 100644 tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:000.jsonc create mode 100644 tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:001.jsonc create mode 100644 tests/results/test_weakref/autorun_method_subscription/store:000.jsonc create mode 100644 tests/results/test_weakref/autorun_method_subscription/store:001.jsonc create mode 100644 tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:000.jsonc create mode 100644 tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:001.jsonc create mode 100644 tests/results/test_weakref/autorun_subscription/store:000.jsonc create mode 100644 tests/results/test_weakref/autorun_subscription/store:001.jsonc create mode 100644 tests/results/test_weakref/autorun_subscription/store:autorun_subscription:000.jsonc create mode 100644 tests/results/test_weakref/autorun_subscription/store:autorun_subscription:001.jsonc create mode 100644 tests/results/test_weakref/event_subscription/store:000.jsonc create mode 100644 tests/results/test_weakref/event_subscription_method/store:event_subscription_method_with_keep_ref:000.jsonc create mode 100644 tests/results/test_weakref/subscription/store:000.jsonc create mode 100644 tests/results/test_weakref/subscription/store:001.jsonc create mode 100644 tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:000.jsonc create mode 100644 tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:001.jsonc create mode 100644 tests/test_async.py create mode 100644 tests/test_autorun.py create mode 100644 tests/test_serialization.py create mode 100644 tests/test_weakref.py diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index 93dba12..dec345f 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - PYTHON_VERSION: '3.11' + PYTHON_VERSION: "3.11" jobs: dependencies: @@ -121,7 +121,7 @@ jobs: key: poetry-${{ hashFiles('poetry.lock') }} - name: Test - run: poetry run poe test --cov-report=xml --cov-report=html + run: poetry run poe test - name: Prepare list of JSON files with mismatching pairs if: failure() diff --git a/CHANGELOG.md b/CHANGELOG.md index 0672b19..3b44aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Version 0.12.3 + +- 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 +- fix: sort JSON keys in `snapshot_store`'s `json_snapshot` +- test: cover most features with tests + ## Version 0.12.2 - docs: update path of demos migrated to tests in `README.md` diff --git a/poetry.lock b/poetry.lock index cf37e82..3258d3f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -211,6 +211,20 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-timeout" +version = "2.3.1" +description = "pytest plugin to abort hanging tests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, + {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "python-immutable" version = "1.0.5" @@ -292,4 +306,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d133175be95d6fa0edb98478c528d11fda8bedc0b30377cbc0f9f9a52a9c5525" +content-hash = "87a68a307610d1b8bea7f8f993a17629f50afd5803333f35ea69d90bb4146278" diff --git a/pyproject.toml b/pyproject.toml index cb54bfe..2baaf90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-redux" -version = "0.12.2" +version = "0.12.3" description = "Redux implementation for Python" authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -11,6 +11,7 @@ packages = [{ include = "redux" }] python = "^3.11" python-immutable = "^1.0.5" typing-extensions = "^4.9.0" +pytest-timeout = "^2.3.1" [tool.poetry.group.dev] optional = true @@ -33,7 +34,7 @@ todo_demo = "todo_demo:main" [tool.poe.tasks] lint = "ruff check . --unsafe-fixes" typecheck = "pyright -p pyproject.toml ." -test = "pytest --cov=redux --cov-report=term-missing" +test = "pytest --cov=redux --cov-report=term-missing --cov-report=html --cov-report=xml" sanity = ["typecheck", "lint", "test"] [tool.ruff] @@ -48,7 +49,7 @@ inline-quotes = "single" multiline-quotes = "double" [tool.ruff.lint.per-file-ignores] -"tests/*" = ["S101"] +"tests/*" = ["S101", "PLR0915"] [tool.ruff.format] quote-style = 'single' @@ -58,3 +59,14 @@ profile = "black" [tool.pyright] exclude = ['typings'] + +[tool.pytest.ini_options] +log_cli = 1 +log_cli_level = 'ERROR' +timeout = 4 + +[tool.coverage.report] +exclude_also = ["if TYPE_CHECKING:"] + +[tool.coverage.run] +omit = ['redux/test.py'] diff --git a/redux/__init__.py b/redux/__init__.py index 1aa3d51..d374f37 100644 --- a/redux/__init__.py +++ b/redux/__init__.py @@ -1,4 +1,5 @@ """Redux-like state management for Python.""" + from .basic_types import ( AutorunDecorator, AutorunOptions, diff --git a/redux/autorun.py b/redux/autorun.py index e206c8a..1d79753 100644 --- a/redux/autorun.py +++ b/redux/autorun.py @@ -64,16 +64,11 @@ def __init__( # noqa: PLR0913 Callable[[AutorunOriginalReturnType], Any] | weakref.ref[Callable[[AutorunOriginalReturnType], Any]] ] = set() - self._immediate_run = ( - not iscoroutinefunction(func) - if options.subscribers_immediate_run is None - else options.subscribers_immediate_run - ) if self._options.initial_run and store._state is not None: # noqa: SLF001 self._check_and_call(store._state) # noqa: SLF001 - store.subscribe(self._check_and_call) + store.subscribe(self._check_and_call, keep_ref=options.keep_ref) def inform_subscribers( self: Autorun[ @@ -142,21 +137,31 @@ def _check_and_call( except AttributeError: return func = self._func() if isinstance(self._func, weakref.ref) else self._func - if func is None: - return - if self._comparator is None: - comparator_result = cast(ComparatorOutput, selector_result) - else: - comparator_result = self._comparator(state) - if comparator_result != self._last_comparator_result: - previous_result = self._last_selector_result - self._last_selector_result = selector_result - self._last_comparator_result = comparator_result - self._latest_value = self.call_func(selector_result, previous_result, func) - if self._immediate_run: - self.inform_subscribers() + if func: + if self._comparator is None: + comparator_result = cast(ComparatorOutput, selector_result) else: - self._store._create_task(cast(Coroutine, self._latest_value)) # noqa: SLF001 + try: + comparator_result = self._comparator(state) + except AttributeError: + return + if comparator_result != self._last_comparator_result: + previous_result = self._last_selector_result + self._last_selector_result = selector_result + self._last_comparator_result = comparator_result + self._latest_value = self.call_func( + selector_result, + previous_result, + func, + ) + if iscoroutinefunction(func): + task = self._store._async_loop.create_task( # noqa: SLF001 + cast(Coroutine, self._latest_value), + ) + task.add_done_callback(lambda _: self.inform_subscribers()) + self._latest_value = cast(AutorunOriginalReturnType, task) + else: + self.inform_subscribers() def __call__( self: Autorun[ @@ -168,8 +173,9 @@ def __call__( AutorunOriginalReturnType, ], ) -> AutorunOriginalReturnType: - if self._store._state is not None: # noqa: SLF001 - self._check_and_call(self._store._state) # noqa: SLF001 + state = self._store._state # noqa: SLF001 + if state is not None: + self._check_and_call(state) return cast(AutorunOriginalReturnType, self._latest_value) def __repr__( @@ -209,11 +215,11 @@ def subscribe( ], callback: Callable[[AutorunOriginalReturnType], Any], *, - immediate_run: bool | None = None, + initial_run: bool | None = None, keep_ref: bool | None = None, ) -> Callable[[], None]: - if immediate_run is None: - immediate_run = self._options.subscribers_immediate_run + if initial_run is None: + initial_run = self._options.subscribers_initial_run if keep_ref is None: keep_ref = self._options.subscribers_keep_ref if keep_ref: @@ -224,10 +230,16 @@ def subscribe( callback_ref = weakref.ref(callback) self._subscriptions.add(callback_ref) - if immediate_run: + if initial_run: callback(self.value) def unsubscribe() -> None: - self._subscriptions.discard(callback_ref) + callback = ( + callback_ref() + if isinstance(callback_ref, weakref.ref) + else callback_ref + ) + if callback is not None: + self._subscriptions.discard(callback) return unsubscribe diff --git a/redux/basic_types.py b/redux/basic_types.py index 58c1335..5dfc1ad 100644 --- a/redux/basic_types.py +++ b/redux/basic_types.py @@ -2,18 +2,19 @@ from __future__ import annotations from types import NoneType -from typing import Any, Callable, Coroutine, Generic, Protocol, TypeAlias, TypeGuard +from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol, TypeAlias, TypeGuard from immutable import Immutable from typing_extensions import TypeVar +if TYPE_CHECKING: + import asyncio -class BaseAction(Immutable): - ... +class BaseAction(Immutable): ... -class BaseEvent(Immutable): - ... + +class BaseEvent(Immutable): ... class EventSubscriptionOptions(Immutable): @@ -51,16 +52,13 @@ def __init__(self: InitializationActionError, action: BaseAction) -> None: ) -class InitAction(BaseAction): - ... +class InitAction(BaseAction): ... -class FinishAction(BaseAction): - ... +class FinishAction(BaseAction): ... -class FinishEvent(BaseEvent): - ... +class FinishEvent(BaseEvent): ... def is_complete_reducer_result( @@ -76,8 +74,7 @@ def is_state_reducer_result( class Scheduler(Protocol): - def __call__(self: Scheduler, callback: Callable, *, interval: bool) -> None: - ... + def __call__(self: Scheduler, callback: Callable, *, interval: bool) -> None: ... class CreateStoreOptions(Immutable): @@ -86,14 +83,14 @@ class CreateStoreOptions(Immutable): scheduler: Scheduler | None = None action_middleware: Callable[[BaseAction], Any] | None = None event_middleware: Callable[[BaseEvent], Any] | None = None - task_creator: Callable[[Coroutine], Any] | None = None + async_loop: asyncio.AbstractEventLoop | None = None class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]): default_value: AutorunOriginalReturnType | None = None initial_run: bool = True keep_ref: bool = True - subscribers_immediate_run: bool | None = None + subscribers_initial_run: bool = True subscribers_keep_ref: bool = True @@ -108,8 +105,7 @@ def __call__( State, SelectorOutput, AutorunOriginalReturnType, - ]: - ... + ]: ... class AutorunDecorator( @@ -124,29 +120,25 @@ def __call__( self: AutorunDecorator, func: Callable[[SelectorOutput], AutorunOriginalReturnType] | Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType], - ) -> AutorunReturnType[AutorunOriginalReturnType]: - ... + ) -> AutorunReturnType[AutorunOriginalReturnType]: ... class AutorunReturnType( Protocol, Generic[AutorunOriginalReturnType], ): - def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType: - ... + def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType: ... @property - def value(self: AutorunReturnType) -> AutorunOriginalReturnType: - ... + def value(self: AutorunReturnType) -> AutorunOriginalReturnType: ... def subscribe( self: AutorunReturnType, callback: Callable[[AutorunOriginalReturnType], Any], *, - immediate_run: bool | None = None, + initial_run: bool | None = None, keep_ref: bool | None = None, - ) -> Callable[[], None]: - ... + ) -> Callable[[], None]: ... class EventSubscriber(Protocol): @@ -156,8 +148,7 @@ def __call__( handler: EventHandler[Event], *, options: EventSubscriptionOptions | None = None, - ) -> Callable[[], None]: - ... + ) -> Callable[[], None]: ... DispatchParameters: TypeAlias = Action | Event | list[Action | Event] @@ -169,8 +160,7 @@ def __call__( *items: Action | Event | list[Action | Event], with_state: Callable[[State | None], Action | Event | list[Action | Event]] | None = None, - ) -> None: - ... + ) -> None: ... class BaseCombineReducerState(Immutable): diff --git a/redux/main.py b/redux/main.py index c0b48fe..4a3dcd5 100644 --- a/redux/main.py +++ b/redux/main.py @@ -1,4 +1,5 @@ """Redux store for managing state and side effects.""" + from __future__ import annotations import dataclasses @@ -6,9 +7,8 @@ import queue import threading import weakref -from asyncio import create_task, iscoroutine +from asyncio import AbstractEventLoop, get_event_loop, iscoroutinefunction from collections import defaultdict -from enum import IntEnum, StrEnum from inspect import signature from threading import Lock from types import NoneType @@ -47,12 +47,16 @@ class _SideEffectRunnerThread(threading.Thread, Generic[Event]): def __init__( self: _SideEffectRunnerThread[Event], + *, task_queue: queue.Queue[tuple[EventHandler[Event], Event] | None], - task_creator: Callable[[Coroutine], Any], + async_loop: AbstractEventLoop, ) -> None: super().__init__() self.task_queue = task_queue - self.create_task = task_creator + self.async_loop = async_loop + + def create_task(self: _SideEffectRunnerThread[Event], coro: Coroutine) -> None: + self.async_loop.call_soon_threadsafe(lambda: self.async_loop.create_task(coro)) def run(self: _SideEffectRunnerThread[Event]) -> None: while True: @@ -67,7 +71,7 @@ def run(self: _SideEffectRunnerThread[Event]) -> None: result = cast(Callable[[Event], Any], event_handler)(event) else: result = cast(Callable[[], Any], event_handler)() - if iscoroutine(result): + if iscoroutinefunction(event_handler): self.create_task(result) finally: self.task_queue.task_done() @@ -84,9 +88,7 @@ def __init__( """Create a new store.""" self.store_options = options or CreateStoreOptions() self.reducer = reducer - self._create_task: Callable[[Coroutine], Any] = ( - self.store_options.task_creator or create_task - ) + self._async_loop = self.store_options.async_loop or get_event_loop() self._state: State | None = None self._listeners: set[ @@ -109,7 +111,10 @@ def __init__( tuple[EventHandler[Event], Event] | None ]() workers = [ - _SideEffectRunnerThread(self._event_handlers_queue, self._create_task) + _SideEffectRunnerThread( + task_queue=self._event_handlers_queue, + async_loop=self._async_loop, + ) for _ in range(self.store_options.threads) ] for worker in workers: @@ -153,8 +158,8 @@ def _run_actions(self: Store[State, Action, Event]) -> None: else: listener = listener_ result = listener(self._state) - if iscoroutine(result): - self._create_task(result) + if iscoroutinefunction(listener): + self._async_loop.create_task(result) def _run_event_handlers(self: Store[State, Action, Event]) -> None: event = self._events.pop(0) @@ -297,29 +302,27 @@ def snapshot(self: Store[State, Action, Event]) -> SnapshotAtom: """Return a snapshot of the current state of the store.""" return self.serialize_value(self._state) - def serialize_value(self: Store, obj: object | type) -> SnapshotAtom: + @classmethod + def serialize_value(cls: type[Store], obj: object | type) -> SnapshotAtom: """Serialize a value to a snapshot atom.""" - if is_immutable(obj): - return self._serialize_dataclass_to_dict(obj) - if isinstance(obj, (list, tuple)): - return [self.serialize_value(i) for i in obj] - if callable(obj): - return self.serialize_value(obj()) - if isinstance(obj, StrEnum): - return str(obj) - if isinstance(obj, IntEnum): - return int(obj) if isinstance(obj, (int, float, str, bool, NoneType)): return obj - msg = f'Unable to serialize object with type {type(obj)}.' - raise ValueError(msg) + if callable(obj): + return Store.serialize_value(obj()) + if isinstance(obj, (list, tuple)): + return [Store.serialize_value(i) for i in obj] + if is_immutable(obj): + return Store._serialize_dataclass_to_dict(obj) + msg = f'Unable to serialize object with type `{type(obj)}`.' + raise TypeError(msg) + @classmethod def _serialize_dataclass_to_dict( - self: Store, + cls: type[Store], obj: Immutable, ) -> dict[str, Any]: result = {} for field in dataclasses.fields(obj): - value = self.serialize_value(getattr(obj, field.name)) + value = Store.serialize_value(getattr(obj, field.name)) result[field.name] = value return result diff --git a/redux/test.py b/redux/test.py index 4b39a4b..8a73a15 100644 --- a/redux/test.py +++ b/redux/test.py @@ -1,10 +1,12 @@ # ruff: noqa: S101 """Let the test check snapshots of the window during execution.""" + from __future__ import annotations import json import os -from typing import TYPE_CHECKING +from collections import defaultdict +from typing import TYPE_CHECKING, Any, Generator, cast import pytest @@ -17,67 +19,111 @@ from redux.main import Store -override_store_snapshots = os.environ.get('REDUX_TEST_OVERRIDE_SNAPSHOTS', '0') == '1' - - class StoreSnapshotContext: """Context object for tests taking snapshots of the store.""" def __init__( self: StoreSnapshotContext, + *, test_id: str, path: Path, logger: Logger, + override: bool, ) -> None: """Create a new store snapshot context.""" - self.test_counter = 0 - self.id = test_id - self.results_dir = path.parent / 'results' + self.closed = False + self.override = override + self.test_counter: dict[str | None, int] = defaultdict(int) + file = path.with_suffix('').name + self.results_dir = path.parent / 'results' / file / test_id.split('::')[-1][5:] + if self.results_dir.exists(): + for file in self.results_dir.glob( + 'store:*' if override else 'store:*.mismatch.json', + ): + file.unlink() + self.results_dir.mkdir(parents=True, exist_ok=True) self.logger = logger - self.results_dir.mkdir(exist_ok=True) def set_store(self: StoreSnapshotContext, store: Store) -> None: """Set the store to take snapshots of.""" self.store = store @property - def snapshot(self: StoreSnapshotContext) -> str: + def json_snapshot(self: StoreSnapshotContext) -> str: """Return the snapshot of the current state of the store.""" return ( - json.dumps(self.store.snapshot, indent=2) + json.dumps(self.store.snapshot, indent=2, sort_keys=True) if self.store._state # noqa: SLF001 else '' ) - def take(self: StoreSnapshotContext, title: str | None = None) -> None: - """Take a snapshot of the current window.""" + def get_filename(self: StoreSnapshotContext, title: str | None) -> str: + """Get the filename for the snapshot.""" if title: - filename = f"""store:{"_".join(self.id.split(":")[-1:])}:{title}-{ - self.test_counter:03d}""" - else: - filename = ( - f'store:{"_".join(self.id.split(":")[-1:])}-{self.test_counter:03d}' + return f"""store:{title}:{self.test_counter[title]:03d}""" + return f"""store:{self.test_counter[title]:03d}""" + + def take(self: StoreSnapshotContext, *, title: str | None = None) -> None: + """Take a snapshot of the current window.""" + if self.closed: + msg = ( + 'Snapshot context is closed, make sure `store_snapshot` is before any ' + 'fixture dispatching actions in the fixtures list' ) + raise RuntimeError(msg) + filename = self.get_filename(title) path = self.results_dir / filename - json_path = path.with_suffix('.json') + json_path = path.with_suffix('.jsonc') + mismatch_path = path.with_suffix('.mismatch.jsonc') - new_snapshot = self.snapshot - if json_path.exists() and not override_store_snapshots: - old_snapshot = json_path.read_text() + new_snapshot = self.json_snapshot + if self.override: + json_path.write_text(f'// {filename}\n{new_snapshot}\n') + else: + if json_path.exists(): + old_snapshot = json_path.read_text().split('\n', 1)[1][:-1] + else: + old_snapshot = None if old_snapshot != new_snapshot: - path.with_suffix('.mismatch.json').write_text(new_snapshot) - assert old_snapshot == new_snapshot - json_path.write_text(new_snapshot) + mismatch_path.write_text( # pragma: no cover + f'// MISMATCH: {filename}\n{new_snapshot}\n', + ) + assert old_snapshot == new_snapshot, f'Snapshot mismatch: {title}' + + self.test_counter[title] += 1 - self.test_counter += 1 + def close(self: StoreSnapshotContext) -> None: + """Close the snapshot context.""" + for title in self.test_counter: + filename = self.get_filename(title) + json_path = (self.results_dir / filename).with_suffix('.jsonc') + + assert not json_path.exists(), f'Snapshot {filename} not taken' + self.closed = True @pytest.fixture() -def snapshot_store(request: SubRequest, logger: Logger) -> StoreSnapshotContext: +def store_snapshot( + request: SubRequest, + logger: Logger, +) -> Generator[StoreSnapshotContext, None, None]: """Take a snapshot of the current state of the store.""" - return StoreSnapshotContext( - request.node.nodeid, - request.node.path, - logger, + override = ( + request.config.getoption( + '--override-store-snapshots', + default=cast( + Any, + os.environ.get('REDUX_TEST_OVERRIDE_SNAPSHOTS', '0') == '1', + ), + ) + is True + ) + context = StoreSnapshotContext( + test_id=request.node.nodeid, + path=request.node.path, + logger=logger, + override=override, ) + yield context + context.close() diff --git a/tests/conftest.py b/tests/conftest.py index 99b79e5..a498594 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Pytest configuration file for the tests.""" + from __future__ import annotations import datetime @@ -9,12 +10,18 @@ import pytest -from redux.test import snapshot_store +pytest.register_assert_rewrite('redux.test') + +from redux.test import store_snapshot # noqa: E402 if TYPE_CHECKING: from logging import Logger -__all__ = ['snapshot_store'] +__all__ = ['store_snapshot'] + + +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addoption('--override-store-snapshots', action='store_true') @pytest.fixture() diff --git a/tests/results/store:test_general-013.json b/tests/results/store:test_general-013.json deleted file mode 100644 index b599301..0000000 --- a/tests/results/store:test_general-013.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "_id": "e3e70682c2094cac629f6fbed82c07cd", - "base10": { - "count": 11 - }, - "inverse": { - "count": 0 - } -} \ No newline at end of file diff --git a/tests/results/store:test_general-014.json b/tests/results/store:test_general-014.json deleted file mode 100644 index b599301..0000000 --- a/tests/results/store:test_general-014.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "_id": "e3e70682c2094cac629f6fbed82c07cd", - "base10": { - "count": 11 - }, - "inverse": { - "count": 0 - } -} \ No newline at end of file diff --git a/tests/results/test_autorun/callability/store:000.jsonc b/tests/results/test_autorun/callability/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/callability/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/callability/store:001.jsonc b/tests/results/test_autorun/callability/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/callability/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/callability/store:002.jsonc b/tests/results/test_autorun/callability/store:002.jsonc new file mode 100644 index 0000000..dfda7e7 --- /dev/null +++ b/tests/results/test_autorun/callability/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 3 +} diff --git a/tests/results/test_autorun/callability/store:003.jsonc b/tests/results/test_autorun/callability/store:003.jsonc new file mode 100644 index 0000000..85310d7 --- /dev/null +++ b/tests/results/test_autorun/callability/store:003.jsonc @@ -0,0 +1,4 @@ +// store:003 +{ + "value": 4 +} diff --git a/tests/results/test_autorun/general/store:000.jsonc b/tests/results/test_autorun/general/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/general/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/general/store:001.jsonc b/tests/results/test_autorun/general/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/general/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/general/store:002.jsonc b/tests/results/test_autorun/general/store:002.jsonc new file mode 100644 index 0000000..dfda7e7 --- /dev/null +++ b/tests/results/test_autorun/general/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 3 +} diff --git a/tests/results/test_autorun/general/store:003.jsonc b/tests/results/test_autorun/general/store:003.jsonc new file mode 100644 index 0000000..85310d7 --- /dev/null +++ b/tests/results/test_autorun/general/store:003.jsonc @@ -0,0 +1,4 @@ +// store:003 +{ + "value": 4 +} diff --git a/tests/results/test_autorun/subscription/store:000.jsonc b/tests/results/test_autorun/subscription/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/subscription/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/subscription/store:001.jsonc b/tests/results/test_autorun/subscription/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/subscription/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/subscription/store:002.jsonc b/tests/results/test_autorun/subscription/store:002.jsonc new file mode 100644 index 0000000..dfda7e7 --- /dev/null +++ b/tests/results/test_autorun/subscription/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 3 +} diff --git a/tests/results/test_autorun/subscription/store:003.jsonc b/tests/results/test_autorun/subscription/store:003.jsonc new file mode 100644 index 0000000..85310d7 --- /dev/null +++ b/tests/results/test_autorun/subscription/store:003.jsonc @@ -0,0 +1,4 @@ +// store:003 +{ + "value": 4 +} diff --git a/tests/results/test_autorun/value_property/store:000.jsonc b/tests/results/test_autorun/value_property/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/value_property/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/value_property/store:001.jsonc b/tests/results/test_autorun/value_property/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/value_property/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/value_property/store:002.jsonc b/tests/results/test_autorun/value_property/store:002.jsonc new file mode 100644 index 0000000..dfda7e7 --- /dev/null +++ b/tests/results/test_autorun/value_property/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 3 +} diff --git a/tests/results/test_autorun/value_property/store:003.jsonc b/tests/results/test_autorun/value_property/store:003.jsonc new file mode 100644 index 0000000..85310d7 --- /dev/null +++ b/tests/results/test_autorun/value_property/store:003.jsonc @@ -0,0 +1,4 @@ +// store:003 +{ + "value": 4 +} diff --git a/tests/results/test_autorun/with_comparator/store:000.jsonc b/tests/results/test_autorun/with_comparator/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/with_comparator/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/with_comparator/store:001.jsonc b/tests/results/test_autorun/with_comparator/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/with_comparator/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/with_comparator/store:002.jsonc b/tests/results/test_autorun/with_comparator/store:002.jsonc new file mode 100644 index 0000000..0e81cd6 --- /dev/null +++ b/tests/results/test_autorun/with_comparator/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 4 +} diff --git a/tests/results/test_autorun/with_comparator_and_old_value/store:000.jsonc b/tests/results/test_autorun/with_comparator_and_old_value/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/with_comparator_and_old_value/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/with_comparator_and_old_value/store:001.jsonc b/tests/results/test_autorun/with_comparator_and_old_value/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/with_comparator_and_old_value/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/with_comparator_and_old_value/store:002.jsonc b/tests/results/test_autorun/with_comparator_and_old_value/store:002.jsonc new file mode 100644 index 0000000..0e81cd6 --- /dev/null +++ b/tests/results/test_autorun/with_comparator_and_old_value/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 4 +} diff --git a/tests/results/test_autorun/with_old_value/store:000.jsonc b/tests/results/test_autorun/with_old_value/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_autorun/with_old_value/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_autorun/with_old_value/store:001.jsonc b/tests/results/test_autorun/with_old_value/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_autorun/with_old_value/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_autorun/with_old_value/store:002.jsonc b/tests/results/test_autorun/with_old_value/store:002.jsonc new file mode 100644 index 0000000..dfda7e7 --- /dev/null +++ b/tests/results/test_autorun/with_old_value/store:002.jsonc @@ -0,0 +1,4 @@ +// store:002 +{ + "value": 3 +} diff --git a/tests/results/test_autorun/with_old_value/store:003.jsonc b/tests/results/test_autorun/with_old_value/store:003.jsonc new file mode 100644 index 0000000..85310d7 --- /dev/null +++ b/tests/results/test_autorun/with_old_value/store:003.jsonc @@ -0,0 +1,4 @@ +// store:003 +{ + "value": 4 +} diff --git a/tests/results/store:test_general-000.json b/tests/results/test_features/general/store:000.jsonc similarity index 88% rename from tests/results/store:test_general-000.json rename to tests/results/test_features/general/store:000.jsonc index fc51f07..4b1583a 100644 --- a/tests/results/store:test_general-000.json +++ b/tests/results/test_features/general/store:000.jsonc @@ -1,9 +1,10 @@ +// store:000 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 0 - }, "base10": { "count": 10 + }, + "straight": { + "count": 0 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-004.json b/tests/results/test_features/general/store:001.jsonc similarity index 88% rename from tests/results/store:test_general-004.json rename to tests/results/test_features/general/store:001.jsonc index 28bc30a..e7b7b8d 100644 --- a/tests/results/store:test_general-004.json +++ b/tests/results/test_features/general/store:001.jsonc @@ -1,9 +1,10 @@ +// store:001 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 1 - }, "base10": { "count": 11 + }, + "straight": { + "count": 1 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-006.json b/tests/results/test_features/general/store:002.jsonc similarity index 91% rename from tests/results/store:test_general-006.json rename to tests/results/test_features/general/store:002.jsonc index e5e257e..5036347 100644 --- a/tests/results/store:test_general-006.json +++ b/tests/results/test_features/general/store:002.jsonc @@ -1,12 +1,13 @@ +// store:002 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 2 - }, "base10": { "count": 12 }, "inverse": { "count": -1 + }, + "straight": { + "count": 2 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-009.json b/tests/results/test_features/general/store:003.jsonc similarity index 88% rename from tests/results/store:test_general-009.json rename to tests/results/test_features/general/store:003.jsonc index fd43740..5b89a4e 100644 --- a/tests/results/store:test_general-009.json +++ b/tests/results/test_features/general/store:003.jsonc @@ -1,3 +1,4 @@ +// store:003 { "_id": "e3e70682c2094cac629f6fbed82c07cd", "base10": { @@ -6,4 +7,4 @@ "inverse": { "count": -1 } -} \ No newline at end of file +} diff --git a/tests/results/test_features/general/store:004.jsonc b/tests/results/test_features/general/store:004.jsonc new file mode 100644 index 0000000..1ee0f1c --- /dev/null +++ b/tests/results/test_features/general/store:004.jsonc @@ -0,0 +1,10 @@ +// store:004 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 10 + }, + "inverse": { + "count": 1 + } +} diff --git a/tests/results/store:test_general-012.json b/tests/results/test_features/general/store:005.jsonc similarity index 65% rename from tests/results/store:test_general-012.json rename to tests/results/test_features/general/store:005.jsonc index b599301..8ae754d 100644 --- a/tests/results/store:test_general-012.json +++ b/tests/results/test_features/general/store:005.jsonc @@ -1,9 +1,10 @@ +// store:005 { "_id": "e3e70682c2094cac629f6fbed82c07cd", "base10": { - "count": 11 + "count": 8 }, "inverse": { - "count": 0 + "count": 3 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-001.json b/tests/results/test_features/general/store:autorun:000.jsonc similarity index 83% rename from tests/results/store:test_general-001.json rename to tests/results/test_features/general/store:autorun:000.jsonc index fc51f07..f1e7fb0 100644 --- a/tests/results/store:test_general-001.json +++ b/tests/results/test_features/general/store:autorun:000.jsonc @@ -1,9 +1,10 @@ +// store:autorun:000 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 0 - }, "base10": { "count": 10 + }, + "straight": { + "count": 0 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-002.json b/tests/results/test_features/general/store:autorun:001.jsonc similarity index 83% rename from tests/results/store:test_general-002.json rename to tests/results/test_features/general/store:autorun:001.jsonc index 28bc30a..001fdd7 100644 --- a/tests/results/store:test_general-002.json +++ b/tests/results/test_features/general/store:autorun:001.jsonc @@ -1,9 +1,10 @@ +// store:autorun:001 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 1 - }, "base10": { "count": 11 + }, + "straight": { + "count": 1 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-007.json b/tests/results/test_features/general/store:autorun:002.jsonc similarity index 86% rename from tests/results/store:test_general-007.json rename to tests/results/test_features/general/store:autorun:002.jsonc index e5e257e..f5409d4 100644 --- a/tests/results/store:test_general-007.json +++ b/tests/results/test_features/general/store:autorun:002.jsonc @@ -1,12 +1,13 @@ +// store:autorun:002 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 2 - }, "base10": { "count": 12 }, "inverse": { "count": -1 + }, + "straight": { + "count": 2 } -} \ No newline at end of file +} diff --git a/tests/results/test_features/general/store:autorun:003.jsonc b/tests/results/test_features/general/store:autorun:003.jsonc new file mode 100644 index 0000000..7748fae --- /dev/null +++ b/tests/results/test_features/general/store:autorun:003.jsonc @@ -0,0 +1,10 @@ +// store:autorun:003 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 10 + }, + "inverse": { + "count": 1 + } +} diff --git a/tests/results/store:test_general-011.json b/tests/results/test_features/general/store:autorun:004.jsonc similarity index 61% rename from tests/results/store:test_general-011.json rename to tests/results/test_features/general/store:autorun:004.jsonc index b599301..d261a38 100644 --- a/tests/results/store:test_general-011.json +++ b/tests/results/test_features/general/store:autorun:004.jsonc @@ -1,9 +1,10 @@ +// store:autorun:004 { "_id": "e3e70682c2094cac629f6fbed82c07cd", "base10": { - "count": 11 + "count": 8 }, "inverse": { - "count": 0 + "count": 3 } -} \ No newline at end of file +} diff --git a/tests/results/test_features/general/store:autorun_subscription:000.jsonc b/tests/results/test_features/general/store:autorun_subscription:000.jsonc new file mode 100644 index 0000000..cf5a1c7 --- /dev/null +++ b/tests/results/test_features/general/store:autorun_subscription:000.jsonc @@ -0,0 +1,10 @@ +// store:autorun_subscription:000 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 10 + }, + "straight": { + "count": 0 + } +} diff --git a/tests/results/test_features/general/store:autorun_subscription:001.jsonc b/tests/results/test_features/general/store:autorun_subscription:001.jsonc new file mode 100644 index 0000000..2a51d2d --- /dev/null +++ b/tests/results/test_features/general/store:autorun_subscription:001.jsonc @@ -0,0 +1,10 @@ +// store:autorun_subscription:001 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 11 + }, + "straight": { + "count": 1 + } +} diff --git a/tests/results/test_features/general/store:autorun_subscription:002.jsonc b/tests/results/test_features/general/store:autorun_subscription:002.jsonc new file mode 100644 index 0000000..489fa2c --- /dev/null +++ b/tests/results/test_features/general/store:autorun_subscription:002.jsonc @@ -0,0 +1,13 @@ +// store:autorun_subscription:002 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 12 + }, + "inverse": { + "count": -1 + }, + "straight": { + "count": 2 + } +} diff --git a/tests/results/test_features/general/store:autorun_subscription:003.jsonc b/tests/results/test_features/general/store:autorun_subscription:003.jsonc new file mode 100644 index 0000000..e1e1467 --- /dev/null +++ b/tests/results/test_features/general/store:autorun_subscription:003.jsonc @@ -0,0 +1,10 @@ +// store:autorun_subscription:003 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 10 + }, + "inverse": { + "count": 1 + } +} diff --git a/tests/results/test_features/general/store:autorun_subscription:004.jsonc b/tests/results/test_features/general/store:autorun_subscription:004.jsonc new file mode 100644 index 0000000..1b4a5d7 --- /dev/null +++ b/tests/results/test_features/general/store:autorun_subscription:004.jsonc @@ -0,0 +1,10 @@ +// store:autorun_subscription:004 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 8 + }, + "inverse": { + "count": 3 + } +} diff --git a/tests/results/test_features/general/store:initialization:000.jsonc b/tests/results/test_features/general/store:initialization:000.jsonc new file mode 100644 index 0000000..f7e0db7 --- /dev/null +++ b/tests/results/test_features/general/store:initialization:000.jsonc @@ -0,0 +1,2 @@ +// store:initialization:000 + diff --git a/tests/results/store:test_general-003.json b/tests/results/test_features/general/store:subscription:000.jsonc similarity index 80% rename from tests/results/store:test_general-003.json rename to tests/results/test_features/general/store:subscription:000.jsonc index 28bc30a..efd4158 100644 --- a/tests/results/store:test_general-003.json +++ b/tests/results/test_features/general/store:subscription:000.jsonc @@ -1,9 +1,10 @@ +// store:subscription:000 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 1 - }, "base10": { "count": 11 + }, + "straight": { + "count": 1 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-005.json b/tests/results/test_features/general/store:subscription:001.jsonc similarity index 84% rename from tests/results/store:test_general-005.json rename to tests/results/test_features/general/store:subscription:001.jsonc index 1538a23..cf24b28 100644 --- a/tests/results/store:test_general-005.json +++ b/tests/results/test_features/general/store:subscription:001.jsonc @@ -1,12 +1,13 @@ +// store:subscription:001 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 1 - }, "base10": { "count": 11 }, "inverse": { "count": 0 + }, + "straight": { + "count": 1 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-008.json b/tests/results/test_features/general/store:subscription:002.jsonc similarity index 84% rename from tests/results/store:test_general-008.json rename to tests/results/test_features/general/store:subscription:002.jsonc index e5e257e..93fbf28 100644 --- a/tests/results/store:test_general-008.json +++ b/tests/results/test_features/general/store:subscription:002.jsonc @@ -1,12 +1,13 @@ +// store:subscription:002 { "_id": "e3e70682c2094cac629f6fbed82c07cd", - "straight": { - "count": 2 - }, "base10": { "count": 12 }, "inverse": { "count": -1 + }, + "straight": { + "count": 2 } -} \ No newline at end of file +} diff --git a/tests/results/store:test_general-010.json b/tests/results/test_features/general/store:subscription:003.jsonc similarity index 80% rename from tests/results/store:test_general-010.json rename to tests/results/test_features/general/store:subscription:003.jsonc index fd43740..4b2a39d 100644 --- a/tests/results/store:test_general-010.json +++ b/tests/results/test_features/general/store:subscription:003.jsonc @@ -1,3 +1,4 @@ +// store:subscription:003 { "_id": "e3e70682c2094cac629f6fbed82c07cd", "base10": { @@ -6,4 +7,4 @@ "inverse": { "count": -1 } -} \ No newline at end of file +} diff --git a/tests/results/test_features/general/store:subscription:004.jsonc b/tests/results/test_features/general/store:subscription:004.jsonc new file mode 100644 index 0000000..f094370 --- /dev/null +++ b/tests/results/test_features/general/store:subscription:004.jsonc @@ -0,0 +1,10 @@ +// store:subscription:004 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 10 + }, + "inverse": { + "count": 1 + } +} diff --git a/tests/results/test_features/general/store:subscription:005.jsonc b/tests/results/test_features/general/store:subscription:005.jsonc new file mode 100644 index 0000000..fa01b95 --- /dev/null +++ b/tests/results/test_features/general/store:subscription:005.jsonc @@ -0,0 +1,10 @@ +// store:subscription:005 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 8 + }, + "inverse": { + "count": 3 + } +} diff --git a/tests/results/test_features/general/store:subscription:006.jsonc b/tests/results/test_features/general/store:subscription:006.jsonc new file mode 100644 index 0000000..fbdd7e1 --- /dev/null +++ b/tests/results/test_features/general/store:subscription:006.jsonc @@ -0,0 +1,10 @@ +// store:subscription:006 +{ + "_id": "e3e70682c2094cac629f6fbed82c07cd", + "base10": { + "count": 8 + }, + "inverse": { + "count": 3 + } +} diff --git a/tests/results/store:test_todo-000.json b/tests/results/test_todo/todo/store:000.jsonc similarity index 90% rename from tests/results/store:test_todo-000.json rename to tests/results/test_todo/todo/store:000.jsonc index 48f5970..6e4f387 100644 --- a/tests/results/store:test_todo-000.json +++ b/tests/results/test_todo/todo/store:000.jsonc @@ -1,9 +1,10 @@ +// store:000 { "items": [ { - "id": "e3e70682c2094cac629f6fbed82c07cd", "content": "Initial Item", + "id": "e3e70682c2094cac629f6fbed82c07cd", "timestamp": 1672531200.0 } ] -} \ No newline at end of file +} diff --git a/tests/results/store:test_todo-001.json b/tests/results/test_todo/todo/store:001.jsonc similarity index 94% rename from tests/results/store:test_todo-001.json rename to tests/results/test_todo/todo/store:001.jsonc index c3e1d4a..a00d821 100644 --- a/tests/results/store:test_todo-001.json +++ b/tests/results/test_todo/todo/store:001.jsonc @@ -1,14 +1,15 @@ +// store:001 { "items": [ { - "id": "e3e70682c2094cac629f6fbed82c07cd", "content": "Initial Item", + "id": "e3e70682c2094cac629f6fbed82c07cd", "timestamp": 1672531200.0 }, { - "id": "f728b4fa42485e3a0a5d2f346baa9455", "content": "New Item", + "id": "f728b4fa42485e3a0a5d2f346baa9455", "timestamp": 1672531200.0 } ] -} \ No newline at end of file +} diff --git a/tests/results/store:test_todo-002.json b/tests/results/test_todo/todo/store:002.jsonc similarity index 94% rename from tests/results/store:test_todo-002.json rename to tests/results/test_todo/todo/store:002.jsonc index c3e1d4a..0215ee6 100644 --- a/tests/results/store:test_todo-002.json +++ b/tests/results/test_todo/todo/store:002.jsonc @@ -1,14 +1,15 @@ +// store:002 { "items": [ { - "id": "e3e70682c2094cac629f6fbed82c07cd", "content": "Initial Item", + "id": "e3e70682c2094cac629f6fbed82c07cd", "timestamp": 1672531200.0 }, { - "id": "f728b4fa42485e3a0a5d2f346baa9455", "content": "New Item", + "id": "f728b4fa42485e3a0a5d2f346baa9455", "timestamp": 1672531200.0 } ] -} \ No newline at end of file +} diff --git a/tests/results/test_weakref/autorun/store:000.jsonc b/tests/results/test_weakref/autorun/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_weakref/autorun/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/autorun/store:001.jsonc b/tests/results/test_weakref/autorun/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_weakref/autorun/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:000.jsonc b/tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:000.jsonc new file mode 100644 index 0000000..880f0cf --- /dev/null +++ b/tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:000.jsonc @@ -0,0 +1,4 @@ +// store:autorun_method_with_keep_ref:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:001.jsonc b/tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:001.jsonc new file mode 100644 index 0000000..6702560 --- /dev/null +++ b/tests/results/test_weakref/autorun_method/store:autorun_method_with_keep_ref:001.jsonc @@ -0,0 +1,4 @@ +// store:autorun_method_with_keep_ref:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/autorun_method_subscription/store:000.jsonc b/tests/results/test_weakref/autorun_method_subscription/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_weakref/autorun_method_subscription/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/autorun_method_subscription/store:001.jsonc b/tests/results/test_weakref/autorun_method_subscription/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_weakref/autorun_method_subscription/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:000.jsonc b/tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:000.jsonc new file mode 100644 index 0000000..880f0cf --- /dev/null +++ b/tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:000.jsonc @@ -0,0 +1,4 @@ +// store:autorun_method_with_keep_ref:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:001.jsonc b/tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:001.jsonc new file mode 100644 index 0000000..6702560 --- /dev/null +++ b/tests/results/test_weakref/autorun_method_subscription/store:autorun_method_with_keep_ref:001.jsonc @@ -0,0 +1,4 @@ +// store:autorun_method_with_keep_ref:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/autorun_subscription/store:000.jsonc b/tests/results/test_weakref/autorun_subscription/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_weakref/autorun_subscription/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/autorun_subscription/store:001.jsonc b/tests/results/test_weakref/autorun_subscription/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_weakref/autorun_subscription/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/autorun_subscription/store:autorun_subscription:000.jsonc b/tests/results/test_weakref/autorun_subscription/store:autorun_subscription:000.jsonc new file mode 100644 index 0000000..bcace53 --- /dev/null +++ b/tests/results/test_weakref/autorun_subscription/store:autorun_subscription:000.jsonc @@ -0,0 +1,4 @@ +// store:autorun_subscription:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/autorun_subscription/store:autorun_subscription:001.jsonc b/tests/results/test_weakref/autorun_subscription/store:autorun_subscription:001.jsonc new file mode 100644 index 0000000..5ade462 --- /dev/null +++ b/tests/results/test_weakref/autorun_subscription/store:autorun_subscription:001.jsonc @@ -0,0 +1,4 @@ +// store:autorun_subscription:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/event_subscription/store:000.jsonc b/tests/results/test_weakref/event_subscription/store:000.jsonc new file mode 100644 index 0000000..474050f --- /dev/null +++ b/tests/results/test_weakref/event_subscription/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/event_subscription_method/store:event_subscription_method_with_keep_ref:000.jsonc b/tests/results/test_weakref/event_subscription_method/store:event_subscription_method_with_keep_ref:000.jsonc new file mode 100644 index 0000000..c025052 --- /dev/null +++ b/tests/results/test_weakref/event_subscription_method/store:event_subscription_method_with_keep_ref:000.jsonc @@ -0,0 +1,4 @@ +// store:event_subscription_method_with_keep_ref:000 +{ + "value": 0 +} diff --git a/tests/results/test_weakref/subscription/store:000.jsonc b/tests/results/test_weakref/subscription/store:000.jsonc new file mode 100644 index 0000000..e30a681 --- /dev/null +++ b/tests/results/test_weakref/subscription/store:000.jsonc @@ -0,0 +1,4 @@ +// store:000 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/subscription/store:001.jsonc b/tests/results/test_weakref/subscription/store:001.jsonc new file mode 100644 index 0000000..c34ea17 --- /dev/null +++ b/tests/results/test_weakref/subscription/store:001.jsonc @@ -0,0 +1,4 @@ +// store:001 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:000.jsonc b/tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:000.jsonc new file mode 100644 index 0000000..a331955 --- /dev/null +++ b/tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:000.jsonc @@ -0,0 +1,4 @@ +// store:subscription_method_with_keep_ref:000 +{ + "value": 1 +} diff --git a/tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:001.jsonc b/tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:001.jsonc new file mode 100644 index 0000000..f218ff4 --- /dev/null +++ b/tests/results/test_weakref/subscription_method/store:subscription_method_with_keep_ref:001.jsonc @@ -0,0 +1,4 @@ +// store:subscription_method_with_keep_ref:001 +{ + "value": 1 +} diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 0000000..0707e29 --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,116 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107 +from __future__ import annotations + +import asyncio +from dataclasses import replace +from typing import Generator + +import pytest +from immutable import Immutable + +from redux.basic_types import ( + BaseAction, + CompleteReducerResult, + CreateStoreOptions, + FinishAction, + FinishEvent, + InitAction, + InitializationActionError, +) +from redux.main import Store + +INCREMENTS = 2 + + +class StateType(Immutable): + value: int + mirrored_value: int + + +class IncrementAction(BaseAction): ... + + +class SetMirroredValueAction(BaseAction): + value: int + + +def reducer( + state: StateType | None, + action: Action, +) -> StateType | CompleteReducerResult[StateType, Action, FinishEvent]: + if state is None: + if isinstance(action, InitAction): + return StateType(value=0, mirrored_value=0) + raise InitializationActionError(action) + + if isinstance(action, IncrementAction): + return replace(state, value=state.value + 1) + if isinstance(action, SetMirroredValueAction): + return replace(state, mirrored_value=action.value) + return state + + +@pytest.fixture() +def loop() -> asyncio.AbstractEventLoop: + return asyncio.get_event_loop() + + +Action = IncrementAction | SetMirroredValueAction | InitAction | FinishAction + + +@pytest.fixture() +def store( + loop: asyncio.AbstractEventLoop, +) -> Generator[Store[StateType, Action, FinishEvent], None, None]: + store = Store( + reducer, + options=CreateStoreOptions(auto_init=True, async_loop=loop), + ) + yield store + for _i in range(INCREMENTS): + store.dispatch(IncrementAction()) + store.dispatch(FinishAction()) + loop.run_forever() + + +def test_autorun( + store: Store[StateType, Action, FinishEvent], + loop: asyncio.AbstractEventLoop, +) -> None: + @store.autorun(lambda state: state.value) + async def _(value: int) -> int: + await asyncio.sleep(value / 10) + store.dispatch(SetMirroredValueAction(value=value)) + return value + + @store.autorun( + lambda state: state.mirrored_value, + lambda state: state.mirrored_value >= INCREMENTS, + ) + async def _(mirrored_value: int) -> None: + if mirrored_value < INCREMENTS: + return + loop.call_soon_threadsafe(loop.stop) + + +def test_subscription( + store: Store[StateType, Action, FinishEvent], + loop: asyncio.AbstractEventLoop, +) -> None: + async def render(state: StateType) -> None: + await asyncio.sleep(0.1) + if state.value == INCREMENTS: + loop.call_soon_threadsafe(loop.stop) + + store.subscribe(render) + + +def test_event_subscription( + store: Store[StateType, Action, FinishEvent], + loop: asyncio.AbstractEventLoop, +) -> None: + async def finish() -> None: + await asyncio.sleep(0.1) + loop.call_soon_threadsafe(loop.stop) + + store.subscribe_event(FinishEvent, finish) diff --git a/tests/test_autorun.py b/tests/test_autorun.py new file mode 100644 index 0000000..d96aebe --- /dev/null +++ b/tests/test_autorun.py @@ -0,0 +1,237 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107 +from __future__ import annotations + +import re +from dataclasses import replace +from typing import TYPE_CHECKING, Generator + +import pytest +from immutable import Immutable + +from redux.basic_types import ( + BaseAction, + BaseEvent, + CompleteReducerResult, + CreateStoreOptions, + FinishAction, + FinishEvent, + InitAction, + InitializationActionError, +) +from redux.main import Store + +if TYPE_CHECKING: + from redux.test import StoreSnapshotContext + + +class StateType(Immutable): + value: int + + +class IncrementAction(BaseAction): ... + + +class IncrementByTwoAction(BaseAction): ... + + +Action = IncrementAction | IncrementByTwoAction | InitAction | FinishAction + + +def reducer( + state: StateType | None, + action: Action, +) -> StateType | CompleteReducerResult[StateType, Action, FinishEvent]: + if state is None: + if isinstance(action, InitAction): + return StateType(value=0) + raise InitializationActionError(action) + + if isinstance(action, IncrementAction): + return replace(state, value=state.value + 1) + + if isinstance(action, IncrementByTwoAction): + return replace(state, value=state.value + 2) + + return state + + +@pytest.fixture() +def store() -> Generator[Store[StateType, Action, FinishEvent], None, None]: + store = Store(reducer, options=CreateStoreOptions(auto_init=True)) + yield store + store.dispatch(IncrementAction()) + store.dispatch(IncrementByTwoAction()) + store.dispatch(IncrementAction()) + store.dispatch(FinishAction()) + + +def test_general( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def _(value: int) -> int: + store_snapshot.take() + return value + + +def test_ignore_attribute_error_in_selector( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.non_existing) # pyright: ignore[reportAttributeAccessIssue] + def _(_: int) -> int: + pytest.fail('This should never be called') + + +def test_ignore_attribute_error_in_comparator( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun( + lambda state: state.value, + lambda state: state.non_existing, # pyright: ignore[reportAttributeAccessIssue] + ) + def _(_: int) -> int: + pytest.fail('This should never be called') + + +def test_with_old_value( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def _(value: int, old_value: int | None) -> int: + store_snapshot.take() + return value - (old_value or 0) + + +def test_with_comparator( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun( + lambda state: state.value, + lambda state: state.value % 2, + ) + def _(value: int) -> int: + store_snapshot.take() + return value + + +def test_with_comparator_and_old_value( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun( + lambda state: state.value, + lambda state: state.value % 2, + ) + def _(value: int, old_value: int | None) -> int: + store_snapshot.take() + return value - (old_value or 0) + + +def test_value_property( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + store_snapshot.take() + return value + + def check(_: int) -> None: + state = store._state # noqa: SLF001 + if not state: + return + assert render.value == state.value + + render.subscribe(check) + + +def test_callability( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + store_snapshot.take() + return value + + def check(state: StateType) -> None: + assert render() == state.value + + store.subscribe(check) + + +def test_subscription( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + return value + + def reaction(_: int) -> None: + store_snapshot.take() + + render.subscribe(reaction, initial_run=True) + + +def test_unsubscription( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + return value + + def reaction(_: int) -> None: + pytest.fail('This should never be called') + + unsubscribe = render.subscribe(reaction, initial_run=False) + unsubscribe() + + +def test_repr( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, Action, BaseEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + return value + + assert re.match( + r'.*\(func: \.render at .*>, last_value: 0\)$', + repr(render), + ) + + store.dispatch(IncrementAction()) + + assert re.match( + r'.*\(func: \.render at .*>, last_value: 1\)$', + repr(render), + ) diff --git a/tests/test_features.py b/tests/test_features.py index f60f04a..beca477 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,14 +1,13 @@ -# ruff: noqa: D100, D101, D102, D103, D104, D107, A003, T201 +# ruff: noqa: D100, D101, D102, D103, D104, D107 from __future__ import annotations import time from typing import TYPE_CHECKING, TypeAlias +import pytest from immutable import Immutable if TYPE_CHECKING: - from logging import Logger - from redux.test import StoreSnapshotContext from redux.basic_types import ( @@ -17,6 +16,7 @@ BaseEvent, CombineReducerAction, CompleteReducerResult, + EventSubscriptionOptions, FinishAction, InitAction, InitializationActionError, @@ -24,20 +24,16 @@ ) -class CountAction(BaseAction): - ... +class CountAction(BaseAction): ... -class IncrementAction(CountAction): - ... +class IncrementAction(CountAction): ... -class DecrementAction(CountAction): - ... +class DecrementByTwoAction(CountAction): ... -class DoNothingAction(CountAction): - ... +class DoNothingAction(CountAction): ... class CountStateType(Immutable): @@ -53,7 +49,8 @@ class StateType(BaseCombineReducerState): ActionType: TypeAlias = InitAction | FinishAction | CountAction | CombineReducerAction -# Reducers < +# Reducers +# -------- def straight_reducer( state: CountStateType | None, action: ActionType, @@ -64,8 +61,8 @@ def straight_reducer( raise InitializationActionError(action) if isinstance(action, IncrementAction): return CountStateType(count=state.count + 1) - if isinstance(action, DecrementAction): - return CountStateType(count=state.count - 1) + if isinstance(action, DecrementByTwoAction): + return CountStateType(count=state.count - 2) return state @@ -79,13 +76,13 @@ def base10_reducer( raise InitializationActionError(action) if isinstance(action, IncrementAction): return CountStateType(count=state.count + 1) - if isinstance(action, DecrementAction): - return CountStateType(count=state.count - 1) + if isinstance(action, DecrementByTwoAction): + return CountStateType(count=state.count - 2) return state class SleepEvent(BaseEvent): - duration: int + duration: float class PrintEvent(BaseEvent): @@ -102,21 +99,18 @@ def inverse_reducer( raise InitializationActionError(action) if isinstance(action, IncrementAction): return CountStateType(count=state.count - 1) - if isinstance(action, DecrementAction): - return CountStateType(count=state.count + 1) + if isinstance(action, DecrementByTwoAction): + return CountStateType(count=state.count + 2) if isinstance(action, DoNothingAction): return CompleteReducerResult( state=state, actions=[IncrementAction()], - events=[SleepEvent(duration=3)], + events=[SleepEvent(duration=0.1)], ) return state -# > - - -def test_general(snapshot_store: StoreSnapshotContext, logger: Logger) -> None: +def test_general(store_snapshot: StoreSnapshotContext) -> None: from redux import ( CombineReducerRegisterAction, CombineReducerUnregisterAction, @@ -133,39 +127,54 @@ def test_general(snapshot_store: StoreSnapshotContext, logger: Logger) -> None: base10=base10_reducer, ) - # Initialization < + # Initialization + # -------------- store = Store( reducer, - CreateStoreOptions(auto_init=True, threads=2), + CreateStoreOptions(threads=2, action_middleware=print, event_middleware=print), ) - snapshot_store.set_store(store) + store_snapshot.set_store(store) + + store_snapshot.take(title='initialization') + + with pytest.raises(InitializationActionError): + store.dispatch(IncrementAction()) + + store.dispatch(InitAction()) + + # Event Subscription + # ------------------ + store.subscribe(lambda _: store_snapshot.take(title='subscription')) def event_handler(event: SleepEvent) -> None: time.sleep(event.duration) - store.subscribe_event(SleepEvent, event_handler) - # > - - # ----- + def event_handler_without_parameter() -> None: + time.sleep(0.1) - # Subscription < - store.subscribe(lambda _: snapshot_store.take()) - # > + store.subscribe_event(SleepEvent, event_handler) + store.subscribe_event( + SleepEvent, + event_handler_without_parameter, + options=EventSubscriptionOptions(immediate_run=True), + ) - # ----- + # Autorun + # ------- - # Autorun < @store.autorun(lambda state: state.base10) def render(base10_value: CountStateType) -> int: - snapshot_store.take() + store_snapshot.take(title='autorun') return base10_value.count - render.subscribe(lambda a: logger.info(a)) + render.subscribe(lambda _: store_snapshot.take(title='autorun_subscription')) - snapshot_store.take() + # Dispatch + # -------- + store_snapshot.take() store.dispatch(IncrementAction()) - snapshot_store.take() + store_snapshot.take() store.dispatch( CombineReducerRegisterAction( @@ -176,7 +185,7 @@ def render(base10_value: CountStateType) -> int: ) store.dispatch(DoNothingAction()) - snapshot_store.take() + store_snapshot.take() store.dispatch( CombineReducerUnregisterAction( @@ -184,10 +193,15 @@ def render(base10_value: CountStateType) -> int: key='straight', ), ) - snapshot_store.take() + store_snapshot.take() - store.dispatch(DecrementAction()) - snapshot_store.take() + store.dispatch(DecrementByTwoAction()) + store_snapshot.take() + store.dispatch( + with_state=lambda state: DecrementByTwoAction() if state else IncrementAction(), + ) + store_snapshot.take() + # Finish + # ------ store.dispatch(FinishAction()) - # > diff --git a/tests/test_serialization.py b/tests/test_serialization.py new file mode 100644 index 0000000..0440d98 --- /dev/null +++ b/tests/test_serialization.py @@ -0,0 +1,84 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107 +from __future__ import annotations + +from typing import Callable + +import pytest + +from redux.main import Store + + +def test_int() -> None: + assert Store.serialize_value(1) == 1 + + +def test_float() -> None: + assert Store.serialize_value(1.0) == 1.0 + + +def test_str() -> None: + assert Store.serialize_value('string') == 'string' + + +def test_bool() -> None: + assert Store.serialize_value(obj=True) is True + assert Store.serialize_value(obj=False) is False + + +def test_none() -> None: + assert Store.serialize_value(None) is None + + +def test_callable() -> None: + def func() -> str: + return 'string' + + assert Store.serialize_value(func) == 'string' + + +def test_list() -> None: + assert Store.serialize_value([1, 2, 3]) == [1, 2, 3] + + +class InvalidType: ... + + +def test_invalid() -> None: + with pytest.raises( + TypeError, + match=f'Unable to serialize object with type `{InvalidType}`', + ): + Store.serialize_value(InvalidType()) + + +def test_immutable() -> None: + from immutable import Immutable + + class State(Immutable): + integer: int + floating_poing: float + string: str + boolean: bool + none: None + function: Callable + list_: list[int] + + assert Store.serialize_value( + State( + integer=1, + floating_poing=1.0, + string='string', + boolean=True, + none=None, + function=lambda: 'string', + list_=[1, 2, 3], + ), + ) == { + 'integer': 1, + 'floating_poing': 1.0, + 'string': 'string', + 'boolean': True, + 'none': None, + 'function': 'string', + 'list_': [1, 2, 3], + } diff --git a/tests/test_todo.py b/tests/test_todo.py index 2f3a657..516b470 100644 --- a/tests/test_todo.py +++ b/tests/test_todo.py @@ -1,4 +1,4 @@ -# ruff: noqa: A003, D100, D101, D102, D103, D104, D105, D107, T201 +# ruff: noqa: D100, D101, D102, D103, D104, D107 from __future__ import annotations import time @@ -14,7 +14,7 @@ from redux.test import StoreSnapshotContext -def test_todo(snapshot_store: StoreSnapshotContext, logger: Logger) -> None: +def test_todo(store_snapshot: StoreSnapshotContext, logger: Logger) -> None: from redux import BaseAction, Store from redux.basic_types import ( BaseEvent, @@ -87,7 +87,7 @@ def reducer( return state store = Store(reducer, options=CreateStoreOptions(auto_init=True)) - snapshot_store.set_store(store) + store_snapshot.set_store(store) # subscription: dummy_render = logger.info @@ -98,7 +98,7 @@ def reducer( lambda state: state.items[0].content if len(state.items) > 0 else None, ) def reaction(_: str | None) -> None: - snapshot_store.take() + store_snapshot.take() _ = reaction @@ -109,7 +109,7 @@ def reaction(_: str | None) -> None: # dispatch: store.dispatch(AddTodoItemAction(content='New Item', timestamp=time.time())) - snapshot_store.take() + store_snapshot.take() store.dispatch(FinishAction()) - snapshot_store.take() + store_snapshot.take() diff --git a/tests/test_weakref.py b/tests/test_weakref.py new file mode 100644 index 0000000..1e9c57d --- /dev/null +++ b/tests/test_weakref.py @@ -0,0 +1,328 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107 +from __future__ import annotations + +import weakref +from dataclasses import replace +from typing import TYPE_CHECKING, Generator + +import pytest +from immutable import Immutable + +from redux.basic_types import ( + AutorunOptions, + BaseAction, + BaseEvent, + CreateStoreOptions, + EventSubscriptionOptions, + FinishAction, + InitAction, + InitializationActionError, +) +from redux.main import Store + +if TYPE_CHECKING: + from redux.test import StoreSnapshotContext + + +class StateType(Immutable): + value: int + + +class IncrementAction(BaseAction): ... + + +class DummyEvent(BaseEvent): ... + + +def reducer(state: StateType | None, action: IncrementAction | InitAction) -> StateType: + if state is None: + if isinstance(action, InitAction): + return StateType(value=0) + raise InitializationActionError(action) + + if isinstance(action, IncrementAction): + return replace(state, value=state.value + 1) + return state + + +class AutorunClass: + def __init__(self: AutorunClass, store_snapshot: StoreSnapshotContext) -> None: + self.store_snapshot = store_snapshot + + def method_with_keep_ref(self: AutorunClass, value: int) -> int: + self.store_snapshot.take(title='autorun_method_with_keep_ref') + return value + + def method_without_keep_ref(self: AutorunClass, _: int) -> int: + pytest.fail('This should never be called') + + +class SubscriptionClass: + def __init__(self: SubscriptionClass, store_snapshot: StoreSnapshotContext) -> None: + self.store_snapshot = store_snapshot + + def method_with_keep_ref(self: SubscriptionClass, _: StateType) -> None: + self.store_snapshot.take(title='subscription_method_with_keep_ref') + + def method_without_keep_ref(self: SubscriptionClass, _: StateType) -> None: + pytest.fail('This should never be called') + + +class EventSubscriptionClass: + def __init__( + self: EventSubscriptionClass, + store_snapshot: StoreSnapshotContext, + ) -> None: + self.store_snapshot = store_snapshot + + def method_with_keep_ref(self: EventSubscriptionClass, _: DummyEvent) -> None: + self.store_snapshot.take(title='event_subscription_method_with_keep_ref') + + def method_without_keep_ref(self: EventSubscriptionClass, _: DummyEvent) -> None: + pytest.fail('This should never be called') + + +@pytest.fixture() +def store() -> ( + Generator[Store[StateType, IncrementAction | InitAction, DummyEvent], None, None] +): + store = Store(reducer, options=CreateStoreOptions(auto_init=True)) + yield store + store.dispatch(FinishAction()) + + +def test_autorun( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render_with_keep_ref(value: int) -> int: + store_snapshot.take() + return value + + @store.autorun( + lambda state: state.value, + options=AutorunOptions(keep_ref=False, initial_run=False), + ) + def render_without_keep_ref(_: int) -> int: + pytest.fail('This should never be called') + + ref = weakref.ref(render_with_keep_ref) + del render_with_keep_ref + assert ref() is not None + + ref = weakref.ref(render_without_keep_ref) + del render_without_keep_ref + assert ref() is None + + store.dispatch(IncrementAction()) + + +def test_autorun_method( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + instance_with_keep_ref = AutorunClass(store_snapshot) + store.autorun(lambda state: state.value)( + instance_with_keep_ref.method_with_keep_ref, + ) + + ref = weakref.ref(instance_with_keep_ref) + del instance_with_keep_ref + assert ref() is not None + + instance_without_keep_ref = AutorunClass(store_snapshot) + store.autorun( + lambda state: state.value, + options=AutorunOptions(keep_ref=False, initial_run=False), + )( + instance_without_keep_ref.method_without_keep_ref, + ) + + ref = weakref.ref(instance_without_keep_ref) + del instance_without_keep_ref + assert ref() is None + + store.dispatch(IncrementAction()) + + +def test_autorun_subscription( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + store_snapshot.take() + return value + + def autorun_subscription_with_keep_ref(_: int) -> None: + store_snapshot.take(title='autorun_subscription') + + render.subscribe(autorun_subscription_with_keep_ref) + ref = weakref.ref(autorun_subscription_with_keep_ref) + del autorun_subscription_with_keep_ref + assert ref() is not None + + def autorun_subscription_without_keep_ref(_: int) -> None: + pytest.fail('This should never be called') + + render.subscribe( + autorun_subscription_without_keep_ref, + keep_ref=False, + initial_run=False, + ) + ref = weakref.ref(autorun_subscription_without_keep_ref) + del autorun_subscription_without_keep_ref + assert ref() is None + + store.dispatch(IncrementAction()) + + +def test_autorun_method_subscription( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + @store.autorun(lambda state: state.value) + def render(value: int) -> int: + store_snapshot.take() + return value + + instance_with_keep_ref = AutorunClass(store_snapshot) + render.subscribe(instance_with_keep_ref.method_with_keep_ref) + + ref = weakref.ref(instance_with_keep_ref) + del instance_with_keep_ref + assert ref() is not None + + instance_without_keep_ref = AutorunClass(store_snapshot) + render.subscribe( + instance_without_keep_ref.method_without_keep_ref, + keep_ref=False, + initial_run=False, + ) + + ref = weakref.ref(instance_without_keep_ref) + del instance_without_keep_ref + assert ref() is None + + store.dispatch(IncrementAction()) + + +def test_subscription( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + def subscription_with_keep_ref(_: StateType) -> None: + store_snapshot.take() + + store.subscribe(subscription_with_keep_ref) + ref = weakref.ref(subscription_with_keep_ref) + del subscription_with_keep_ref + assert ref() is not None + + def subscription_without_keep_ref(_: StateType) -> None: + pytest.fail('This should never be called') + + store.subscribe(subscription_without_keep_ref, keep_ref=False) + ref = weakref.ref(subscription_without_keep_ref) + del subscription_without_keep_ref + assert ref() is None + + store.dispatch(IncrementAction()) + + +def test_subscription_method( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + instance_with_keep_ref = SubscriptionClass(store_snapshot) + store.subscribe(instance_with_keep_ref.method_with_keep_ref) + + ref = weakref.ref(instance_with_keep_ref) + del instance_with_keep_ref + assert ref() is not None + + instance_without_keep_ref = SubscriptionClass(store_snapshot) + store.subscribe(instance_without_keep_ref.method_without_keep_ref, keep_ref=False) + + ref = weakref.ref(instance_without_keep_ref) + del instance_without_keep_ref + assert ref() is None + + store.dispatch(IncrementAction()) + + +def test_event_subscription( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + def event_subscription_with_keep_ref(_: DummyEvent) -> None: + store_snapshot.take() + + store.subscribe_event( + DummyEvent, + event_subscription_with_keep_ref, + options=EventSubscriptionOptions(immediate_run=True), + ) + ref = weakref.ref(event_subscription_with_keep_ref) + del event_subscription_with_keep_ref + assert ref() is not None + + def event_subscription_without_keep_ref(_: DummyEvent) -> None: + pytest.fail('This should never be called') + + store.subscribe_event( + DummyEvent, + event_subscription_without_keep_ref, + options=EventSubscriptionOptions(keep_ref=False, immediate_run=True), + ) + ref = weakref.ref(event_subscription_without_keep_ref) + del event_subscription_without_keep_ref + assert ref() is None + + store.dispatch(DummyEvent()) + + +def test_event_subscription_method( + store_snapshot: StoreSnapshotContext, + store: Store[StateType, IncrementAction | InitAction, DummyEvent], +) -> None: + store_snapshot.set_store(store) + + instance_with_keep_ref = EventSubscriptionClass(store_snapshot) + store.subscribe_event( + DummyEvent, + instance_with_keep_ref.method_with_keep_ref, + options=EventSubscriptionOptions(immediate_run=True), + ) + + ref = weakref.ref(instance_with_keep_ref) + del instance_with_keep_ref + assert ref() is not None + + instance_without_keep_ref = EventSubscriptionClass(store_snapshot) + store.subscribe_event( + DummyEvent, + instance_without_keep_ref.method_without_keep_ref, + options=EventSubscriptionOptions(keep_ref=False), + ) + + ref = weakref.ref(instance_without_keep_ref) + del instance_without_keep_ref + assert ref() is None + + store.dispatch(DummyEvent())