Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't run effects created in a MockSession #1049

Merged
merged 1 commit into from
Jan 23, 2024
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
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