Skip to content

Commit

Permalink
feat: computed reactive variables
Browse files Browse the repository at this point in the history
Creates a reactive variable that is set to the return value of the function.

The value will be updated when any of the reactive variables used in the function
change.

Example:
```solara
import solara
import solara.lab

a = solara.reactive(1)
b = solara.reactive(2)

@solara.lab.computed
def total():
    return a.value + b.value

def reset():
    a.value = 1
    b.value = 2

@solara.component
def Page():
    print(a, b, total)
    solara.IntSlider("a", value=a)
    solara.IntSlider("b", value=b)
    solara.Text(f"a + b = {total.value}")
    solara.Button("reset", on_click=reset)
```

z.value will be lazily executed the first time, and will be updated
when one of the dependencies changes.
  • Loading branch information
maartenbreddels committed Feb 9, 2024
1 parent 5b55459 commit bbb7584
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 1 deletion.
16 changes: 15 additions & 1 deletion solara/toestand.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,22 +381,36 @@ def __set__(self, obj, value):


class Computed(Reactive[S]):
_storage: KernelStore[S]

def __init__(self, f: Callable[[], S], key=None):
self.f = f

def on_change(*ignore):
with self._auto_subscriber.value:
self.set(f())

self._auto_subscriber = Singleton(lambda: AutoSubscribeContextManager(on_change))
import functools

self._auto_subscriber = Singleton(functools.wraps(AutoSubscribeContextManager)(lambda: AutoSubscribeContextManager(on_change)))

@functools.wraps(f)
def factory():
v = self._auto_subscriber.value
with v:
return f()

super().__init__(KernelStoreFactory(factory, key=key))

# reset on kernel restart (e.g. hot reload)
def reset():
def cleanup():
self._storage.clear()

return cleanup

solara.server.kernel_context.on_kernel_start(reset)

def __repr__(self):
value = super().__repr__()
return "<Computed" + value[len("<Reactive") : -1]
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/toestand_computed_reload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import solara
import solara.lab

value_reactive = solara.reactive(1.0)


@solara.lab.computed
def computed_reactive():
return value_reactive.value + 1.0


@solara.component
def Page():
solara.FloatSlider("test", value=value_reactive)
solara.InputFloat("test", value=computed_reactive.value)
41 changes: 41 additions & 0 deletions tests/unit/toestand_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dataclasses
import threading
import unittest.mock
from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, TypeVar

import ipyvuetify as v
Expand All @@ -15,6 +16,8 @@

from .common import click

HERE = Path(__file__).parent


@dataclasses.dataclass(frozen=True)
class Bears:
Expand Down Expand Up @@ -1127,3 +1130,41 @@ def conditional_add():
assert z.value == 42
assert z._auto_subscriber.value.reactive_used == {x}
assert calls == 4


def test_computed_reload(no_kernel_context):
import solara.server.reload
from solara.server.app import AppScript

name = str(HERE / "toestand_computed_reload.py")
app = AppScript(name)
try:
assert len(app.routes) == 1
route = app.routes[0]
c = app.run()
kernel_shared = kernel.Kernel()
kernel_context = solara.server.kernel_context.VirtualKernelContext(id="1", kernel=kernel_shared, session_id="session-1")
with kernel_context:
root = solara.RoutingProvider(children=[c], routes=app.routes, pathname="/")
box, rc = solara.render(root, handle_error=False)
kernel_context.app_object = rc
text = rc.find(v.TextField)
assert text.widget.v_model == "2.0"
route.module.value_reactive.value = 2 # type: ignore
module = route.module
assert text.widget.v_model == "3.0"
solara.server.reload.reloader._on_change("--unused--")
kernel_context.restart()
c = app.run()
with kernel_context:
route = app.routes[0]
root = solara.RoutingProvider(children=[c], routes=app.routes, pathname="/")
box, rc = solara.render(root, handle_error=False)
kernel_context.app_object = rc
text = rc.find(v.TextField)
assert text.widget.v_model == "3.0"
assert route.module is not module
route.module.value_reactive.value = 3 # type: ignore
assert text.widget.v_model == "4.0"
finally:
app.close()

0 comments on commit bbb7584

Please sign in to comment.