Skip to content

Commit

Permalink
Move away from ModuleSession/ModuleInput/ModuleOutput toward SessionP…
Browse files Browse the repository at this point in the history
…roxy
  • Loading branch information
cpsievert committed Jun 8, 2022
1 parent 67239b8 commit b445500
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 259 deletions.
150 changes: 13 additions & 137 deletions shiny/_modules.py
@@ -1,90 +1,24 @@
__all__ = ("namespaced_id", "module_ui", "module_server")

from typing import Any, Callable, Optional, TypeVar, ParamSpec, Concatenate
import sys
from typing import Callable, TypeVar

from htmltools import TagChildArg
if sys.version_info < (3, 10):
from typing_extensions import ParamSpec, Concatenate
else:
from typing import ParamSpec, Concatenate

from ._docstring import add_example
from ._namespaces import namespaced_id
from ._namespaces import namespaced_id, namespace_context, get_current_namespaces
from .session import Inputs, Outputs, Session, require_active_session, session_context


class ModuleInputs(Inputs):
"""
A class representing a module's outputs.
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. Furthermore, you
probably shouldn't need this class for type checking either since it has the same
signature as :class:`shiny.session.Session`.
"""

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 a module's outputs.
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. Furthermore, you
probably shouldn't need this class for type checking either since it has the same
signature as :class:`shiny.session.Session`.
"""

def __init__(self, ns: str, parent_outputs: Outputs):
self._ns = namespaced_id(ns, parent_outputs._ns) # Support nested modules
self._parent = parent_outputs

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


class ModuleSession(Session):
"""
A class representing a module's outputs.
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. Furthermore, you
probably shouldn't need this class for type checking either since it has the same
signature as :class:`shiny.session.Session`.
"""

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)

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


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


P = ParamSpec("P")
R = TypeVar("R")


def module_ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]:
def wrapper(ns: str, *args: P.args, **kwargs: P.kwargs) -> R:
with session_context(MockModuleSession(ns)):
# TODO: what should happen if this is called *inside* of a session? Do we tack on the parent session's namespace as well?
with namespace_context(get_current_namespaces() + [ns]):
return fn(*args, **kwargs)

return wrapper
Expand All @@ -94,67 +28,9 @@ def module_server(
fn: Callable[Concatenate[Inputs, Outputs, Session, P], R]
) -> Callable[Concatenate[str, P], R]:
def wrapper(ns: str, *args: P.args, **kwargs: P.kwargs) -> R:
mod_sess = ModuleSession(ns, require_active_session(None))
with session_context(mod_sess):
return fn(mod_sess.input, mod_sess.output, mod_sess, *args, **kwargs)
sess = require_active_session(None)
child_sess = sess.make_scope(ns)
with session_context(child_sess):
return fn(child_sess.input, child_sess.output, child_sess, *args, **kwargs)

return wrapper


# @add_example()
# class Module:
# """
# Modularize UI and server-side logic.
#
# Parameters
# ----------
# ui
# The module's UI definition.
# server
# The module's server-side logic.
# """
#
# def __init__(
# self,
# ui: Callable[..., TagChildArg],
# server: Callable[[ModuleInputs, ModuleOutputs, ModuleSession], None],
# ) -> None:
# self._ui: Callable[..., TagChildArg] = ui
# self._server: Callable[
# [ModuleInputs, ModuleOutputs, ModuleSession], None
# ] = server
#
# def ui(self, ns: str, *args: Any) -> TagChildArg:
# """
# Render the module's UI.
#
# Parameters
# ----------
# namespace
# A namespace for the module.
# args
# Additional arguments to pass to the module's UI definition.
# """
#
# # 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:
# """
# Invoke the module's server-side logic.
#
# Parameters
# ----------
# ns
# A namespace for the module.
# session
# A :class:`~shiny.Session` instance. If not provided, it is inferred via
# :func:`~shiny.session.get_current_session`.
# """
#
# mod_sess = ModuleSession(ns, require_active_session(session))
# with session_context(mod_sess):
# return self._server(mod_sess.input, mod_sess.output, mod_sess)
#
51 changes: 28 additions & 23 deletions shiny/_namespaces.py
@@ -1,34 +1,39 @@
# TODO: make this available under the shiny.modules API
__all__ = ("namespaced_id",)
from contextlib import contextmanager
from contextvars import ContextVar, Token
from typing import Union, List

from typing import Union, Optional

from .types import MISSING, MISSING_TYPE
class ResolvedId(str):
pass


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

Parameters
----------
id
The ID to namespace..
"""
if isinstance(ns, MISSING_TYPE):
ns = get_current_namespace()

if ns is None:
def namespaced_id(id: Id) -> Id:
return namespaced_id_ns(id, get_current_namespaces())


def namespaced_id_ns(id: Id, namespaces: List[str] = []) -> Id:
if isinstance(id, ResolvedId) or len(namespaces) == 0:
return id
else:
return ns + "_" + id
return ResolvedId("_".join(namespaces) + "_" + id)


def get_current_namespace() -> Optional[str]:
from .session import get_current_session
def get_current_namespaces() -> List[str]:
return _current_namespaces.get()

session = get_current_session()
if session is None:
return None
else:
return session._ns

_current_namespaces: ContextVar[List[str]] = ContextVar(
"current_namespaces", default=[]
)


@contextmanager
def namespace_context(namespaces: List[str]):
token: Token[List[str]] = _current_namespaces.set(namespaces)
try:
yield
finally:
_current_namespaces.reset(token)

0 comments on commit b445500

Please sign in to comment.