Skip to content

Commit 124e117

Browse files
jcheng5wch
andauthored
Modules for Shiny Express (#1220)
Co-authored-by: Winston Chang <winston@posit.co>
1 parent 06c3e42 commit 124e117

File tree

5 files changed

+69
-9
lines changed

5 files changed

+69
-9
lines changed

shiny/express/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .. import render
1212
from . import ui
1313
from ._is_express import is_express_app
14+
from ._module import module
1415
from ._output import ( # noqa: F401
1516
output_args, # pyright: ignore[reportUnusedImport]
1617
suspend_display, # pyright: ignore[reportUnusedImport] - Deprecated
@@ -29,6 +30,7 @@
2930
"wrap_express_app",
3031
"ui",
3132
"expressify",
33+
"module",
3234
)
3335

3436
# Add types to help type checkers

shiny/express/_mock_session.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import textwrap
44
from typing import TYPE_CHECKING, Awaitable, Callable, cast
55

6-
from .._namespaces import Root
6+
from .._namespaces import Id, ResolvedId, Root
77
from ..session import Inputs, Outputs, Session
88

99
if TYPE_CHECKING:
@@ -21,8 +21,8 @@ class ExpressMockSession:
2121
the `app_opts()` function.
2222
"""
2323

24-
def __init__(self):
25-
self.ns = Root
24+
def __init__(self, ns: ResolvedId = Root):
25+
self.ns = ns
2626
self.input = Inputs({})
2727
self.output = Outputs(cast(Session, self), self.ns, outputs={})
2828

@@ -39,6 +39,10 @@ def on_ended(
3939
) -> Callable[[], None]:
4040
return lambda: None
4141

42+
def make_scope(self, id: Id) -> Session:
43+
ns = self.ns(id)
44+
return cast(Session, ExpressMockSession(ns))
45+
4246
def __getattr__(self, name: str):
4347
raise AttributeError(
4448
textwrap.dedent(

shiny/express/_module.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import functools
2+
from typing import Callable, Concatenate, ParamSpec, TypeVar
3+
4+
from ..module import Id
5+
from ..session._session import Inputs, Outputs, Session
6+
from ..session._utils import require_active_session, session_context
7+
from .expressify_decorator import expressify
8+
9+
T = TypeVar("T")
10+
P = ParamSpec("P")
11+
R = TypeVar("R")
12+
13+
__all__ = ("module",)
14+
15+
16+
def module(
17+
fn: Callable[Concatenate[Inputs, Outputs, Session, P], R]
18+
) -> Callable[Concatenate[Id, P], R]:
19+
fn = expressify(fn)
20+
21+
@functools.wraps(fn)
22+
def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R:
23+
parent_session = require_active_session(None)
24+
module_session = parent_session.make_scope(id)
25+
26+
with session_context(module_session):
27+
return fn(
28+
module_session.input,
29+
module_session.output,
30+
module_session,
31+
*args,
32+
**kwargs
33+
)
34+
35+
return wrapper

shiny/render/renderer/_renderer.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
from __future__ import annotations
22

3-
from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, Union, cast
3+
from typing import (
4+
TYPE_CHECKING,
5+
Any,
6+
Awaitable,
7+
Callable,
8+
Generic,
9+
Optional,
10+
TypeVar,
11+
Union,
12+
cast,
13+
)
414

515
from htmltools import MetadataNode, Tag, TagList
616

@@ -9,6 +19,9 @@
919
from ..._utils import is_async_callable, wrap_async
1020
from ...types import Jsonifiable
1121

22+
if TYPE_CHECKING:
23+
from ...session import Session
24+
1225
# TODO-barret-docs: Double check docs are rendererd
1326
# Missing first paragraph from some classes: Example: TransformerMetadata.
1427
# No init method for TransformerParams. This is because the `DocClass` object does not
@@ -204,6 +217,7 @@ def __init__(
204217
super().__init__()
205218

206219
self._auto_registered: bool = False
220+
self._session: Session | None = None
207221

208222
# Must be done last
209223
if callable(_fn):
@@ -278,10 +292,13 @@ def tagify(self) -> DefaultUIFnResult:
278292
return rendered_ui
279293

280294
def _render_auto_output_ui(self) -> DefaultUIFnResultOrNone:
281-
return self.auto_output_ui(
282-
# Pass the `@output_args(foo="bar")` kwargs through to the auto_output_ui function.
283-
**self._auto_output_ui_kwargs,
284-
)
295+
from ...session import session_context
296+
297+
with session_context(self._session):
298+
return self.auto_output_ui(
299+
# Pass the `@output_args(foo="bar")` kwargs through to the auto_output_ui function.
300+
**self._auto_output_ui_kwargs,
301+
)
285302

286303
# ######
287304
# Auto registering output
@@ -297,6 +314,7 @@ def _on_register(self) -> None:
297314
ns_name = session.output._ns(self.__name__)
298315
session.output.remove(ns_name)
299316
self._auto_registered = False
317+
self._session = session
300318

301319
def _auto_register(self) -> None:
302320
"""
@@ -317,6 +335,7 @@ def _auto_register(self) -> None:
317335
# We mark the fact that we're auto-registered so that, if an explicit
318336
# registration now occurs, we can undo this auto-registration.
319337
self._auto_registered = True
338+
self._session = s
320339

321340

322341
# Not inheriting from `WrapAsync[[], IT]` as python 3.8 needs typing extensions that

shiny/session/_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def session_context(session: Optional[Session]):
7171
"""
7272
token: Token[Session | None] = _current_session.set(session)
7373
try:
74-
with namespace_context(session.ns if session else None):
74+
with namespace_context(session.ns if session is not None else None):
7575
yield
7676
finally:
7777
_current_session.reset(token)

0 commit comments

Comments
 (0)