Skip to content

Commit

Permalink
Implement 'auto' id namespacing for shiny's Module()
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Mar 5, 2022
1 parent d3b1fc8 commit 81d4346
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 161 deletions.
35 changes: 22 additions & 13 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, counter0: reactive.Calc_[int]
) -> int:
count: reactive.Value[int] = reactive.Value(0)

@reactive.Effect()
Expand All @@ -30,6 +27,13 @@ def _():
def out() -> str:
return f"Click count is {count()}"

@reactive.Effect()
def _():
print("Clicked global counter", counter0())
# print("Clicked global counter", inputs.counter0())

return 1


counter_module = Module(counter_ui, counter_server)

Expand All @@ -38,14 +42,19 @@ 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"),
ui.input_action_button("counter0", "Print global counter"),
)


def server(input: Inputs, output: Outputs, session: Session):
counter_module.server("counter1")
counter_module.server("counter2")
def server(inputs: Inputs, output: Outputs, sess: Session):
@reactive.Calc()
def counter0():
return inputs.counter0()

counter_module.server("counter1", counter0=counter0)
counter_module.server("counter2", counter0=counter0)


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
97 changes: 97 additions & 0 deletions shiny/_namespaces.py
@@ -0,0 +1,97 @@
__all__ = ("namespace_context", "namespaced_id")

from contextlib import contextmanager
from contextvars import ContextVar, Token
import functools

from typing import Awaitable, Union, Optional, TypeVar, Callable, cast

from shiny.types import MISSING, MISSING_TYPE

from ._utils import is_async_callable

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


def get_current_namespace() -> Optional[str]:
"""
Get the current namespace.
"""
return _current_namespace.get()


@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)


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.
Warning
-------
This is only provided so that htmltools can use it to namespace ids within a
``Module()`` UI function.
"""
if isinstance(ns, MISSING_TYPE):
ns = _current_namespace.get()

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


T = TypeVar("T")


def add_namespace(
fn: Union[Callable[..., T], Callable[..., Awaitable[T]]],
ns: Union[str, MISSING_TYPE, None] = MISSING,
) -> Union[Callable[..., T], Callable[..., Awaitable[T]]]:
"""
Modify a function to use the **current** namespace when it executes.
Note
----
This is useful for marking user supplied functions to use the **current** namespace
when they execute **later on**.
"""

if isinstance(ns, MISSING_TYPE):
ns = _current_namespace.get()

if ns is None:
return fn

if is_async_callable(fn):

@functools.wraps(fn)
async def wrapper(*args: object, **kwargs: object) -> T:
with namespace_context(ns):
return await fn(*args, **kwargs)

else:
fn = cast(Callable[..., T], fn)

@functools.wraps(fn)
def wrapper(*args: object, **kwargs: object) -> T:
with namespace_context(ns):
return fn(*args, **kwargs)

return wrapper
11 changes: 4 additions & 7 deletions shiny/examples/Module/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):
count: reactive.Value[int] = reactive.Value(0)

@reactive.Effect()
Expand Down

0 comments on commit 81d4346

Please sign in to comment.