From debf12ded6cf8e8ddffbb85c908250e46eb7ef3e Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Sat, 4 May 2024 23:38:31 +0400 Subject: [PATCH] feat(core): add `view` method to `Store` to allow computing a derived value from the state only when it is accessed and caching the result until the relevant parts of the state change --- CHANGELOG.md | 6 +++ poetry.lock | 46 ++++++++++---------- pyproject.toml | 4 +- redux/__init__.py | 16 ++++--- redux/autorun.py | 99 +++++++++++++++++-------------------------- redux/basic_types.py | 91 ++++++++++++++++++++++++++------------- redux/main.py | 51 +++++++++++++++++++--- tests/test_autorun.py | 48 ++++++++++++--------- 8 files changed, 215 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e141555..cb758c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 0.15.1 + +- feat(core): add `view` method to `Store` to allow computing a derived value from + the state only when it is accessed and caching the result until the relevant parts + of the state change + ## Version 0.15.0 - refactor(autorun)!: setting `initial_run` option of autorun to `False` used to diff --git a/poetry.lock b/poetry.lock index 25ecb0d..d51ad8f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "colorama" @@ -157,13 +157,13 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "pyright" -version = "1.1.357" +version = "1.1.361" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.357-py3-none-any.whl", hash = "sha256:1cf29ee38e4928131895cd8e90eef37b5b77e2ed72a14e6e8e2405266f5f0aca"}, - {file = "pyright-1.1.357.tar.gz", hash = "sha256:7c66261116c78c5fa9629134fe85c54cc5302ab73e376be4b0a99d89c80a9403"}, + {file = "pyright-1.1.361-py3-none-any.whl", hash = "sha256:c50fc94ce92b5c958cfccbbe34142e7411d474da43d6c14a958667e35b9df7ea"}, + {file = "pyright-1.1.361.tar.gz", hash = "sha256:1d67933315666b05d230c85ea8fb97aaa2056e4092a13df87b7765bb9e8f1a8d"}, ] [package.dependencies] @@ -258,28 +258,28 @@ typing-extensions = ">=4.10.0,<5.0.0" [[package]] name = "ruff" -version = "0.3.5" +version = "0.4.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"}, - {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"}, - {file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"}, - {file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"}, - {file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"}, - {file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"}, - {file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"}, - {file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"}, - {file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"}, - {file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"}, - {file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"}, + {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, + {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, + {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, + {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, + {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, + {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, ] [[package]] @@ -337,4 +337,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "31b4818ff5368cfa488f7abffd4145458ef65de25cc7f3cd41141e42f161df24" +content-hash = "2bed3b9c22fb7750df3ff6adeb44835bc683e519d7214358bcac9e5bf70a34a7" diff --git a/pyproject.toml b/pyproject.toml index 6dacbe4..7d07d36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,8 +16,8 @@ optional = true [tool.poetry.group.dev.dependencies] poethepoet = "^0.24.4" -pyright = "^1.1.357" -ruff = "^0.3.5" +pyright = "^1.1.361" +ruff = "^0.4.3" pytest = "^8.1.1" pytest-cov = "^4.1.0" pytest-timeout = "^2.3.1" diff --git a/redux/__init__.py b/redux/__init__.py index 5af9af3..5a66436 100644 --- a/redux/__init__.py +++ b/redux/__init__.py @@ -4,7 +4,6 @@ AutorunDecorator, AutorunOptions, AutorunReturnType, - AutorunType, BaseAction, BaseCombineReducerState, BaseEvent, @@ -24,6 +23,8 @@ ReducerResult, ReducerType, Scheduler, + ViewDecorator, + ViewReturnType, is_complete_reducer_result, is_state_reducer_result, ) @@ -34,9 +35,13 @@ 'AutorunDecorator', 'AutorunOptions', 'AutorunReturnType', - 'AutorunType', 'BaseAction', + 'BaseCombineReducerState', 'BaseEvent', + 'CombineReducerAction', + 'CombineReducerInitAction', + 'CombineReducerRegisterAction', + 'CombineReducerUnregisterAction', 'CompleteReducerResult', 'CreateStoreOptions', 'Dispatch', @@ -49,13 +54,10 @@ 'ReducerResult', 'ReducerType', 'Scheduler', + 'ViewDecorator', + 'ViewReturnType', 'is_complete_reducer_result', 'is_state_reducer_result', - 'BaseCombineReducerState', - 'CombineReducerAction', - 'CombineReducerInitAction', - 'CombineReducerRegisterAction', - 'CombineReducerUnregisterAction', 'combine_reducers', 'Store', ) diff --git a/redux/autorun.py b/redux/autorun.py index 11beaf0..3ba1372 100644 --- a/redux/autorun.py +++ b/redux/autorun.py @@ -1,15 +1,14 @@ # ruff: noqa: D100, D101, D102, D103, D104, D105, D107 from __future__ import annotations -import functools import inspect import weakref from asyncio import Task, iscoroutine -from inspect import signature -from typing import TYPE_CHECKING, Any, Callable, Generic, cast +from typing import TYPE_CHECKING, Any, Callable, Concatenate, Generic, cast from redux.basic_types import ( Action, + AutorunArgs, AutorunOptions, AutorunOriginalReturnType, ComparatorOutput, @@ -19,8 +18,6 @@ ) if TYPE_CHECKING: - from types import MethodType - from redux.main import Store @@ -32,6 +29,7 @@ class Autorun( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], ): def __init__( # noqa: PLR0913 @@ -40,8 +38,10 @@ def __init__( # noqa: PLR0913 store: Store[State, Action, Event], selector: Callable[[State], SelectorOutput], comparator: Callable[[State], Any] | None, - func: Callable[[SelectorOutput], AutorunOriginalReturnType] - | Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType], + func: Callable[ + Concatenate[SelectorOutput, AutorunArgs], + AutorunOriginalReturnType, + ], options: AutorunOptions[AutorunOriginalReturnType], ) -> None: if not options.reactive and options.auto_call: @@ -54,9 +54,9 @@ def __init__( # noqa: PLR0913 if options.keep_ref: self._func = func elif inspect.ismethod(func): - self._func = weakref.WeakMethod(func) + self._func = weakref.WeakMethod(func, self.unsubscribe) else: - self._func = weakref.ref(func, lambda _: self.unsubscribe()) + self._func = weakref.ref(func, self.unsubscribe) self._options = options self._last_selector_result: SelectorOutput | None = None @@ -70,12 +70,19 @@ def __init__( # noqa: PLR0913 | weakref.ref[Callable[[AutorunOriginalReturnType], Any]] ] = set() - self._check_and_call(store._state, call=self._options.initial_call) # noqa: SLF001 + self._check_and_call(store._state, self._options.initial_call) # noqa: SLF001 if self._options.reactive: - self.unsubscribe = store.subscribe( - functools.partial(self._check_and_call, call=self._options.auto_call), + self._unsubscribe = store.subscribe( + lambda state: self._check_and_call(state, self._options.auto_call), ) + else: + self._unsubscribe = None + + def unsubscribe(self: Autorun, _: weakref.ref | None = None) -> None: + if self._unsubscribe: + self._unsubscribe() + self._unsubscribe = None def inform_subscribers( self: Autorun[ @@ -85,6 +92,7 @@ def inform_subscribers( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], ) -> None: for subscriber_ in self._subscriptions.copy(): @@ -97,37 +105,6 @@ def inform_subscribers( subscriber = subscriber_ subscriber(self._latest_value) - def call_func( - self: Autorun[ - State, - Action, - Event, - SelectorOutput, - ComparatorOutput, - AutorunOriginalReturnType, - ], - selector_result: SelectorOutput, - previous_result: SelectorOutput | None, - func: Callable[ - [SelectorOutput, SelectorOutput], - AutorunOriginalReturnType, - ] - | Callable[[SelectorOutput], AutorunOriginalReturnType] - | MethodType, - ) -> AutorunOriginalReturnType: - if len(signature(func).parameters) == 1: - return cast( - Callable[[SelectorOutput], AutorunOriginalReturnType], - func, - )(selector_result) - return cast( - Callable[ - [SelectorOutput, SelectorOutput | None], - AutorunOriginalReturnType, - ], - func, - )(selector_result, previous_result) - def _task_callback( self: Autorun[ State, @@ -136,6 +113,7 @@ def _task_callback( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], task: Task, ) -> None: @@ -150,10 +128,12 @@ def _check_and_call( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], state: State, - *, - call: bool = True, + _call: bool, # noqa: FBT001 + *args: AutorunArgs.args, + **kwargs: AutorunArgs.kwargs, ) -> None: try: selector_result = self._selector(state) @@ -167,26 +147,19 @@ def _check_and_call( except AttributeError: return if self._should_be_called or comparator_result != self._last_comparator_result: - self._should_be_called = False - previous_result = self._last_selector_result self._last_selector_result = selector_result self._last_comparator_result = comparator_result - func = self._func() if isinstance(self._func, weakref.ref) else self._func - if func: - if call: - self._latest_value = self.call_func( - selector_result, - previous_result, - func, - ) + self._should_be_called = not _call + if _call: + func = ( + self._func() if isinstance(self._func, weakref.ref) else self._func + ) + if func: + self._latest_value = func(selector_result, *args, **kwargs) create_task = self._store._create_task # noqa: SLF001 if iscoroutine(self._latest_value) and create_task: create_task(self._latest_value, callback=self._task_callback) self.inform_subscribers() - else: - self._should_be_called = True - else: - self.unsubscribe() def __call__( self: Autorun[ @@ -196,11 +169,14 @@ def __call__( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], + *args: AutorunArgs.args, + **kwargs: AutorunArgs.kwargs, ) -> AutorunOriginalReturnType: state = self._store._state # noqa: SLF001 if state is not None: - self._check_and_call(state, call=True) + self._check_and_call(state, True, *args, **kwargs) # noqa: FBT003 return cast(AutorunOriginalReturnType, self._latest_value) def __repr__( @@ -211,6 +187,7 @@ def __repr__( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], ) -> str: return f"""{super().__repr__()}(func: {self._func}, last_value: { @@ -225,6 +202,7 @@ def value( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], ) -> AutorunOriginalReturnType: return cast(AutorunOriginalReturnType, self._latest_value) @@ -237,6 +215,7 @@ def subscribe( SelectorOutput, ComparatorOutput, AutorunOriginalReturnType, + AutorunArgs, ], callback: Callable[[AutorunOriginalReturnType], Any], *, diff --git a/redux/basic_types.py b/redux/basic_types.py index 74413ee..6e47e1d 100644 --- a/redux/basic_types.py +++ b/redux/basic_types.py @@ -7,8 +7,10 @@ TYPE_CHECKING, Any, Callable, + Concatenate, Coroutine, Generic, + ParamSpec, Protocol, Sequence, TypeAlias, @@ -36,8 +38,11 @@ class BaseEvent(Immutable): ... SelectorOutput = TypeVar('SelectorOutput', infer_variance=True) ComparatorOutput = TypeVar('ComparatorOutput', infer_variance=True) AutorunOriginalReturnType = TypeVar('AutorunOriginalReturnType', infer_variance=True) +ViewOriginalReturnType = TypeVar('ViewOriginalReturnType', infer_variance=True) Comparator = Callable[[State], ComparatorOutput] EventHandler = Callable[[Event], Any] | Callable[[], Any] +AutorunArgs = ParamSpec('AutorunArgs') +ViewArgs = ParamSpec('ViewArgs') class CompleteReducerResult(Immutable, Generic[State, Action, Event]): @@ -125,40 +130,22 @@ class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]): subscribers_keep_ref: bool = True -class AutorunType(Protocol, Generic[State]): - def __call__( - self: AutorunType, - selector: Callable[[State], SelectorOutput], - comparator: Callable[[State], Any] | None = None, - *, - options: AutorunOptions[AutorunOriginalReturnType] | None = None, - ) -> AutorunDecorator[ - State, - SelectorOutput, - AutorunOriginalReturnType, - ]: ... - - -class AutorunDecorator( - Protocol, - Generic[ - State, - SelectorOutput, - AutorunOriginalReturnType, - ], -): - def __call__( - self: AutorunDecorator, - func: Callable[[SelectorOutput], AutorunOriginalReturnType] - | Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType], - ) -> AutorunReturnType[AutorunOriginalReturnType]: ... +class ViewOptions(Immutable, Generic[ViewOriginalReturnType]): + default_value: ViewOriginalReturnType | None = None + keep_ref: bool = True + subscribers_initial_run: bool = True + subscribers_keep_ref: bool = True class AutorunReturnType( Protocol, - Generic[AutorunOriginalReturnType], + Generic[AutorunOriginalReturnType, AutorunArgs], ): - def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType: ... + def __call__( + self: AutorunReturnType, + *args: AutorunArgs.args, + **kwargs: AutorunArgs.kwargs, + ) -> AutorunOriginalReturnType: ... @property def value(self: AutorunReturnType) -> AutorunOriginalReturnType: ... @@ -174,6 +161,52 @@ def subscribe( def unsubscribe(self: AutorunReturnType) -> None: ... +AutorunDecorator = Callable[ + [ + Callable[ + Concatenate[SelectorOutput, AutorunArgs], + AutorunOriginalReturnType, + ], + ], + AutorunReturnType[AutorunOriginalReturnType, AutorunArgs], +] + + +ViewDecorator = Callable[ + [ + Callable[ + Concatenate[SelectorOutput, ViewArgs], + ViewOriginalReturnType, + ], + ], + Callable[ViewArgs, ViewOriginalReturnType], +] + + +class ViewReturnType( + Protocol, + Generic[ViewOriginalReturnType, ViewArgs], +): + def __call__( + self: ViewReturnType, + *args: ViewArgs.args, + **kwargs: ViewArgs.kwargs, + ) -> ViewOriginalReturnType: ... + + @property + def value(self: ViewReturnType) -> ViewOriginalReturnType: ... + + def subscribe( + self: ViewReturnType, + callback: Callable[[ViewOriginalReturnType], Any], + *, + initial_run: bool | None = None, + keep_ref: bool | None = None, + ) -> Callable[[], None]: ... + + def unsubscribe(self: ViewReturnType) -> None: ... + + class EventSubscriber(Protocol): def __call__( self: EventSubscriber, diff --git a/redux/main.py b/redux/main.py index 1e37e9c..081ba62 100644 --- a/redux/main.py +++ b/redux/main.py @@ -8,12 +8,13 @@ import weakref from collections import defaultdict from threading import Lock, Thread -from typing import Any, Callable, Generic, cast +from typing import Any, Callable, Concatenate, Generic, cast from redux.autorun import Autorun from redux.basic_types import ( Action, ActionMiddleware, + AutorunArgs, AutorunDecorator, AutorunOptions, AutorunOriginalReturnType, @@ -34,6 +35,11 @@ SelectorOutput, SnapshotAtom, State, + ViewArgs, + ViewDecorator, + ViewOptions, + ViewOriginalReturnType, + ViewReturnType, is_complete_reducer_result, is_state_reducer_result, ) @@ -258,16 +264,18 @@ def autorun( *, options: AutorunOptions[AutorunOriginalReturnType] | None = None, ) -> AutorunDecorator[ - State, SelectorOutput, + AutorunArgs, AutorunOriginalReturnType, ]: """Create a new autorun, reflecting on state changes.""" def decorator( - func: Callable[[SelectorOutput], AutorunOriginalReturnType] - | Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType], - ) -> AutorunReturnType[AutorunOriginalReturnType]: + func: Callable[ + Concatenate[SelectorOutput, AutorunArgs], + AutorunOriginalReturnType, + ], + ) -> AutorunReturnType[AutorunOriginalReturnType, AutorunArgs]: return Autorun( store=self, selector=selector, @@ -278,6 +286,39 @@ def decorator( return decorator + def view( + self: Store[State, Action, Event], + selector: Callable[[State], SelectorOutput], + *, + options: ViewOptions[ViewOriginalReturnType] | None = None, + ) -> ViewDecorator[SelectorOutput, ViewArgs, ViewOriginalReturnType]: + """Create a new view, throttling calls for unchanged selector results.""" + + def decorator( + func: Callable[ + Concatenate[SelectorOutput, ViewArgs], + ViewOriginalReturnType, + ], + ) -> ViewReturnType[ViewOriginalReturnType, ViewArgs]: + _options = options or ViewOptions() + return Autorun( + store=self, + selector=selector, + comparator=None, + func=func, + options=AutorunOptions( + default_value=_options.default_value, + initial_call=False, + auto_call=False, + reactive=False, + keep_ref=_options.keep_ref, + subscribers_initial_run=_options.subscribers_initial_run, + subscribers_keep_ref=_options.subscribers_keep_ref, + ), + ) + + return decorator + @property def snapshot(self: Store[State, Action, Event]) -> SnapshotAtom: """Return a snapshot of the current state of the store.""" diff --git a/tests/test_autorun.py b/tests/test_autorun.py index e788090..8576946 100644 --- a/tests/test_autorun.py +++ b/tests/test_autorun.py @@ -102,13 +102,6 @@ def _(_: int) -> int: pytest.fail('This should never be called') -def test_with_old_value(store_snapshot: StoreSnapshot, store: StoreType) -> None: - @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: StoreSnapshot, store: StoreType, @@ -122,19 +115,6 @@ def _(value: int) -> int: return value -def test_with_comparator_and_old_value( - store_snapshot: StoreSnapshot, - store: StoreType, -) -> None: - @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: StoreSnapshot, store: StoreType) -> None: @store.autorun(lambda state: state.value) def render(value: int) -> int: @@ -310,3 +290,31 @@ def render(_: int) -> None: ... call(2), call(1), ] + + +def test_view_mode_autorun( + store: StoreType, +) -> None: + @store.autorun( + lambda state: state.value, + options=AutorunOptions( + reactive=False, + auto_call=False, + initial_call=False, + default_value=0, + ), + ) + def render(_: int, *, some_other_value: int) -> int: + return some_other_value + + assert render(some_other_value=12345) == 12345 + + +def test_view( + store: StoreType, +) -> None: + @store.view(lambda state: state.value) + def render(_: int, *, some_other_value: int) -> int: + return some_other_value + + assert render(some_other_value=12345) == 12345