Skip to content

Commit

Permalink
Don't run effects created in a MockSession (#1049)
Browse files Browse the repository at this point in the history
  • Loading branch information
wch authored Jan 23, 2024
1 parent abf9252 commit bfd5192
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 45 deletions.
46 changes: 9 additions & 37 deletions shiny/express/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

# Import these with underscore names so they won't show in autocomplete from the Python
# console.
from ..session import Inputs as _Inputs, Outputs as _Outputs, Session as _Session
from ..session import _utils as _session_utils
from ..session import (
Inputs as _Inputs,
Outputs as _Outputs,
Session as _Session,
get_current_session as _get_current_session,
)
from .. import render
from . import ui
from ._is_express import is_express_app
Expand Down Expand Up @@ -39,42 +43,10 @@
# cases, but when it fails, it will be very confusing.
def __getattr__(name: str) -> object:
if name == "input":
return _get_current_session_or_mock().input
return _get_current_session().input # pyright: ignore
elif name == "output":
return _get_current_session_or_mock().output
return _get_current_session().output # pyright: ignore
elif name == "session":
return _get_current_session_or_mock()
return _get_current_session()

raise AttributeError(f"Module 'shiny.express' has no attribute '{name}'")


# A very bare-bones mock session class that is used only in shiny.express.
class _MockSession:
def __init__(self):
from typing import cast

from .._namespaces import Root

self.input = _Inputs({})
self.output = _Outputs(cast(_Session, self), Root, {}, {})

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


_current_mock_session: _MockSession | None = None


def _get_current_session_or_mock() -> _Session:
from typing import cast

session = _session_utils.get_current_session()
if session is None:
global _current_mock_session
if _current_mock_session is None:
_current_mock_session = _MockSession()
return cast(_Session, _current_mock_session)

else:
return session
39 changes: 39 additions & 0 deletions shiny/express/_mock_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import textwrap
from typing import Awaitable, Callable, cast

from .._namespaces import Root
from ..session import Inputs, Outputs, Session

all = ("MockSession",)


# A very bare-bones mock session class that is used only in shiny.express's UI rendering
# phase.
class MockSession:
def __init__(self):
self.ns = Root
self.input = Inputs({})
self.output = Outputs(cast(Session, self), self.ns, {}, {})

# 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 __getattr__(self, name: str):
raise AttributeError(
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 session:` to
only run this code when the session is established.
"""
)
)
19 changes: 11 additions & 8 deletions shiny/express/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from htmltools import Tag, TagList

from .._app import App
from ..session import Inputs, Outputs, Session
from ..session import Inputs, Outputs, Session, session_context
from ._mock_session import MockSession
from ._recall_context import RecallContextManager
from .expressify_decorator._func_displayhook import _expressify_decorator_function_def
from .expressify_decorator._node_transformers import (
Expand All @@ -33,13 +34,15 @@ def wrap_express_app(file: Path) -> App:
A `shiny.App` object.
"""
try:
# 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
# catch them here and convert them to a different type of error, because uvicorn
# specifically catches AttributeErrors and prints an error message that is
# misleading for Shiny Express. https://github.com/posit-dev/py-shiny/issues/937
app_ui = run_express(file).tagify()
with session_context(cast(Session, MockSession())):
# 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
# catch them here and convert them to a different type of error, because uvicorn
# specifically catches AttributeErrors and prints an error message that is
# misleading for Shiny Express. https://github.com/posit-dev/py-shiny/issues/937
app_ui = run_express(file).tagify()

except AttributeError as e:
raise RuntimeError(e) from e

Expand Down
7 changes: 7 additions & 0 deletions shiny/reactive/_reactives.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ def __init__(
self.__name__ = fn.__name__
self.__doc__ = fn.__doc__

from ..express._mock_session import MockSession
from ..render.renderer import Renderer

if isinstance(fn, Renderer):
Expand Down Expand Up @@ -514,6 +515,12 @@ def __init__(
# If no session is provided, autodetect the current session (this
# could be None if outside of a session).
session = get_current_session()

if isinstance(session, MockSession):
# If we're in a MockSession, then don't actually set up this Effect -- we
# don't want it to try to run later.
return

self._session = session

if self._session is not None:
Expand Down

0 comments on commit bfd5192

Please sign in to comment.