From 40e47165f52f628b82b317c217a8fe964c2356c8 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Sat, 9 Mar 2024 20:33:23 -0500 Subject: [PATCH 1/2] Implement express.module --- shiny/express/__init__.py | 2 ++ shiny/express/_mock_session.py | 10 ++++++--- shiny/express/_module.py | 35 ++++++++++++++++++++++++++++++ shiny/render/renderer/_renderer.py | 13 +++++++---- shiny/session/_utils.py | 2 +- 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 shiny/express/_module.py diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index aa9cd1fa0..d51ccd3af 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -11,6 +11,7 @@ from .. import render from . import ui from ._is_express import is_express_app +from ._module import module from ._output import ( # noqa: F401 output_args, # pyright: ignore[reportUnusedImport] suspend_display, # pyright: ignore[reportUnusedImport] - Deprecated @@ -29,6 +30,7 @@ "wrap_express_app", "ui", "expressify", + "module", ) # Add types to help type checkers diff --git a/shiny/express/_mock_session.py b/shiny/express/_mock_session.py index 998af63d7..4167ea78f 100644 --- a/shiny/express/_mock_session.py +++ b/shiny/express/_mock_session.py @@ -3,7 +3,7 @@ import textwrap from typing import TYPE_CHECKING, Awaitable, Callable, cast -from .._namespaces import Root +from .._namespaces import Id, ResolvedId, Root from ..session import Inputs, Outputs, Session if TYPE_CHECKING: @@ -21,8 +21,8 @@ class ExpressMockSession: the `app_opts()` function. """ - def __init__(self): - self.ns = Root + def __init__(self, ns: ResolvedId = Root): + self.ns = ns self.input = Inputs({}) self.output = Outputs(cast(Session, self), self.ns, outputs={}) @@ -39,6 +39,10 @@ def on_ended( ) -> Callable[[], None]: return lambda: None + def make_scope(self, id: Id) -> Session: + ns = self.ns(id) + return cast(Session, ExpressMockSession(ns)) + def __getattr__(self, name: str): raise AttributeError( textwrap.dedent( diff --git a/shiny/express/_module.py b/shiny/express/_module.py new file mode 100644 index 000000000..7b8006803 --- /dev/null +++ b/shiny/express/_module.py @@ -0,0 +1,35 @@ +import functools +from typing import Callable, Concatenate, ParamSpec, TypeVar + +from ..module import Id +from ..session._session import Inputs, Outputs, Session +from ..session._utils import require_active_session, session_context +from .expressify_decorator import expressify + +T = TypeVar("T") +P = ParamSpec("P") +R = TypeVar("R") + +__all__ = ("module",) + + +def module( + fn: Callable[Concatenate[Inputs, Outputs, Session, P], R] +) -> Callable[Concatenate[Id, P], R]: + fn = expressify(fn) + + @functools.wraps(fn) + def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: + parent_session = require_active_session(None) + module_session = parent_session.make_scope(id) + + with session_context(module_session): + return fn( + module_session.input, + module_session.output, + module_session, + *args, + **kwargs + ) + + return wrapper diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 548b0c186..e9a23b8ae 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -278,10 +278,13 @@ def tagify(self) -> DefaultUIFnResult: return rendered_ui def _render_auto_output_ui(self) -> DefaultUIFnResultOrNone: - return self.auto_output_ui( - # Pass the `@output_args(foo="bar")` kwargs through to the auto_output_ui function. - **self._auto_output_ui_kwargs, - ) + from ...session import session_context + + with session_context(self._session): + return self.auto_output_ui( + # Pass the `@output_args(foo="bar")` kwargs through to the auto_output_ui function. + **self._auto_output_ui_kwargs, + ) # ###### # Auto registering output @@ -297,6 +300,7 @@ def _on_register(self) -> None: ns_name = session.output._ns(self.__name__) session.output.remove(ns_name) self._auto_registered = False + self._session = session def _auto_register(self) -> None: """ @@ -317,6 +321,7 @@ def _auto_register(self) -> None: # We mark the fact that we're auto-registered so that, if an explicit # registration now occurs, we can undo this auto-registration. self._auto_registered = True + self._session = s # Not inheriting from `WrapAsync[[], IT]` as python 3.8 needs typing extensions that diff --git a/shiny/session/_utils.py b/shiny/session/_utils.py index 380505a53..637b6e83d 100644 --- a/shiny/session/_utils.py +++ b/shiny/session/_utils.py @@ -71,7 +71,7 @@ def session_context(session: Optional[Session]): """ token: Token[Session | None] = _current_session.set(session) try: - with namespace_context(session.ns if session else None): + with namespace_context(session.ns if session is not None else None): yield finally: _current_session.reset(token) From 7e7c4ebc7371927346bcdcbc749b41fb9d96d51d Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 22 Apr 2024 16:42:31 -0500 Subject: [PATCH 2/2] Fix missing _session --- shiny/render/renderer/_renderer.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index e9a23b8ae..24c43cfcf 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -1,6 +1,16 @@ from __future__ import annotations -from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Generic, + Optional, + TypeVar, + Union, + cast, +) from htmltools import MetadataNode, Tag, TagList @@ -9,6 +19,9 @@ from ..._utils import is_async_callable, wrap_async from ...types import Jsonifiable +if TYPE_CHECKING: + from ...session import Session + # TODO-barret-docs: Double check docs are rendererd # Missing first paragraph from some classes: Example: TransformerMetadata. # No init method for TransformerParams. This is because the `DocClass` object does not @@ -204,6 +217,7 @@ def __init__( super().__init__() self._auto_registered: bool = False + self._session: Session | None = None # Must be done last if callable(_fn):