Skip to content

Commit

Permalink
refactor: put use_thread and use_reactive in separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenbreddels committed Jun 22, 2023
1 parent 48cd315 commit 8aaab96
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 267 deletions.
2 changes: 2 additions & 0 deletions solara/hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from .dataframe import * # noqa: #F401 F403
from .misc import * # noqa: #F401 F403
from .use_reactive import use_reactive # noqa: #F401 F403
from .use_thread import use_thread # noqa: #F401 F403
272 changes: 6 additions & 266 deletions solara/hooks/misc.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import contextlib
import dataclasses
import functools
import inspect
import io
import json
import logging
import os
import sys

# import tempfile
import threading
import time
import urllib.request
import uuid
from typing import IO, Any, Callable, Iterator, Optional, Tuple, TypeVar, Union, cast

import reacton
from typing import IO, Any, Callable, Tuple, TypeVar, Union, cast

import solara
from solara.datatypes import FileContentResult, Result, ResultState

SOLARA_ALLOW_OTHER_TRACER = os.environ.get("SOLARA_ALLOW_OTHER_TRACER", False) in (True, "True", "true", "1")
from solara.datatypes import FileContentResult, Result

logger = logging.getLogger("react-ipywidgets.extra.hooks")
chunk_size_default = 1024**2

__all__ = [
"use_thread",
"use_download",
"use_fetch",
"use_json_load",
Expand All @@ -36,7 +28,6 @@
"use_unique_key",
"use_state_or_update",
"use_previous",
"use_reactive",
]
T = TypeVar("T")
U = TypeVar("U")
Expand All @@ -56,12 +47,6 @@ def __exit__(self, *excinfo):
pass


# inherit from BaseException so less change of being caught
# in an except
class CancelledError(BaseException):
pass


def use_retry(*actions: Callable[[], Any]):
counter, set_counter = solara.use_state(0)

Expand All @@ -73,155 +58,11 @@ def retry():
return counter, retry


def use_thread(
callback=Union[
Callable[[threading.Event], T],
Iterator[Callable[[threading.Event], T]],
Callable[[], T],
Iterator[Callable[[], T]],
],
dependencies=[],
intrusive_cancel=True,
) -> Result[T]:
def make_event(*_ignore_dependencies):
return threading.Event()

def make_lock():
return threading.Lock()

lock: threading.Lock = solara.use_memo(make_lock, [])
updater = use_force_update()
result_state, set_result_state = solara.use_state(ResultState.INITIAL)
error = solara.use_ref(cast(Optional[Exception], None))
result = solara.use_ref(cast(Optional[T], None))
running_thread = solara.use_ref(cast(Optional[threading.Thread], None))
counter, retry = use_retry()
cancel: threading.Event = solara.use_memo(make_event, [*dependencies, counter])

@contextlib.contextmanager
def cancel_guard():
if not intrusive_cancel:
yield
return

def tracefunc(frame, event, arg):
# this gets called at least for every line executed
if cancel.is_set():
rc = reacton.core._get_render_context(required=False)
# we do not want to cancel the rendering cycle
if rc is None or not rc._is_rendering:
# this will bubble up
raise CancelledError()
if prev and SOLARA_ALLOW_OTHER_TRACER:
prev(frame, event, arg)
# keep tracing:
return tracefunc

# see https://docs.python.org/3/library/sys.html#sys.settrace
# it is for the calling thread only
# not every Python implementation has it
prev = None
if hasattr(sys, "gettrace"):
prev = sys.gettrace()
if hasattr(sys, "settrace"):
sys.settrace(tracefunc)
try:
yield
finally:
if hasattr(sys, "settrace"):
sys.settrace(prev)

def run():
set_result_state(ResultState.STARTING)

def runner():
wait_for_thread = None
with lock:
# if there is a current thread already, we'll need
# to wait for it. copy the ref, and set ourselves
# as the current one
if running_thread.current:
wait_for_thread = running_thread.current
running_thread.current = threading.current_thread()
if wait_for_thread is not None:
set_result_state(ResultState.WAITING)
# don't start before the previous is stopped
try:
wait_for_thread.join()
except: # noqa
pass
if threading.current_thread() != running_thread.current:
# in case a new thread was started that also was waiting for the previous
# thread to st stop, we can finish this
return
# we previously set current to None, but if we do not do that, we can still render the old value
# while we can still show a loading indicator using the .state
# result.current = None
set_result_state(ResultState.RUNNING)

sig = inspect.signature(callback)
if sig.parameters:
f = functools.partial(callback, cancel)
else:
f = callback
try:
try:
# we only use the cancel_guard context manager around
# the function calls to f. We don't want to guard around
# a call to react, since that might slow down rendering
# during rendering
with cancel_guard():
value = f()
if inspect.isgenerator(value):
while True:
try:
with cancel_guard():
result.current = next(value)
error.current = None
except StopIteration:
break
# assigning to the ref doesn't trigger a rerender, so do it manually
updater()
if threading.current_thread() == running_thread.current:
set_result_state(ResultState.FINISHED)
else:
result.current = value
error.current = None
if threading.current_thread() == running_thread.current:
set_result_state(ResultState.FINISHED)
except Exception as e:
error.current = e
if threading.current_thread() == running_thread.current:
logger.exception(e)
set_result_state(ResultState.ERROR)
return
except CancelledError:
pass
# this means this thread is cancelled not be request, but because
# a new thread is running, we can ignore this
finally:
if threading.current_thread() == running_thread.current:
running_thread.current = None
logger.info("thread done!")
if cancel.is_set():
set_result_state(ResultState.CANCELLED)

logger.info("starting thread: %r", runner)
thread = threading.Thread(target=runner, daemon=True)
thread.start()

def cleanup():
cancel.set() # cleanup for use effect

return cleanup

solara.use_side_effect(run, dependencies + [counter])
return Result[T](value=result.current, error=error.current, state=result_state, cancel=cancel.set, _retry=retry)


def use_download(
f: MaybeResult[Union[str, os.PathLike, IO]], url, expected_size=None, delay=None, return_content=False, chunk_size=chunk_size_default
) -> Result:
from .use_thread import use_thread

if not isinstance(f, Result):
f = Result(value=f)
assert isinstance(f, Result)
Expand Down Expand Up @@ -299,6 +140,8 @@ def ensure_result(input: MaybeResult[T]) -> Result[T]:


def make_use_thread(f: Callable[[T], U]):
from .use_thread import use_thread

def use_result(input: MaybeResult[T]) -> Result[U]:
input_result = ensure_result(input)

Expand Down Expand Up @@ -402,106 +245,3 @@ def assign():

solara.use_effect(assign, [value])
return ref.current


def use_reactive(
value: Union[T, solara.Reactive[T]],
on_change: Optional[Callable[[T], None]] = None,
) -> solara.Reactive[T]:
"""Creates a reactive variable with the a local component scope.
It is a useful alternative to `use_state` when you want to use a
reactive variable for the component state.
See also [our documentation on state management](/docs/fundamentals/state-management).
If the variable passed is a reactive variable, it will be returned instead and no
new reactive variable will be created. This is useful for implementing component
that accept either a reactive variable or a normal value along with an optional `on_change`
callback.
## Arguments:
* value (Union[T, solara.Reactive[T]]): The value of the
reactive variable. If a reactive variable is provided, it will be
used directly. Otherwise, a new reactive variable will be created
with the provided initial value. If the argument passed changes
the reactive variable will be updated.
* on_change (Optional[Callable[[T], None]]): An optional callback function
that will be called when the reactive variable's value changes.
Returns:
solara.Reactive[T]: A reactive variable with the specified initial value
or the provided reactive variable.
## Examples
### Replacement for use_state
```solara
import solara
@solara.component
def ReusableComponent():
color = solara.use_reactive("red") # another possibility
solara.Select(label="Color",values=["red", "green", "blue", "orange"],
value=color)
solara.Markdown("### Solara is awesome", style={"color": color.value})
@solara.component
def Page():
# this component is used twice, but each instance has its own state
ReusableComponent()
ReusableComponent()
```
### Flexible arguments
The `MyComponent` component can be passed a reactive variable or a normal
Python variable and a `on_value` callback.
```python
import solara
from typing import Union, Optional, Callable
@solara.component
def MyComponent(value: Union[T, solara.Reactive[T]],
on_value: Optional[Callable[[T], None]] = None,
):
reactive_value = solara.use_reactive(value, on_value_change)
# Use the `reactive_value` in the component
```
"""

on_change_ref = solara.use_ref(on_change)
on_change_ref.current = on_change

def create():
if not isinstance(value, solara.Reactive):
return solara.reactive(value)

reactive_value = solara.use_memo(create, dependencies=[])
if isinstance(value, solara.Reactive):
reactive_value = value
assert reactive_value is not None
updating = solara.use_ref(False)

def forward_on_change():
def forward(value):
if on_change_ref.current and not updating.current:
on_change_ref.current(value)

return reactive_value.subscribe(forward)

def update():
updating.current = True
try:
if not isinstance(value, solara.Reactive):
reactive_value.value = value
finally:
updating.current = False

solara.use_memo(update, [value])
solara.use_effect(forward_on_change, [])

return reactive_value
Loading

0 comments on commit 8aaab96

Please sign in to comment.