Skip to content

Commit bfd5192

Browse files
authored
Don't run effects created in a MockSession (#1049)
1 parent abf9252 commit bfd5192

File tree

4 files changed

+66
-45
lines changed

4 files changed

+66
-45
lines changed

shiny/express/__init__.py

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
# Import these with underscore names so they won't show in autocomplete from the Python
44
# console.
5-
from ..session import Inputs as _Inputs, Outputs as _Outputs, Session as _Session
6-
from ..session import _utils as _session_utils
5+
from ..session import (
6+
Inputs as _Inputs,
7+
Outputs as _Outputs,
8+
Session as _Session,
9+
get_current_session as _get_current_session,
10+
)
711
from .. import render
812
from . import ui
913
from ._is_express import is_express_app
@@ -39,42 +43,10 @@
3943
# cases, but when it fails, it will be very confusing.
4044
def __getattr__(name: str) -> object:
4145
if name == "input":
42-
return _get_current_session_or_mock().input
46+
return _get_current_session().input # pyright: ignore
4347
elif name == "output":
44-
return _get_current_session_or_mock().output
48+
return _get_current_session().output # pyright: ignore
4549
elif name == "session":
46-
return _get_current_session_or_mock()
50+
return _get_current_session()
4751

4852
raise AttributeError(f"Module 'shiny.express' has no attribute '{name}'")
49-
50-
51-
# A very bare-bones mock session class that is used only in shiny.express.
52-
class _MockSession:
53-
def __init__(self):
54-
from typing import cast
55-
56-
from .._namespaces import Root
57-
58-
self.input = _Inputs({})
59-
self.output = _Outputs(cast(_Session, self), Root, {}, {})
60-
61-
# This is needed so that Outputs don't throw an error.
62-
def _is_hidden(self, name: str) -> bool:
63-
return False
64-
65-
66-
_current_mock_session: _MockSession | None = None
67-
68-
69-
def _get_current_session_or_mock() -> _Session:
70-
from typing import cast
71-
72-
session = _session_utils.get_current_session()
73-
if session is None:
74-
global _current_mock_session
75-
if _current_mock_session is None:
76-
_current_mock_session = _MockSession()
77-
return cast(_Session, _current_mock_session)
78-
79-
else:
80-
return session

shiny/express/_mock_session.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
import textwrap
4+
from typing import Awaitable, Callable, cast
5+
6+
from .._namespaces import Root
7+
from ..session import Inputs, Outputs, Session
8+
9+
all = ("MockSession",)
10+
11+
12+
# A very bare-bones mock session class that is used only in shiny.express's UI rendering
13+
# phase.
14+
class MockSession:
15+
def __init__(self):
16+
self.ns = Root
17+
self.input = Inputs({})
18+
self.output = Outputs(cast(Session, self), self.ns, {}, {})
19+
20+
# This is needed so that Outputs don't throw an error.
21+
def _is_hidden(self, name: str) -> bool:
22+
return False
23+
24+
def on_ended(
25+
self,
26+
fn: Callable[[], None] | Callable[[], Awaitable[None]],
27+
) -> Callable[[], None]:
28+
return lambda: None
29+
30+
def __getattr__(self, name: str):
31+
raise AttributeError(
32+
textwrap.dedent(
33+
f"""
34+
The session attribute `{name}` is not yet available for use. Since this code
35+
will run again when the session is initialized, you can use `if session:` to
36+
only run this code when the session is established.
37+
"""
38+
)
39+
)

shiny/express/_run.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from htmltools import Tag, TagList
99

1010
from .._app import App
11-
from ..session import Inputs, Outputs, Session
11+
from ..session import Inputs, Outputs, Session, session_context
12+
from ._mock_session import MockSession
1213
from ._recall_context import RecallContextManager
1314
from .expressify_decorator._func_displayhook import _expressify_decorator_function_def
1415
from .expressify_decorator._node_transformers import (
@@ -33,13 +34,15 @@ def wrap_express_app(file: Path) -> App:
3334
A `shiny.App` object.
3435
"""
3536
try:
36-
# We tagify here, instead of waiting for the App object to do it when it wraps
37-
# the UI in a HTMLDocument and calls render() on it. This is because
38-
# AttributeErrors can be thrown during the tagification process, and we need to
39-
# catch them here and convert them to a different type of error, because uvicorn
40-
# specifically catches AttributeErrors and prints an error message that is
41-
# misleading for Shiny Express. https://github.com/posit-dev/py-shiny/issues/937
42-
app_ui = run_express(file).tagify()
37+
with session_context(cast(Session, MockSession())):
38+
# We tagify here, instead of waiting for the App object to do it when it wraps
39+
# the UI in a HTMLDocument and calls render() on it. This is because
40+
# AttributeErrors can be thrown during the tagification process, and we need to
41+
# catch them here and convert them to a different type of error, because uvicorn
42+
# specifically catches AttributeErrors and prints an error message that is
43+
# misleading for Shiny Express. https://github.com/posit-dev/py-shiny/issues/937
44+
app_ui = run_express(file).tagify()
45+
4346
except AttributeError as e:
4447
raise RuntimeError(e) from e
4548

shiny/reactive/_reactives.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ def __init__(
480480
self.__name__ = fn.__name__
481481
self.__doc__ = fn.__doc__
482482

483+
from ..express._mock_session import MockSession
483484
from ..render.renderer import Renderer
484485

485486
if isinstance(fn, Renderer):
@@ -514,6 +515,12 @@ def __init__(
514515
# If no session is provided, autodetect the current session (this
515516
# could be None if outside of a session).
516517
session = get_current_session()
518+
519+
if isinstance(session, MockSession):
520+
# If we're in a MockSession, then don't actually set up this Effect -- we
521+
# don't want it to try to run later.
522+
return
523+
517524
self._session = session
518525

519526
if self._session is not None:

0 commit comments

Comments
 (0)