Skip to content

Commit

Permalink
wip attempt at simplifying Module() API and implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Mar 5, 2022
1 parent d3b1fc8 commit 56449c3
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 156 deletions.
26 changes: 14 additions & 12 deletions examples/moduleapp/app.py
@@ -1,23 +1,20 @@
from typing import Callable
from shiny import *
from shiny.modules 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"),
)


def counter_server(input: ModuleInputs, output: ModuleOutputs, session: ModuleSession):
def counter_server(
input: Inputs, output: Outputs, session: Session, foo: int = 1, bar: int = 2
):
count: reactive.Value[int] = reactive.Value(0)

@reactive.Effect()
Expand All @@ -28,8 +25,11 @@ def _():
@output()
@render_text()
def out() -> str:
print(session)
return f"Click count is {count()}"

return dict(foo=foo, bar=bar)


counter_module = Module(counter_ui, counter_server)

Expand All @@ -38,14 +38,16 @@ def out() -> str:
# App that uses module
# =============================================================================
app_ui = ui.page_fluid(
counter_module.ui("counter1", "Counter 1"),
counter_module.ui("counter1", label="Counter 1"),
counter_module.ui("counter2", "Counter 2"),
)


def server(input: Inputs, output: Outputs, session: Session):
counter_module.server("counter1")
counter_module.server("counter2")
a = counter_module.server("counter1")
b = counter_module.server("counter2", foo=3, bar=4)
print(a)
print(b)


app = App(app_ui, server)
4 changes: 3 additions & 1 deletion shiny/__init__.py
Expand Up @@ -12,6 +12,7 @@
from ._app import App
from ._decorators import event
from ._validation import req
from .modules import Module

if _is_pyodide:
# In pyodide, avoid importing _main because it imports packages that aren't
Expand All @@ -21,7 +22,6 @@
from ._main import run_app

# User-facing submodules that should *not* be available on `from shiny import *`
from . import modules
from . import types

# N.B.: we intentionally don't import 'developer-facing' submodules (e.g.,
Expand All @@ -41,6 +41,8 @@
"event",
# _main.py
"run_app",
# _modules.py
"Module",
# _render.py
"render_text",
"render_plot",
Expand Down
48 changes: 48 additions & 0 deletions shiny/_namespaces.py
@@ -0,0 +1,48 @@
__all__ = ("namespace_context",)

from contextlib import contextmanager
from contextvars import ContextVar, Token

from typing import Union, Optional

_current_namespace: ContextVar[Optional[str]] = ContextVar(
"current_namespace", default=None
)


@contextmanager
def namespace_context(ns: str):
"""
Set a namespace for the duration of the context.
"""
token: Token[Union[str, None]] = _current_namespace.set(ns)
try:
yield
finally:
_current_namespace.reset(token)


# Protect id's from being namespaced twice.
class Namespaced(str):
pass


def namespaced(id: str) -> Union[str, Namespaced]:
"""
Namespace an ID based on the current ``Module()``'s namespace.
Parameters
----------
id
The ID to namespace.
Warning
-------
This is only provided so that htmltools can use it to namespace ids within a
``Module()`` UI function.
"""
ns = _current_namespace.get()
if ns is None or isinstance(id, Namespaced):
return id
else:
return Namespaced(ns + "-" + id)
155 changes: 30 additions & 125 deletions shiny/modules.py
@@ -1,102 +1,12 @@
__all__ = (
"ModuleInputs",
"ModuleOutputs",
"ModuleSession",
"Module",
)
__all__ = ("Module",)

from typing import Any, Callable, Optional, Dict
from typing import Any, Callable, Optional, TypeVar

from htmltools import TagChildArg

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


class ModuleInputs(Inputs):
"""
A class representing the inputs of a module.
Warning
-------
An instance of this class is created for each request and passed as an argument to
the :class:`shiny.modules.Module`'s ``server`` function. For this reason, you
shouldn't need to create instances of this class yourself.
"""

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)


class ModuleOutputs(Outputs):
"""
A class representing the outputs of a module.
Warning
-------
An instance of this class is created for each request and passed as an argument to
the :class:`shiny.modules.Module`'s ``server`` function. For this reason, you
shouldn't need to create instances of this class yourself.
"""

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 __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


NSFunc = Callable[[Optional[str]], str]
from ._namespaces import namespace_context
from .session import Session, require_active_session, session_context


class ModuleSession(Session):
Expand All @@ -106,24 +16,24 @@ class ModuleSession(Session):
Warning
-------
An instance of this class is created for each request and passed as an argument to
the :class:`shiny.modules.Module`'s ``server`` function. For this reason, you
shouldn't need to create instances of this class yourself.
the :class:`Module`'s ``server`` function. For this reason, you shouldn't need to
create instances of this class yourself.
"""

def __init__(self, ns_func: NSFunc, parent_session: Session) -> None:
self._ns_func: NSFunc = ns_func
self._parent: Session = parent_session
self.input: ModuleInputs = ModuleInputs(ns_func(None), parent_session.input)
self.output: ModuleOutputs = ModuleOutputs(ns_func(None), parent_session.output)
def __init__(self, ns: str, parent_session: Session) -> None:
self._ns = ns
self._parent = parent_session

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

def send_input_message(self, id: str, message: Dict[str, object]) -> None:
return super().send_input_message(self.ns(id), message)

def ns(self, id: str) -> str:
return self._ns_func(id)
if self._ns is None:
return id
return self._ns + "-" + id


T = TypeVar("T", bound=Any)


@add_example()
Expand All @@ -142,12 +52,10 @@ class Module:
def __init__(
self,
ui: Callable[..., TagChildArg],
server: Callable[[ModuleInputs, ModuleOutputs, ModuleSession], None],
server: Callable[..., object],
) -> None:
self._ui: Callable[..., TagChildArg] = ui
self._server: Callable[
[ModuleInputs, ModuleOutputs, ModuleSession], None
] = server
self._server: Callable[..., object] = server

def ui(self, ns: str, *args: Any, **kwargs: Any) -> TagChildArg:
"""
Expand All @@ -157,35 +65,32 @@ def ui(self, ns: str, *args: Any, **kwargs: Any) -> TagChildArg:
----------
namespace
A namespace for the module.
args
*args
Additional arguments to pass to the module's UI definition.
**kwargs
Additional keyword arguments to pass to the module's UI definition.
"""
return self._ui(Module._make_ns_fn(ns), *args, **kwargs)
with namespace_context(ns):
return self._ui(*args, **kwargs)

def server(
self, ns: str, *args: Any, session: Optional[Session] = None, **kwargs: Any
) -> Any:
) -> object:
"""
Invoke the module's server-side logic.
Parameters
----------
ns
A namespace for the module.
*args
Additional arguments to pass to the module's server-side logic.
session
A :class:`~shiny.Session` instance. If not provided, it is inferred via
:func:`~shiny.session.get_current_session`.
**kwargs
Additional keyword arguments to pass to the module's server-side logic.
"""
ms = ModuleSession(Module._make_ns_fn(ns), require_active_session(session))
with session_context(ms):
return self._server(ms.input, ms.output, ms, *args, **kwargs)

@staticmethod
def _make_ns_fn(ns: str) -> NSFunc:
def ns_fn(id: Optional[str] = None) -> str:
if id is None:
return ns
else:
return ns + "-" + id

return ns_fn
session = ModuleSession(ns, require_active_session(session))
with session_context(session):
return self._server(session.input, session.output, session, *args, **kwargs)

0 comments on commit 56449c3

Please sign in to comment.