From 870491c2cf90a0d5eab59bb1c2ca9e9b3f040424 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 28 Jan 2022 21:22:14 -0600 Subject: [PATCH 01/23] Rename ShinySession -> Session --- examples/download/app.py | 2 +- examples/dynamic_ui/app.py | 2 +- examples/inputs-update/app.py | 2 +- examples/inputs/app.py | 2 +- examples/moduleapp/app.py | 2 +- examples/myapp/app.py | 2 +- examples/req/app.py | 2 +- examples/simple/app.py | 2 +- shiny/__init__.py | 2 +- shiny/dynamic_ui.py | 6 ++--- shiny/input_handlers.py | 18 +++++++------- shiny/input_update.py | 26 ++++++++++---------- shiny/modal.py | 6 ++--- shiny/notifications.py | 6 ++--- shiny/progress.py | 6 ++--- shiny/reactives.py | 34 ++++++++++++++------------- shiny/render.py | 8 +++---- shiny/{shinysession.py => session.py} | 22 ++++++++--------- shiny/shinyapp.py | 26 ++++++++++---------- shiny/shinymodule.py | 10 ++++---- tests/test_shinysession.py | 2 +- 21 files changed, 93 insertions(+), 95 deletions(-) rename shiny/{shinysession.py => session.py} (97%) diff --git a/examples/download/app.py b/examples/download/app.py index 02171a89f..a11414dbd 100644 --- a/examples/download/app.py +++ b/examples/download/app.py @@ -82,7 +82,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): ) -def server(session: ShinySession): +def server(session: Session): @session.download() def download1(): """ diff --git a/examples/dynamic_ui/app.py b/examples/dynamic_ui/app.py index cd43e0e5d..aef24e34c 100644 --- a/examples/dynamic_ui/app.py +++ b/examples/dynamic_ui/app.py @@ -25,7 +25,7 @@ ) -def server(session: ShinySession): +def server(session: Session): @reactive() def r(): if session.input["n"] is None: diff --git a/examples/inputs-update/app.py b/examples/inputs-update/app.py index 45633c40e..75ef35e30 100644 --- a/examples/inputs-update/app.py +++ b/examples/inputs-update/app.py @@ -91,7 +91,7 @@ ) -def server(sess: ShinySession): +def server(sess: Session): @observe() def _(): # We'll use these multiple times, so use short var names for diff --git a/examples/inputs/app.py b/examples/inputs/app.py index 2e869643f..a8b19a4af 100644 --- a/examples/inputs/app.py +++ b/examples/inputs/app.py @@ -94,7 +94,7 @@ import matplotlib.pyplot as plt -def server(s: ShinySession): +def server(s: Session): @s.output("inputs") @render_ui() def _() -> Tag: diff --git a/examples/moduleapp/app.py b/examples/moduleapp/app.py index 9c97ceb05..2e27ccd64 100644 --- a/examples/moduleapp/app.py +++ b/examples/moduleapp/app.py @@ -54,7 +54,7 @@ def _() -> str: ) -def server(session: ShinySession): +def server(session: Session): counter_module.server("counter1") counter_module.server("counter2") diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 4ea868d28..8e0f877b8 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -40,7 +40,7 @@ shared_val = ReactiveVal(None) -def server(session: ShinySession): +def server(session: Session): @reactive() def r(): if session.input["n"] is None: diff --git a/examples/req/app.py b/examples/req/app.py index 2ae8b170a..fd88996fa 100644 --- a/examples/req/app.py +++ b/examples/req/app.py @@ -13,7 +13,7 @@ ) -def server(session: ShinySession): +def server(session: Session): @reactive() def safe_click(): req(session.input["safe"]) diff --git a/examples/simple/app.py b/examples/simple/app.py index 74ddce6ed..30897d478 100644 --- a/examples/simple/app.py +++ b/examples/simple/app.py @@ -37,7 +37,7 @@ shared_val = ReactiveVal(None) -def server(session: ShinySession): +def server(session: Session): @reactive() def r(): if session.input["n"] is None: diff --git a/shiny/__init__.py b/shiny/__init__.py index 1fde7efce..a141506f2 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -28,6 +28,6 @@ from .render import * from .reactives import * from .shinyapp import * -from .shinysession import * +from .session import * from .shinymodule import * from .validation import * diff --git a/shiny/dynamic_ui.py b/shiny/dynamic_ui.py index 10c0238d8..5e87c0089 100644 --- a/shiny/dynamic_ui.py +++ b/shiny/dynamic_ui.py @@ -8,7 +8,7 @@ from htmltools import TagChildArg -from .shinysession import ShinySession, _require_active_session, _process_deps +from .session import Session, _require_active_session, _process_deps def ui_insert( @@ -17,7 +17,7 @@ def ui_insert( where: Literal["beforeBegin", "afterBegin", "beforeEnd", "afterEnd"] = "beforeEnd", multiple: bool = False, immediate: bool = False, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) @@ -40,7 +40,7 @@ def ui_remove( selector: str, multiple: bool = False, immediate: bool = False, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) diff --git a/shiny/input_handlers.py b/shiny/input_handlers.py index 95f587176..2a745ef64 100644 --- a/shiny/input_handlers.py +++ b/shiny/input_handlers.py @@ -2,9 +2,9 @@ from typing import TYPE_CHECKING, Callable, Dict, Union, List, Any, TypeVar if TYPE_CHECKING: - from .shinysession import ShinySession + from .session import Session -InputHandlerType = Callable[[Any, str, "ShinySession"], Any] +InputHandlerType = Callable[[Any, str, "Session"], Any] class _InputHandlers(Dict[str, InputHandlerType]): @@ -24,7 +24,7 @@ def remove(self, name: str): del self[name] def process_value( - self, type: str, value: Any, name: str, session: "ShinySession" + self, type: str, value: Any, name: str, session: "Session" ) -> Any: handler = self.get(type) if handler is None: @@ -39,19 +39,19 @@ def process_value( # Doesn't do anything since it seems weird to coerce None into some sort of NA (like we do in R)? @input_handlers.add("shiny.number") -def _(value: _NumberType, name: str, session: "ShinySession") -> _NumberType: +def _(value: _NumberType, name: str, session: "Session") -> _NumberType: return value # TODO: implement when we have bookmarking @input_handlers.add("shiny.password") -def _(value: str, name: str, session: "ShinySession") -> str: +def _(value: str, name: str, session: "Session") -> str: return value @input_handlers.add("shiny.date") def _( - value: Union[str, List[str]], name: str, session: "ShinySession" + value: Union[str, List[str]], name: str, session: "Session" ) -> Union[date, List[date]]: if isinstance(value, str): return datetime.strptime(value, "%Y-%m-%d").date() @@ -60,7 +60,7 @@ def _( @input_handlers.add("shiny.datetime") def _( - value: Union[int, float, List[int], List[float]], name: str, session: "ShinySession" + value: Union[int, float, List[int], List[float]], name: str, session: "Session" ) -> Union[datetime, List[datetime]]: if isinstance(value, (int, float)): return datetime.utcfromtimestamp(value) @@ -72,11 +72,11 @@ class ActionButtonValue(int): @input_handlers.add("shiny.action") -def _(value: int, name: str, session: "ShinySession") -> ActionButtonValue: +def _(value: int, name: str, session: "Session") -> ActionButtonValue: return ActionButtonValue(value) # TODO: implement when we have bookmarking @input_handlers.add("shiny.file") -def _(value: Any, name: str, session: "ShinySession") -> Any: +def _(value: Any, name: str, session: "Session") -> Any: return value diff --git a/shiny/input_update.py b/shiny/input_update.py index f584b230d..62ceb34ea 100644 --- a/shiny/input_update.py +++ b/shiny/input_update.py @@ -14,7 +14,7 @@ from .input_select import SelectChoicesArg, _normalize_choices, _render_choices from .input_slider import SliderValueArg, SliderStepArg, _slider_type, _as_numeric from .utils import drop_none -from .shinysession import ShinySession, _require_active_session, _process_deps +from .session import Session, _require_active_session, _process_deps # ----------------------------------------------------------------------------- # input_action_button.py @@ -24,7 +24,7 @@ def update_action_button( *, label: Optional[str] = None, icon: TagChildArg = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) # TODO: supporting a TagChildArg for label would require changes to shiny.js @@ -43,7 +43,7 @@ def update_checkbox( *, label: Optional[str] = None, value: Optional[bool] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) msg = {"label": label, "value": value} @@ -57,7 +57,7 @@ def update_checkbox_group( choices: Optional[ChoicesArg] = None, selected: Optional[Union[str, List[str]]] = None, inline: bool = False, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: _update_choice_input( id=id, @@ -77,7 +77,7 @@ def update_radio_buttons( choices: Optional[ChoicesArg] = None, selected: Optional[str] = None, inline: bool = False, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: _update_choice_input( id=id, @@ -98,7 +98,7 @@ def _update_choice_input( choices: Optional[ChoicesArg] = None, selected: Optional[Union[str, List[str]]] = None, inline: bool = False, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) options = None @@ -121,7 +121,7 @@ def update_date( value: Optional[date] = None, min: Optional[date] = None, max: Optional[date] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) @@ -142,7 +142,7 @@ def update_date_range( end: Optional[date] = None, min: Optional[date] = None, max: Optional[date] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) value = {"start": str(start), "end": str(end)} @@ -166,7 +166,7 @@ def update_numeric( min: Optional[float] = None, max: Optional[float] = None, step: Optional[float] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) msg = { @@ -188,7 +188,7 @@ def update_select( label: Optional[str] = None, choices: Optional[SelectChoicesArg] = None, selected: Optional[str] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) @@ -221,7 +221,7 @@ def update_slider( step: Optional[SliderStepArg] = None, time_format: Optional[str] = None, timezone: Optional[str] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) @@ -259,7 +259,7 @@ def update_text( label: Optional[str] = None, value: Optional[str] = None, placeholder: Optional[str] = None, - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) msg = {"label": label, "value": value, "placeholder": placeholder} @@ -275,7 +275,7 @@ def update_text( # TODO: we should probably provide a nav_select() alias for this as well def update_navs( - id: str, selected: Optional[str] = None, session: Optional[ShinySession] = None + id: str, selected: Optional[str] = None, session: Optional[Session] = None ) -> None: session = _require_active_session(session) msg = {"value": selected} diff --git a/shiny/modal.py b/shiny/modal.py index 7f455906a..a4640a971 100644 --- a/shiny/modal.py +++ b/shiny/modal.py @@ -9,7 +9,7 @@ from htmltools import tags, Tag, div, HTML, TagChildArg, TagAttrArg from .utils import run_coro_sync -from .shinysession import ShinySession, _require_active_session, _process_deps +from .session import Session, _require_active_session, _process_deps def modal_button(label: str, icon: TagChildArg = None) -> Tag: @@ -79,12 +79,12 @@ def modal( ) -def modal_show(modal: Tag, session: Optional[ShinySession] = None) -> None: +def modal_show(modal: Tag, session: Optional[Session] = None) -> None: session = _require_active_session(session) msg = _process_deps(modal) run_coro_sync(session.send_message({"modal": {"type": "show", "message": msg}})) -def modal_remove(session: Optional[ShinySession] = None) -> None: +def modal_remove(session: Optional[Session] = None) -> None: session = _require_active_session(session) run_coro_sync(session.send_message({"modal": {"type": "remove", "message": None}})) diff --git a/shiny/notifications.py b/shiny/notifications.py index 1d535082e..b250a6116 100644 --- a/shiny/notifications.py +++ b/shiny/notifications.py @@ -9,7 +9,7 @@ from htmltools import TagList, TagChildArg from .utils import run_coro_sync, rand_hex -from .shinysession import ShinySession, _require_active_session, _process_deps +from .session import Session, _require_active_session, _process_deps def notification_show( @@ -19,7 +19,7 @@ def notification_show( close_button: bool = True, id: Optional[str] = None, type: Literal["default", "message", "warning", "error"] = "default", - session: Optional[ShinySession] = None, + session: Optional[Session] = None, ) -> None: session = _require_active_session(session) @@ -43,7 +43,7 @@ def notification_show( ) -def notification_remove(id: str, session: Optional[ShinySession] = None) -> str: +def notification_remove(id: str, session: Optional[Session] = None) -> str: session = _require_active_session(session) run_coro_sync( session.send_message({"notification": {"type": "remove", "message": None}}) diff --git a/shiny/progress.py b/shiny/progress.py index ec0d0ae5d..80ac3b961 100644 --- a/shiny/progress.py +++ b/shiny/progress.py @@ -1,15 +1,13 @@ from typing import Optional, Dict, Any from warnings import warn from .utils import run_coro_sync, rand_hex -from .shinysession import ShinySession, _require_active_session +from .session import Session, _require_active_session class Progress: _style = "notification" - def __init__( - self, min: int = 0, max: int = 1, session: Optional[ShinySession] = None - ): + def __init__(self, min: int = 0, max: int = 1, session: Optional[Session] = None): self.min = min self.max = max self.value = None diff --git a/shiny/reactives.py b/shiny/reactives.py index 73da6c947..6dfdd3889 100644 --- a/shiny/reactives.py +++ b/shiny/reactives.py @@ -41,7 +41,7 @@ from .validation import SilentException if TYPE_CHECKING: - from .shinysession import ShinySession + from .session import Session T = TypeVar("T") @@ -113,7 +113,7 @@ def __init__( self, func: Callable[[], T], *, - session: Union[MISSING_TYPE, "ShinySession", None] = MISSING, + session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if inspect.iscoroutinefunction(func): raise TypeError("Reactive requires a non-async function") @@ -128,14 +128,14 @@ def __init__( self._ctx: Optional[Context] = None self._exec_count: int = 0 - self._session: Optional[ShinySession] + self._session: Optional[Session] # Use `isinstance(x, MISSING_TYPE)`` instead of `x is MISSING` because # the type checker doesn't know that MISSING is the only instance of # MISSING_TYPE; this saves us from casting later on. if isinstance(session, MISSING_TYPE): # If no session is provided, autodetect the current session (this # could be None if outside of a session). - session = shinysession.get_current_session() + session = shiny_session.get_current_session() self._session = session # Use lists to hold (optional) value and error, instead of Optional[T], @@ -174,7 +174,7 @@ async def update_value(self) -> None: was_running = self._running self._running = True - with shinysession.session_context(self._session): + with shiny_session.session_context(self._session): try: with self._ctx(): await self._run_func() @@ -200,7 +200,7 @@ def __init__( self, func: Callable[[], Awaitable[T]], *, - session: Union[MISSING_TYPE, "ShinySession", None] = MISSING, + session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if not inspect.iscoroutinefunction(func): raise TypeError("ReactiveAsync requires an async function") @@ -217,7 +217,7 @@ async def __call__(self) -> T: def reactive( - *, session: Union[MISSING_TYPE, "ShinySession", None] = MISSING + *, session: Union[MISSING_TYPE, "Session", None] = MISSING ) -> Callable[[Callable[[], T]], Reactive[T]]: def create_reactive(fn: Callable[[], T]) -> Reactive[T]: return Reactive(fn, session=session) @@ -226,7 +226,7 @@ def create_reactive(fn: Callable[[], T]) -> Reactive[T]: def reactive_async( - *, session: Union[MISSING_TYPE, "ShinySession", None] = MISSING + *, session: Union[MISSING_TYPE, "Session", None] = MISSING ) -> Callable[[Callable[[], Awaitable[T]]], ReactiveAsync[T]]: def create_reactive_async(fn: Callable[[], Awaitable[T]]) -> ReactiveAsync[T]: return ReactiveAsync(fn, session=session) @@ -243,7 +243,7 @@ def __init__( func: Callable[[], None], *, priority: int = 0, - session: Union[MISSING_TYPE, "ShinySession", None] = MISSING, + session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if inspect.iscoroutinefunction(func): raise TypeError("Observer requires a non-async function") @@ -258,14 +258,14 @@ def __init__( self._ctx: Optional[Context] = None self._exec_count: int = 0 - self._session: Optional[ShinySession] + self._session: Optional[Session] # Use `isinstance(x, MISSING_TYPE)`` instead of `x is MISSING` because # the type checker doesn't know that MISSING is the only instance of # MISSING_TYPE; this saves us from casting later on. if isinstance(session, MISSING_TYPE): # If no session is provided, autodetect the current session (this # could be None if outside of a session). - session = shinysession.get_current_session() + session = shiny_session.get_current_session() self._session = session if self._session is not None: @@ -305,7 +305,7 @@ async def run(self) -> None: ctx = self._create_context() self._exec_count += 1 - with shinysession.session_context(self._session): + with shiny_session.session_context(self._session): try: with ctx(): await self._func() @@ -338,7 +338,7 @@ def __init__( func: Callable[[], Awaitable[None]], *, priority: int = 0, - session: Union[MISSING_TYPE, "ShinySession", None] = MISSING, + session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if not inspect.iscoroutinefunction(func): raise TypeError("ObserverAsync requires an async function") @@ -351,7 +351,7 @@ def __init__( def observe( - *, priority: int = 0, session: Union[MISSING_TYPE, "ShinySession", None] = MISSING + *, priority: int = 0, session: Union[MISSING_TYPE, "Session", None] = MISSING ) -> Callable[[Callable[[], None]], Observer]: """[summary] @@ -372,7 +372,7 @@ def create_observer(fn: Callable[[], None]) -> Observer: def observe_async( *, priority: int = 0, - session: Union[MISSING_TYPE, "ShinySession", None] = MISSING, + session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> Callable[[Callable[[], Awaitable[None]]], ObserverAsync]: def create_observer_async(fn: Callable[[], Awaitable[None]]) -> ObserverAsync: return ObserverAsync(fn, priority=priority, session=session) @@ -438,4 +438,6 @@ def cancel_task(): # Import here at the bottom seems to fix a circular dependency problem. -from . import shinysession +# Need to import as shiny_session to avoid naming conflicts with function params named +# `session`. +from . import session as shiny_session diff --git a/shiny/render.py b/shiny/render.py index 077d628b5..243e12e41 100644 --- a/shiny/render.py +++ b/shiny/render.py @@ -21,7 +21,7 @@ from htmltools import TagChildArg if TYPE_CHECKING: - from .shinysession import ShinySession + from .session import Session from . import utils @@ -57,11 +57,11 @@ def __init__(self, fn: Callable[[], object]) -> None: def __call__(self) -> object: raise NotImplementedError - def set_metadata(self, session: "ShinySession", name: str) -> None: + def set_metadata(self, session: "Session", name: str) -> None: """When RenderFunctions are assigned to Output object slots, this method is used to pass along session and name information. """ - self._session: ShinySession = session + self._session: Session = session self._name: str = name @@ -300,7 +300,7 @@ async def run(self) -> object: if ui is None: return None # TODO: better a better workaround for the circular dependency - from .shinysession import _process_deps + from .session import _process_deps return _process_deps(ui, self._session) diff --git a/shiny/shinysession.py b/shiny/session.py similarity index 97% rename from shiny/shinysession.py rename to shiny/session.py index 5d249b930..53ff00f9a 100644 --- a/shiny/shinysession.py +++ b/shiny/session.py @@ -1,5 +1,5 @@ __all__ = ( - "ShinySession", + "Session", "Outputs", "get_current_session", "session_context", @@ -114,7 +114,7 @@ def _empty_outbound_message_queues() -> _OutBoundMessageQueues: return {"values": [], "input_messages": [], "errors": []} -class ShinySession: +class Session: # ========================================================================== # Initialization # ========================================================================== @@ -529,9 +529,9 @@ def _(): class Outputs: - def __init__(self, session: ShinySession) -> None: + def __init__(self, session: Session) -> None: self._output_obervers: Dict[str, Observer] = {} - self._session: ShinySession = session + self._session: Session = session def __call__( self, name: str @@ -602,25 +602,25 @@ async def output_obs(): # ============================================================================== # Context manager for current session (AKA current reactive domain) # ============================================================================== -_current_session: ContextVar[Optional[ShinySession]] = ContextVar( +_current_session: ContextVar[Optional[Session]] = ContextVar( "current_session", default=None ) -def get_current_session() -> Optional[ShinySession]: +def get_current_session() -> Optional[Session]: return _current_session.get() @contextmanager -def session_context(session: Optional[ShinySession]): - token: Token[Union[ShinySession, None]] = _current_session.set(session) +def session_context(session: Optional[Session]): + token: Token[Union[Session, None]] = _current_session.set(session) try: yield finally: _current_session.reset(token) -def _require_active_session(session: Optional[ShinySession]) -> ShinySession: +def _require_active_session(session: Optional[Session]) -> Session: if session is None: session = get_current_session() if session is None: @@ -656,9 +656,7 @@ class _RenderedDeps(TypedDict): html: str -def _process_deps( - ui: TagChildArg, session: Optional[ShinySession] = None -) -> _RenderedDeps: +def _process_deps(ui: TagChildArg, session: Optional[Session] = None) -> _RenderedDeps: session = _require_active_session(session) diff --git a/shiny/shinyapp.py b/shiny/shinyapp.py index 936ce4f9c..986f1f216 100644 --- a/shiny/shinyapp.py +++ b/shiny/shinyapp.py @@ -13,7 +13,7 @@ from starlette.responses import Response, HTMLResponse, JSONResponse from .http_staticfiles import StaticFiles -from .shinysession import ShinySession, session_context +from .session import Session, session_context from . import reactcore from .connmanager import ( Connection, @@ -30,19 +30,19 @@ class ShinyApp: def __init__( self, ui: Union[Tag, TagList], - server: Callable[[ShinySession], None], + server: Callable[[Session], None], *, debug: bool = False, ) -> None: self.ui: RenderedHTML = _render_page(ui, lib_prefix=self.LIB_PREFIX) - self.server: Callable[[ShinySession], None] = server + self.server: Callable[[Session], None] = server self._debug: bool = debug - self._sessions: Dict[str, ShinySession] = {} + self._sessions: Dict[str, Session] = {} self._last_session_id: int = 0 # Counter for generating session IDs - self._sessions_needing_flush: Dict[int, ShinySession] = {} + self._sessions_needing_flush: Dict[int, Session] = {} self._registered_dependencies: Dict[str, HTMLDependency] = {} self._dependency_handler: Any = starlette.routing.Router() @@ -58,12 +58,12 @@ def __init__( ), starlette.routing.Mount("/", app=self._dependency_handler), ], - lifespan=self.lifespan + lifespan=self.lifespan, ) @contextlib.asynccontextmanager async def lifespan(self, app: starlette.applications.Starlette): - unreg = reactcore.on_flushed(self._on_reactive_flushed, once = False) + unreg = reactcore.on_flushed(self._on_reactive_flushed, once=False) try: yield finally: @@ -72,15 +72,15 @@ async def lifespan(self, app: starlette.applications.Starlette): async def _on_reactive_flushed(self): await self.flush_pending_sessions() - def create_session(self, conn: Connection) -> ShinySession: + def create_session(self, conn: Connection) -> Session: self._last_session_id += 1 id = str(self._last_session_id) - session = ShinySession(self, id, conn, debug=self._debug) + session = Session(self, id, conn, debug=self._debug) self._sessions[id] = session return session - def remove_session(self, session: Union[ShinySession, str]) -> None: - if isinstance(session, ShinySession): + def remove_session(self, session: Union[Session, str]) -> None: + if isinstance(session, Session): session = session.id if self._debug: @@ -162,7 +162,7 @@ async def _on_session_request_cb(self, request: Request) -> ASGIApp: subpath: str = request.path_params["subpath"] # type: ignore if session_id in self._sessions: - session: ShinySession = self._sessions[session_id] + session: Session = self._sessions[session_id] with session_context(session): return await session.handle_request(request, action, subpath) @@ -171,7 +171,7 @@ async def _on_session_request_cb(self, request: Request) -> ASGIApp: # ========================================================================== # Flush # ========================================================================== - def request_flush(self, session: ShinySession) -> None: + def request_flush(self, session: Session) -> None: # TODO: Until we have reactive domains, because we can't yet keep track # of which sessions need a flush. pass diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 0f12bc088..6de155caa 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -9,7 +9,7 @@ from htmltools.core import TagChildArg -from .shinysession import ShinySession, Outputs, _require_active_session +from .session import Session, Outputs, _require_active_session from .reactives import ReactiveValues from .render import RenderFunction @@ -46,10 +46,10 @@ def __call__( return self._outputs(self._ns_key(name)) -class ShinySessionProxy(ShinySession): - def __init__(self, ns: str, parent_session: ShinySession) -> None: +class ShinySessionProxy(Session): + def __init__(self, ns: str, parent_session: Session) -> None: self._ns: str = ns - self._parent: ShinySession = parent_session + self._parent: Session = parent_session self.input: ReactiveValuesProxy = ReactiveValuesProxy(ns, parent_session.input) self.output: OutputsProxy = OutputsProxy(ns, parent_session.output) @@ -67,7 +67,7 @@ def ui(self, namespace: str, *args: Any) -> TagChildArg: ns = ShinyModule._make_ns_fn(namespace) return self._ui(ns, *args) - def server(self, ns: str, *, session: Optional[ShinySession] = None) -> None: + def server(self, ns: str, *, session: Optional[Session] = None) -> None: self.ns: str = ns session = _require_active_session(session) session_proxy = ShinySessionProxy(ns, session) diff --git a/tests/test_shinysession.py b/tests/test_shinysession.py index d31ee70be..09740d881 100644 --- a/tests/test_shinysession.py +++ b/tests/test_shinysession.py @@ -1,4 +1,4 @@ -"""Tests for `shiny.shinysession`.""" +"""Tests for `shiny.Session`.""" import pytest From 69c55aa297109656c903fa955c8a8092480780b1 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 28 Jan 2022 21:27:47 -0600 Subject: [PATCH 02/23] Rename ShinyApp -> App --- examples/download/app.py | 2 +- examples/dynamic_ui/app.py | 2 +- examples/inputs-update/app.py | 2 +- examples/inputs/app.py | 2 +- examples/moduleapp/app.py | 2 +- examples/myapp/app.py | 2 +- examples/req/app.py | 2 +- examples/simple/app.py | 2 +- shiny/__init__.py | 2 +- shiny/{shinyapp.py => app.py} | 4 ++-- shiny/session.py | 6 +++--- 11 files changed, 14 insertions(+), 14 deletions(-) rename shiny/{shinyapp.py => app.py} (99%) diff --git a/examples/download/app.py b/examples/download/app.py index a11414dbd..5698ebd4d 100644 --- a/examples/download/app.py +++ b/examples/download/app.py @@ -127,7 +127,7 @@ async def _(): raise Exception("This error was caused intentionally") -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/examples/dynamic_ui/app.py b/examples/dynamic_ui/app.py index aef24e34c..a639cba84 100644 --- a/examples/dynamic_ui/app.py +++ b/examples/dynamic_ui/app.py @@ -61,7 +61,7 @@ def _(): ui_remove("#thanks") -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/examples/inputs-update/app.py b/examples/inputs-update/app.py index 75ef35e30..b92b5b7f0 100644 --- a/examples/inputs-update/app.py +++ b/examples/inputs-update/app.py @@ -200,7 +200,7 @@ def _(): update_navs("inTabset", selected="panel2" if c_num % 2 else "panel1") -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/examples/inputs/app.py b/examples/inputs/app.py index a8b19a4af..c07d35979 100644 --- a/examples/inputs/app.py +++ b/examples/inputs/app.py @@ -151,6 +151,6 @@ def _(): p.close() -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/examples/moduleapp/app.py b/examples/moduleapp/app.py index 2e27ccd64..89a7e4287 100644 --- a/examples/moduleapp/app.py +++ b/examples/moduleapp/app.py @@ -59,7 +59,7 @@ def server(session: Session): counter_module.server("counter2") -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 8e0f877b8..320c5c1be 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -91,7 +91,7 @@ def _(): return out_str -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/examples/req/app.py b/examples/req/app.py index fd88996fa..867414369 100644 --- a/examples/req/app.py +++ b/examples/req/app.py @@ -45,6 +45,6 @@ def _(): return session.input["txt"] -app = ShinyApp(ui, server) +app = App(ui, server) app.SANITIZE_ERRORS = True app.run() diff --git a/examples/simple/app.py b/examples/simple/app.py index 30897d478..e65c020cd 100644 --- a/examples/simple/app.py +++ b/examples/simple/app.py @@ -50,7 +50,7 @@ async def _(): return f"n*2 is {val}, session id is {get_current_session().id}" -app = ShinyApp(ui, server) +app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/shiny/__init__.py b/shiny/__init__.py index a141506f2..b5e9399f7 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -27,7 +27,7 @@ from .progress import * from .render import * from .reactives import * -from .shinyapp import * +from .app import * from .session import * from .shinymodule import * from .validation import * diff --git a/shiny/shinyapp.py b/shiny/app.py similarity index 99% rename from shiny/shinyapp.py rename to shiny/app.py index 986f1f216..c974696a2 100644 --- a/shiny/shinyapp.py +++ b/shiny/app.py @@ -1,4 +1,4 @@ -__all__ = ("ShinyApp",) +__all__ = ("App",) import contextlib from typing import Any, List, Optional, Union, Dict, Callable, cast @@ -22,7 +22,7 @@ from .html_dependencies import jquery_deps, shiny_deps -class ShinyApp: +class App: LIB_PREFIX: str = "lib/" SANITIZE_ERRORS: bool = False SANITIZE_ERROR_MSG: str = "An error has occurred. Check your logs or contact the app author for clarification." diff --git a/shiny/session.py b/shiny/session.py index 53ff00f9a..a76c98aab 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -48,7 +48,7 @@ from typing_extensions import TypedDict if TYPE_CHECKING: - from .shinyapp import ShinyApp + from .app import App from htmltools import TagChildArg, TagList @@ -119,9 +119,9 @@ class Session: # Initialization # ========================================================================== def __init__( - self, app: "ShinyApp", id: str, conn: Connection, debug: bool = False + self, app: "App", id: str, conn: Connection, debug: bool = False ) -> None: - self.app: ShinyApp = app + self.app: App = app self.id: str = id self._conn: Connection = conn self._debug: bool = debug From 6e0ac61d6c9e7e95d11a671f0bab41b0707a1545 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 31 Jan 2022 15:51:29 -0600 Subject: [PATCH 03/23] Rename ShinySessionProxy -> SessionProxy --- examples/moduleapp/app.py | 2 +- shiny/shinymodule.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/moduleapp/app.py b/examples/moduleapp/app.py index 89a7e4287..47b8bc043 100644 --- a/examples/moduleapp/app.py +++ b/examples/moduleapp/app.py @@ -28,7 +28,7 @@ def counter_module_ui( ) -def counter_module_server(session: ShinySessionProxy): +def counter_module_server(session: SessionProxy): count: ReactiveVal[int] = ReactiveVal(0) @observe() diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 6de155caa..3c14df274 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -1,7 +1,7 @@ __all__ = ( "ReactiveValuesProxy", "OutputsProxy", - "ShinySessionProxy", + "SessionProxy", "ShinyModule", ) @@ -46,7 +46,7 @@ def __call__( return self._outputs(self._ns_key(name)) -class ShinySessionProxy(Session): +class SessionProxy(Session): def __init__(self, ns: str, parent_session: Session) -> None: self._ns: str = ns self._parent: Session = parent_session @@ -58,10 +58,10 @@ class ShinyModule: def __init__( self, ui: Callable[..., TagChildArg], - server: Callable[[ShinySessionProxy], None], + server: Callable[[SessionProxy], None], ) -> None: self._ui: Callable[..., TagChildArg] = ui - self._server: Callable[[ShinySessionProxy], None] = server + self._server: Callable[[SessionProxy], None] = server def ui(self, namespace: str, *args: Any) -> TagChildArg: ns = ShinyModule._make_ns_fn(namespace) @@ -70,7 +70,7 @@ def ui(self, namespace: str, *args: Any) -> TagChildArg: def server(self, ns: str, *, session: Optional[Session] = None) -> None: self.ns: str = ns session = _require_active_session(session) - session_proxy = ShinySessionProxy(ns, session) + session_proxy = SessionProxy(ns, session) self._server(session_proxy) @staticmethod From 7b4b7fcd19b75e832fc6dc55d0c9de718ea53475 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 31 Jan 2022 15:52:13 -0600 Subject: [PATCH 04/23] Rename reactives.py -> reactive.py --- shiny/__init__.py | 2 +- shiny/decorators.py | 2 +- shiny/{reactives.py => reactive.py} | 0 shiny/session.py | 2 +- shiny/shinymodule.py | 2 +- tests/test_reactives.py | 4 ++-- 6 files changed, 6 insertions(+), 6 deletions(-) rename shiny/{reactives.py => reactive.py} (100%) diff --git a/shiny/__init__.py b/shiny/__init__.py index b5e9399f7..aefd8f20f 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -26,7 +26,7 @@ from .page import * from .progress import * from .render import * -from .reactives import * +from .reactive import * from .app import * from .session import * from .shinymodule import * diff --git a/shiny/decorators.py b/shiny/decorators.py index 4122cc88f..03698cb51 100644 --- a/shiny/decorators.py +++ b/shiny/decorators.py @@ -2,7 +2,7 @@ from typing import TypeVar, Callable, List, Awaitable, Union, cast from .input_handlers import ActionButtonValue -from .reactives import isolate +from .reactive import isolate from .validation import req from .utils import is_async_callable, run_coro_sync diff --git a/shiny/reactives.py b/shiny/reactive.py similarity index 100% rename from shiny/reactives.py rename to shiny/reactive.py diff --git a/shiny/session.py b/shiny/session.py index a76c98aab..3d4b331f2 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -52,7 +52,7 @@ from htmltools import TagChildArg, TagList -from .reactives import ReactiveValues, Observer, ObserverAsync, isolate +from .reactive import ReactiveValues, Observer, ObserverAsync, isolate from .http_staticfiles import FileResponse from .connmanager import Connection, ConnectionClosed from . import reactcore diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 3c14df274..0602db0ad 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -10,7 +10,7 @@ from htmltools.core import TagChildArg from .session import Session, Outputs, _require_active_session -from .reactives import ReactiveValues +from .reactive import ReactiveValues from .render import RenderFunction diff --git a/tests/test_reactives.py b/tests/test_reactives.py index 4b09ffddf..5efeeea12 100644 --- a/tests/test_reactives.py +++ b/tests/test_reactives.py @@ -3,12 +3,12 @@ import pytest import asyncio from typing import List -from shiny import reactives +from shiny import reactive from shiny.input_handlers import ActionButtonValue import shiny.reactcore as reactcore from shiny.decorators import * -from shiny.reactives import * +from shiny.reactive import * from shiny.validation import req from .mocktime import MockTime From 5098d99f94a2e9c9a29bab0082071c6433e31242 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 31 Jan 2022 17:00:53 -0600 Subject: [PATCH 05/23] Move UI stuff to ui_toolkit/ --- shiny/__init__.py | 19 +--- shiny/html_dependencies.py | 96 +----------------- shiny/ui_toolkit/__init__.py | 20 ++++ shiny/{ => ui_toolkit}/bootstrap.py | 0 shiny/{ => ui_toolkit}/download_button.py | 2 +- shiny/ui_toolkit/html_dependencies.py | 98 +++++++++++++++++++ shiny/{ => ui_toolkit}/input_action_button.py | 0 shiny/{ => ui_toolkit}/input_check_radio.py | 0 shiny/{ => ui_toolkit}/input_date.py | 0 shiny/{ => ui_toolkit}/input_file.py | 0 shiny/{ => ui_toolkit}/input_numeric.py | 0 shiny/{ => ui_toolkit}/input_password.py | 0 shiny/{ => ui_toolkit}/input_select.py | 0 shiny/{ => ui_toolkit}/input_slider.py | 0 shiny/{ => ui_toolkit}/input_text.py | 0 shiny/{ => ui_toolkit}/input_update.py | 4 +- shiny/{ => ui_toolkit}/input_utils.py | 0 shiny/{ => ui_toolkit}/modal.py | 4 +- shiny/{ => ui_toolkit}/navs.py | 0 shiny/{ => ui_toolkit}/output.py | 0 shiny/{ => ui_toolkit}/page.py | 0 21 files changed, 126 insertions(+), 117 deletions(-) create mode 100644 shiny/ui_toolkit/__init__.py rename shiny/{ => ui_toolkit}/bootstrap.py (100%) rename shiny/{ => ui_toolkit}/download_button.py (97%) create mode 100644 shiny/ui_toolkit/html_dependencies.py rename shiny/{ => ui_toolkit}/input_action_button.py (100%) rename shiny/{ => ui_toolkit}/input_check_radio.py (100%) rename shiny/{ => ui_toolkit}/input_date.py (100%) rename shiny/{ => ui_toolkit}/input_file.py (100%) rename shiny/{ => ui_toolkit}/input_numeric.py (100%) rename shiny/{ => ui_toolkit}/input_password.py (100%) rename shiny/{ => ui_toolkit}/input_select.py (100%) rename shiny/{ => ui_toolkit}/input_slider.py (100%) rename shiny/{ => ui_toolkit}/input_text.py (100%) rename shiny/{ => ui_toolkit}/input_update.py (98%) rename shiny/{ => ui_toolkit}/input_utils.py (100%) rename shiny/{ => ui_toolkit}/modal.py (95%) rename shiny/{ => ui_toolkit}/navs.py (100%) rename shiny/{ => ui_toolkit}/output.py (100%) rename shiny/{ => ui_toolkit}/page.py (100%) diff --git a/shiny/__init__.py b/shiny/__init__.py index aefd8f20f..4842faf20 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -5,29 +5,14 @@ __version__ = "0.0.0.9000" # All objects imported into this scope will be available as shiny.foo -from .bootstrap import * from .decorators import * -from .download_button import * from .dynamic_ui import * -from .input_action_button import * -from .input_check_radio import * -from .input_date import * -from .input_file import * -from .input_numeric import * -from .input_password import * -from .input_select import * -from .input_slider import * -from .input_text import * -from .input_update import * -from .modal import * -from .navs import * from .notifications import * -from .output import * -from .page import * from .progress import * from .render import * -from .reactive import * from .app import * from .session import * from .shinymodule import * from .validation import * + +from . import reactive diff --git a/shiny/html_dependencies.py b/shiny/html_dependencies.py index d003d4209..63b88e09a 100644 --- a/shiny/html_dependencies.py +++ b/shiny/html_dependencies.py @@ -1,5 +1,4 @@ -from htmltools import HTMLDependency, HTML -from typing import List, Union +from htmltools import HTMLDependency def shiny_deps() -> HTMLDependency: @@ -12,30 +11,6 @@ def shiny_deps() -> HTMLDependency: ) -def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]: - dep = HTMLDependency( - name="bootstrap", - version="5.0.1", - source={"package": "shiny", "subdir": "www/shared/bootstrap/"}, - script={"src": "bootstrap.bundle.min.js"}, - stylesheet={"href": "bootstrap.min.css"}, - ) - deps = [jquery_deps(), dep] - if bs3compat: - deps.append(bs3compat_deps()) - return deps - - -# TODO: if we want to support glyphicons we'll need to bundle font files, too -def bs3compat_deps() -> HTMLDependency: - return HTMLDependency( - name="bs3-compat", - version="1.0", - source={"package": "shiny", "subdir": "www/shared/bs3compat/"}, - script=[{"src": "transition.js"}, {"src": "tabs.js"}, {"src": "bs3compat.js"}], - ) - - def jquery_deps() -> HTMLDependency: return HTMLDependency( name="jquery", @@ -43,72 +18,3 @@ def jquery_deps() -> HTMLDependency: source={"package": "shiny", "subdir": "www/shared/jquery/"}, script={"src": "jquery-3.6.0.min.js"}, ) - - -def nav_deps( - include_bootstrap: bool = True, -) -> Union[HTMLDependency, List[HTMLDependency]]: - dep = HTMLDependency( - name="bslib-navs", - version="1.0", - source={"package": "shiny", "subdir": "www/shared/bslib/dist/"}, - script={"src": "navs.min.js"}, - ) - return [dep, *bootstrap_deps()] if include_bootstrap else dep - - -def ionrangeslider_deps() -> List[HTMLDependency]: - return [ - HTMLDependency( - name="ionrangeslider", - version="2.3.1", - source={"package": "shiny", "subdir": "www/shared/ionrangeslider/"}, - script={"src": "js/ion.rangeSlider.min.js"}, - stylesheet={"href": "css/ion.rangeSlider.css"}, - ), - HTMLDependency( - name="strftime", - version="0.9.2", - source={"package": "shiny", "subdir": "www/shared/strftime/"}, - script={"src": "strftime-min.js"}, - ), - ] - - -def datepicker_deps() -> HTMLDependency: - return HTMLDependency( - name="bootstrap-datepicker", - version="1.9.0", - source={"package": "shiny", "subdir": "www/shared/datepicker/"}, - # TODO: pre-compile the Bootstrap 5 version? - stylesheet={"href": "css/bootstrap-datepicker3.min.css"}, - script={"src": "js/bootstrap-datepicker.min.js"}, - # Need to enable noConflict mode. See #1346. - head=HTML( - "" - ), - ) - - -def selectize_deps() -> HTMLDependency: - return HTMLDependency( - name="selectize", - version="0.12.6", - source={"package": "shiny", "subdir": "www/shared/selectize/"}, - script=[ - {"src": "js/selectize.min.js"}, - {"src": "accessibility/js/selectize-plugin-a11y.min.js"}, - ], - # TODO: pre-compile the Bootstrap 5 version? - stylesheet={"href": "css/selectize.bootstrap3.css"}, - ) - - -def jqui_deps() -> HTMLDependency: - return HTMLDependency( - name="jquery-ui", - version="1.12.1", - source={"package": "shiny", "subdir": "www/shared/jqueryui/"}, - script={"src": "jquery-ui.min.js"}, - stylesheet={"href": "jquery-ui.min.css"}, - ) diff --git a/shiny/ui_toolkit/__init__.py b/shiny/ui_toolkit/__init__.py new file mode 100644 index 000000000..ad89ff38b --- /dev/null +++ b/shiny/ui_toolkit/__init__.py @@ -0,0 +1,20 @@ +"""UI Toolkit for Shiny.""" + +# All objects imported into this scope will be available as shiny.ui_toolkit.foo +from .bootstrap import * +from .download_button import * +from .html_dependencies import * +from .input_action_button import * +from .input_check_radio import * +from .input_date import * +from .input_file import * +from .input_numeric import * +from .input_password import * +from .input_select import * +from .input_slider import * +from .input_text import * +from .input_update import * +from .modal import * +from .navs import * +from .output import * +from .page import * diff --git a/shiny/bootstrap.py b/shiny/ui_toolkit/bootstrap.py similarity index 100% rename from shiny/bootstrap.py rename to shiny/ui_toolkit/bootstrap.py diff --git a/shiny/download_button.py b/shiny/ui_toolkit/download_button.py similarity index 97% rename from shiny/download_button.py rename to shiny/ui_toolkit/download_button.py index 2ce52fa5b..a907306eb 100644 --- a/shiny/download_button.py +++ b/shiny/ui_toolkit/download_button.py @@ -2,7 +2,7 @@ from htmltools import tags, Tag, TagChildArg, TagAttrArg, css -from .shinyenv import is_pyodide +from ..shinyenv import is_pyodide # TODO: implement icon diff --git a/shiny/ui_toolkit/html_dependencies.py b/shiny/ui_toolkit/html_dependencies.py new file mode 100644 index 000000000..60c98e8df --- /dev/null +++ b/shiny/ui_toolkit/html_dependencies.py @@ -0,0 +1,98 @@ +from typing import List, Union + +from htmltools import HTML, HTMLDependency + +from ..html_dependencies import jquery_deps + + +def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]: + dep = HTMLDependency( + name="bootstrap", + version="5.0.1", + source={"package": "shiny", "subdir": "www/shared/bootstrap/"}, + script={"src": "bootstrap.bundle.min.js"}, + stylesheet={"href": "bootstrap.min.css"}, + ) + deps = [jquery_deps(), dep] + if bs3compat: + deps.append(bs3compat_deps()) + return deps + + +# TODO: if we want to support glyphicons we'll need to bundle font files, too +def bs3compat_deps() -> HTMLDependency: + return HTMLDependency( + name="bs3-compat", + version="1.0", + source={"package": "shiny", "subdir": "www/shared/bs3compat/"}, + script=[{"src": "transition.js"}, {"src": "tabs.js"}, {"src": "bs3compat.js"}], + ) + + +def nav_deps( + include_bootstrap: bool = True, +) -> Union[HTMLDependency, List[HTMLDependency]]: + dep = HTMLDependency( + name="bslib-navs", + version="1.0", + source={"package": "shiny", "subdir": "www/shared/bslib/dist/"}, + script={"src": "navs.min.js"}, + ) + return [dep, *bootstrap_deps()] if include_bootstrap else dep + + +def ionrangeslider_deps() -> List[HTMLDependency]: + return [ + HTMLDependency( + name="ionrangeslider", + version="2.3.1", + source={"package": "shiny", "subdir": "www/shared/ionrangeslider/"}, + script={"src": "js/ion.rangeSlider.min.js"}, + stylesheet={"href": "css/ion.rangeSlider.css"}, + ), + HTMLDependency( + name="strftime", + version="0.9.2", + source={"package": "shiny", "subdir": "www/shared/strftime/"}, + script={"src": "strftime-min.js"}, + ), + ] + + +def datepicker_deps() -> HTMLDependency: + return HTMLDependency( + name="bootstrap-datepicker", + version="1.9.0", + source={"package": "shiny", "subdir": "www/shared/datepicker/"}, + # TODO: pre-compile the Bootstrap 5 version? + stylesheet={"href": "css/bootstrap-datepicker3.min.css"}, + script={"src": "js/bootstrap-datepicker.min.js"}, + # Need to enable noConflict mode. See #1346. + head=HTML( + "" + ), + ) + + +def selectize_deps() -> HTMLDependency: + return HTMLDependency( + name="selectize", + version="0.12.6", + source={"package": "shiny", "subdir": "www/shared/selectize/"}, + script=[ + {"src": "js/selectize.min.js"}, + {"src": "accessibility/js/selectize-plugin-a11y.min.js"}, + ], + # TODO: pre-compile the Bootstrap 5 version? + stylesheet={"href": "css/selectize.bootstrap3.css"}, + ) + + +def jqui_deps() -> HTMLDependency: + return HTMLDependency( + name="jquery-ui", + version="1.12.1", + source={"package": "shiny", "subdir": "www/shared/jqueryui/"}, + script={"src": "jquery-ui.min.js"}, + stylesheet={"href": "jquery-ui.min.css"}, + ) diff --git a/shiny/input_action_button.py b/shiny/ui_toolkit/input_action_button.py similarity index 100% rename from shiny/input_action_button.py rename to shiny/ui_toolkit/input_action_button.py diff --git a/shiny/input_check_radio.py b/shiny/ui_toolkit/input_check_radio.py similarity index 100% rename from shiny/input_check_radio.py rename to shiny/ui_toolkit/input_check_radio.py diff --git a/shiny/input_date.py b/shiny/ui_toolkit/input_date.py similarity index 100% rename from shiny/input_date.py rename to shiny/ui_toolkit/input_date.py diff --git a/shiny/input_file.py b/shiny/ui_toolkit/input_file.py similarity index 100% rename from shiny/input_file.py rename to shiny/ui_toolkit/input_file.py diff --git a/shiny/input_numeric.py b/shiny/ui_toolkit/input_numeric.py similarity index 100% rename from shiny/input_numeric.py rename to shiny/ui_toolkit/input_numeric.py diff --git a/shiny/input_password.py b/shiny/ui_toolkit/input_password.py similarity index 100% rename from shiny/input_password.py rename to shiny/ui_toolkit/input_password.py diff --git a/shiny/input_select.py b/shiny/ui_toolkit/input_select.py similarity index 100% rename from shiny/input_select.py rename to shiny/ui_toolkit/input_select.py diff --git a/shiny/input_slider.py b/shiny/ui_toolkit/input_slider.py similarity index 100% rename from shiny/input_slider.py rename to shiny/ui_toolkit/input_slider.py diff --git a/shiny/input_text.py b/shiny/ui_toolkit/input_text.py similarity index 100% rename from shiny/input_text.py rename to shiny/ui_toolkit/input_text.py diff --git a/shiny/input_update.py b/shiny/ui_toolkit/input_update.py similarity index 98% rename from shiny/input_update.py rename to shiny/ui_toolkit/input_update.py index 62ceb34ea..9b2a44ae0 100644 --- a/shiny/input_update.py +++ b/shiny/ui_toolkit/input_update.py @@ -13,8 +13,8 @@ from .input_check_radio import ChoicesArg, _generate_options from .input_select import SelectChoicesArg, _normalize_choices, _render_choices from .input_slider import SliderValueArg, SliderStepArg, _slider_type, _as_numeric -from .utils import drop_none -from .session import Session, _require_active_session, _process_deps +from ..utils import drop_none +from ..session import Session, _require_active_session, _process_deps # ----------------------------------------------------------------------------- # input_action_button.py diff --git a/shiny/input_utils.py b/shiny/ui_toolkit/input_utils.py similarity index 100% rename from shiny/input_utils.py rename to shiny/ui_toolkit/input_utils.py diff --git a/shiny/modal.py b/shiny/ui_toolkit/modal.py similarity index 95% rename from shiny/modal.py rename to shiny/ui_toolkit/modal.py index a4640a971..e7e523371 100644 --- a/shiny/modal.py +++ b/shiny/ui_toolkit/modal.py @@ -8,8 +8,8 @@ from htmltools import tags, Tag, div, HTML, TagChildArg, TagAttrArg -from .utils import run_coro_sync -from .session import Session, _require_active_session, _process_deps +from ..utils import run_coro_sync +from ..session import Session, _require_active_session, _process_deps def modal_button(label: str, icon: TagChildArg = None) -> Tag: diff --git a/shiny/navs.py b/shiny/ui_toolkit/navs.py similarity index 100% rename from shiny/navs.py rename to shiny/ui_toolkit/navs.py diff --git a/shiny/output.py b/shiny/ui_toolkit/output.py similarity index 100% rename from shiny/output.py rename to shiny/ui_toolkit/output.py diff --git a/shiny/page.py b/shiny/ui_toolkit/page.py similarity index 100% rename from shiny/page.py rename to shiny/ui_toolkit/page.py From 2a62d972ac841fc58b77988959675aedc055dd78 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 1 Feb 2022 15:14:43 -0600 Subject: [PATCH 06/23] ReactiveVal -> Value, ReactiveValues -> Values --- shiny/reactive.py | 19 ++++++------ shiny/session.py | 4 +-- shiny/shinymodule.py | 8 +++--- tests/test_reactives.py | 64 ++++++++++++++++++++--------------------- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/shiny/reactive.py b/shiny/reactive.py index 6dfdd3889..0aa6cc513 100644 --- a/shiny/reactive.py +++ b/shiny/reactive.py @@ -1,8 +1,8 @@ """Reactive components""" __all__ = ( - "ReactiveVal", - "ReactiveValues", + "Value", + "Values", "Reactive", "ReactiveAsync", "reactive", @@ -16,7 +16,6 @@ ) import asyncio -import sys import time import traceback from typing import ( @@ -46,9 +45,9 @@ T = TypeVar("T") # ============================================================================== -# ReactiveVal and ReactiveValues +# Value and Values # ============================================================================== -class ReactiveVal(Generic[T]): +class Value(Generic[T]): def __init__(self, value: T) -> None: self._value: T = value self._dependents: Dependents = Dependents() @@ -80,24 +79,24 @@ def set(self, value: T) -> bool: return True -class ReactiveValues: +class Values: def __init__(self, **kwargs: object) -> None: - self._map: dict[str, ReactiveVal[Any]] = {} + self._map: dict[str, Value[Any]] = {} for key, value in kwargs.items(): - self._map[key] = ReactiveVal(value) + self._map[key] = Value(value) def __setitem__(self, key: str, value: object) -> None: if key in self._map: self._map[key](value) else: - self._map[key] = ReactiveVal(value) + self._map[key] = Value(value) def __getitem__(self, key: str) -> Any: # Auto-populate key if accessed but not yet set. Needed to take reactive # dependencies on input values that haven't been received from client # yet. if key not in self._map: - self._map[key] = ReactiveVal(None) + self._map[key] = Value(None) return self._map[key]() diff --git a/shiny/session.py b/shiny/session.py index 3d4b331f2..fe2869442 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -52,7 +52,7 @@ from htmltools import TagChildArg, TagList -from .reactive import ReactiveValues, Observer, ObserverAsync, isolate +from .reactive import Values, Observer, ObserverAsync, isolate from .http_staticfiles import FileResponse from .connmanager import Connection, ConnectionClosed from . import reactcore @@ -126,7 +126,7 @@ def __init__( self._conn: Connection = conn self._debug: bool = debug - self.input: ReactiveValues = ReactiveValues() + self.input: Values = Values() self.output: Outputs = Outputs(self) self._outbound_message_queues = _empty_outbound_message_queues() diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 0602db0ad..97da5db41 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -10,14 +10,14 @@ from htmltools.core import TagChildArg from .session import Session, Outputs, _require_active_session -from .reactive import ReactiveValues +from .reactive import Values from .render import RenderFunction -class ReactiveValuesProxy(ReactiveValues): - def __init__(self, ns: str, values: ReactiveValues): +class ReactiveValuesProxy(Values): + def __init__(self, ns: str, values: Values): self._ns: str = ns - self._values: ReactiveValues = values + self._values: Values = values def _ns_key(self, key: str) -> str: return self._ns + "-" + key diff --git a/tests/test_reactives.py b/tests/test_reactives.py index 5efeeea12..8604f2916 100644 --- a/tests/test_reactives.py +++ b/tests/test_reactives.py @@ -21,8 +21,8 @@ async def test_flush_runs_newly_invalidated(): during the flush. """ - v1 = ReactiveVal(1) - v2 = ReactiveVal(2) + v1 = Value(1) + v2 = Value(2) v2_result = None @@ -50,8 +50,8 @@ async def test_flush_runs_newly_invalidated_async(): during the flush. (Same as previous test, but async.) """ - v1 = ReactiveVal(1) - v2 = ReactiveVal(2) + v1 = Value(1) + v2 = Value(2) v2_result = None @@ -77,7 +77,7 @@ async def o1(): # ====================================================================== @pytest.mark.asyncio async def test_reactive_val_same_no_invalidate(): - v = ReactiveVal(1) + v = Value(1) @observe() def o(): @@ -96,7 +96,7 @@ def o(): # ====================================================================== @pytest.mark.asyncio async def test_recursive_reactive(): - v = ReactiveVal(5) + v = Value(5) @reactive() def r(): @@ -118,7 +118,7 @@ def o(): @pytest.mark.asyncio async def test_recursive_reactive_async(): - v = ReactiveVal(5) + v = Value(5) @reactive_async() async def r(): @@ -145,7 +145,7 @@ async def o(): @pytest.mark.asyncio async def test_async_sequential(): - x: ReactiveVal[int] = ReactiveVal(1) + x: Value[int] = Value(1) results: list[int] = [] exec_order: list[str] = [] @@ -195,7 +195,7 @@ async def _(): async def test_isolate_basic_without_context(): # isolate() works with Reactive and ReactiveVal; allows executing without a # reactive context. - v = ReactiveVal(1) + v = Value(1) @reactive() def r(): @@ -214,13 +214,13 @@ def get_r(): @pytest.mark.asyncio async def test_isolate_prevents_dependency(): - v = ReactiveVal(1) + v = Value(1) @reactive() def r(): return v() + 10 - v_dep = ReactiveVal(1) # Use this only for invalidating the observer + v_dep = Value(1) # Use this only for invalidating the observer o_val = None @observe() @@ -262,7 +262,7 @@ async def f(): async def test_isolate_async_basic_without_context(): # async isolate works with Reactive and ReactiveVal; allows executing # without a reactive context. - v = ReactiveVal(1) + v = Value(1) @reactive_async() async def r(): @@ -278,13 +278,13 @@ async def get_r(): @pytest.mark.asyncio async def test_isolate_async_prevents_dependency(): - v = ReactiveVal(1) + v = Value(1) @reactive_async() async def r(): return v() + 10 - v_dep = ReactiveVal(1) # Use this only for invalidating the observer + v_dep = Value(1) # Use this only for invalidating the observer o_val = None @observe_async() @@ -315,7 +315,7 @@ async def o(): # ====================================================================== @pytest.mark.asyncio async def test_observer_priority(): - v = ReactiveVal(1) + v = Value(1) results: list[int] = [] @observe(priority=1) @@ -366,7 +366,7 @@ def o4(): # Same as previous, but with async @pytest.mark.asyncio async def test_observer_async_priority(): - v = ReactiveVal(1) + v = Value(1) results: list[int] = [] @observe_async(priority=1) @@ -419,7 +419,7 @@ async def o4(): # ====================================================================== @pytest.mark.asyncio async def test_observer_destroy(): - v = ReactiveVal(1) + v = Value(1) results: list[int] = [] @observe() @@ -437,7 +437,7 @@ def o1(): assert results == [1] # Same as above, but destroy before running first time - v = ReactiveVal(1) + v = Value(1) results: list[int] = [] @observe() @@ -505,7 +505,7 @@ def _(): async def test_reactive_error_rethrow(): # Make sure reactives re-throw errors. vals: List[str] = [] - v = ReactiveVal(1) + v = Value(1) @reactive() def r(): @@ -542,8 +542,8 @@ def _(): # For https://github.com/rstudio/prism/issues/26 @pytest.mark.asyncio async def test_dependent_invalidation(): - trigger = ReactiveVal(0) - v = ReactiveVal(0) + trigger = Value(0) + v = Value(0) error_occurred = False @observe() @@ -667,7 +667,7 @@ def obs1(): async def test_invalidate_later_invalidation(): mock_time = MockTime() with mock_time(): - rv = ReactiveVal(0) + rv = Value(0) @observe() def obs1(): @@ -751,7 +751,7 @@ def _(): assert n_times == 2 # Is invalidated properly by reactive vals - r = ReactiveVal(1) + r = Value(1) @observe() @event(r) @@ -771,7 +771,7 @@ def _(): assert n_times == 4 # Doesn't run on init - r = ReactiveVal(1) + r = Value(1) @observe() @event(r, ignore_init=True) @@ -787,8 +787,8 @@ def _(): assert n_times == 5 # Isolates properly - r = ReactiveVal(1) - r2 = ReactiveVal(1) + r = Value(1) + r2 = Value(1) @observe() @event(r) @@ -804,7 +804,7 @@ def _(): assert n_times == 6 # works with @reactive() - r2 = ReactiveVal(1) + r2 = Value(1) @reactive() @event(lambda: r2(), ignore_init=True) @@ -861,7 +861,7 @@ async def _(): assert n_times == 2 # Is invalidated properly by reactive vals - r = ReactiveVal(1) + r = Value(1) @observe_async() @event(r) @@ -881,7 +881,7 @@ async def _(): assert n_times == 4 # Doesn't run on init - r = ReactiveVal(1) + r = Value(1) @observe_async() @event(r, ignore_init=True) @@ -897,8 +897,8 @@ async def _(): assert n_times == 5 # Isolates properly - r = ReactiveVal(1) - r2 = ReactiveVal(1) + r = Value(1) + r2 = Value(1) @observe_async() @event(r) @@ -914,7 +914,7 @@ async def _(): assert n_times == 6 # works with @reactive() - r2 = ReactiveVal(1) + r2 = Value(1) @reactive_async() @event(lambda: r2(), ignore_init=True) From 9c281cbaa182d0b3bc839445ca6c079e6d3ed09f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 1 Feb 2022 15:51:48 -0600 Subject: [PATCH 07/23] Tweak type checker settings --- .vscode/settings.json | 6 ++++++ pyrightconfig.json | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 pyrightconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 472078d24..1a74bfc58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,12 @@ "editor.formatOnSave": true, "editor.tabSize": 4, }, + "python.analysis.diagnosticSeverityOverrides": { + "reportImportCycles": "none", + "reportUnusedFunction": "none", + "reportPrivateUsage": "none", + "reportUnnecessaryIsInstance": "none" + }, "editor.rulers": [ 88 ], diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 9b634b7b6..000000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "reportImportCycles": false, - "reportUnusedFunction": false -} From bb43680176fedc8297a5d6a6f4f4746fb9343ec1 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 1 Feb 2022 17:47:38 -0600 Subject: [PATCH 08/23] Make reactive.Values callable --- shiny/reactive.py | 47 +++++++++++-------- shiny/render.py | 6 +-- shiny/session.py | 4 +- shiny/shinymodule.py | 25 ++++++++-- tests/test_reactives.py | 100 ++++++++++++++++++++-------------------- 5 files changed, 103 insertions(+), 79 deletions(-) diff --git a/shiny/reactive.py b/shiny/reactive.py index 0aa6cc513..04c3bbd40 100644 --- a/shiny/reactive.py +++ b/shiny/reactive.py @@ -27,7 +27,6 @@ Union, Generic, Any, - overload, ) import typing import inspect @@ -52,19 +51,10 @@ def __init__(self, value: T) -> None: self._value: T = value self._dependents: Dependents = Dependents() - @overload + # Calling the object is equivalent to `.get()` def __call__(self) -> T: - ... - - @overload - def __call__(self, value: T) -> bool: - ... - - def __call__(self, value: Union[MISSING_TYPE, T] = MISSING) -> Union[T, bool]: - if isinstance(value, MISSING_TYPE): - return self.get() - else: - return self.set(value) + self._dependents.register() + return self._value def get(self) -> T: self._dependents.register() @@ -85,24 +75,41 @@ def __init__(self, **kwargs: object) -> None: for key, value in kwargs.items(): self._map[key] = Value(value) - def __setitem__(self, key: str, value: object) -> None: - if key in self._map: - self._map[key](value) - else: - self._map[key] = Value(value) + def __setitem__(self, key: str, value: Value[Any]) -> None: + if not isinstance(value, Value): + raise TypeError("`value` must be a shiny.reactive.Value object.") - def __getitem__(self, key: str) -> Any: + self._map[key] = value + + def __getitem__(self, key: str) -> Value[Any]: # Auto-populate key if accessed but not yet set. Needed to take reactive # dependencies on input values that haven't been received from client # yet. if key not in self._map: self._map[key] = Value(None) - return self._map[key]() + return self._map[key] def __delitem__(self, key: str) -> None: del self._map[key] + # Allow access of values as attributes. + def __setattr__(self, attr: str, value: Value[Any]) -> None: + # Need special handling of "_map". + if attr == "_map": + super().__setattr__(attr, value) + return + + self.__setitem__(attr, value) + + def __getattr__(self, attr: str) -> Value[Any]: + if attr == "_map": + return object.__getattribute__(self, attr) + return self.__getitem__(attr) + + def __delattr__(self, key: str) -> None: + self.__delitem__(key) + # ============================================================================== # Reactive diff --git a/shiny/render.py b/shiny/render.py index 243e12e41..638351993 100644 --- a/shiny/render.py +++ b/shiny/render.py @@ -83,13 +83,13 @@ def __call__(self) -> object: async def run(self) -> object: # Reactively read some information about the plot. pixelratio: float = typing.cast( - float, self._session.input[".clientdata_pixelratio"] + float, self._session.input[".clientdata_pixelratio"]() ) width: float = typing.cast( - float, self._session.input[f".clientdata_output_{self._name}_width"] + float, self._session.input[f".clientdata_output_{self._name}_width"]() ) height: float = typing.cast( - float, self._session.input[f".clientdata_output_{self._name}_height"] + float, self._session.input[f".clientdata_output_{self._name}_height"]() ) fig = await self._fn() diff --git a/shiny/session.py b/shiny/session.py index fe2869442..78a21c774 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -237,7 +237,7 @@ def _manage_inputs(self, data: Dict[str, object]) -> None: if len(keys) == 2: val = input_handlers.process_value(keys[1], val, keys[0], self) - self.input[keys[0]] = val + self.input[keys[0]].set(val) # ========================================================================== # Message handlers @@ -290,7 +290,7 @@ async def uploadEnd(job_id: str, input_id: str) -> None: ) return None file_data = upload_op.finish() - self.input[input_id] = file_data + self.input[input_id].set(file_data) # Explicitly return None to signal that the message was handled. return None diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 97da5db41..163360b73 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -10,7 +10,7 @@ from htmltools.core import TagChildArg from .session import Session, Outputs, _require_active_session -from .reactive import Values +from .reactive import Values, Value from .render import RenderFunction @@ -22,15 +22,32 @@ def __init__(self, ns: str, values: Values): def _ns_key(self, key: str) -> str: return self._ns + "-" + key - def __setitem__(self, key: str, value: object) -> None: - self._values[self._ns_key(key)] = value + def __setitem__(self, key: str, value: Value[Any]) -> None: + self._values[self._ns_key(key)].set(value) - def __getitem__(self, key: str) -> object: + def __getitem__(self, key: str) -> Value[Any]: return self._values[self._ns_key(key)] def __delitem__(self, key: str) -> None: del self._values[self._ns_key(key)] + # Allow access of values as attributes. + def __setattr__(self, attr: str, value: Value[Any]) -> None: + if attr in ("_values", "_ns"): + super().__setattr__(attr, value) + return + else: + self.__setitem__(attr, value) + + def __getattr__(self, attr: str) -> Value[Any]: + if attr in ("_values", "_ns"): + return object.__getattribute__(self, attr) + else: + return self.__getitem__(attr) + + def __delattr__(self, key: str) -> None: + self.__delitem__(key) + class OutputsProxy(Outputs): def __init__(self, ns: str, outputs: Outputs): diff --git a/tests/test_reactives.py b/tests/test_reactives.py index 8604f2916..050a15d9d 100644 --- a/tests/test_reactives.py +++ b/tests/test_reactives.py @@ -1,4 +1,4 @@ -"""Tests for `shiny.reactives` and `shiny.reactcore`.""" +"""Tests for `shiny.reactive` and `shiny.reactcore`.""" import pytest import asyncio @@ -35,7 +35,7 @@ def o2(): @observe() def o1(): - v2(v1()) + v2.set(v1()) await reactcore.flush() assert v2_result == 1 @@ -64,7 +64,7 @@ async def o2(): @observe_async() async def o1(): - v2(v1()) + v2.set(v1()) await reactcore.flush() assert v2_result == 1 @@ -86,7 +86,7 @@ def o(): await reactcore.flush() assert o._exec_count == 1 - v(1) + v.set(1) await reactcore.flush() assert o._exec_count == 1 @@ -102,7 +102,7 @@ async def test_recursive_reactive(): def r(): if v() == 0: return 0 - v(v() - 1) + v.set(v() - 1) r() @observe() @@ -124,7 +124,7 @@ async def test_recursive_reactive_async(): async def r(): if v() == 0: return 0 - v(v() - 1) + v.set(v() - 1) await r() @observe_async() @@ -170,7 +170,7 @@ async def _(): await asyncio.gather(react_chain(1), react_chain(2)) await reactcore.flush() - x(5) + x.set(5) await reactcore.flush() assert results == [111, 211, 115, 215] @@ -234,13 +234,13 @@ def o(): assert o_val == 11 # Changing v() shouldn't invalidate o - v(2) + v.set(2) await reactcore.flush() assert o_val == 11 assert o._exec_count == 1 # v_dep() should invalidate the observer - v_dep(2) + v_dep.set(2) await reactcore.flush() assert o_val == 12 assert o._exec_count == 2 @@ -298,13 +298,13 @@ async def o(): assert o_val == 11 # Changing v() shouldn't invalidate o - v(2) + v.set(2) await reactcore.flush() assert o_val == 11 assert o._exec_count == 1 # v_dep() should invalidate the observer - v_dep(2) + v_dep.set(2) await reactcore.flush() assert o_val == 12 assert o._exec_count == 2 @@ -353,12 +353,12 @@ def o4(): # Change v and run again, to make sure results are stable results.clear() - v(2) + v.set(2) await reactcore.flush() assert results == [2, 4, 1, 3] results.clear() - v(3) + v.set(3) await reactcore.flush() assert results == [2, 4, 1, 3] @@ -404,12 +404,12 @@ async def o4(): # Change v and run again, to make sure results are stable results.clear() - v(2) + v.set(2) await reactcore.flush() assert results == [2, 4, 1, 3] results.clear() - v(3) + v.set(3) await reactcore.flush() assert results == [2, 4, 1, 3] @@ -431,7 +431,7 @@ def o1(): await reactcore.flush() assert results == [1] - v(2) + v.set(2) o1.destroy() await reactcore.flush() assert results == [1] @@ -530,7 +530,7 @@ def _(): await reactcore.flush() assert vals == ["o1-1", "r", "o2-2"] - v(2) + v.set(2) with pytest.warns(reactcore.ReactiveWarning): await reactcore.flush() assert vals == ["o1-1", "r", "o2-2", "o1-1", "o2-2"] @@ -554,7 +554,7 @@ def _(): with isolate(): r() val = v() - v(val + 1) + v.set(val + 1) except Exception: nonlocal error_occurred error_occurred = True @@ -568,7 +568,7 @@ def r(): return v() await reactcore.flush() - trigger(1) + trigger.set(1) await reactcore.flush() with isolate(): @@ -679,7 +679,7 @@ def obs1(): # Change rv, triggering invalidation of obs1. The expected behavior is that # the invalidation causes the invalidate_later call to be cancelled. - rv(1) + rv.set(1) await reactcore.flush() assert obs1._exec_count == 2 @@ -751,10 +751,10 @@ def _(): assert n_times == 2 # Is invalidated properly by reactive vals - r = Value(1) + v = Value(1) @observe() - @event(r) + @event(v) def _(): nonlocal n_times n_times += 1 @@ -762,19 +762,19 @@ def _(): asyncio.run(reactcore.flush()) assert n_times == 3 - r(1) + v.set(1) asyncio.run(reactcore.flush()) assert n_times == 3 - r(2) + v.set(2) asyncio.run(reactcore.flush()) assert n_times == 4 # Doesn't run on init - r = Value(1) + v = Value(1) @observe() - @event(r, ignore_init=True) + @event(v, ignore_init=True) def _(): nonlocal n_times n_times += 1 @@ -782,32 +782,32 @@ def _(): asyncio.run(reactcore.flush()) assert n_times == 4 - r(2) + v.set(2) asyncio.run(reactcore.flush()) assert n_times == 5 # Isolates properly - r = Value(1) - r2 = Value(1) + v = Value(1) + v2 = Value(1) @observe() - @event(r) + @event(v) def _(): nonlocal n_times - n_times += r2() + n_times += v2() asyncio.run(reactcore.flush()) assert n_times == 6 - r2(2) + v2.set(2) asyncio.run(reactcore.flush()) assert n_times == 6 # works with @reactive() - r2 = Value(1) + v2 = Value(1) @reactive() - @event(lambda: r2(), ignore_init=True) + @event(lambda: v2(), ignore_init=True) def r2b(): return 1 @@ -819,7 +819,7 @@ def _(): asyncio.run(reactcore.flush()) assert n_times == 6 - r2(2) + v2.set(2) asyncio.run(reactcore.flush()) assert n_times == 7 @@ -861,10 +861,10 @@ async def _(): assert n_times == 2 # Is invalidated properly by reactive vals - r = Value(1) + v = Value(1) @observe_async() - @event(r) + @event(v) async def _(): nonlocal n_times n_times += 1 @@ -872,19 +872,19 @@ async def _(): asyncio.run(reactcore.flush()) assert n_times == 3 - r(1) + v.set(1) asyncio.run(reactcore.flush()) assert n_times == 3 - r(2) + v.set(2) asyncio.run(reactcore.flush()) assert n_times == 4 # Doesn't run on init - r = Value(1) + v = Value(1) @observe_async() - @event(r, ignore_init=True) + @event(v, ignore_init=True) async def _(): nonlocal n_times n_times += 1 @@ -892,32 +892,32 @@ async def _(): asyncio.run(reactcore.flush()) assert n_times == 4 - r(2) + v.set(2) asyncio.run(reactcore.flush()) assert n_times == 5 # Isolates properly - r = Value(1) - r2 = Value(1) + v = Value(1) + v2 = Value(1) @observe_async() - @event(r) + @event(v) async def _(): nonlocal n_times - n_times += r2() + n_times += v2() asyncio.run(reactcore.flush()) assert n_times == 6 - r2(2) + v2.set(2) asyncio.run(reactcore.flush()) assert n_times == 6 # works with @reactive() - r2 = Value(1) + v2 = Value(1) @reactive_async() - @event(lambda: r2(), ignore_init=True) + @event(lambda: v2(), ignore_init=True) async def r2b(): return 1 @@ -929,6 +929,6 @@ async def _(): asyncio.run(reactcore.flush()) assert n_times == 6 - r2(2) + v2.set(2) asyncio.run(reactcore.flush()) assert n_times == 7 From 60cad305d72b80277112264d60da55b812533bac Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 1 Feb 2022 17:53:22 -0600 Subject: [PATCH 09/23] Update myapp example --- examples/myapp/app.py | 63 +++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 320c5c1be..5ce5eadff 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -4,61 +4,54 @@ # Then point web browser to: # http://localhost:8000/ -# Add parent directory to path, so we can find the prism module. -# (This is just a temporary fix) -import os -import sys - -# This will load the shiny module dynamically, without having to install it. -# This makes the debug/run cycle quicker. -shiny_module_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) -sys.path.insert(0, shiny_module_dir) - -from shiny import * -from shiny.fileupload import FileInfo +import matplotlib.pyplot as plt # For plot rendering import numpy as np -import matplotlib.pyplot as plt -ui = page_fluid( - layout_sidebar( - panel_sidebar( - input_slider("n", "N", 0, 100, 20), - input_file("file1", "Choose file", multiple=True), +import shiny.ui_toolkit as st +import shiny +from shiny import Session, reactive +from shiny.fileupload import FileInfo + +ui = st.page_fluid( + st.layout_sidebar( + st.panel_sidebar( + st.input_slider("n", "N", 0, 100, 20), + st.input_file("file1", "Choose file", multiple=True), ), - panel_main( - output_text_verbatim("txt"), - output_text_verbatim("shared_txt"), - output_plot("plot"), - output_text_verbatim("file_content"), + st.panel_main( + st.output_text_verbatim("txt"), + st.output_text_verbatim("shared_txt"), + st.output_plot("plot"), + st.output_text_verbatim("file_content"), ), ), ) # A ReactiveVal which is shared across all sessions. -shared_val = ReactiveVal(None) +shared_val = reactive.Value(None) def server(session: Session): - @reactive() + @reactive.reactive() def r(): - if session.input["n"] is None: + if session.input.n() is None: return - return session.input["n"] * 2 + return session.input.n() * 2 @session.output("txt") async def _(): val = r() - return f"n*2 is {val}, session id is {get_current_session().id}" + return f"n*2 is {val}, session id is {shiny.session.get_current_session().id}" # This observer watches n, and changes shared_val, which is shared across # all running sessions. - @observe() + @reactive.observe() def _(): - if session.input["n"] is None: + if session.input.n() is None: return - shared_val(session.input["n"] * 10) + shared_val.set(session.input["n"]() * 10) # Print the value of shared_val(). Changing it in one session should cause # this to run in all sessions. @@ -67,18 +60,18 @@ def _(): return f"shared_val() is {shared_val()}" @session.output("plot") - @render_plot(alt="A histogram") + @shiny.render_plot(alt="A histogram") def _(): np.random.seed(19680801) x = 100 + 15 * np.random.randn(437) fig, ax = plt.subplots() - ax.hist(x, session.input["n"], density=True) + ax.hist(x, session.input.n(), density=True) return fig @session.output("file_content") def _(): - file_infos: list[FileInfo] = session.input["file1"] + file_infos: list[FileInfo] = session.input.file1() if not file_infos: return @@ -91,7 +84,7 @@ def _(): return out_str -app = App(ui, server) +app = shiny.App(ui, server) if __name__ == "__main__": app.run() From 297847b3dd3c421b86d3e6662870428d4eec7558 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 1 Feb 2022 18:03:00 -0600 Subject: [PATCH 10/23] Pass input and output to server function --- examples/myapp/app.py | 24 ++++++++++++------------ shiny/app.py | 7 ++++--- shiny/session.py | 2 +- shiny/shinymodule.py | 8 +++++--- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 5ce5eadff..e2b07c78b 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -11,7 +11,7 @@ import shiny.ui_toolkit as st import shiny -from shiny import Session, reactive +from shiny import Session, Outputs, reactive from shiny.fileupload import FileInfo ui = st.page_fluid( @@ -33,14 +33,14 @@ shared_val = reactive.Value(None) -def server(session: Session): +def server(input: reactive.Values, output: Outputs, session: Session): @reactive.reactive() def r(): - if session.input.n() is None: + if input.n() is None: return - return session.input.n() * 2 + return input.n() * 2 - @session.output("txt") + @output("txt") async def _(): val = r() return f"n*2 is {val}, session id is {shiny.session.get_current_session().id}" @@ -49,29 +49,29 @@ async def _(): # all running sessions. @reactive.observe() def _(): - if session.input.n() is None: + if input.n() is None: return - shared_val.set(session.input["n"]() * 10) + shared_val.set(input["n"]() * 10) # Print the value of shared_val(). Changing it in one session should cause # this to run in all sessions. - @session.output("shared_txt") + @output("shared_txt") def _(): return f"shared_val() is {shared_val()}" - @session.output("plot") + @output("plot") @shiny.render_plot(alt="A histogram") def _(): np.random.seed(19680801) x = 100 + 15 * np.random.randn(437) fig, ax = plt.subplots() - ax.hist(x, session.input.n(), density=True) + ax.hist(x, input.n(), density=True) return fig - @session.output("file_content") + @output("file_content") def _(): - file_infos: list[FileInfo] = session.input.file1() + file_infos: list[FileInfo] = input.file1() if not file_infos: return diff --git a/shiny/app.py b/shiny/app.py index c974696a2..6c0594cad 100644 --- a/shiny/app.py +++ b/shiny/app.py @@ -13,8 +13,9 @@ from starlette.responses import Response, HTMLResponse, JSONResponse from .http_staticfiles import StaticFiles -from .session import Session, session_context +from .session import Outputs, Session, session_context from . import reactcore +from .reactive import Values from .connmanager import ( Connection, StarletteConnection, @@ -30,12 +31,12 @@ class App: def __init__( self, ui: Union[Tag, TagList], - server: Callable[[Session], None], + server: Callable[[Values, Outputs, Session], None], *, debug: bool = False, ) -> None: self.ui: RenderedHTML = _render_page(ui, lib_prefix=self.LIB_PREFIX) - self.server: Callable[[Session], None] = server + self.server: Callable[[Values, Outputs, Session], None] = server self._debug: bool = debug diff --git a/shiny/session.py b/shiny/session.py index 78a21c774..9ad5809b6 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -145,7 +145,7 @@ def __init__( self._flushed_callbacks = utils.Callbacks() with session_context(self): - self.app.server(self) + self.app.server(self.input, self.output, self) def _register_session_end_callbacks(self) -> None: # This is to be called from the initialization. It registers functions diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 163360b73..b2c5c84cd 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -75,10 +75,12 @@ class ShinyModule: def __init__( self, ui: Callable[..., TagChildArg], - server: Callable[[SessionProxy], None], + server: Callable[[ReactiveValuesProxy, OutputsProxy, SessionProxy], None], ) -> None: self._ui: Callable[..., TagChildArg] = ui - self._server: Callable[[SessionProxy], None] = server + self._server: Callable[ + [ReactiveValuesProxy, OutputsProxy, SessionProxy], None + ] = server def ui(self, namespace: str, *args: Any) -> TagChildArg: ns = ShinyModule._make_ns_fn(namespace) @@ -88,7 +90,7 @@ def server(self, ns: str, *, session: Optional[Session] = None) -> None: self.ns: str = ns session = _require_active_session(session) session_proxy = SessionProxy(ns, session) - self._server(session_proxy) + self._server(session_proxy.input, session_proxy.output, session_proxy) @staticmethod def _make_ns_fn(namespace: str) -> Callable[[str], str]: From b4b1f977529cfb367c2b6c95335212085c85f7e8 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 11:10:05 -0600 Subject: [PATCH 11/23] Clean up tests --- tests/test_reactives.py | 63 +++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/tests/test_reactives.py b/tests/test_reactives.py index 050a15d9d..df9af2ea4 100644 --- a/tests/test_reactives.py +++ b/tests/test_reactives.py @@ -717,7 +717,8 @@ async def add_result_later(delay: float, msg: str): # ------------------------------------------------------------ # @event() works as expected # ------------------------------------------------------------ -def test_event_decorator(): +@pytest.mark.asyncio +async def test_event_decorator(): n_times = 0 # By default, runs every time that event expression is _not_ None (ignore_none=True) @@ -727,7 +728,7 @@ def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 0 # Unless ignore_none=False @@ -737,7 +738,7 @@ def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 1 # Or if one of the args is not None @@ -747,7 +748,7 @@ def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 2 # Is invalidated properly by reactive vals @@ -759,15 +760,15 @@ def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 3 v.set(1) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 3 v.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 4 # Doesn't run on init @@ -779,11 +780,11 @@ def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 4 v.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 5 # Isolates properly @@ -796,11 +797,11 @@ def _(): nonlocal n_times n_times += v2() - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 6 v2.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 6 # works with @reactive() @@ -816,18 +817,19 @@ def _(): nonlocal n_times n_times += r2b() - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 6 v2.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 7 # ------------------------------------------------------------ # @event() works as expected with async # ------------------------------------------------------------ -def test_event_async_decorator(): +@pytest.mark.asyncio +async def test_event_async_decorator(): n_times = 0 # By default, runs every time that event expression is _not_ None (ignore_none=True) @@ -837,7 +839,7 @@ async def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 0 # Unless ignore_none=False @@ -847,7 +849,7 @@ async def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 1 # Or if one of the args is not None @@ -857,7 +859,7 @@ async def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 2 # Is invalidated properly by reactive vals @@ -869,15 +871,15 @@ async def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 3 v.set(1) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 3 v.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 4 # Doesn't run on init @@ -889,11 +891,11 @@ async def _(): nonlocal n_times n_times += 1 - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 4 v.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 5 # Isolates properly @@ -906,29 +908,36 @@ async def _(): nonlocal n_times n_times += v2() - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 6 v2.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 6 # works with @reactive() v2 = Value(1) @reactive_async() - @event(lambda: v2(), ignore_init=True) + async def r_a(): + await asyncio.sleep(0) # Make sure the async function yields control + return 1 + + @reactive_async() + @event(lambda: v2(), r_a, ignore_init=True) async def r2b(): + await asyncio.sleep(0) # Make sure the async function yields control return 1 @observe_async() async def _(): nonlocal n_times + await asyncio.sleep(0) n_times += await r2b() - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 6 v2.set(2) - asyncio.run(reactcore.flush()) + await reactcore.flush() assert n_times == 7 From 01a95e08bf8c933f3bec70b179c9c05c67c84fb2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 15:20:21 -0600 Subject: [PATCH 12/23] Get name of output from function name --- examples/myapp/app.py | 16 ++++++++-------- shiny/render.py | 6 +++++- shiny/session.py | 24 ++++++++++++------------ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index e2b07c78b..65120baa4 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -40,8 +40,8 @@ def r(): return return input.n() * 2 - @output("txt") - async def _(): + @output() + async def txt(): val = r() return f"n*2 is {val}, session id is {shiny.session.get_current_session().id}" @@ -55,13 +55,13 @@ def _(): # Print the value of shared_val(). Changing it in one session should cause # this to run in all sessions. - @output("shared_txt") - def _(): + @output() + def shared_txt(): return f"shared_val() is {shared_val()}" - @output("plot") + @output() @shiny.render_plot(alt="A histogram") - def _(): + def plot() -> object: np.random.seed(19680801) x = 100 + 15 * np.random.randn(437) @@ -69,8 +69,8 @@ def _(): ax.hist(x, input.n(), density=True) return fig - @output("file_content") - def _(): + @output() + def file_content(): file_infos: list[FileInfo] = input.file1() if not file_infos: return diff --git a/shiny/render.py b/shiny/render.py index 638351993..d5e6f767b 100644 --- a/shiny/render.py +++ b/shiny/render.py @@ -52,7 +52,8 @@ class ImgData(TypedDict): class RenderFunction: def __init__(self, fn: Callable[[], object]) -> None: - raise NotImplementedError + self.__name__ = fn.__name__ + self.__doc__ = fn.__doc__ def __call__(self) -> object: raise NotImplementedError @@ -74,6 +75,7 @@ class RenderPlot(RenderFunction): _ppi: float = 96 def __init__(self, fn: RenderPlotFunc, alt: Optional[str] = None) -> None: + super().__init__(fn) self._fn: RenderPlotFuncAsync = utils.wrap_async(fn) self._alt: Optional[str] = alt @@ -236,6 +238,7 @@ def try_render_plot_pil( class RenderImage(RenderFunction): def __init__(self, fn: RenderImageFunc, delete_file: bool = False) -> None: + super().__init__(fn) self._fn: RenderImageFuncAsync = utils.wrap_async(fn) self._delete_file: bool = delete_file @@ -290,6 +293,7 @@ def wrapper(fn: Union[RenderImageFunc, RenderImageFuncAsync]) -> RenderImage: class RenderUI(RenderFunction): def __init__(self, fn: RenderUIFunc) -> None: + super().__init__(fn) self._fn: RenderUIFuncAsync = utils.wrap_async(fn) def __call__(self) -> object: diff --git a/shiny/session.py b/shiny/session.py index 9ad5809b6..66fea04a7 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -519,7 +519,7 @@ def wrapper(fn: _DownloadHandler): encoding=encoding, ) - @self.output(effective_name) + @self.output(name=effective_name) @functools.wraps(fn) def _(): # TODO: the `w=` parameter should eventually be a worker ID, if we add those @@ -534,32 +534,32 @@ def __init__(self, session: Session) -> None: self._session: Session = session def __call__( - self, name: str + self, *, name: Optional[str] = None ) -> Callable[[Union[Callable[[], object], render.RenderFunction]], None]: def set_fn(fn: Union[Callable[[], object], render.RenderFunction]) -> None: - + fn_name = name or fn.__name__ # fn is either a regular function or a RenderFunction object. If # it's the latter, we can give it a bit of metadata, which can be # used by the if isinstance(fn, render.RenderFunction): - fn.set_metadata(self._session, name) + fn.set_metadata(self._session, fn_name) - if name in self._output_obervers: - self._output_obervers[name].destroy() + if fn_name in self._output_obervers: + self._output_obervers[fn_name].destroy() @ObserverAsync async def output_obs(): await self._session.send_message( - {"recalculating": {"name": name, "status": "recalculating"}} + {"recalculating": {"name": fn_name, "status": "recalculating"}} ) message: Dict[str, object] = {} try: if utils.is_async_callable(fn): fn2 = typing.cast(Callable[[], Awaitable[object]], fn) - message[name] = await fn2() + message[fn_name] = await fn2() else: - message[name] = fn() + message[fn_name] = fn() except SilentCancelOutputException: return except SilentException: @@ -576,7 +576,7 @@ async def output_obs(): err_msg = str(e) # Register the outbound error message msg: Dict[str, object] = { - name: { + fn_name: { "message": err_msg, # TODO: is it possible to get the call? "call": None, @@ -589,10 +589,10 @@ async def output_obs(): self._session._outbound_message_queues["values"].append(message) await self._session.send_message( - {"recalculating": {"name": name, "status": "recalculated"}} + {"recalculating": {"name": fn_name, "status": "recalculated"}} ) - self._output_obervers[name] = output_obs + self._output_obervers[fn_name] = output_obs return None From 559c5d7883fdf9a7711b9fded8b62477e265c170 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 15:41:26 -0600 Subject: [PATCH 13/23] Add render_text --- shiny/render.py | 104 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/shiny/render.py b/shiny/render.py index d5e6f767b..f3d4a47bf 100644 --- a/shiny/render.py +++ b/shiny/render.py @@ -26,30 +26,15 @@ from . import utils __all__ = ( + "render_text", "render_plot", "render_image", "render_ui", ) -# It would be nice to specify the return type of RenderPlotFunc to be something like: -# Union[matplotlib.figure.Figure, PIL.Image.Image] -# However, if we did that, we'd have to import those modules at load time, which adds -# a nontrivial amount of overhead. So for now, we're just using `object`. -RenderPlotFunc = Callable[[], object] -RenderPlotFuncAsync = Callable[[], Awaitable[object]] - - -class ImgData(TypedDict): - src: str - width: Union[str, float] - height: Union[str, float] - alt: Optional[str] - - -RenderImageFunc = Callable[[], ImgData] -RenderImageFuncAsync = Callable[[], Awaitable[ImgData]] - - +# ====================================================================================== +# RenderFunction/RenderFunctionAsync base class +# ====================================================================================== class RenderFunction: def __init__(self, fn: Callable[[], object]) -> None: self.__name__ = fn.__name__ @@ -66,11 +51,76 @@ def set_metadata(self, session: "Session", name: str) -> None: self._name: str = name +# The reason for having a separate RenderFunctionAsync class is because the __call__ +# method is marked here as async; you can't have a single class where one method could +# be either sync or async. class RenderFunctionAsync(RenderFunction): async def __call__(self) -> object: raise NotImplementedError +# ====================================================================================== +# RenderText +# ====================================================================================== +RenderTextFunc = Callable[[], str] +RenderTextFuncAsync = Callable[[], Awaitable[str]] + + +class RenderText(RenderFunction): + def __init__(self, fn: RenderTextFunc) -> None: + super().__init__(fn) + self._fn: RenderTextFuncAsync = utils.wrap_async(fn) + + def __call__(self) -> str: + return utils.run_coro_sync(self.run()) + + async def run(self) -> str: + return await self._fn() + + +class RenderTextAsync(RenderText, RenderFunctionAsync): + def __init__(self, fn: RenderTextFuncAsync) -> None: + if not inspect.iscoroutinefunction(fn): + raise TypeError(self.__class__.__name__ + " requires an async function") + # Init the base class with a placeholder synchronous function so it won't throw + # an error, then replace it with the async function. + super().__init__(typing.cast(RenderTextFunc, lambda: None)) + self._fn: RenderTextFuncAsync = fn + + async def __call__(self) -> str: # type: ignore + return await self.run() + + +def render_text() -> Callable[[Union[RenderTextFunc, RenderTextFuncAsync]], RenderText]: + def wrapper(fn: Union[RenderTextFunc, RenderTextFuncAsync]) -> RenderText: + if inspect.iscoroutinefunction(fn): + fn = typing.cast(RenderTextFuncAsync, fn) + return RenderTextAsync(fn) + else: + fn = typing.cast(RenderTextFunc, fn) + return RenderText(fn) + + return wrapper + + +# ====================================================================================== +# RenderPlot +# ====================================================================================== +# It would be nice to specify the return type of RenderPlotFunc to be something like: +# Union[matplotlib.figure.Figure, PIL.Image.Image] +# However, if we did that, we'd have to import those modules at load time, which adds +# a nontrivial amount of overhead. So for now, we're just using `object`. +RenderPlotFunc = Callable[[], object] +RenderPlotFuncAsync = Callable[[], Awaitable[object]] + + +class ImgData(TypedDict): + src: str + width: Union[str, float] + height: Union[str, float] + alt: Optional[str] + + class RenderPlot(RenderFunction): _ppi: float = 96 @@ -127,7 +177,7 @@ async def run(self) -> object: class RenderPlotAsync(RenderPlot, RenderFunctionAsync): def __init__(self, fn: RenderPlotFuncAsync, alt: Optional[str] = None) -> None: if not inspect.iscoroutinefunction(fn): - raise TypeError("PlotAsync requires an async function") + raise TypeError(self.__class__.__name__ + " requires an async function") # Init the Plot base class with a placeholder synchronous function so it # won't throw an error, then replace it with the async function. @@ -236,6 +286,13 @@ def try_render_plot_pil( return "TYPE_MISMATCH" +# ====================================================================================== +# RenderImage +# ====================================================================================== +RenderImageFunc = Callable[[], ImgData] +RenderImageFuncAsync = Callable[[], Awaitable[ImgData]] + + class RenderImage(RenderFunction): def __init__(self, fn: RenderImageFunc, delete_file: bool = False) -> None: super().__init__(fn) @@ -263,7 +320,7 @@ async def run(self) -> object: class RenderImageAsync(RenderImage, RenderFunctionAsync): def __init__(self, fn: RenderImageFuncAsync, delete_file: bool = False) -> None: if not inspect.iscoroutinefunction(fn): - raise TypeError("ImageAsync requires an async function") + raise TypeError(self.__class__.__name__ + " requires an async function") # Init the Image base class with a placeholder synchronous function so it # won't throw an error, then replace it with the async function. super().__init__(lambda: None, delete_file) @@ -287,6 +344,9 @@ def wrapper(fn: Union[RenderImageFunc, RenderImageFuncAsync]) -> RenderImage: return wrapper +# ====================================================================================== +# RenderUI +# ====================================================================================== RenderUIFunc = Callable[[], TagChildArg] RenderUIFuncAsync = Callable[[], Awaitable[TagChildArg]] @@ -312,7 +372,7 @@ async def run(self) -> object: class RenderUIAsync(RenderUI, RenderFunctionAsync): def __init__(self, fn: RenderUIFuncAsync) -> None: if not inspect.iscoroutinefunction(fn): - raise TypeError("PlotAsync requires an async function") + raise TypeError(self.__class__.__name__ + " requires an async function") super().__init__(lambda: None) self._fn: RenderUIFuncAsync = fn From 73d302d6a347830638ec1d9ee878d9d34f93bc02 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 15:41:43 -0600 Subject: [PATCH 14/23] Require render_ function for outputs --- examples/myapp/app.py | 5 ++++- shiny/session.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 65120baa4..ef0f0fc9b 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -41,6 +41,7 @@ def r(): return input.n() * 2 @output() + @shiny.render_text() async def txt(): val = r() return f"n*2 is {val}, session id is {shiny.session.get_current_session().id}" @@ -56,6 +57,7 @@ def _(): # Print the value of shared_val(). Changing it in one session should cause # this to run in all sessions. @output() + @shiny.render_text() def shared_txt(): return f"shared_val() is {shared_val()}" @@ -70,10 +72,11 @@ def plot() -> object: return fig @output() + @shiny.render_text() def file_content(): file_infos: list[FileInfo] = input.file1() if not file_infos: - return + return "" out_str = "" for file_info in file_infos: diff --git a/shiny/session.py b/shiny/session.py index 66fea04a7..142363e12 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -520,6 +520,7 @@ def wrapper(fn: _DownloadHandler): ) @self.output(name=effective_name) + @render.render_text() @functools.wraps(fn) def _(): # TODO: the `w=` parameter should eventually be a worker ID, if we add those @@ -535,8 +536,8 @@ def __init__(self, session: Session) -> None: def __call__( self, *, name: Optional[str] = None - ) -> Callable[[Union[Callable[[], object], render.RenderFunction]], None]: - def set_fn(fn: Union[Callable[[], object], render.RenderFunction]) -> None: + ) -> Callable[[render.RenderFunction], None]: + def set_fn(fn: render.RenderFunction) -> None: fn_name = name or fn.__name__ # fn is either a regular function or a RenderFunction object. If # it's the latter, we can give it a bit of metadata, which can be From d5a96a3cac740079440f7fd6efe99c0acc229808 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 17:15:26 -0600 Subject: [PATCH 15/23] Move reactive.Values to session.Inputs --- examples/myapp/app.py | 4 ++-- shiny/app.py | 7 +++--- shiny/reactive.py | 46 +------------------------------------ shiny/session.py | 53 +++++++++++++++++++++++++++++++++++++++++-- shiny/shinymodule.py | 21 ++++++++--------- 5 files changed, 67 insertions(+), 64 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index ef0f0fc9b..9d4115794 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -11,7 +11,7 @@ import shiny.ui_toolkit as st import shiny -from shiny import Session, Outputs, reactive +from shiny import Session, Inputs, Outputs, reactive from shiny.fileupload import FileInfo ui = st.page_fluid( @@ -33,7 +33,7 @@ shared_val = reactive.Value(None) -def server(input: reactive.Values, output: Outputs, session: Session): +def server(input: Inputs, output: Outputs, session: Session): @reactive.reactive() def r(): if input.n() is None: diff --git a/shiny/app.py b/shiny/app.py index 6c0594cad..c43593bb4 100644 --- a/shiny/app.py +++ b/shiny/app.py @@ -13,9 +13,8 @@ from starlette.responses import Response, HTMLResponse, JSONResponse from .http_staticfiles import StaticFiles -from .session import Outputs, Session, session_context +from .session import Inputs, Outputs, Session, session_context from . import reactcore -from .reactive import Values from .connmanager import ( Connection, StarletteConnection, @@ -31,12 +30,12 @@ class App: def __init__( self, ui: Union[Tag, TagList], - server: Callable[[Values, Outputs, Session], None], + server: Callable[[Inputs, Outputs, Session], None], *, debug: bool = False, ) -> None: self.ui: RenderedHTML = _render_page(ui, lib_prefix=self.LIB_PREFIX) - self.server: Callable[[Values, Outputs, Session], None] = server + self.server: Callable[[Inputs, Outputs, Session], None] = server self._debug: bool = debug diff --git a/shiny/reactive.py b/shiny/reactive.py index 04c3bbd40..f3ff062f2 100644 --- a/shiny/reactive.py +++ b/shiny/reactive.py @@ -2,7 +2,6 @@ __all__ = ( "Value", - "Values", "Reactive", "ReactiveAsync", "reactive", @@ -26,7 +25,6 @@ TypeVar, Union, Generic, - Any, ) import typing import inspect @@ -44,7 +42,7 @@ T = TypeVar("T") # ============================================================================== -# Value and Values +# Value # ============================================================================== class Value(Generic[T]): def __init__(self, value: T) -> None: @@ -69,48 +67,6 @@ def set(self, value: T) -> bool: return True -class Values: - def __init__(self, **kwargs: object) -> None: - self._map: dict[str, Value[Any]] = {} - for key, value in kwargs.items(): - self._map[key] = Value(value) - - def __setitem__(self, key: str, value: Value[Any]) -> None: - if not isinstance(value, Value): - raise TypeError("`value` must be a shiny.reactive.Value object.") - - self._map[key] = value - - def __getitem__(self, key: str) -> Value[Any]: - # Auto-populate key if accessed but not yet set. Needed to take reactive - # dependencies on input values that haven't been received from client - # yet. - if key not in self._map: - self._map[key] = Value(None) - - return self._map[key] - - def __delitem__(self, key: str) -> None: - del self._map[key] - - # Allow access of values as attributes. - def __setattr__(self, attr: str, value: Value[Any]) -> None: - # Need special handling of "_map". - if attr == "_map": - super().__setattr__(attr, value) - return - - self.__setitem__(attr, value) - - def __getattr__(self, attr: str) -> Value[Any]: - if attr == "_map": - return object.__getattribute__(self, attr) - return self.__getitem__(attr) - - def __delattr__(self, key: str) -> None: - self.__delitem__(key) - - # ============================================================================== # Reactive # ============================================================================== diff --git a/shiny/session.py b/shiny/session.py index 142363e12..8099d7bb2 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -1,5 +1,6 @@ __all__ = ( "Session", + "Inputs", "Outputs", "get_current_session", "session_context", @@ -52,7 +53,7 @@ from htmltools import TagChildArg, TagList -from .reactive import Values, Observer, ObserverAsync, isolate +from .reactive import Value, Observer, ObserverAsync, isolate from .http_staticfiles import FileResponse from .connmanager import Connection, ConnectionClosed from . import reactcore @@ -126,7 +127,7 @@ def __init__( self._conn: Connection = conn self._debug: bool = debug - self.input: Values = Values() + self.input: Inputs = Inputs() self.output: Outputs = Outputs(self) self._outbound_message_queues = _empty_outbound_message_queues() @@ -529,6 +530,54 @@ def _(): return wrapper +# ====================================================================================== +# Inputs +# ====================================================================================== +class Inputs: + def __init__(self, **kwargs: object) -> None: + self._map: dict[str, Value[Any]] = {} + for key, value in kwargs.items(): + self._map[key] = Value(value) + + def __setitem__(self, key: str, value: Value[Any]) -> None: + if not isinstance(value, Value): + raise TypeError("`value` must be a shiny.reactive.Value object.") + + self._map[key] = value + + def __getitem__(self, key: str) -> Value[Any]: + # Auto-populate key if accessed but not yet set. Needed to take reactive + # dependencies on input values that haven't been received from client + # yet. + if key not in self._map: + self._map[key] = Value(None) + + return self._map[key] + + def __delitem__(self, key: str) -> None: + del self._map[key] + + # Allow access of values as attributes. + def __setattr__(self, attr: str, value: Value[Any]) -> None: + # Need special handling of "_map". + if attr == "_map": + super().__setattr__(attr, value) + return + + self.__setitem__(attr, value) + + def __getattr__(self, attr: str) -> Value[Any]: + if attr == "_map": + return object.__getattribute__(self, attr) + return self.__getitem__(attr) + + def __delattr__(self, key: str) -> None: + self.__delitem__(key) + + +# ====================================================================================== +# Outputs +# ====================================================================================== class Outputs: def __init__(self, session: Session) -> None: self._output_obervers: Dict[str, Observer] = {} diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index b2c5c84cd..6f8d9000d 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -1,5 +1,6 @@ __all__ = ( - "ReactiveValuesProxy", + "InputsProxy", + "InputsProxy", "OutputsProxy", "SessionProxy", "ShinyModule", @@ -9,15 +10,15 @@ from htmltools.core import TagChildArg -from .session import Session, Outputs, _require_active_session -from .reactive import Values, Value +from .session import Session, Inputs, Outputs, _require_active_session +from .reactive import Value from .render import RenderFunction -class ReactiveValuesProxy(Values): - def __init__(self, ns: str, values: Values): +class InputsProxy(Inputs): + def __init__(self, ns: str, values: Inputs): self._ns: str = ns - self._values: Values = values + self._values: Inputs = values def _ns_key(self, key: str) -> str: return self._ns + "-" + key @@ -67,7 +68,7 @@ class SessionProxy(Session): def __init__(self, ns: str, parent_session: Session) -> None: self._ns: str = ns self._parent: Session = parent_session - self.input: ReactiveValuesProxy = ReactiveValuesProxy(ns, parent_session.input) + self.input: InputsProxy = InputsProxy(ns, parent_session.input) self.output: OutputsProxy = OutputsProxy(ns, parent_session.output) @@ -75,12 +76,10 @@ class ShinyModule: def __init__( self, ui: Callable[..., TagChildArg], - server: Callable[[ReactiveValuesProxy, OutputsProxy, SessionProxy], None], + server: Callable[[InputsProxy, OutputsProxy, SessionProxy], None], ) -> None: self._ui: Callable[..., TagChildArg] = ui - self._server: Callable[ - [ReactiveValuesProxy, OutputsProxy, SessionProxy], None - ] = server + self._server: Callable[[InputsProxy, OutputsProxy, SessionProxy], None] = server def ui(self, namespace: str, *args: Any) -> TagChildArg: ns = ShinyModule._make_ns_fn(namespace) From b440540241e6076f6c31ccaf7f6f5a7c6fbdaa53 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 17:16:04 -0600 Subject: [PATCH 16/23] Simplify fetching session ID --- examples/myapp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 9d4115794..85bc5050d 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -44,7 +44,7 @@ def r(): @shiny.render_text() async def txt(): val = r() - return f"n*2 is {val}, session id is {shiny.session.get_current_session().id}" + return f"n*2 is {val}, session id is {session.id}" # This observer watches n, and changes shared_val, which is shared across # all running sessions. From 9899a0cf9beb32b0e0b24d69108c38ee47013f03 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 17:17:02 -0600 Subject: [PATCH 17/23] Help with type hint --- shiny/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/render.py b/shiny/render.py index f3d4a47bf..c3f1f7977 100644 --- a/shiny/render.py +++ b/shiny/render.py @@ -323,7 +323,7 @@ def __init__(self, fn: RenderImageFuncAsync, delete_file: bool = False) -> None: raise TypeError(self.__class__.__name__ + " requires an async function") # Init the Image base class with a placeholder synchronous function so it # won't throw an error, then replace it with the async function. - super().__init__(lambda: None, delete_file) + super().__init__(typing.cast(RenderImageFunc, lambda: None), delete_file) self._fn: RenderImageFuncAsync = fn async def __call__(self) -> object: From d25e1353712b58915b645cc8ed43cfe6ddb18249 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 17:49:38 -0600 Subject: [PATCH 18/23] Observer -> Effect --- examples/myapp/app.py | 2 +- shiny/reactcore.py | 5 +- shiny/reactive.py | 46 ++++++------- shiny/session.py | 6 +- shiny/shinymodule.py | 6 +- tests/test_reactives.py | 140 ++++++++++++++++++++-------------------- 6 files changed, 102 insertions(+), 103 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 85bc5050d..e8e67bfef 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -48,7 +48,7 @@ async def txt(): # This observer watches n, and changes shared_val, which is shared across # all running sessions. - @reactive.observe() + @reactive.effect() def _(): if input.n() is None: return diff --git a/shiny/reactcore.py b/shiny/reactcore.py index a1545d4fd..1170f3af0 100644 --- a/shiny/reactcore.py +++ b/shiny/reactcore.py @@ -142,9 +142,8 @@ async def flush(self) -> None: await self._flushed_callbacks.invoke() async def _flush_sequential(self) -> None: - # Sequential flush: instead of storing the tasks in a list and - # calling gather() on them later, just run each observer in - # sequence. + # Sequential flush: instead of storing the tasks in a list and calling gather() + # on them later, just run each effect in sequence. while not self._pending_flush_queue.empty(): ctx = self._pending_flush_queue.get() await ctx.execute_flush_callbacks() diff --git a/shiny/reactive.py b/shiny/reactive.py index f3ff062f2..b3a9877b9 100644 --- a/shiny/reactive.py +++ b/shiny/reactive.py @@ -6,10 +6,10 @@ "ReactiveAsync", "reactive", "reactive_async", - "Observer", - "ObserverAsync", - "observe", - "observe_async", + "Effect", + "EffectAsync", + "effect", + "effect_async", "isolate", "invalidate_later", ) @@ -197,9 +197,9 @@ def create_reactive_async(fn: Callable[[], Awaitable[T]]) -> ReactiveAsync[T]: # ============================================================================== -# Observer +# Effect # ============================================================================== -class Observer: +class Effect: def __init__( self, func: Callable[[], None], @@ -208,7 +208,7 @@ def __init__( session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if inspect.iscoroutinefunction(func): - raise TypeError("Observer requires a non-async function") + raise TypeError("Effect requires a non-async function") self._func: Callable[[], Awaitable[None]] = utils.wrap_async(func) self._is_async: bool = False @@ -239,7 +239,7 @@ def __init__( def _create_context(self) -> Context: ctx = Context() - # Store the context explicitly in Observer object + # Store the context explicitly in Effect object # TODO: More explanation here self._ctx = ctx @@ -272,12 +272,12 @@ async def run(self) -> None: with ctx(): await self._func() except SilentException: - # It's OK for SilentException to cause an observer to stop running + # It's OK for SilentException to cause an Effect to stop running pass except Exception as e: traceback.print_exc() - warnings.warn("Error in observer: " + str(e), ReactiveWarning) + warnings.warn("Error in Effect: " + str(e), ReactiveWarning) if self._session: await self._session.unhandled_error(e) @@ -294,7 +294,7 @@ def _on_session_ended_cb(self) -> None: self.destroy() -class ObserverAsync(Observer): +class EffectAsync(Effect): def __init__( self, func: Callable[[], Awaitable[None]], @@ -303,18 +303,18 @@ def __init__( session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if not inspect.iscoroutinefunction(func): - raise TypeError("ObserverAsync requires an async function") + raise TypeError("EffectAsync requires an async function") - # Init the Observer base class with a placeholder synchronous function + # Init the Efect base class with a placeholder synchronous function # so it won't throw an error, then replace it with the async function. super().__init__(lambda: None, session=session, priority=priority) self._func: Callable[[], Awaitable[None]] = func self._is_async = True -def observe( +def effect( *, priority: int = 0, session: Union[MISSING_TYPE, "Session", None] = MISSING -) -> Callable[[Callable[[], None]], Observer]: +) -> Callable[[Callable[[], None]], Effect]: """[summary] Args: @@ -325,21 +325,21 @@ def observe( [description] """ - def create_observer(fn: Callable[[], None]) -> Observer: - return Observer(fn, priority=priority, session=session) + def create_effect(fn: Callable[[], None]) -> Effect: + return Effect(fn, priority=priority, session=session) - return create_observer + return create_effect -def observe_async( +def effect_async( *, priority: int = 0, session: Union[MISSING_TYPE, "Session", None] = MISSING, -) -> Callable[[Callable[[], Awaitable[None]]], ObserverAsync]: - def create_observer_async(fn: Callable[[], Awaitable[None]]) -> ObserverAsync: - return ObserverAsync(fn, priority=priority, session=session) +) -> Callable[[Callable[[], Awaitable[None]]], EffectAsync]: + def create_effect_async(fn: Callable[[], Awaitable[None]]) -> EffectAsync: + return EffectAsync(fn, priority=priority, session=session) - return create_observer_async + return create_effect_async # ============================================================================== diff --git a/shiny/session.py b/shiny/session.py index 8099d7bb2..573afa922 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -53,7 +53,7 @@ from htmltools import TagChildArg, TagList -from .reactive import Value, Observer, ObserverAsync, isolate +from .reactive import Value, Effect, EffectAsync, isolate from .http_staticfiles import FileResponse from .connmanager import Connection, ConnectionClosed from . import reactcore @@ -580,7 +580,7 @@ def __delattr__(self, key: str) -> None: # ====================================================================================== class Outputs: def __init__(self, session: Session) -> None: - self._output_obervers: Dict[str, Observer] = {} + self._output_obervers: Dict[str, Effect] = {} self._session: Session = session def __call__( @@ -597,7 +597,7 @@ def set_fn(fn: render.RenderFunction) -> None: if fn_name in self._output_obervers: self._output_obervers[fn_name].destroy() - @ObserverAsync + @EffectAsync async def output_obs(): await self._session.send_message( {"recalculating": {"name": fn_name, "status": "recalculating"}} diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py index 6f8d9000d..417f947c6 100644 --- a/shiny/shinymodule.py +++ b/shiny/shinymodule.py @@ -59,9 +59,9 @@ def _ns_key(self, key: str) -> str: return self._ns + "-" + key def __call__( - self, name: str - ) -> Callable[[Union[Callable[[], object], RenderFunction]], None]: - return self._outputs(self._ns_key(name)) + self, *, name: Optional[str] = None + ) -> Callable[[RenderFunction], None]: + return self._outputs(name=self._ns_key(name)) class SessionProxy(Session): diff --git a/tests/test_reactives.py b/tests/test_reactives.py index df9af2ea4..99750f68c 100644 --- a/tests/test_reactives.py +++ b/tests/test_reactives.py @@ -26,14 +26,14 @@ async def test_flush_runs_newly_invalidated(): v2_result = None - # In practice, on the first flush, Observers run in the order that they were - # created. Our test checks that o2 runs _after_ o1. - @observe() + # In practice, on the first flush, Effects run in the order that they were created. + # Our test checks that o2 runs _after_ o1. + @effect() def o2(): nonlocal v2_result v2_result = v2() - @observe() + @effect() def o1(): v2.set(v1()) @@ -55,14 +55,14 @@ async def test_flush_runs_newly_invalidated_async(): v2_result = None - # In practice, on the first flush, Observers run in the order that they were + # In practice, on the first flush, Effects run in the order that they were # created. Our test checks that o2 runs _after_ o1. - @observe_async() + @effect_async() async def o2(): nonlocal v2_result v2_result = v2() - @observe_async() + @effect_async() async def o1(): v2.set(v1()) @@ -79,7 +79,7 @@ async def o1(): async def test_reactive_val_same_no_invalidate(): v = Value(1) - @observe() + @effect() def o(): v() @@ -105,7 +105,7 @@ def r(): v.set(v() - 1) r() - @observe() + @effect() def o(): r() @@ -127,7 +127,7 @@ async def r(): v.set(v() - 1) await r() - @observe_async() + @effect_async() async def o(): await r() @@ -158,7 +158,7 @@ async def r(): exec_order.append(f"r{n}-2") return x() + 10 - @observe_async() + @effect_async() async def _(): nonlocal exec_order exec_order.append(f"o{n}-1") @@ -175,9 +175,9 @@ async def _(): assert results == [111, 211, 115, 215] - # This is the order of execution if the async observers are run - # sequentially. The `asyncio.sleep(0)` still yields control, but since there - # are no other observers scheduled, it will simply resume at the same point. + # This is the order of execution if the async effects are run sequentially. The + # `asyncio.sleep(0)` still yields control, but since there are no other effects + # scheduled, it will simply resume at the same point. # fmt: off assert exec_order == [ 'o1-1', 'o1-2', 'r1-1', 'r1-2', 'o1-3', @@ -220,10 +220,10 @@ async def test_isolate_prevents_dependency(): def r(): return v() + 10 - v_dep = Value(1) # Use this only for invalidating the observer + v_dep = Value(1) # Use this only for invalidating the effect o_val = None - @observe() + @effect() def o(): nonlocal o_val v_dep() @@ -239,7 +239,7 @@ def o(): assert o_val == 11 assert o._exec_count == 1 - # v_dep() should invalidate the observer + # v_dep() should invalidate the effect v_dep.set(2) await reactcore.flush() assert o_val == 12 @@ -284,10 +284,10 @@ async def test_isolate_async_prevents_dependency(): async def r(): return v() + 10 - v_dep = Value(1) # Use this only for invalidating the observer + v_dep = Value(1) # Use this only for invalidating the effect o_val = None - @observe_async() + @effect_async() async def o(): nonlocal o_val v_dep() @@ -303,7 +303,7 @@ async def o(): assert o_val == 11 assert o._exec_count == 1 - # v_dep() should invalidate the observer + # v_dep() should invalidate the effect v_dep.set(2) await reactcore.flush() assert o_val == 12 @@ -311,26 +311,26 @@ async def o(): # ====================================================================== -# Priority for observers +# Priority for effects # ====================================================================== @pytest.mark.asyncio -async def test_observer_priority(): +async def test_effect_priority(): v = Value(1) results: list[int] = [] - @observe(priority=1) + @effect(priority=1) def o1(): nonlocal results v() results.append(1) - @observe(priority=2) + @effect(priority=2) def o2(): nonlocal results v() results.append(2) - @observe(priority=1) + @effect(priority=1) def o3(): nonlocal results v() @@ -339,9 +339,9 @@ def o3(): await reactcore.flush() assert results == [2, 1, 3] - # Add another observer with priority 2. Only this one will run (until we + # Add another effect with priority 2. Only this one will run (until we # invalidate others by changing v). - @observe(priority=2) + @effect(priority=2) def o4(): nonlocal results v() @@ -365,23 +365,23 @@ def o4(): # Same as previous, but with async @pytest.mark.asyncio -async def test_observer_async_priority(): +async def test_effect_async_priority(): v = Value(1) results: list[int] = [] - @observe_async(priority=1) + @effect_async(priority=1) async def o1(): nonlocal results v() results.append(1) - @observe_async(priority=2) + @effect_async(priority=2) async def o2(): nonlocal results v() results.append(2) - @observe_async(priority=1) + @effect_async(priority=1) async def o3(): nonlocal results v() @@ -390,9 +390,9 @@ async def o3(): await reactcore.flush() assert results == [2, 1, 3] - # Add another observer with priority 2. Only this one will run (until we + # Add another effect with priority 2. Only this one will run (until we # invalidate others by changing v). - @observe_async(priority=2) + @effect_async(priority=2) async def o4(): nonlocal results v() @@ -415,14 +415,14 @@ async def o4(): # ====================================================================== -# Destroying observers +# Destroying effects # ====================================================================== @pytest.mark.asyncio -async def test_observer_destroy(): +async def test_effect_destroy(): v = Value(1) results: list[int] = [] - @observe() + @effect() def o1(): nonlocal results v() @@ -440,7 +440,7 @@ def o1(): v = Value(1) results: list[int] = [] - @observe() + @effect() def o2(): nonlocal results v() @@ -458,24 +458,24 @@ def o2(): async def test_error_handling(): vals: List[str] = [] - @observe() + @effect() def _(): vals.append("o1") - @observe() + @effect() def _(): vals.append("o2-1") raise Exception("Error here!") vals.append("o2-2") - @observe() + @effect() def _(): vals.append("o3") - # Error in observer should get converted to warning. + # Error in effect should get converted to warning. with pytest.warns(reactcore.ReactiveWarning): await reactcore.flush() - # All observers should have executed. + # All effects should have executed. assert vals == ["o1", "o2-1", "o3"] vals: List[str] = [] @@ -485,17 +485,17 @@ def r(): vals.append("r") raise Exception("Error here!") - @observe() + @effect() def _(): vals.append("o1-1") r() vals.append("o1-2") - @observe() + @effect() def _(): vals.append("o2") - # Error in observer should get converted to warning. + # Error in effect should get converted to warning. with pytest.warns(reactcore.ReactiveWarning): await reactcore.flush() assert vals == ["o1-1", "r", "o2"] @@ -512,14 +512,14 @@ def r(): vals.append("r") raise Exception("Error here!") - @observe() + @effect() def _(): v() vals.append("o1-1") r() vals.append("o1-2") - @observe() + @effect() def _(): v() vals.append("o2-2") @@ -546,7 +546,7 @@ async def test_dependent_invalidation(): v = Value(0) error_occurred = False - @observe() + @effect() def _(): trigger() @@ -559,7 +559,7 @@ def _(): nonlocal error_occurred error_occurred = True - @observe() + @effect() def _(): r() @@ -579,13 +579,13 @@ def r(): # ------------------------------------------------------------ -# req() pauses execution in @observe() and @reactive() +# req() pauses execution in @effect() and @calc() # ------------------------------------------------------------ @pytest.mark.asyncio async def test_req(): n_times = 0 - @observe() + @effect() def _(): req(False) nonlocal n_times @@ -594,7 +594,7 @@ def _(): await reactcore.flush() assert n_times == 0 - @observe() + @effect() def _(): req(True) nonlocal n_times @@ -610,7 +610,7 @@ def r(): val = None - @observe() + @effect() def _(): nonlocal val val = r() @@ -623,7 +623,7 @@ def r2(): req(True) return 1 - @observe() + @effect() def _(): nonlocal val val = r2() @@ -637,7 +637,7 @@ async def test_invalidate_later(): mock_time = MockTime() with mock_time(): - @observe() + @effect() def obs1(): invalidate_later(1) @@ -669,7 +669,7 @@ async def test_invalidate_later_invalidation(): with mock_time(): rv = Value(0) - @observe() + @effect() def obs1(): if rv() == 0: invalidate_later(1) @@ -722,7 +722,7 @@ async def test_event_decorator(): n_times = 0 # By default, runs every time that event expression is _not_ None (ignore_none=True) - @observe() + @effect() @event(lambda: None, lambda: ActionButtonValue(0)) def _(): nonlocal n_times @@ -732,7 +732,7 @@ def _(): assert n_times == 0 # Unless ignore_none=False - @observe() + @effect() @event(lambda: None, lambda: ActionButtonValue(0), ignore_none=False) def _(): nonlocal n_times @@ -742,7 +742,7 @@ def _(): assert n_times == 1 # Or if one of the args is not None - @observe() + @effect() @event(lambda: None, lambda: ActionButtonValue(0), lambda: True) def _(): nonlocal n_times @@ -754,7 +754,7 @@ def _(): # Is invalidated properly by reactive vals v = Value(1) - @observe() + @effect() @event(v) def _(): nonlocal n_times @@ -774,7 +774,7 @@ def _(): # Doesn't run on init v = Value(1) - @observe() + @effect() @event(v, ignore_init=True) def _(): nonlocal n_times @@ -791,7 +791,7 @@ def _(): v = Value(1) v2 = Value(1) - @observe() + @effect() @event(v) def _(): nonlocal n_times @@ -812,7 +812,7 @@ def _(): def r2b(): return 1 - @observe() + @effect() def _(): nonlocal n_times n_times += r2b() @@ -833,7 +833,7 @@ async def test_event_async_decorator(): n_times = 0 # By default, runs every time that event expression is _not_ None (ignore_none=True) - @observe_async() + @effect_async() @event(lambda: None, lambda: ActionButtonValue(0)) async def _(): nonlocal n_times @@ -843,7 +843,7 @@ async def _(): assert n_times == 0 # Unless ignore_none=False - @observe_async() + @effect_async() @event(lambda: None, lambda: ActionButtonValue(0), ignore_none=False) async def _(): nonlocal n_times @@ -853,7 +853,7 @@ async def _(): assert n_times == 1 # Or if one of the args is not None - @observe_async() + @effect_async() @event(lambda: None, lambda: ActionButtonValue(0), lambda: True) async def _(): nonlocal n_times @@ -865,7 +865,7 @@ async def _(): # Is invalidated properly by reactive vals v = Value(1) - @observe_async() + @effect_async() @event(v) async def _(): nonlocal n_times @@ -885,7 +885,7 @@ async def _(): # Doesn't run on init v = Value(1) - @observe_async() + @effect_async() @event(v, ignore_init=True) async def _(): nonlocal n_times @@ -902,7 +902,7 @@ async def _(): v = Value(1) v2 = Value(1) - @observe_async() + @effect_async() @event(v) async def _(): nonlocal n_times @@ -929,7 +929,7 @@ async def r2b(): await asyncio.sleep(0) # Make sure the async function yields control return 1 - @observe_async() + @effect_async() async def _(): nonlocal n_times await asyncio.sleep(0) From 2cdb1f7ecc2b6966743694d94e561946dfa1e9c3 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 17:58:31 -0600 Subject: [PATCH 19/23] Reactive -> Calc --- examples/myapp/app.py | 2 +- shiny/reactive.py | 42 ++++++++++++------------- tests/test_reactives.py | 68 ++++++++++++++++++++--------------------- 3 files changed, 55 insertions(+), 57 deletions(-) diff --git a/examples/myapp/app.py b/examples/myapp/app.py index e8e67bfef..579338c05 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -34,7 +34,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.reactive() + @reactive.calc() def r(): if input.n() is None: return diff --git a/shiny/reactive.py b/shiny/reactive.py index b3a9877b9..c90f0db9f 100644 --- a/shiny/reactive.py +++ b/shiny/reactive.py @@ -2,10 +2,10 @@ __all__ = ( "Value", - "Reactive", - "ReactiveAsync", - "reactive", - "reactive_async", + "Calc", + "CalcAsync", + "calc", + "calc_async", "Effect", "EffectAsync", "effect", @@ -68,9 +68,9 @@ def set(self, value: T) -> bool: # ============================================================================== -# Reactive +# Calc # ============================================================================== -class Reactive(Generic[T]): +class Calc(Generic[T]): def __init__( self, func: Callable[[], T], @@ -157,7 +157,7 @@ async def _run_func(self) -> None: self._error.append(err) -class ReactiveAsync(Reactive[T]): +class CalcAsync(Calc[T]): def __init__( self, func: Callable[[], Awaitable[T]], @@ -165,11 +165,11 @@ def __init__( session: Union[MISSING_TYPE, "Session", None] = MISSING, ) -> None: if not inspect.iscoroutinefunction(func): - raise TypeError("ReactiveAsync requires an async function") + raise TypeError("CalcAsync requires an async function") - # Init the Reactive base class with a placeholder synchronous function - # so it won't throw an error, then replace it with the async function. - # Need the `cast` to satisfy the type checker. + # Init the Calc base class with a placeholder synchronous function so it won't + # throw an error, then replace it with the async function. Need the `cast` to + # satisfy the type checker. super().__init__(lambda: typing.cast(T, None), session=session) self._func: Callable[[], Awaitable[T]] = func self._is_async = True @@ -178,22 +178,22 @@ async def __call__(self) -> T: return await self.get_value() -def reactive( +def calc( *, session: Union[MISSING_TYPE, "Session", None] = MISSING -) -> Callable[[Callable[[], T]], Reactive[T]]: - def create_reactive(fn: Callable[[], T]) -> Reactive[T]: - return Reactive(fn, session=session) +) -> Callable[[Callable[[], T]], Calc[T]]: + def create_calc(fn: Callable[[], T]) -> Calc[T]: + return Calc(fn, session=session) - return create_reactive + return create_calc -def reactive_async( +def calc_async( *, session: Union[MISSING_TYPE, "Session", None] = MISSING -) -> Callable[[Callable[[], Awaitable[T]]], ReactiveAsync[T]]: - def create_reactive_async(fn: Callable[[], Awaitable[T]]) -> ReactiveAsync[T]: - return ReactiveAsync(fn, session=session) +) -> Callable[[Callable[[], Awaitable[T]]], CalcAsync[T]]: + def create_calc_async(fn: Callable[[], Awaitable[T]]) -> CalcAsync[T]: + return CalcAsync(fn, session=session) - return create_reactive_async + return create_calc_async # ============================================================================== diff --git a/tests/test_reactives.py b/tests/test_reactives.py index 99750f68c..50277264b 100644 --- a/tests/test_reactives.py +++ b/tests/test_reactives.py @@ -3,7 +3,6 @@ import pytest import asyncio from typing import List -from shiny import reactive from shiny.input_handlers import ActionButtonValue import shiny.reactcore as reactcore @@ -17,8 +16,8 @@ @pytest.mark.asyncio async def test_flush_runs_newly_invalidated(): """ - Make sure that a flush will also run any reactives that were invalidated - during the flush. + Make sure that a flush will also run any calcs that were invalidated during the + flush. """ v1 = Value(1) @@ -46,8 +45,8 @@ def o1(): @pytest.mark.asyncio async def test_flush_runs_newly_invalidated_async(): """ - Make sure that a flush will also run any reactives that were invalidated - during the flush. (Same as previous test, but async.) + Make sure that a flush will also run any calcs that were invalidated during the + flush. (Same as previous test, but async.) """ v1 = Value(1) @@ -73,10 +72,10 @@ async def o1(): # ====================================================================== -# Setting ReactiveVal to same value doesn't invalidate downstream +# Setting reactive.Value to same value doesn't invalidate downstream # ====================================================================== @pytest.mark.asyncio -async def test_reactive_val_same_no_invalidate(): +async def test_reactive_value_same_no_invalidate(): v = Value(1) @effect() @@ -92,13 +91,13 @@ def o(): # ====================================================================== -# Recursive calls to reactives +# Recursive calls to calcs # ====================================================================== @pytest.mark.asyncio -async def test_recursive_reactive(): +async def test_recursive_calc(): v = Value(5) - @reactive() + @calc() def r(): if v() == 0: return 0 @@ -117,10 +116,10 @@ def o(): @pytest.mark.asyncio -async def test_recursive_reactive_async(): +async def test_recursive_calc_async(): v = Value(5) - @reactive_async() + @calc_async() async def r(): if v() == 0: return 0 @@ -150,7 +149,7 @@ async def test_async_sequential(): exec_order: list[str] = [] async def react_chain(n: int): - @reactive_async() + @calc_async() async def r(): nonlocal exec_order exec_order.append(f"r{n}-1") @@ -193,11 +192,10 @@ async def _(): # ====================================================================== @pytest.mark.asyncio async def test_isolate_basic_without_context(): - # isolate() works with Reactive and ReactiveVal; allows executing without a - # reactive context. + # isolate() works with calc and Value; allows executing without a reactive context. v = Value(1) - @reactive() + @calc() def r(): return v() + 10 @@ -216,7 +214,7 @@ def get_r(): async def test_isolate_prevents_dependency(): v = Value(1) - @reactive() + @calc() def r(): return v() + 10 @@ -260,11 +258,11 @@ async def f(): @pytest.mark.asyncio async def test_isolate_async_basic_without_context(): - # async isolate works with Reactive and ReactiveVal; allows executing - # without a reactive context. + # async isolate works with calc and Value; allows executing without a reactive + # context. v = Value(1) - @reactive_async() + @calc_async() async def r(): return v() + 10 @@ -280,7 +278,7 @@ async def get_r(): async def test_isolate_async_prevents_dependency(): v = Value(1) - @reactive_async() + @calc_async() async def r(): return v() + 10 @@ -480,7 +478,7 @@ def _(): vals: List[str] = [] - @reactive() + @calc() def r(): vals.append("r") raise Exception("Error here!") @@ -502,12 +500,12 @@ def _(): @pytest.mark.asyncio -async def test_reactive_error_rethrow(): - # Make sure reactives re-throw errors. +async def test_calc_error_rethrow(): + # Make sure calcs re-throw errors. vals: List[str] = [] v = Value(1) - @reactive() + @calc() def r(): vals.append("r") raise Exception("Error here!") @@ -563,7 +561,7 @@ def _(): def _(): r() - @reactive() + @calc() def r(): return v() @@ -603,7 +601,7 @@ def _(): await reactcore.flush() assert n_times == 1 - @reactive() + @calc() def r(): req(False) return 1 @@ -618,7 +616,7 @@ def _(): await reactcore.flush() assert val is None - @reactive() + @calc() def r2(): req(True) return 1 @@ -751,7 +749,7 @@ def _(): await reactcore.flush() assert n_times == 2 - # Is invalidated properly by reactive vals + # Is invalidated properly by reactive values v = Value(1) @effect() @@ -804,10 +802,10 @@ def _(): await reactcore.flush() assert n_times == 6 - # works with @reactive() + # works with @calc() v2 = Value(1) - @reactive() + @calc() @event(lambda: v2(), ignore_init=True) def r2b(): return 1 @@ -862,7 +860,7 @@ async def _(): await reactcore.flush() assert n_times == 2 - # Is invalidated properly by reactive vals + # Is invalidated properly by reactive values v = Value(1) @effect_async() @@ -915,15 +913,15 @@ async def _(): await reactcore.flush() assert n_times == 6 - # works with @reactive() + # works with @calc() v2 = Value(1) - @reactive_async() + @calc_async() async def r_a(): await asyncio.sleep(0) # Make sure the async function yields control return 1 - @reactive_async() + @calc_async() @event(lambda: v2(), r_a, ignore_init=True) async def r2b(): await asyncio.sleep(0) # Make sure the async function yields control From d1977123fe400f200ad714d8a9ca7fad6b70dba5 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 2 Feb 2022 20:38:46 -0600 Subject: [PATCH 20/23] Bump version to 0.0.0.9001 --- HISTORY.rst | 2 +- setup.cfg | 2 +- setup.py | 2 +- shiny/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index fb803ef3b..6e77dd902 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,4 +5,4 @@ History 0.0.0.9000 (2021-08-10) ------------------ -* First release on PyPI. +* Initial version diff --git a/setup.cfg b/setup.cfg index d57a3f120..d5ebf0475 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.0.0.9000 +current_version = 0.0.0.9001 commit = True tag = True diff --git a/setup.py b/setup.py index 8c433f976..8ffcd9804 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/rstudio/prism", - version="0.0.0.9000", + version="0.0.0.9001", zip_safe=False, ) diff --git a/shiny/__init__.py b/shiny/__init__.py index 4842faf20..ce5fb14a7 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -2,7 +2,7 @@ __author__ = """Winston Chang""" __email__ = "winston@rstudio.com" -__version__ = "0.0.0.9000" +__version__ = "0.0.0.9001" # All objects imported into this scope will be available as shiny.foo from .decorators import * From a3a2f496b7329150c5d16a08952ebdd405e58154 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 3 Feb 2022 09:39:51 -0600 Subject: [PATCH 21/23] Add return type for Session.download --- shiny/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/session.py b/shiny/session.py index 573afa922..03e82d9fa 100644 --- a/shiny/session.py +++ b/shiny/session.py @@ -506,7 +506,7 @@ def download( filename: Optional[Union[str, Callable[[], str]]] = None, media_type: Union[None, str, Callable[[], str]] = None, encoding: str = "utf-8", - ): + ) -> Callable[[_DownloadHandler], None]: def wrapper(fn: _DownloadHandler): if name is None: effective_name = fn.__name__ From 4ed645de5fe9be317d3c3daddc139dddf6d759ad Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 3 Feb 2022 11:07:03 -0600 Subject: [PATCH 22/23] Make async render functions correctly pass name --- shiny/render.py | 55 +++++++++++++++++++++++++++---------------------- shiny/utils.py | 21 ++++++++++++++----- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/shiny/render.py b/shiny/render.py index c3f1f7977..a327aaaae 100644 --- a/shiny/render.py +++ b/shiny/render.py @@ -62,19 +62,22 @@ async def __call__(self) -> object: # ====================================================================================== # RenderText # ====================================================================================== -RenderTextFunc = Callable[[], str] -RenderTextFuncAsync = Callable[[], Awaitable[str]] +RenderTextFunc = Callable[[], Union[str, None]] +RenderTextFuncAsync = Callable[[], Awaitable[Union[str, None]]] class RenderText(RenderFunction): def __init__(self, fn: RenderTextFunc) -> None: super().__init__(fn) + # The Render*Async subclass will pass in an async function, but it tells the + # static type checker that it's synchronous. wrap_async() is smart -- if is + # passed an async function, it will not change it. self._fn: RenderTextFuncAsync = utils.wrap_async(fn) - def __call__(self) -> str: + def __call__(self) -> Union[str, None]: return utils.run_coro_sync(self.run()) - async def run(self) -> str: + async def run(self) -> Union[str, None]: return await self._fn() @@ -82,12 +85,9 @@ class RenderTextAsync(RenderText, RenderFunctionAsync): def __init__(self, fn: RenderTextFuncAsync) -> None: if not inspect.iscoroutinefunction(fn): raise TypeError(self.__class__.__name__ + " requires an async function") - # Init the base class with a placeholder synchronous function so it won't throw - # an error, then replace it with the async function. - super().__init__(typing.cast(RenderTextFunc, lambda: None)) - self._fn: RenderTextFuncAsync = fn + super().__init__(typing.cast(RenderTextFunc, fn)) - async def __call__(self) -> str: # type: ignore + async def __call__(self) -> Union[str, None]: # type: ignore return await self.run() @@ -124,10 +124,13 @@ class ImgData(TypedDict): class RenderPlot(RenderFunction): _ppi: float = 96 - def __init__(self, fn: RenderPlotFunc, alt: Optional[str] = None) -> None: + def __init__(self, fn: RenderPlotFunc, *, alt: Optional[str] = None) -> None: super().__init__(fn) - self._fn: RenderPlotFuncAsync = utils.wrap_async(fn) self._alt: Optional[str] = alt + # The Render*Async subclass will pass in an async function, but it tells the + # static type checker that it's synchronous. wrap_async() is smart -- if is + # passed an async function, it will not change it. + self._fn: RenderPlotFuncAsync = utils.wrap_async(fn) def __call__(self) -> object: return utils.run_coro_sync(self.run()) @@ -178,11 +181,7 @@ class RenderPlotAsync(RenderPlot, RenderFunctionAsync): def __init__(self, fn: RenderPlotFuncAsync, alt: Optional[str] = None) -> None: if not inspect.iscoroutinefunction(fn): raise TypeError(self.__class__.__name__ + " requires an async function") - - # Init the Plot base class with a placeholder synchronous function so it - # won't throw an error, then replace it with the async function. - super().__init__(lambda: None, alt) - self._fn: RenderPlotFuncAsync = fn + super().__init__(typing.cast(RenderPlotFunc, fn), alt=alt) async def __call__(self) -> object: return await self.run() @@ -294,10 +293,18 @@ def try_render_plot_pil( class RenderImage(RenderFunction): - def __init__(self, fn: RenderImageFunc, delete_file: bool = False) -> None: + def __init__( + self, + fn: RenderImageFunc, + *, + delete_file: bool = False, + ) -> None: super().__init__(fn) - self._fn: RenderImageFuncAsync = utils.wrap_async(fn) self._delete_file: bool = delete_file + # The Render*Async subclass will pass in an async function, but it tells the + # static type checker that it's synchronous. wrap_async() is smart -- if is + # passed an async function, it will not change it. + self._fn: RenderImageFuncAsync = utils.wrap_async(fn) def __call__(self) -> object: return utils.run_coro_sync(self.run()) @@ -321,10 +328,7 @@ class RenderImageAsync(RenderImage, RenderFunctionAsync): def __init__(self, fn: RenderImageFuncAsync, delete_file: bool = False) -> None: if not inspect.iscoroutinefunction(fn): raise TypeError(self.__class__.__name__ + " requires an async function") - # Init the Image base class with a placeholder synchronous function so it - # won't throw an error, then replace it with the async function. - super().__init__(typing.cast(RenderImageFunc, lambda: None), delete_file) - self._fn: RenderImageFuncAsync = fn + super().__init__(typing.cast(RenderImageFunc, fn), delete_file=delete_file) async def __call__(self) -> object: return await self.run() @@ -354,6 +358,9 @@ def wrapper(fn: Union[RenderImageFunc, RenderImageFuncAsync]) -> RenderImage: class RenderUI(RenderFunction): def __init__(self, fn: RenderUIFunc) -> None: super().__init__(fn) + # The Render*Async subclass will pass in an async function, but it tells the + # static type checker that it's synchronous. wrap_async() is smart -- if is + # passed an async function, it will not change it. self._fn: RenderUIFuncAsync = utils.wrap_async(fn) def __call__(self) -> object: @@ -373,9 +380,7 @@ class RenderUIAsync(RenderUI, RenderFunctionAsync): def __init__(self, fn: RenderUIFuncAsync) -> None: if not inspect.iscoroutinefunction(fn): raise TypeError(self.__class__.__name__ + " requires an async function") - - super().__init__(lambda: None) - self._fn: RenderUIFuncAsync = fn + super().__init__(typing.cast(RenderUIFunc, fn)) async def __call__(self) -> object: return await self.run() diff --git a/shiny/utils.py b/shiny/utils.py index 83f7f3062..a1e1ebe4a 100644 --- a/shiny/utils.py +++ b/shiny/utils.py @@ -1,11 +1,14 @@ from typing import ( Callable, Awaitable, + Union, Tuple, TypeVar, Dict, Any, + cast, ) +import functools import os import tempfile import importlib @@ -31,12 +34,20 @@ def rand_hex(bytes: int) -> str: T = TypeVar("T") -def wrap_async(fn: Callable[[], T]) -> Callable[[], Awaitable[T]]: +def wrap_async( + fn: Union[Callable[[], T], Callable[[], Awaitable[T]]] +) -> Callable[[], Awaitable[T]]: """ - Wrap a synchronous function that returns T, and return an async function - that wraps the original function. + Given a synchronous function that returns T, return an async function that wraps the + original function. If the input function is already async, then return it unchanged. """ + if is_async_callable(fn): + return cast(Callable[[], Awaitable[T]], fn) + + fn = cast(Callable[[], T], fn) + + @functools.wraps(fn) async def fn_async() -> T: return fn() @@ -45,8 +56,8 @@ async def fn_async() -> T: def is_async_callable(obj: object) -> bool: """ - Returns True if `obj` is an `async def` function, or if it's an object with - a `__call__` method which is an `async def` function. + Returns True if `obj` is an `async def` function, or if it's an object with a + `__call__` method which is an `async def` function. """ if inspect.iscoroutinefunction(obj): return True From 7202b22e70434edb6a556e2c3c0f8229d02b74c5 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 3 Feb 2022 11:08:38 -0600 Subject: [PATCH 23/23] Update examples --- examples/bind_event/app.py | 79 +++++++++++++----------- examples/download/app.py | 50 ++++++++------- examples/dynamic_ui/app.py | 58 ++++++++--------- examples/inputs-update/app.py | 105 ++++++++++++++++--------------- examples/inputs/app.py | 113 +++++++++++++++++----------------- examples/myapp/app.py | 7 +-- examples/req/app.py | 52 ++++++++-------- examples/simple/app.py | 53 +++++----------- 8 files changed, 249 insertions(+), 268 deletions(-) diff --git a/examples/bind_event/app.py b/examples/bind_event/app.py index 3c1d197fc..82fa0c42a 100644 --- a/examples/bind_event/app.py +++ b/examples/bind_event/app.py @@ -1,6 +1,9 @@ +import asyncio +import shiny.ui_toolkit as st from shiny import * +from htmltools import tags -ui = page_fluid( +ui = st.page_fluid( tags.p( """ The first time you click the button, you should see a 1 appear below the button, @@ -9,68 +12,72 @@ print the number of clicks in the console twice. """ ), - navs_tab_card( - nav( + st.navs_tab_card( + st.nav( "Sync", - input_action_button("btn", "Click me"), - output_ui("foo"), + st.input_action_button("btn", "Click me"), + st.output_ui("btn_value"), ), - nav( + st.nav( "Async", - input_action_button("btn_async", "Click me"), - output_ui("foo_async"), + st.input_action_button("btn_async", "Click me"), + st.output_ui("btn_async_value"), ), ), ) -def server(session: ShinySession): +def server(input: Inputs, output: Outputs, session: Session): # i.e., observeEvent(once=False) - @observe() - @event(lambda: session.input["btn"]) + @reactive.effect() + @event(input.btn) def _(): - print("@observe() event: ", str(session.input["btn"])) + print("@effect() event: ", str(input.btn())) # i.e., eventReactive() - @reactive() - @event(lambda: session.input["btn"]) + @reactive.calc() + @event(input.btn) def btn() -> int: - return session.input["btn"] + return input.btn() - @observe() + @reactive.effect() def _(): - print("@reactive() event: ", str(btn())) + print("@calc() event: ", str(btn())) - @session.output("foo") + @output() @render_ui() - @event(lambda: session.input["btn"]) - def _(): - return session.input["btn"] + @event(input.btn) + def btn_value(): + return str(input.btn()) # ----------------------------------------------------------------------------- # Async # ----------------------------------------------------------------------------- - @observe_async() - @event(lambda: session.input["btn_async"]) + @reactive.effect_async() + @event(input.btn_async) async def _(): - print("@observe_async() event: ", str(session.input["btn_async"])) + await asyncio.sleep(0) + print("@effect_async() event: ", str(input.btn_async())) - @reactive_async() - @event(lambda: session.input["btn_async"]) - async def btn_async() -> int: - return session.input["btn_async"] + @reactive.calc_async() + @event(input.btn_async) + async def btn_async_r() -> int: + await asyncio.sleep(0) + return input.btn_async() - @observe_async() + @reactive.effect_async() async def _(): - val = await btn_async() - print("@reactive_async() event: ", str(val)) + val = await btn_async_r() + print("@calc_async() event: ", str(val)) - @session.output("foo_async") + @output() @render_ui() - @event(lambda: session.input["btn_async"]) - async def _(): - return session.input["btn_async"] + @event(btn_async_r) + async def btn_async_value(): + val = await btn_async_r() + print("== " + str(val)) + return str(val) -ShinyApp(ui, server).run() +app = App(ui, server, debug=True) diff --git a/examples/download/app.py b/examples/download/app.py index 5698ebd4d..744157672 100644 --- a/examples/download/app.py +++ b/examples/download/app.py @@ -4,28 +4,32 @@ # Then point web browser to: # http://localhost:8000/ import asyncio -from datetime import date import os import io -import matplotlib.pyplot as plt -import numpy as np +from datetime import date +from typing import Any +import shiny.ui_toolkit as st from shiny import * +from htmltools import * + +import matplotlib.pyplot as plt +import numpy as np def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): - return column( - 3, - tags.div( + return st.column( + 4, + div( class_="card mb-4", children=[ - tags.div(title, class_="card-header"), - tags.div( + div(title, class_="card-header"), + div( class_="card-body", children=[ - tags.p(desc, class_="card-text text-muted"), + p(desc, class_="card-text text-muted"), extra, - download_button(id, label, class_="btn-primary"), + st.download_button(id, label, class_="btn-primary"), ], ), ], @@ -33,9 +37,8 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): ) -ui = page_fluid( - tags.h1("Download examples"), - row( +ui = st.page_fluid( + st.row( make_example( "download1", label="Download CSV", @@ -43,19 +46,19 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): desc="Downloads a pre-existing file, using its existing name on disk.", ), ), - row( + st.row( make_example( "download2", label="Download plot", title="Dynamic data generation", desc="Downloads a PNG that's generated on the fly.", extra=[ - input_text("title", "Plot title", "Random scatter plot"), - input_slider("num_points", "Number of data points", 1, 100, 50), + st.input_text("title", "Plot title", "Random scatter plot"), + st.input_slider("num_points", "Number of data points", 1, 100, 50), ], ), ), - row( + st.row( make_example( "download3", "Download", @@ -63,7 +66,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): "Demonstrates that filenames can be generated on the fly (and use Unicode characters!).", ), ), - row( + st.row( make_example( "download4", "Download", @@ -71,7 +74,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): "Throws an error in the download handler, download should not succeed.", ), ), - row( + st.row( make_example( "download5", "Download", @@ -82,7 +85,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): ) -def server(session: Session): +def server(input: Inputs, output: Outputs, session: Session): @session.download() def download1(): """ @@ -107,7 +110,7 @@ def download2(): y = np.random.uniform(size=session.input["num_points"]) plt.figure() plt.scatter(x, y) - plt.title(session.input["title"]) + plt.title(input.title()) with io.BytesIO() as buf: plt.savefig(buf, format="png") yield buf.getvalue() @@ -128,8 +131,3 @@ async def _(): app = App(ui, server) - -if __name__ == "__main__": - app.run() - # Alternately, to listen on a TCP port: - # app.run(conn_type = "tcp") diff --git a/examples/dynamic_ui/app.py b/examples/dynamic_ui/app.py index a639cba84..bc7db2171 100644 --- a/examples/dynamic_ui/app.py +++ b/examples/dynamic_ui/app.py @@ -4,57 +4,62 @@ # Then point web browser to: # http://localhost:8000/ +import shiny.ui_toolkit as st from shiny import * +from htmltools import * # For plot rendering import numpy as np import matplotlib.pyplot as plt -ui = page_fluid( - layout_sidebar( - panel_sidebar( - h2("Dynamic UI"), - output_ui("ui"), - input_action_button("btn", "Trigger insert/remove ui"), +ui = st.page_fluid( + st.layout_sidebar( + st.panel_sidebar( + st.h2("Dynamic UI"), + st.output_ui("ui"), + st.input_action_button("btn", "Trigger insert/remove ui"), ), - panel_main( - output_text_verbatim("txt"), - output_plot("plot"), + st.panel_main( + st.output_text_verbatim("txt"), + st.output_plot("plot"), ), ), ) -def server(session: Session): - @reactive() +def server(input: Inputs, output: Outputs, session: Session): + @reactive.calc() def r(): - if session.input["n"] is None: + if input.n() is None: return - return session.input["n"] * 2 + return input.n() * 2 - @session.output("txt") - async def _(): + @output() + @render_text() + async def txt(): val = r() - return f"n*2 is {val}, session id is {get_current_session().id}" + return f"n*2 is {val}, session id is {session.id}" - @session.output("plot") + @output() @render_plot(alt="A histogram") - def _(): + def plot(): np.random.seed(19680801) x = 100 + 15 * np.random.randn(437) fig, ax = plt.subplots() - ax.hist(x, session.input["n"], density=True) + ax.hist(x, input.n(), density=True) return fig - @session.output("ui") + @output() @render_ui() - def _(): - return input_slider("This slider is rendered via @render_ui()", "N", 0, 100, 20) + def ui(): + return st.input_slider( + "This slider is rendered via @render_ui()", "N", 0, 100, 20 + ) - @observe() + @reactive.effect() def _(): - btn = session.input["btn"] + btn = input.btn() if btn % 2 == 1: ui_insert(tags.p("Thanks for clicking!", id="thanks"), "body") elif btn > 0: @@ -62,8 +67,3 @@ def _(): app = App(ui, server) - -if __name__ == "__main__": - app.run() - # Alternately, to listen on a TCP port: - # app.run(conn_type = "tcp") diff --git a/examples/inputs-update/app.py b/examples/inputs-update/app.py index b92b5b7f0..63087f6fd 100644 --- a/examples/inputs-update/app.py +++ b/examples/inputs-update/app.py @@ -4,50 +4,54 @@ # Then point web browser to: # http://localhost:8000/ -from shiny import * +from datetime import date -ui = page_fluid( - panel_title("Changing the values of inputs from the server"), - row( - column( - 3, - panel_well( +import shiny.ui_toolkit as st +from shiny import * +from htmltools import * + +ui = st.page_fluid( + st.panel_title("Changing the values of inputs from the server"), + st.row( + st.column( + 4, + st.panel_well( tags.h4("These inputs control the other inputs on the page"), - input_text( + st.input_text( "control_label", "This controls some of the labels:", "LABEL TEXT" ), - input_slider( + st.input_slider( "control_num", "This controls values:", min=1, max=20, value=15 ), ), ), - column( - 3, - panel_well( + st.column( + 4, + st.panel_well( tags.h4("These inputs are controlled by the other inputs"), - input_text("inText", "Text input:", value="start text"), - input_numeric( + st.input_text("inText", "Text input:", value="start text"), + st.input_numeric( "inNumber", "Number input:", min=1, max=20, value=5, step=0.5 ), - input_numeric( + st.input_numeric( "inNumber2", "Number input 2:", min=1, max=20, value=5, step=0.5 ), - input_slider("inSlider", "Slider input:", min=1, max=20, value=15), - input_slider( + st.input_slider("inSlider", "Slider input:", min=1, max=20, value=15), + st.input_slider( "inSlider2", "Slider input 2:", min=1, max=20, value=(5, 15) ), - input_slider( + st.input_slider( "inSlider3", "Slider input 3:", min=1, max=20, value=(5, 15) ), - input_date("inDate", "Date input:"), - input_date_range("inDateRange", "Date range input:"), + st.input_date("inDate", "Date input:"), + st.input_date_range("inDateRange", "Date range input:"), ), ), - column( - 3, - panel_well( - input_checkbox("inCheckbox", "Checkbox input", value=False), - input_checkbox_group( + st.column( + 4, + st.panel_well( + st.input_checkbox("inCheckbox", "Checkbox input", value=False), + st.input_checkbox_group( "inCheckboxGroup", "Checkbox group input:", { @@ -55,7 +59,7 @@ "option2": "label 2", }, ), - input_radio_buttons( + st.input_radio_buttons( "inRadio", "Radio buttons:", { @@ -63,7 +67,7 @@ "option2": "label 2", }, ), - input_select( + st.input_select( "inSelect", "Select input:", { @@ -71,7 +75,7 @@ "option2": "label 2", }, ), - input_select( + st.input_select( "inSelect2", "Select input 2:", { @@ -81,9 +85,9 @@ multiple=True, ), ), - navs_tab( - nav("panel1", h2("This is the first panel.")), - nav("panel2", h2("This is the second panel.")), + st.navs_tab( + st.nav("panel1", h2("This is the first panel.")), + st.nav("panel2", h2("This is the second panel.")), id="inTabset", ), ), @@ -91,17 +95,17 @@ ) -def server(sess: Session): - @observe() +def server(input: Inputs, output: Outputs, session: Session): + @reactive.effect() def _(): # We'll use these multiple times, so use short var names for # convenience. - c_label = sess.input["control_label"] - c_num = sess.input["control_num"] + c_label = input.control_label() + c_num = input.control_num() # Text ===================================================== # Change both the label and the text - update_text( + st.update_text( "inText", label="New " + c_label, value="New text " + str(c_num), @@ -109,10 +113,10 @@ def _(): # Number =================================================== # Change the value - update_numeric("inNumber", value=c_num) + st.update_numeric("inNumber", value=c_num) # Change the label, value, min, and max - update_numeric( + st.update_numeric( "inNumber2", label="Number " + c_label, value=c_num, @@ -123,23 +127,23 @@ def _(): # Slider input ============================================= # Only label and value can be set for slider - update_slider("inSlider", label="Slider " + c_label, value=c_num) + st.update_slider("inSlider", label="Slider " + c_label, value=c_num) # Slider range input ======================================= # For sliders that pick out a range, pass in a vector of 2 # values. - update_slider("inSlider2", value=(c_num - 1, c_num + 1)) + st.update_slider("inSlider2", value=(c_num - 1, c_num + 1)) # Only change the upper handle - update_slider("inSlider3", value=(sess.input["inSlider3"][0], c_num + 2)) + st.update_slider("inSlider3", value=(input.inSlider3()[0], c_num + 2)) # Date input =============================================== # Only label and value can be set for date input - update_date("inDate", label="Date " + c_label, value=date(2013, 4, c_num)) + st.update_date("inDate", label="Date " + c_label, value=date(2013, 4, c_num)) # Date range input ========================================= # Only label and value can be set for date range input - update_date_range( + st.update_date_range( "inDateRange", label="Date range " + c_label, start=date(2013, 1, c_num), @@ -149,7 +153,7 @@ def _(): ) # # Checkbox =============================================== - update_checkbox("inCheckbox", value=c_num % 2) + st.update_checkbox("inCheckbox", value=c_num % 2) # Checkbox group =========================================== # Create a list of new options, where the name of the items @@ -160,7 +164,7 @@ def _(): opts_dict = dict(zip(opt_vals, opt_labels)) # Set the label, choices, and selected item - update_checkbox_group( + st.update_checkbox_group( "inCheckboxGroup", label="Checkbox group " + c_label, choices=opts_dict, @@ -168,7 +172,7 @@ def _(): ) # Radio group ============================================== - update_radio_buttons( + st.update_radio_buttons( "inRadio", label="Radio " + c_label, choices=opts_dict, @@ -178,7 +182,7 @@ def _(): # Create a list of new options, where the name of the items # is something like 'option label x A', and the values are # 'option-x-A'. - update_select( + st.update_select( "inSelect", label="Select " + c_label, choices=opts_dict, @@ -187,7 +191,7 @@ def _(): # Can also set the label and select an item (or more than # one if it's a multi-select) - update_select( + st.update_select( "inSelect2", label="Select label " + c_label, choices=opts_dict, @@ -197,10 +201,11 @@ def _(): # Tabset input ============================================= # Change the selected tab. # The tabsetPanel must have been created with an 'id' argument - update_navs("inTabset", selected="panel2" if c_num % 2 else "panel1") + st.update_navs("inTabset", selected="panel2" if c_num % 2 else "panel1") + +app = App(ui, server, debug=True) -app = App(ui, server) if __name__ == "__main__": app.run() diff --git a/examples/inputs/app.py b/examples/inputs/app.py index c07d35979..9f1308824 100644 --- a/examples/inputs/app.py +++ b/examples/inputs/app.py @@ -7,37 +7,38 @@ sys.path.insert(0, shiny_module_dir) +import shiny.ui_toolkit as st from shiny import * -from htmltools import tags, HTML +from htmltools import tags, HTML, Tag from fontawesome import icon_svg -ui = page_fluid( - panel_title("Hello prism ui"), - layout_sidebar( - panel_sidebar( - input_slider( +ui = st.page_fluid( + st.panel_title("Hello prism ui"), + st.layout_sidebar( + st.panel_sidebar( + st.input_slider( "n", "input_slider()", min=10, max=100, value=50, step=5, animate=True ), - input_date("date", "input_date()"), - input_date_range("date_rng", "input_date_range()"), - input_text("txt", "input_text()", placeholder="Input some text"), - input_text_area( + st.input_date("date", "input_date()"), + st.input_date_range("date_rng", "input_date_range()"), + st.input_text("txt", "input_text()", placeholder="Input some text"), + st.input_text_area( "txt_area", "input_text_area()", placeholder="Input some text" ), - input_numeric("num", "input_numeric()", 20), - input_password("password", "input_password()"), - input_checkbox("checkbox", "input_checkbox()"), - input_checkbox_group( + st.input_numeric("num", "input_numeric()", 20), + st.input_password("password", "input_password()"), + st.input_checkbox("checkbox", "input_checkbox()"), + st.input_checkbox_group( "checkbox_group", "input_checkbox_group()", {"a": "Choice 1", "b": "Choice 2"}, selected=["a", "b"], inline=True, ), - input_radio_buttons( + st.input_radio_buttons( "radio", "input_radio()", {"a": "Choice 1", "b": "Choice 2"} ), - input_select( + st.input_select( "select", "input_select()", { @@ -46,33 +47,35 @@ "Group C": {"c1": "c1", "c2": "c2"}, }, ), - input_action_button( + st.input_action_button( "button", "input_action_button()", icon=icon_svg("check") ), - input_file("file", "File upload"), + st.input_file("file", "File upload"), ), - panel_main( - output_plot("plot"), - navs_tab_card( + st.panel_main( + st.output_plot("plot"), + st.navs_tab_card( # TODO: output_plot() within a tab not working? - nav("Inputs", output_ui("inputs"), icon=icon_svg("code")), - nav( - "Image", output_image("image", inline=True), icon=icon_svg("image") + st.nav("Inputs", st.output_ui("inputs"), icon=icon_svg("code")), + st.nav( + "Image", + st.output_image("image", inline=True), + icon=icon_svg("image"), ), - nav( + st.nav( "Misc", - input_action_link( + st.input_action_link( "link", "Show notification/progress", icon=icon_svg("info") ), tags.br(), - input_action_button( + st.input_action_button( "btn", "Show modal", icon=icon_svg("info-circle") ), - panel_fixed( - panel_well( + st.panel_fixed( + st.panel_well( "A fixed, draggable, panel", - input_checkbox("checkbox2", "Check me!"), - panel_conditional( + st.input_checkbox("checkbox2", "Check me!"), + st.panel_conditional( "input.checkbox2 == true", "Thanks for checking!" ), ), @@ -94,52 +97,52 @@ import matplotlib.pyplot as plt -def server(s: Session): - @s.output("inputs") +def server(input: Inputs, output: Outputs, session: Session): + @output() @render_ui() - def _() -> Tag: + def inputs() -> Tag: vals = [ - f"input_date() {s.input['date']}", - f"input_date_range(): {s.input['date_rng']}", - f"input_text(): {s.input['txt']}", - f"input_text_area(): {s.input['txt_area']}", - f"input_numeric(): {s.input['num']}", - f"input_password(): {s.input['password']}", - f"input_checkbox(): {s.input['checkbox']}", - f"input_checkbox_group(): {s.input['checkbox_group']}", - f"input_radio(): {s.input['radio']}", - f"input_select(): {s.input['select']}", - f"input_action_button(): {s.input['button']}", + f"input_date() {input.date()}", + f"input_date_range(): {input.date_rng()}", + f"input_text(): {input.txt()}", + f"input_text_area(): {input.txt_area()}", + f"input_numeric(): {input.num()}", + f"input_password(): {input.password()}", + f"input_checkbox(): {input.checkbox()}", + f"input_checkbox_group(): {input.checkbox_group()}", + f"input_radio(): {input.radio()}", + f"input_select(): {input.select()}", + f"input_action_button(): {input.button()}", ] return tags.pre(HTML("\n".join(vals))) np.random.seed(19680801) x_rand = 100 + 15 * np.random.randn(437) - @s.output("plot") + @output() @render_plot(alt="A histogram") - def _(): + def plot(): fig, ax = plt.subplots() - ax.hist(x_rand, int(s.input["n"]), density=True) + ax.hist(x_rand, int(input.n()), density=True) return fig - @s.output("image") + @output() @render_image() - def _(): + def image(): from pathlib import Path dir = Path(__file__).resolve().parent return {"src": dir / "rstudio-logo.png", "width": "150px"} - @observe() + @reactive.effect() def _(): - btn = s.input["btn"] + btn = input.btn() if btn and btn > 0: - modal_show(modal("Hello there!", easy_close=True)) + st.modal_show(st.modal("Hello there!", easy_close=True)) - @observe() + @reactive.effect() def _(): - link = s.input["link"] + link = input.link() if link and link > 0: notification_show("A notification!") p = Progress() diff --git a/examples/myapp/app.py b/examples/myapp/app.py index 579338c05..bd794d244 100644 --- a/examples/myapp/app.py +++ b/examples/myapp/app.py @@ -52,7 +52,7 @@ async def txt(): def _(): if input.n() is None: return - shared_val.set(input["n"]() * 10) + shared_val.set(input.n() * 10) # Print the value of shared_val(). Changing it in one session should cause # this to run in all sessions. @@ -88,8 +88,3 @@ def file_content(): app = shiny.App(ui, server) - -if __name__ == "__main__": - app.run() - # Alternately, to listen on a TCP port: - # app.run(conn_type = "tcp") diff --git a/examples/req/app.py b/examples/req/app.py index 867414369..624753a1a 100644 --- a/examples/req/app.py +++ b/examples/req/app.py @@ -1,50 +1,48 @@ +import shiny.ui_toolkit as st from shiny import * -ui = page_fluid( - input_action_button("safe", "Throw a safe error"), - output_ui("safe"), - input_action_button("unsafe", "Throw an unsafe error"), - output_ui("unsafe"), - input_text( +ui = st.page_fluid( + st.input_action_button("safe", "Throw a safe error"), + st.output_ui("safe"), + st.input_action_button("unsafe", "Throw an unsafe error"), + st.output_ui("unsafe"), + st.input_text( "txt", "Enter some text below, then remove it. Notice how the text is never fully removed.", ), - output_ui("txt_out"), + st.output_ui("txt_out"), ) -def server(session: Session): - @reactive() +def server(input: Inputs, output: Outputs, session: Session): + @reactive.calc() def safe_click(): - req(session.input["safe"]) - return session.input["safe"] + req(input.safe()) + return input.safe() - @session.output("safe") + @output() @render_ui() - def _(): + def safe(): raise SafeException(f"You've clicked {str(safe_click())} times") - @session.output("unsafe") + @output() @render_ui() - def _(): - req(session.input["unsafe"]) - raise Exception( - f"Super secret number of clicks: {str(session.input['unsafe'])}" - ) + def unsafe(): + req(input.unsafe()) + raise Exception(f"Super secret number of clicks: {str(input.unsafe())}") - @observe() + @reactive.effect() def _(): - req(session.input["unsafe"]) - print("unsafe clicks:", session.input["unsafe"]) + req(input.unsafe()) + print("unsafe clicks:", input.unsafe()) # raise Exception("Observer exception: this should cause a crash") - @session.output("txt_out") + @output() @render_ui() - def _(): - req(session.input["txt"], cancel_output=True) - return session.input["txt"] + def txt_out(): + req(input.txt(), cancel_output=True) + return input.txt() app = App(ui, server) app.SANITIZE_ERRORS = True -app.run() diff --git a/examples/simple/app.py b/examples/simple/app.py index e65c020cd..4704a9d25 100644 --- a/examples/simple/app.py +++ b/examples/simple/app.py @@ -4,55 +4,30 @@ # Then point web browser to: # http://localhost:8000/ -# Add parent directory to path, so we can find the prism module. -# (This is just a temporary fix) -import os -import sys - -# This will load the shiny module dynamically, without having to install it. -# This makes the debug/run cycle quicker. -shiny_module_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) -sys.path.insert(0, shiny_module_dir) - +import shiny.ui_toolkit as st from shiny import * -ui = page_fluid( - layout_sidebar( - panel_sidebar( - input_slider("n", "N", 0, 100, 20), - ), - panel_main( - output_text_verbatim("txt", placeholder=True), - output_plot("plot"), - ), - ), +ui = st.page_fluid( + st.input_slider("n", "N", 0, 100, 20), + st.output_text_verbatim("txt", placeholder=True), ) -# from htmltools.core import HTMLDocument -# from shiny import html_dependencies -# HTMLDocument(TagList(ui, html_dependencies.shiny_deps())).save_html("temp/app.html") - +# A reactive.Value which is exists outside of the session. +shared_val = reactive.Value(None) -# A ReactiveVal which is exists outside of the session. -shared_val = ReactiveVal(None) - -def server(session: Session): - @reactive() +def server(input: Inputs, output: Outputs, session: Session): + @reactive.calc() def r(): - if session.input["n"] is None: + if input.n() is None: return - return session.input["n"] * 2 + return input.n() * 2 - @session.output("txt") - async def _(): + @output() + @render_text() + async def txt(): val = r() - return f"n*2 is {val}, session id is {get_current_session().id}" + return f"n*2 is {val}, session id is {session.id}" app = App(ui, server) - -if __name__ == "__main__": - app.run() - # Alternately, to listen on a TCP port: - # app.run(conn_type = "tcp")