Skip to content

Commit

Permalink
wip reduce need to explicitly namespace Module() UI code
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Mar 10, 2022
1 parent d55bcc8 commit 5b04b6b
Show file tree
Hide file tree
Showing 20 changed files with 165 additions and 143 deletions.
10 changes: 3 additions & 7 deletions examples/moduleapp/app.py
@@ -1,18 +1,14 @@
from typing import Callable
from shiny import *


# ============================================================
# Counter module
# ============================================================
def counter_ui(
ns: Callable[[str], str], label: str = "Increment counter"
) -> ui.TagChildArg:
def counter_ui(label: str = "Increment counter") -> ui.TagChildArg:
return ui.div(
{"style": "border: 1px solid #ccc; border-radius: 5px; margin: 5px 0;"},
ui.h2("This is " + label),
ui.input_action_button(id=ns("button"), label=label),
ui.output_text_verbatim(id=ns("out")),
ui.input_action_button(id="button", label=label),
ui.output_text_verbatim(id="out"),
)


Expand Down
106 changes: 31 additions & 75 deletions shiny/_modules.py
@@ -1,18 +1,17 @@
__all__ = ("Module",)
__all__ = ("Module", "namespaced_id")

from typing import Any, Callable, Optional

from htmltools import TagChildArg

from ._docstring import add_example
from .reactive import Value
from .render import RenderFunction
from ._namespaces import namespaced_id
from .session import Inputs, Outputs, Session, require_active_session, session_context


class ModuleInputs(Inputs):
"""
A class representing the inputs of a module.
A class representing a module's outputs.
Warning
-------
Expand All @@ -23,43 +22,16 @@ class ModuleInputs(Inputs):
signature as :class:`shiny.session.Session`.
"""

def __init__(self, ns: str, values: Inputs):
self._ns: str = ns
self._values: Inputs = values

def _ns_key(self, key: str) -> str:
return self._ns + "-" + key

def __setitem__(self, key: str, value: Value[Any]) -> None:
self._values[self._ns_key(key)].set(value)

def __getitem__(self, key: str) -> Value[Any]:
return self._values[self._ns_key(key)]

def __delitem__(self, key: str) -> None:
del self._values[self._ns_key(key)]

# Allow access of values as attributes.
def __setattr__(self, attr: str, value: Value[Any]) -> None:
if attr in ("_values", "_ns", "_ns_key"):
object.__setattr__(self, attr, value)
return
else:
self.__setitem__(attr, value)

def __getattr__(self, attr: str) -> Value[Any]:
if attr in ("_values", "_ns", "_ns_key"):
return object.__getattribute__(self, attr)
else:
return self.__getitem__(attr)

def __delattr__(self, key: str) -> None:
self.__delitem__(key)
def __init__(self, ns: str, parent_inputs: Inputs):
self._ns = namespaced_id(ns, parent_inputs._ns) # Support nested modules
# Don't set _parent attribute like the other classes since Inputs redefines
# __setattr__
self._map = parent_inputs._map


class ModuleOutputs(Outputs):
"""
A class representing the outputs of a module.
A class representing a module's outputs.
Warning
-------
Expand All @@ -70,34 +42,17 @@ class ModuleOutputs(Outputs):
signature as :class:`shiny.session.Session`.
"""

def __init__(self, ns: str, outputs: Outputs):
self._ns: str = ns
self._outputs: Outputs = outputs

def _ns_key(self, key: str) -> str:
return self._ns + "-" + key
def __init__(self, ns: str, parent_outputs: Outputs):
self._ns = namespaced_id(ns, parent_outputs._ns) # Support nested modules
self._parent = parent_outputs

def __call__(
self,
*,
name: Optional[str] = None,
suspend_when_hidden: bool = True,
priority: int = 0
) -> Callable[[RenderFunction], None]:
def set_fn(fn: RenderFunction) -> None:
fn_name = name or fn.__name__
fn_name = self._ns_key(fn_name)
out_fn = self._outputs(
name=fn_name, suspend_when_hidden=suspend_when_hidden, priority=priority
)
return out_fn(fn)

return set_fn
def __getattr__(self, attr: str) -> Any:
return getattr(self._parent, attr)


class ModuleSession(Session):
"""
A class representing the session of a module.
A class representing a module's outputs.
Warning
-------
Expand All @@ -108,8 +63,8 @@ class ModuleSession(Session):
signature as :class:`shiny.session.Session`.
"""

def __init__(self, ns: str, parent_session: Session) -> None:
self._ns: str = ns
def __init__(self, ns: str, parent_session: Session):
self._ns: str = namespaced_id(ns, parent_session._ns) # Support nested modules
self._parent: Session = parent_session
self.input: ModuleInputs = ModuleInputs(ns, parent_session.input)
self.output: ModuleOutputs = ModuleOutputs(ns, parent_session.output)
Expand All @@ -118,6 +73,11 @@ def __getattr__(self, attr: str) -> Any:
return getattr(self._parent, attr)


class MockModuleSession(ModuleSession):
def __init__(self, ns: str):
self._ns = ns


@add_example()
class Module:
"""
Expand Down Expand Up @@ -152,7 +112,11 @@ def ui(self, ns: str, *args: Any) -> TagChildArg:
args
Additional arguments to pass to the module's UI definition.
"""
return self._ui(Module._make_ns_fn(ns), *args)

# Create a fake session so that namespaced_id() knows
# what the relevant namespace is
with session_context(MockModuleSession(ns)):
return self._ui(*args)

def server(self, ns: str, *, session: Optional[Session] = None) -> None:
"""
Expand All @@ -166,15 +130,7 @@ def server(self, ns: str, *, session: Optional[Session] = None) -> None:
A :class:`~shiny.Session` instance. If not provided, it is inferred via
:func:`~shiny.session.get_current_session`.
"""
self.ns: str = ns
session = require_active_session(session)
session_proxy = ModuleSession(ns, session)
with session_context(session_proxy):
self._server(session_proxy.input, session_proxy.output, session_proxy)

@staticmethod
def _make_ns_fn(namespace: str) -> Callable[[str], str]:
def ns_fn(id: str) -> str:
return namespace + "-" + id

return ns_fn

mod_sess = ModuleSession(ns, require_active_session(session))
with session_context(mod_sess):
return self._server(mod_sess.input, mod_sess.output, mod_sess)
34 changes: 34 additions & 0 deletions shiny/_namespaces.py
@@ -0,0 +1,34 @@
# TODO: make this available under the shiny.modules API
__all__ = ("namespaced_id",)

from typing import Union, Optional

from .types import MISSING, MISSING_TYPE


def namespaced_id(id: str, ns: Union[str, MISSING_TYPE, None] = MISSING) -> str:
"""
Namespace an ID based on the current ``Module()``'s namespace.
Parameters
----------
id
The ID to namespace..
"""
if isinstance(ns, MISSING_TYPE):
ns = get_current_namespace()

if ns is None:
return id
else:
return ns + "_" + id


def get_current_namespace() -> Optional[str]:
from .session import get_current_session

session = get_current_session()
if session is None:
return None
else:
return session._ns
10 changes: 3 additions & 7 deletions shiny/examples/Module/app.py
@@ -1,18 +1,14 @@
from typing import Callable
from shiny import *


# ============================================================
# Counter module
# ============================================================
def counter_ui(
ns: Callable[[str], str], label: str = "Increment counter"
) -> ui.TagChildArg:
def counter_ui(label: str = "Increment counter") -> ui.TagChildArg:
return ui.div(
{"style": "border: 1px solid #ccc; border-radius: 5px; margin: 5px 0;"},
ui.h2("This is " + label),
ui.input_action_button(id=ns("button"), label=label),
ui.output_text_verbatim(id=ns("out")),
ui.input_action_button(id="button", label=label),
ui.output_text_verbatim(id="out"),
)


Expand Down
24 changes: 17 additions & 7 deletions shiny/session/_session.py
Expand Up @@ -50,6 +50,7 @@
from .._fileupload import FileInfo, FileUploadManager
from ..http_staticfiles import FileResponse
from ..input_handler import input_handlers
from .._namespaces import namespaced_id
from ..reactive import Value, Effect, Effect_, isolate, flush
from ..reactive._core import lock
from ..types import SafeException, SilentCancelOutputException, SilentException
Expand Down Expand Up @@ -136,6 +137,8 @@ def __init__(
self.input: Inputs = Inputs()
self.output: Outputs = Outputs(self)

self._ns: Optional[str] = None # Only relevant for ModuleSession

self._outbound_message_queues = empty_outbound_message_queues()

self._message_handlers: Dict[
Expand Down Expand Up @@ -316,6 +319,7 @@ async def uploadEnd(job_id: str, input_id: str) -> None:
# ==========================================================================
# Handling /session/{session_id}/{action}/{subpath} requests
# ==========================================================================
# TODO: anything to be done here for module support?
async def _handle_request(
self, request: Request, action: str, subpath: Optional[str]
) -> ASGIApp:
Expand Down Expand Up @@ -438,7 +442,7 @@ def send_input_message(self, id: str, message: Dict[str, object]) -> None:
message
The message to send.
"""
msg: Dict[str, object] = {"id": id, "message": message}
msg: Dict[str, object] = {"id": namespaced_id(id, self._ns), "message": message}
self._outbound_message_queues["input_messages"].append(msg)
self._request_flush()

Expand Down Expand Up @@ -589,6 +593,7 @@ async def _unhandled_error(self, e: Exception) -> None:
await self.close()

# TODO: probably name should be id
# TODO: anything to be done here for module support?
@add_example()
def download(
self,
Expand Down Expand Up @@ -673,13 +678,16 @@ def __init__(self, **kwargs: object) -> None:
for key, value in kwargs.items():
self._map[key] = Value(value, read_only=True)

self._ns: Optional[str] = None # Only relevant for ModuleInputs()

def __setitem__(self, key: str, value: Value[Any]) -> None:
if not isinstance(value, Value):
raise TypeError("`value` must be a shiny.Value object.")

self._map[key] = value
self._map[namespaced_id(key, self._ns)] = value

def __getitem__(self, key: str) -> Value[Any]:
key = namespaced_id(key, self._ns)
# Auto-populate key if accessed but not yet set. Needed to take reactive
# dependencies on input values that haven't been received from client
# yet.
Expand All @@ -689,19 +697,18 @@ def __getitem__(self, key: str) -> Value[Any]:
return self._map[key]

def __delitem__(self, key: str) -> None:
del self._map[key]
del self._map[namespaced_id(key, self._ns)]

# Allow access of values as attributes.
def __setattr__(self, attr: str, value: Value[Any]) -> None:
# Need special handling of "_map".
if attr == "_map":
if attr in ("_map", "_ns"):
super().__setattr__(attr, value)
return

self.__setitem__(attr, value)

def __getattr__(self, attr: str) -> Value[Any]:
if attr == "_map":
if attr in ("_map", "_ns"):
return object.__getattribute__(self, attr)
return self.__getitem__(attr)

Expand All @@ -728,6 +735,7 @@ def __init__(self, session: Session) -> None:
self._effects: Dict[str, Effect_] = {}
self._suspend_when_hidden: Dict[str, bool] = {}
self._session: Session = session
self._ns: Optional[str] = None # Only relevant for ModuleOutputs()

def __call__(
self,
Expand All @@ -737,7 +745,9 @@ def __call__(
priority: int = 0,
) -> Callable[[render.RenderFunction], None]:
def set_fn(fn: render.RenderFunction) -> None:
fn_name = name or fn.__name__
# Get the (possibly namespaced) output id
fn_name = namespaced_id(name or fn.__name__, self._ns)

# fn is either a regular function or a RenderFunction object. If
# it's the latter, we can give it a bit of metadata, which can be
# used by the
Expand Down
5 changes: 3 additions & 2 deletions shiny/ui/_download_button.py
Expand Up @@ -5,6 +5,7 @@
from htmltools import tags, Tag, TagChildArg, TagAttrArg, css

from .._docstring import add_example
from .._namespaces import namespaced_id
from .._shinyenv import is_pyodide


Expand Down Expand Up @@ -46,7 +47,7 @@ def download_button(
icon,
label,
{"class": "btn btn-default shiny-download-link", "style": css(width=width)},
id=id,
id=namespaced_id(id),
# This is a fake link that just results in a 404. It will be replaced by a
# working link after the server side logic runs, so this link will only be
# visited in cases where the user clicks the button too fast, or if the server
Expand Down Expand Up @@ -99,7 +100,7 @@ def download_link(
icon,
label,
{"class": "shiny-download-link", "style": css(width=width)},
id=id,
id=namespaced_id(id),
href="session/0/download/missing_download",
target="_blank",
download=None if is_pyodide else True,
Expand Down

0 comments on commit 5b04b6b

Please sign in to comment.