Skip to content

[docs]: Mention current_namespace as a way to get a module id #2127

@mykolaskrynnyk

Description

@mykolaskrynnyk

Category

UI Components

Scope

Minor Enhancement

Problem

Perhaps this is a niche use case, but consider a simple example of using Shiny modules from the documentation. If one wants to render conditional output, it is of course possible to define arguments to do so, e.g.,

@module.ui
def row_ui(is_special: bool = False):
    label = "Enter special text" if is_special else "Enter text"
    return ui.layout_columns(
        ui.card(ui.input_text("text_in", label)),
        ui.card(ui.output_text("text_out")),
    )

...

app_ui = ui.page_fluid(
    row_ui("row_1"),
    row_ui("row_2"),
    [row_ui(x, x.endswith(("3", "5"))) for x in extra_ids]
)

(Full app)

However, if this conditional behaviour is dependent on the module ID (as is the case in the example above), the implementation becomes repetitive and verbose. Even more so when the behaviour is complex and needs to be specified inside the module functions, e.g.,

@module.ui
def row_ui(module_id: str):
    if foo(module_id):
        # complex things like calling another module
    if bar(module_id):
        # more complex logic
    return ui.layout_columns(
        ui.card(ui.input_text("text_in", "Enter text")),
        ui.card(ui.output_text("text_out")),
    )

...

app_ui = ui.page_fluid(
    row_ui("row_1", "row_1"),
    row_ui("row_2", "row_2"),
    [row_ui(x, x) for x in extra_ids]
)

It would be much cleaner to access the namespace ID from within the module functions.

Solution

Currently, module decorators do not pass the IDs. For example, here is the module UI function decorator:

def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]:
    
    def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R:
        with namespace_context(id):
            return fn(*args, **kwargs)

    return wrapper

It is fairly trivial to update it to pass along the raw (unresolved) ID:

def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]:

    def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R:
        with namespace_context(id):
-            return fn(*args, **kwargs)
+            return fn(id, *args, **kwargs)

    return wrapper

This would naturally require users to always define the first argument, which is not desirable, so the ID could be passed only if expected by module function signature:

+ from inspect import signature


def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]:
    
+    # Assuming the argument should be called `module_id`
+    if 'module_id' in signature(fn).parameters:
+        kwargs['module_id'] = id
    def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R:
        with namespace_context(id):
-            return fn(*args, **kwargs)
+            return fn(id, *args, **kwargs)

    return wrapper

Alternatives (Optional)

No response

Example (Optional)

@module.ui
def row_ui(module_id: str):
    if foo(module_id):
        # complex things like calling another module
    if bar(module_id):
        # more complex logic
    return ui.layout_columns(
        ui.card(ui.input_text("text_in", "Enter text")),
        ui.card(ui.output_text("text_out")),
    )

...

app_ui = ui.page_fluid(
    row_ui("row_1"),
    row_ui("row_2"),
    [row_ui(x) for x in extra_ids]
)

Impact (Optional)

No response

Contribution? (Optional)

Yes, I can implement (or help).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions