From 01ecce9b3593596faf0d0bbe9241ef5b6ab79ea2 Mon Sep 17 00:00:00 2001 From: Jochem Smit Date: Wed, 6 Nov 2024 15:37:36 +0100 Subject: [PATCH] reactives as stores --- dont_fret/web/bursts/components.py | 2 +- dont_fret/web/home/burst_settings.py | 36 ++++-- dont_fret/web/home/info_cards.py | 20 ++- dont_fret/web/models.py | 177 ++++++++++++++++++++++----- dont_fret/web/reactive.py | 58 --------- dont_fret/web/state.py | 11 +- 6 files changed, 194 insertions(+), 110 deletions(-) delete mode 100644 dont_fret/web/reactive.py diff --git a/dont_fret/web/bursts/components.py b/dont_fret/web/bursts/components.py index e9de87b..d37ee82 100644 --- a/dont_fret/web/bursts/components.py +++ b/dont_fret/web/bursts/components.py @@ -462,7 +462,7 @@ class BurstFigureSelection: ) def __post_init__(self): - self.fret_store._items.subscribe(self.on_fret_store) + self.fret_store._reactive.subscribe(self.on_fret_store) self.reset() def on_fret_store(self, new_value: list[FRETNode]): diff --git a/dont_fret/web/home/burst_settings.py b/dont_fret/web/home/burst_settings.py index f283604..f4a9f1c 100644 --- a/dont_fret/web/home/burst_settings.py +++ b/dont_fret/web/home/burst_settings.py @@ -5,15 +5,17 @@ from solara.alias import rv from dont_fret.config import cfg -from dont_fret.web.reactive import BurstSettingsReactive +from dont_fret.config.config import BurstColor +from dont_fret.web.models import BurstSettingsStore, ListStore, use_liststore @solara.component -def BurstColorSettingForm( - burst_settings: BurstSettingsReactive, settings_name: str, color_idx: int -): - burst_color = burst_settings.get_color(settings_name, color_idx) - setter = partial(burst_settings.update_color, settings_name, color_idx) +def BurstColorSettingForm(color_store: ListStore[BurstColor], color_idx: int): + burst_color = color_store[color_idx] + # burst_color = burst_settings.get_color(settings_name, color_idx) + + setter = partial(color_store.update, color_idx) + # setter = partial(burst_settings.update_color, settings_name, color_idx) with solara.ColumnsResponsive([8, 4]): with solara.Card("Search Thresholds"): with solara.Column(): @@ -63,8 +65,9 @@ def on_value(val, stream: str = stream): @solara.component -def BurstSettingsDialog(burst_settings: BurstSettingsReactive, settings_name, on_close): - tab, set_tab = solara.use_state(0) +def BurstSettingsDialog(colors: list[BurstColor], settings_name, on_value, on_close): + tab, set_tab = solara.use_state(0) # active tab number + color_store = use_liststore(colors) title = f"Editing burst search settings: '{settings_name}'" with solara.Card(title): @@ -73,14 +76,17 @@ def on_tab_change(val): set_tab(val) def on_tab_remove(*args): - burst_settings.remove_color(settings_name) + color_store.pop(len(color_store) - 1) + # burst_settings.remove_color(settings_name) def on_tab_add(*args): - burst_settings.add_color(settings_name) + color_store.append(BurstColor()) + # burst_settings.add_color(settings_name) try: - n_tabs = len(burst_settings.value[settings_name]) + n_tabs = len(color_store) # This can happen if the settings are reset while a new set was added and is selected + # update: not sure how we can get here atm except KeyError: return @@ -97,14 +103,18 @@ def on_tab_add(*args): with rv.TabsItems(v_model=tab): for i in range(n_tabs): with rv.TabItem(): - BurstColorSettingForm(burst_settings, settings_name, i) + BurstColorSettingForm(color_store, i) + + def save_close(): + on_value(color_store.items) + on_close(False) with solara.CardActions(): # todo align center with rv.Layout(row=True): solara.Button( label="Save & close", - on_click=lambda *args: on_close(False), + on_click=save_close, text=True, classes=["centered"], ) diff --git a/dont_fret/web/home/info_cards.py b/dont_fret/web/home/info_cards.py index 4720ca6..e2d9d2d 100644 --- a/dont_fret/web/home/info_cards.py +++ b/dont_fret/web/home/info_cards.py @@ -12,14 +12,16 @@ from solara.alias import rv import dont_fret.web.state as state +from dont_fret.config.config import BurstColor from dont_fret.web.components import EditableTitle from dont_fret.web.home.burst_settings import BurstSettingsDialog from dont_fret.web.home.methods import task_burst_search from dont_fret.web.methods import format_size -from dont_fret.web.models import BurstNode, PhotonNode -from dont_fret.web.models import ListStore -from dont_fret.web.reactive import ( - BurstSettingsReactive, +from dont_fret.web.models import ( + BurstNode, + BurstSettingsStore, + ListStore, + PhotonNode, ) from dont_fret.web.utils import has_photons @@ -46,7 +48,7 @@ def PhotonInfoCard( photon_store: ListStore[PhotonNode], burst_store: ListStore[BurstNode], filebrowser_folder: solara.Reactive[Path], - burst_settings: BurstSettingsReactive, + burst_settings: BurstSettingsStore, # open_: solara.Reactive[list[str]], ): # todo reactives @@ -229,7 +231,13 @@ def reset(): persistent=False, max_width=800, ): - BurstSettingsDialog(burst_settings, bs_name, on_close=set_show_settings_dialog) + + def on_value(value: list[BurstColor], name=bs_name): + burst_settings[name] = value + + BurstSettingsDialog( + burst_settings[bs_name], bs_name, on_value=on_value, on_close=set_show_settings_dialog + ) @solara.component # type: ignore diff --git a/dont_fret/web/models.py b/dont_fret/web/models.py index 2575381..b40cf86 100644 --- a/dont_fret/web/models.py +++ b/dont_fret/web/models.py @@ -3,15 +3,27 @@ import dataclasses import uuid from collections import UserList -from dataclasses import dataclass, field, make_dataclass +from dataclasses import dataclass, field, make_dataclass, replace from pathlib import Path -from typing import Callable, ContextManager, Generic, Optional, Tuple, TypedDict, TypeVar +from typing import ( + Callable, + ContextManager, + Dict, + Generic, + List, + Optional, + Tuple, + TypedDict, + TypeVar, +) import numpy as np import polars as pl import solara +from solara import Reactive from solara.toestand import merge_state +from dont_fret import cfg from dont_fret.config.config import BurstColor, BurstFilterItem T = TypeVar("T") @@ -248,11 +260,25 @@ class _NoDefault: NO_DEFAULT = _NoDefault() -class ListStore(Generic[T]): +class Store(Generic[T]): + def __init__(self, initial_value: T): + self._reactive = solara.reactive(initial_value) + + def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None): + return self._reactive.subscribe(listener, scope=scope) + + def subscribe_change( + self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None + ): + return self._reactive.subscribe_change(listener, scope=scope) + + +class ListStore(Store[list[T]]): """baseclass for reactive list""" def __init__(self, items: Optional[list[T]] = None): - self._items = solara.reactive(items if items is not None else []) + super().__init__(items if items is not None else []) + # self._reactive = solara.reactive(items if items is not None else []) def __len__(self): return len(self.items) @@ -263,65 +289,66 @@ def __getitem__(self, idx: int) -> T: def __iter__(self): return iter(self.items) - @property + @property # TODO perhaps refactor to value def items(self): - return self._items.value + return self._reactive.value def get_item(self, idx: int, default: R = NO_DEFAULT) -> T | R: try: - return self._items.value[idx] + return self._reactive.value[idx] except IndexError: if default is NO_DEFAULT: raise IndexError(f"Index {idx} is out of range") return default def set(self, items: list[T]) -> None: - self._items.value = items + self._reactive.value = items def set_item(self, idx: int, item: T) -> None: - new_items = self._items.value.copy() + new_items = self._reactive.value.copy() if idx == len(new_items): new_items.append(item) elif idx < len(new_items): new_items[idx] = item else: raise IndexError(f"Index {idx} is out of range") - self._items.value = new_items + self._reactive.value = new_items def append(self, item: T) -> None: - self._items.value = [*self._items.value, item] + self._reactive.value = [*self._reactive.value, item] def extend(self, items: list[T]) -> None: new_value = self.items.copy() new_value.extend(items) - self._items.value = new_value + self._reactive.value = new_value + + def insert(self, idx: int, item: T) -> None: + new_value = self.items.copy() + new_value.insert(idx, item) + self._reactive.value = new_value + + def remove(self, item: T) -> None: + self._reactive.value = [it for it in self.items if it != item] def pop(self, idx: int) -> T: item = self.items[idx] - self._items.value = self.items[:idx] + self.items[idx + 1 :] + self._reactive.value = self.items[:idx] + self.items[idx + 1 :] return item - def remove(self, item: T) -> None: - self._items.value = [it for it in self.items if it != item] + def clear(self) -> None: + self._reactive.value = [] + + def index(self, item: T) -> int: + return self.items.index(item) def update(self, idx: int, **kwargs): new_value = self.items.copy() updated_item = merge_state(new_value[idx], **kwargs) new_value[idx] = updated_item - self._items.value = new_value + self._reactive.value = new_value - def index(self, item: T) -> int: - return self.items.index(item) - - def subscribe( - self, listener: Callable[[list[T]], None], scope: Optional[ContextManager] = None - ): - return self._items.subscribe(listener, scope=scope) - - def subscribe_change( - self, listener: Callable[[list[T], list[T]], None], scope: Optional[ContextManager] = None - ): - return self._items.subscribe_change(listener, scope=scope) + def count(self, item: T) -> int: + return self.items.count(item) def use_liststore(value: list[T] | ListStore[T]) -> ListStore[T]: @@ -331,14 +358,65 @@ def make_liststore(): if not isinstance(value, ListStore): return ListStore(value) - store = solara.use_memo(make_liststore, []) + store = solara.use_memo(make_liststore, [value]) # type ignore if isinstance(value, ListStore): + raise ValueError("look at use_reactive to implement all cases properly") store = value assert store is not None return store +solara.use_reactive +K = TypeVar("K") +V = TypeVar("V") + + +class DictStore(Store[dict[K, V]]): + # todo maybe require values to be a dict + def __init__(self, values: Optional[dict] = None): + super().__init__(values if values is not None else {}) + + @property + def value(self): + return self._reactive.value + + def set(self, items: dict[K, V]) -> None: + self._reactive.value = items + + def __len__(self) -> int: + return len(self._reactive.value) + + def __getitem__(self, key): + return self._reactive.value[key] + + def __setitem__(self, key, value): + new_value = self._reactive.value.copy() + new_value[key] = value + self._reactive.value = new_value + + def items(self): + return self._reactive.value.items() + + def keys(self): + return self._reactive.value.keys() + + def values(self): + return self._reactive.value.values() + + def pop(self, key) -> V: + new_value = self._reactive.value.copy() + item = new_value.pop(key) + self.set(new_value) + return item + + def popitem(self) -> tuple[K, V]: + new_value = self._reactive.value.copy() + item = new_value.popitem() + self.set(new_value) + return item + + @dataclasses.dataclass class FRETNode: name: solara.Reactive[str] # displayed name @@ -398,3 +476,44 @@ def __post_init__(self): @property def record(self) -> dict: return {"text": self.text, "value": self.value} + + +class BurstSettingsStore(DictStore[str, list[BurstColor]]): + """TODO most of these methods are now unused""" + + def reset(self) -> None: + self.set({k: v for k, v in cfg.burst_search.items()}) + + def add_settings(self, setting_name: str): + """Adds a new burst settings name with default settings.""" + new_value = self.value.copy() + new_value[setting_name] = [BurstColor()] + self.set(new_value) + + def remove_settings(self, setting_name: str): + self.pop(setting_name) + + def remove_color(self, setting_name: str): + """Removes the last color from the list of colors for a given burst settings name.""" + new_colors = self.value[setting_name].copy() + if len(new_colors) == 1: + return + new_colors.pop() + self[setting_name] = new_colors + + def update_color(self, settings_name: str, color_idx: int, **kwargs): + new_colors = self.value[settings_name].copy() + new_colors[color_idx] = replace(new_colors[color_idx], **kwargs) + self[settings_name] = new_colors + + def get_color(self, settings_name: str, color_idx: int) -> BurstColor: + return self.value[settings_name][color_idx] + + def add_color(self, settings_name: str): + colors = self.value[settings_name] + colors.append(BurstColor()) + self[settings_name] = colors + + @property + def settings_names(self) -> list[str]: + return list(self.keys()) diff --git a/dont_fret/web/reactive.py b/dont_fret/web/reactive.py deleted file mode 100644 index 935b574..0000000 --- a/dont_fret/web/reactive.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Dict, List, TypeVar - -from solara import Reactive -from solara.lab import Ref - -from dont_fret import cfg -from dont_fret.config.config import BurstColor -from dont_fret.web.models import ( - BurstColorList, -) - -S = TypeVar("S") - - -# make composed instead of inherited -class BurstSettingsReactive(Reactive[Dict[str, List[BurstColor]]]): - def reset(self) -> None: - self.value = {k: BurstColorList(v) for k, v in cfg.burst_search.items()} - - def add_settings(self, setting_name: str): - """Adds a new burst settings name with default settings.""" - new_value = self.value.copy() - new_value[setting_name] = [BurstColor()] - self.value = new_value - - def remove_settings(self, setting_name: str): - """Removes a burst settings name.""" - new_value = self.value.copy() - new_value.pop(setting_name) - self.value = new_value - - def remove_color(self, setting_name: str): - """Removes the last color from the list of colors for a given burst settings name.""" - colors_ref = Ref(self.fields[setting_name]) - new_colors = colors_ref.get().copy() - if len(new_colors) == 1: - return - new_colors.pop() - colors_ref.set(new_colors) # isnt this a copy? - - def update_color(self, settings_name: str, color_idx: int, **kwargs): - # calling update / setter is fine because there are no listeners attached - Ref(self.fields[settings_name][color_idx]).update(**kwargs) - - def get_color(self, settings_name: str, color_idx: int) -> BurstColor: - # this is not allowed because it creates a listener with a too high idx - # return Ref(self.fields[setting_name][color_idx]).get() - - # do this instead - return self.value[settings_name][color_idx] - - def add_color(self, settings_name: str): - colors_ref = Ref(self.fields[settings_name]) - colors_ref.set(colors_ref.get().copy() + [BurstColor()]) - - @property - def settings_names(self) -> List[str]: - return list(self.value.keys()) diff --git a/dont_fret/web/state.py b/dont_fret/web/state.py index 19e508d..382ea49 100644 --- a/dont_fret/web/state.py +++ b/dont_fret/web/state.py @@ -5,15 +5,20 @@ from dont_fret.config import cfg from dont_fret.web.datamanager import ThreadedDataManager -from dont_fret.web.models import BurstColorList, FRETStore, ListStore, Snackbar -from dont_fret.web.reactive import BurstSettingsReactive +from dont_fret.web.models import ( + BurstColorList, + BurstSettingsStore, + FRETStore, + ListStore, + Snackbar, +) APP_TITLE = "Don't FRET!" filebrowser_folder = solara.Reactive[Path](cfg.web.default_dir) # TODO as liststore -burst_settings = BurstSettingsReactive( +burst_settings = BurstSettingsStore( {k: BurstColorList(v) for k, v in cfg.burst_search.items()} # type ignore )