Skip to content

Commit

Permalink
feat: avoid use of Ref, make use of descriptor to add reactive behavior
Browse files Browse the repository at this point in the history
This already works with pyright due to:
    microsoft/pyright#3245

but mypy does not yet support this:
    python/mypy#13856

See also:
    python/mypy#14868
  • Loading branch information
maartenbreddels committed Mar 24, 2023
1 parent 63265c9 commit 20cc910
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 11 deletions.
54 changes: 53 additions & 1 deletion solara/lab/toestand.py
@@ -1,6 +1,7 @@
import dataclasses
import sys
import threading
import typing
from collections import defaultdict
from operator import getitem
from typing import (
Expand All @@ -15,6 +16,7 @@
TypeVar,
Union,
cast,
overload,
)

import react_ipywidgets as react
Expand Down Expand Up @@ -319,6 +321,25 @@ def __init__(self, field: "FieldBase"):
self._root = field
assert isinstance(self._root, ValueBase)

if typing.TYPE_CHECKING:

@overload
def __get__(self, obj: None = None, cls: Any = ...) -> "ValueSubField[T]":
...

@overload
def __get__(self, obj: "Any", cls: Any = ...) -> T:
...

def __get__(self, obj: Any = None, cls: Any = None) -> Union[T, "ValueSubField[T]"]:
if obj is None:
return self
else:
return obj.__dict__["test"]

def __set__(self, obj: Any, value: T) -> None:
pass

def __str__(self):
return str(self._field)

Expand Down Expand Up @@ -365,10 +386,21 @@ class FieldBase:
_parent: Any

def __getattr__(self, key):
if key in ["_parent", "set", "_lock"] or key.startswith("__"):
if key in ["_parent", "set", "_lock", "subscribe", "value"] or key.startswith("__"):
return self.__dict__[key]
return FieldAttr(self, key)

@property
def value(self):
return self.get()

@value.setter
def value(self, value):
self.set(value)

def subscribe(self, listener: Callable[[Any], None]):
return ValueSubField(self).subscribe(listener)

def __getitem__(self, key):
return FieldItem(self, key)

Expand Down Expand Up @@ -513,3 +545,23 @@ def cleanup():

auto_subscribe_context_manager = AutoSubscribeContextManager
reacton.core._component_context_manager_classes.append(auto_subscribe_context_manager)


if typing.TYPE_CHECKING:
Field = ValueSubField
else:

class FieldRuntime:
def __call__(self, arg):
return arg

def __getitem__(self, item):
return item

Field = FieldRuntime()

T = typing.TypeVar("T")


def field(arg: T) -> Field[T]:
return cast(Field[T], arg)
21 changes: 11 additions & 10 deletions tests/unit/lab/toestand_test.py
Expand Up @@ -11,24 +11,25 @@
import solara as sol
import solara.lab
from solara.lab import State
from solara.lab.toestand import Reactive, Ref, use_sync_external_store
from solara.lab.toestand import Field, Reactive, Ref, field, use_sync_external_store
from solara.server import app, kernel

from ..common import click


@dataclasses.dataclass(frozen=True)
class Bears:
type: str
count: int = dataclasses.field()
count: Field[int]
type: Field[str] = field(dataclasses.field(default_factory=str))


B = TypeVar("B", bound=Bears)


class BearReactive(Reactive[B]):
def increase_population(self):
self.update(count=self.get().count + 1)
c = self.get().count
self.update(count=c + 1)


bears: Bears = Bears(type="brown", count=1)
Expand Down Expand Up @@ -58,8 +59,8 @@ def test_subscribe():
mock_count = unittest.mock.Mock()
unsub = []
unsub += [bear_store.subscribe(mock)]
unsub += [Ref(bear_store.fields.type).subscribe(mock_type)]
unsub += [Ref(bear_store.fields.count).subscribe(mock_count)]
unsub += [bear_store.fields.type.subscribe(mock_type)]
unsub += [bear_store.fields.count.subscribe(mock_count)]
mock.assert_not_called()
bear_store.update(type="purple")
mock.assert_called_with(Bears(type="purple", count=1))
Expand Down Expand Up @@ -129,14 +130,14 @@ def test_nested_update():
mock_count = unittest.mock.Mock()
unsub = []
unsub += [bear_store.subscribe(mock)]
unsub += [Ref(bear_store.fields.type).subscribe(mock_type)]
unsub += [Ref(bear_store.fields.count).subscribe(mock_count)]
unsub += [bear_store.fields.type.subscribe(mock_type)]
unsub += [bear_store.fields.count.subscribe(mock_count)]

def reset_count(new_type):
bear_store.update(count=0)

Ref(bear_store.fields.type).subscribe(reset_count)
Ref(bear_store.fields.type).value = "purple"
bear_store.fields.type.subscribe(reset_count)
bear_store.fields.type.value = "purple"
mock.assert_called_with(Bears(type="purple", count=0))
mock_type.assert_called_with("purple")
mock_count.assert_called_with(0)
Expand Down

0 comments on commit 20cc910

Please sign in to comment.