Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions shiny/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from ._utils import guess_mime_type, is_async_callable, sort_keys_length
from .html_dependencies import jquery_deps, require_deps, shiny_deps
from .http_staticfiles import FileResponse, StaticFiles
from .session._session import Inputs, Outputs, Session, session_context
from .session._session import AppSession, Inputs, Outputs, Session, session_context

T = TypeVar("T")

Expand Down Expand Up @@ -165,9 +165,9 @@ def __init__(
static_assets_map = sort_keys_length(static_assets_map, descending=True)
self._static_assets: dict[str, Path] = static_assets_map

self._sessions: dict[str, Session] = {}
self._sessions: dict[str, AppSession] = {}

self._sessions_needing_flush: dict[int, Session] = {}
self._sessions_needing_flush: dict[int, AppSession] = {}

self._registered_dependencies: dict[str, HTMLDependency] = {}
self._dependency_handler = starlette.routing.Router()
Expand Down Expand Up @@ -243,14 +243,14 @@ async def _lifespan(self, app: starlette.applications.Starlette):
async with self._exit_stack:
yield

def _create_session(self, conn: Connection) -> Session:
def _create_session(self, conn: Connection) -> AppSession:
id = secrets.token_hex(32)
session = Session(self, id, conn, debug=self._debug)
session = AppSession(self, id, conn, debug=self._debug)
self._sessions[id] = session
return session

def _remove_session(self, session: Session | str) -> None:
if isinstance(session, Session):
def _remove_session(self, session: AppSession | str) -> None:
if isinstance(session, AppSession):
session = session.id

if self._debug:
Expand Down Expand Up @@ -379,7 +379,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: Session = self._sessions[session_id]
session: AppSession = self._sessions[session_id]
with session_context(session):
return await session._handle_request(request, action, subpath)

Expand All @@ -388,7 +388,7 @@ async def _on_session_request_cb(self, request: Request) -> ASGIApp:
# ==========================================================================
# Flush
# ==========================================================================
def _request_flush(self, session: Session) -> None:
def _request_flush(self, session: AppSession) -> None:
# TODO: Until we have reactive domains, because we can't yet keep track
# of which sessions need a flush.
pass
Expand Down
5 changes: 3 additions & 2 deletions shiny/_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"Concatenate",
"ParamSpec",
"TypeGuard",
"Never",
"NotRequired",
"Self",
"TypedDict",
Expand All @@ -30,9 +31,9 @@
# they should both come from the same typing module.
# https://peps.python.org/pep-0655/#usage-in-python-3-11
if sys.version_info >= (3, 11):
from typing import NotRequired, Self, TypedDict, assert_type
from typing import Never, NotRequired, Self, TypedDict, assert_type
else:
from typing_extensions import NotRequired, Self, TypedDict, assert_type
from typing_extensions import Never, NotRequired, Self, TypedDict, assert_type


# The only purpose of the following line is so that pyright will put all of the
Expand Down
55 changes: 0 additions & 55 deletions shiny/express/_mock_session.py

This file was deleted.

18 changes: 9 additions & 9 deletions shiny/express/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from ..session import Inputs, Outputs, Session, get_current_session, session_context
from ..types import MISSING, MISSING_TYPE
from ._is_express import find_magic_comment_mode
from ._mock_session import ExpressMockSession
from ._recall_context import RecallContextManager
from ._stub_session import ExpressStubSession
from .expressify_decorator._func_displayhook import _expressify_decorator_function_def
from .expressify_decorator._node_transformers import (
DisplayFuncsTransformer,
Expand Down Expand Up @@ -49,8 +49,8 @@ def wrap_express_app(file: Path) -> App:
with session_context(None):
import_module_from_path("globals", globals_file)

mock_session = ExpressMockSession()
with session_context(cast(Session, mock_session)):
stub_session = ExpressStubSession()
with session_context(stub_session):
# We tagify here, instead of waiting for the App object to do it when it wraps
# the UI in a HTMLDocument and calls render() on it. This is because
# AttributeErrors can be thrown during the tagification process, and we need to
Expand Down Expand Up @@ -79,7 +79,7 @@ def express_server(input: Inputs, output: Outputs, session: Session):
if www_dir.is_dir():
app_opts["static_assets"] = {"/": www_dir}

app_opts = _merge_app_opts(app_opts, mock_session.app_opts)
app_opts = _merge_app_opts(app_opts, stub_session.app_opts)
app_opts = _normalize_app_opts(app_opts, file.parent)

app = App(
Expand Down Expand Up @@ -231,17 +231,17 @@ def app_opts(
Whether to enable debug mode.
"""

mock_session = get_current_session()
stub_session = get_current_session()

if mock_session is None:
if stub_session is None:
# We can get here if a Shiny Core app, or if we're in the UI rendering phase of
# a Quarto-Shiny dashboard.
raise RuntimeError(
"express.app_opts() can only be used in a standalone Shiny Express app."
)

# Store these options only if we're in the UI-rendering phase of Shiny Express.
if not isinstance(mock_session, ExpressMockSession):
if not isinstance(stub_session, ExpressStubSession):
return

if not isinstance(static_assets, MISSING_TYPE):
Expand All @@ -251,10 +251,10 @@ def app_opts(
# Convert string values to Paths. (Need new var name to help type checker.)
static_assets_paths = {k: Path(v) for k, v in static_assets.items()}

mock_session.app_opts["static_assets"] = static_assets_paths
stub_session.app_opts["static_assets"] = static_assets_paths

if not isinstance(debug, MISSING_TYPE):
mock_session.app_opts["debug"] = debug
stub_session.app_opts["debug"] = debug


def _merge_app_opts(app_opts: AppOpts, app_opts_new: AppOpts) -> AppOpts:
Expand Down
160 changes: 160 additions & 0 deletions shiny/express/_stub_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from __future__ import annotations

import textwrap
from typing import TYPE_CHECKING, Awaitable, Callable, Literal, Optional

from htmltools import TagChild

from .._namespaces import Id, ResolvedId, Root
from ..session import Inputs, Outputs, Session
from ..session._session import SessionProxy

if TYPE_CHECKING:
from .._app import App
from .._typing_extensions import Never
from ..session._session import DownloadHandler, DynamicRouteHandler, RenderedDeps
from ..types import Jsonifiable
from ._run import AppOpts

all = ("ExpressStubSession",)


class ExpressStubSession(Session):
"""
A very bare-bones stub session class that is used only in shiny.express's UI
rendering phase.

Note that this class is also used to hold application-level options that are set via
the `app_opts()` function.
"""

def __init__(self, ns: ResolvedId = Root):
self.ns = ns
self.input = Inputs({})
self.output = Outputs(self, self.ns, outputs={})

# Set these values to None just to satisfy the abstract base class to make this
# code run -- these things should not be used at run time, so None will work as
# a placeholder. But we also need to tell pyright to ignore that the Nones don't
# match the type declared in the Session abstract base class.
self._outbound_message_queues = None # pyright: ignore
self._downloads = None # pyright: ignore

# Application-level (not session-level) options that may be set via app_opts().
self.app_opts: AppOpts = {}

def is_stub_session(self) -> Literal[True]:
return True

@property
def id(self) -> str:
self._not_implemented("id")

@id.setter
def id(self, value: str) -> None: # pyright: ignore
self._not_implemented("id")

@property
def app(self) -> App:
self._not_implemented("app")

@app.setter
def app(self, value: App) -> None: # pyright: ignore
self._not_implemented("app")

async def close(self, code: int = 1001) -> None:
return

# This is needed so that Outputs don't throw an error.
def _is_hidden(self, name: str) -> bool:
return False

def on_ended(
self,
fn: Callable[[], None] | Callable[[], Awaitable[None]],
) -> Callable[[], None]:
return lambda: None

def make_scope(self, id: Id) -> Session:
ns = self.ns(id)
return SessionProxy(parent=self, ns=ns)

def root_scope(self) -> ExpressStubSession:
return self

def _process_ui(self, ui: TagChild) -> RenderedDeps:
return {"deps": [], "html": ""}

def send_input_message(self, id: str, message: dict[str, object]) -> None:
return

def _send_insert_ui(
self, selector: str, multiple: bool, where: str, content: RenderedDeps
) -> None:
return

def _send_remove_ui(self, selector: str, multiple: bool) -> None:
return

def _send_progress(self, type: str, message: object) -> None:
return

async def send_custom_message(self, type: str, message: dict[str, object]) -> None:
return

def set_message_handler(
self,
name: str,
handler: (
Callable[..., Jsonifiable] | Callable[..., Awaitable[Jsonifiable]] | None
),
*,
_handler_session: Optional[Session] = None,
) -> str:
return ""

async def _send_message(self, message: dict[str, object]) -> None:
return

def _send_message_sync(self, message: dict[str, object]) -> None:
return

def on_flush(
self,
fn: Callable[[], None] | Callable[[], Awaitable[None]],
once: bool = True,
) -> Callable[[], None]:
return lambda: None

def on_flushed(
self,
fn: Callable[[], None] | Callable[[], Awaitable[None]],
once: bool = True,
) -> Callable[[], None]:
return lambda: None

def dynamic_route(self, name: str, handler: DynamicRouteHandler) -> str:
return ""

async def _unhandled_error(self, e: Exception) -> None:
return

def download(
self,
id: Optional[str] = None,
filename: Optional[str | Callable[[], str]] = None,
media_type: None | str | Callable[[], str] = None,
encoding: str = "utf-8",
) -> Callable[[DownloadHandler], None]:
return lambda x: None

def _not_implemented(self, name: str) -> Never:
raise NotImplementedError(
textwrap.dedent(
f"""
The session attribute `{name}` is not yet available for use. Since this code
will run again when the session is initialized, you can use `if not session.is_stub_session():`
to only run this code when the session is established.
"""
)
)
10 changes: 5 additions & 5 deletions shiny/reactive/_reactives.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from ._core import Context, Dependents, ReactiveWarning, isolate

if TYPE_CHECKING:
from ..session import Session
from .. import Session

T = TypeVar("T")

Expand Down Expand Up @@ -477,8 +477,8 @@ def __init__(
self.__name__ = fn.__name__
self.__doc__ = fn.__doc__

from ..express._mock_session import ExpressMockSession
from ..render.renderer import Renderer
from ..session import Session

if isinstance(fn, Renderer):
raise TypeError(
Expand Down Expand Up @@ -513,9 +513,9 @@ def __init__(
# could be None if outside of a session).
session = get_current_session()

if isinstance(session, ExpressMockSession):
# If we're in an ExpressMockSession, then don't actually set up this effect
# -- we don't want it to try to run later.
if isinstance(session, Session) and session.is_stub_session():
# If we're in an ExpressStubSession or a SessionProxy of one, then don't
# actually set up this effect -- we don't want it to try to run later.
return

self._session = session
Expand Down
2 changes: 1 addition & 1 deletion shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
if TYPE_CHECKING:
import pandas as pd

from ..session._utils import Session
from ..session import Session

DataFrameT = TypeVar("DataFrameT", bound=pd.DataFrame)
# TODO-barret-render.data_frame; Pandas, Polars, api compat, etc.; Today, we only support Pandas
Expand Down
Loading