Skip to content

Commit

Permalink
Allow outputs to stay in progress mode after flush
Browse files Browse the repository at this point in the history
  • Loading branch information
jcheng5 committed Jan 16, 2024
1 parent 62754c7 commit e5df0a1
Show file tree
Hide file tree
Showing 18 changed files with 14,297 additions and 9,754 deletions.
2 changes: 1 addition & 1 deletion scripts/htmlDependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ versions <- list()
message("Installing GitHub packages: bslib, shiny, htmltools")
withr::local_temp_libpaths()
ignore <- capture.output({
pak::pkg_install(c("rstudio/bslib", "cran::shiny", "cran::htmltools"))
pak::pkg_install(c("rstudio/bslib", "rstudio/shiny@main", "cran::htmltools"))
#pak::pkg_install(c("rstudio/bslib@main", "rstudio/shiny@main", "rstudio/htmltools@main"))
})

Expand Down
14 changes: 10 additions & 4 deletions shiny/_validation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from __future__ import annotations

from typing import TypeVar, overload
from typing import Literal, TypeVar, overload

from ._docstring import add_example
from .types import SilentCancelOutputException, SilentException
from .types import (
SilentCancelOutputException,
SilentException,
SilentOperationInProgressException,
)

T = TypeVar("T")

Expand All @@ -19,7 +23,7 @@ def req(*args: T, cancel_output: bool = False) -> T:


@add_example()
def req(*args: T, cancel_output: bool = False) -> T | None:
def req(*args: T, cancel_output: bool | Literal["progress"] = False) -> T | None:
"""
Throw a silent exception for falsy values.
Expand Down Expand Up @@ -51,8 +55,10 @@ def req(*args: T, cancel_output: bool = False) -> T | None:

for arg in args:
if not arg:
if cancel_output:
if cancel_output is True:
raise SilentCancelOutputException()
elif cancel_output == "progress":
raise SilentOperationInProgressException()
else:
raise SilentException()

Expand Down
2 changes: 1 addition & 1 deletion shiny/_versions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
shiny_html_deps = "1.8.0"
shiny_html_deps = "1.8.0.9000"
bslib = "0.6.1.9000"
htmltools = "0.5.7"
bootstrap = "5.3.1"
Expand Down
147 changes: 107 additions & 40 deletions shiny/session/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Awaitable,
Callable,
Iterable,
Literal,
Optional,
Union,
cast,
Expand All @@ -34,6 +35,8 @@
from starlette.responses import HTMLResponse, PlainTextResponse, StreamingResponse
from starlette.types import ASGIApp

from .._typing_extensions import NotRequired

if TYPE_CHECKING:
from .._app import App

Expand All @@ -49,8 +52,13 @@
from ..input_handler import input_handlers
from ..reactive import Effect_, Value, effect, flush, isolate
from ..reactive._core import lock, on_flushed
from ..render.renderer import Jsonifiable, RendererBase, RendererBaseT
from ..types import SafeException, SilentCancelOutputException, SilentException
from ..render.renderer import RendererBase, RendererBaseT
from ..types import (
SafeException,
SilentCancelOutputException,
SilentException,
SilentOperationInProgressException,
)
from ._utils import RenderedDeps, read_thunk_opt, session_context


Expand Down Expand Up @@ -121,14 +129,26 @@ class DownloadInfo:
encoding: str


class OutBoundMessageQueues(TypedDict):
values: list[dict[str, Any]]
input_messages: list[dict[str, Any]]
errors: list[dict[str, Any]]
class OutBoundMessageQueues:
def __init__(self):
self.values: dict[str, Any] = {}
self.errors: dict[str, Any] = {}
self.input_messages: list[dict[str, Any]] = []

def set_value(self, id: str, value: Any) -> None:
self.values[id] = value
# remove from self.errors
if id in self.errors:
del self.errors[id]

def set_error(self, id: str, error: Any) -> None:
self.errors[id] = error
# remove from self.values
if id in self.values:
del self.values[id]

def empty_outbound_message_queues() -> OutBoundMessageQueues:
return {"values": [], "input_messages": [], "errors": []}
def add_input_message(self, id: str, message: dict[str, Any]) -> None:
self.input_messages.append({"id": id, "message": message})


# Makes isinstance(x, Session) also return True when x is a SessionProxy (i.e., a module
Expand Down Expand Up @@ -191,7 +211,7 @@ def __init__(
except Exception as e:
print("Error parsing credentials header: " + str(e))

self._outbound_message_queues = empty_outbound_message_queues()
self._outbound_message_queues = OutBoundMessageQueues()

self._message_handlers: dict[
str, Callable[..., Awaitable[object]]
Expand Down Expand Up @@ -561,8 +581,7 @@ def send_input_message(self, id: str, message: dict[str, object]) -> None:
message
The message to send.
"""
msg: dict[str, object] = {"id": id, "message": message}
self._outbound_message_queues["input_messages"].append(msg)
self._outbound_message_queues.add_input_message(id, message)
self._request_flush()

def _send_insert_ui(
Expand All @@ -580,6 +599,30 @@ def _send_remove_ui(self, selector: str, multiple: bool) -> None:
msg = {"selector": selector, "multiple": multiple}
self._send_message_sync({"shiny-remove-ui": msg})

@overload
def _send_progress(
self, type: Literal["binding"], message: BindingProgressMessage
) -> None:
...

@overload
def _send_progress(
self, type: Literal["open"], message: OpenProgressMessage
) -> None:
...

@overload
def _send_progress(
self, type: Literal["close"], message: CloseProgressMessage
) -> None:
...

@overload
def _send_progress(
self, type: Literal["update"], message: UpdateProgressMessage
) -> None:
...

def _send_progress(self, type: str, message: object) -> None:
msg: dict[str, object] = {"progress": {"type": type, "message": message}}
self._send_message_sync(msg)
Expand Down Expand Up @@ -688,24 +731,16 @@ async def _flush(self) -> None:
try:
omq = self._outbound_message_queues

values: dict[str, object] = {}
for v in omq["values"]:
values.update(v)

errors: dict[str, object] = {}
for err in omq["errors"]:
errors.update(err)

message: dict[str, object] = {
"values": values,
"inputMessages": omq["input_messages"],
"errors": errors,
"values": omq.values,
"inputMessages": omq.input_messages,
"errors": omq.errors,
}

try:
await self._send_message(message)
finally:
self._outbound_message_queues = empty_outbound_message_queues()
self._outbound_message_queues = OutBoundMessageQueues()
finally:
with session_context(self):
await self._flushed_callbacks.invoke()
Expand Down Expand Up @@ -838,6 +873,29 @@ def root_scope(self) -> Session:
return self


class BindingProgressMessage(TypedDict):
id: ResolvedId
persistent: NotRequired[bool]


class OpenProgressMessage(TypedDict):
id: ResolvedId
style: str


class CloseProgressMessage(TypedDict):
id: ResolvedId
style: str


class UpdateProgressMessage(TypedDict):
id: ResolvedId
message: NotRequired[str | None]
detail: NotRequired[str | None]
value: NotRequired[float | int | None]
style: str


class SessionProxy:
ns: ResolvedId
input: Inputs
Expand Down Expand Up @@ -1022,13 +1080,18 @@ async def output_obs():
{"recalculating": {"name": output_name, "status": "recalculating"}}
)

message: dict[str, Jsonifiable] = {}
try:
message[output_name] = await renderer.render()
value = await renderer.render()
self._session._outbound_message_queues.set_value(output_name, value)
except SilentOperationInProgressException:
self._session._send_progress(
"binding", {"id": output_name, "persistent": True}
)
return
except SilentCancelOutputException:
return
except SilentException:
message[output_name] = None
self._session._outbound_message_queues.set_value(output_name, None)
except Exception as e:
# Print traceback to the console
traceback.print_exc()
Expand All @@ -1041,21 +1104,25 @@ async def output_obs():
err_msg = str(e)
# Register the outbound error message
err_message = {
str(output_name): {
"message": err_msg,
# TODO: is it possible to get the call?
"call": None,
# TODO: I don't think we actually use this for anything client-side
"type": None,
}
"message": err_msg,
# TODO: is it possible to get the call?
"call": None,
# TODO: I don't think we actually use this for anything client-side
"type": None,
}
self._session._outbound_message_queues["errors"].append(err_message)

self._session._outbound_message_queues["values"].append(message)

await self._session._send_message(
{"recalculating": {"name": output_name, "status": "recalculated"}}
)
self._session._outbound_message_queues.set_error(
output_name, err_message
)
return
finally:
await self._session._send_message(
{
"recalculating": {
"name": output_name,
"status": "recalculated",
}
}
)

output_obs.on_invalidate(
lambda: self._session._send_progress("binding", {"id": output_name})
Expand Down
10 changes: 10 additions & 0 deletions shiny/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ class SilentCancelOutputException(Exception):
pass


class SilentOperationInProgressException(SilentException):
# Throw a silent exception to indicate that an operation is in progress

# Similar to :class:`~SilentException`, but if thrown in an output context, existing
# output isn't cleared and stays in recalculating mode until the next time it is
# invalidated.

pass


class ActionButtonValue(int):
pass

Expand Down
23 changes: 12 additions & 11 deletions shiny/ui/_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .._docstring import add_example
from .._utils import rand_hex
from ..session import Session, require_active_session
from ..session._session import UpdateProgressMessage


@add_example()
Expand Down Expand Up @@ -46,12 +47,11 @@ def __init__(
self.min = min
self.max = max
self.value = None
self._id = rand_hex(8)
self._closed = False
self._session = require_active_session(session)
self._id = self._session.ns(rand_hex(8))

msg = {"id": self._id, "style": self._style}
self._session._send_progress("open", msg)
self._session._send_progress("open", {"id": self._id, "style": self._style})

def __enter__(self) -> "Progress":
return self
Expand Down Expand Up @@ -100,17 +100,18 @@ def set(
# Normalize value to number between 0 and 1
value = min(1, max(0, (value - self.min) / (self.max - self.min)))

msg = {
msg: UpdateProgressMessage = {
"id": self._id,
"message": message,
"detail": detail,
"value": value,
"style": self._style,
}

self._session._send_progress(
"update", {k: v for k, v in msg.items() if v is not None}
)
if message is not None:
msg["message"] = message
if detail is not None:
msg["detail"] = detail
if value is not None:
msg["value"] = value

self._session._send_progress("update", msg)

def inc(
self,
Expand Down
2 changes: 1 addition & 1 deletion shiny/www/shared/_version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"note!": "This file is auto-generated by scripts/htmlDependencies.R",
"package": "shiny",
"version": "CRAN (R 4.3.1)"
"version": "Github (rstudio/shiny@300fb217d113e846d4f2ad8ccb02e9cb4e5b7322)"
}
4 changes: 2 additions & 2 deletions shiny/www/shared/bootstrap/_version.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"note!": "This file is auto-generated by scripts/htmlDependencies.R",
"shiny_version": "CRAN (R 4.3.1)",
"shiny_version": "Github (rstudio/shiny@300fb217d113e846d4f2ad8ccb02e9cb4e5b7322)",
"bslib_version": "Github (rstudio/bslib@f05bd23d7df5a7465e418a5794925dacdd27bb6b)",
"htmltools_version": "CRAN (R 4.3.1)",
"htmltools_version": "CRAN (R 4.2.0)",
"bootstrap_version": "5.3.1"
}
2 changes: 1 addition & 1 deletion shiny/www/shared/htmltools/_version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"note!": "This file is auto-generated by scripts/htmlDependencies.R",
"package": "htmltools",
"version": "CRAN (R 4.3.1)"
"version": "CRAN (R 4.2.0)"
}
2 changes: 1 addition & 1 deletion shiny/www/shared/shiny-autoreload.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion shiny/www/shared/shiny-showcase.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion shiny/www/shared/shiny-showcase.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion shiny/www/shared/shiny-testmode.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e5df0a1

Please sign in to comment.