diff --git a/shiny/_namespaces.py b/shiny/_namespaces.py index 58a3cb8df..c6c876167 100644 --- a/shiny/_namespaces.py +++ b/shiny/_namespaces.py @@ -1,37 +1,9 @@ -__all__ = ("namespace_context", "namespaced_id") +# TODO: make this available under the shiny.modules API +__all__ = ("namespaced_id",) -from contextlib import contextmanager -from contextvars import ContextVar, Token -import functools +from typing import Union, Optional -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) +from .types import MISSING, MISSING_TYPE def namespaced_id(id: str, ns: Union[str, MISSING_TYPE, None] = MISSING) -> str: @@ -41,12 +13,7 @@ def namespaced_id(id: str, ns: Union[str, MISSING_TYPE, None] = MISSING) -> str: 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. + The ID to namespace.. """ if isinstance(ns, MISSING_TYPE): ns = get_current_namespace() @@ -57,33 +24,11 @@ def namespaced_id(id: str, ns: Union[str, MISSING_TYPE, None] = MISSING) -> str: return ns + "_" + id -T = TypeVar("T") -Fn = Union[Callable[..., T], Callable[..., Awaitable[T]]] - - -def add_namespace(fn: Fn[T], ns: str) -> Fn[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 is_async_callable(fn): - - @functools.wraps(fn) - async def wrapper(*args: object, **kwargs: object) -> T: - with namespace_context(ns): - return await fn(*args, **kwargs) +def get_current_namespace() -> Optional[str]: + from .session import get_current_session + session = get_current_session() + if session is None: + return None 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 + return session._ns diff --git a/shiny/modules.py b/shiny/modules.py index 940606623..0f3461d7d 100644 --- a/shiny/modules.py +++ b/shiny/modules.py @@ -1,11 +1,11 @@ -__all__ = ("Module",) +__all__ = ("Module", "namespaced_id") from typing import Any, Callable, Optional from htmltools import TagChildArg from ._docstring import add_example -from ._namespaces import namespace_context, namespaced_id, get_current_namespace +from ._namespaces import namespaced_id from .session import Inputs, Outputs, Session, require_active_session, session_context @@ -43,8 +43,8 @@ class ModuleOutputs(Outputs): """ def __init__(self, ns: str, parent_outputs: Outputs): - self._parent = parent_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) @@ -64,8 +64,8 @@ class ModuleSession(Session): """ def __init__(self, ns: str, parent_session: Session): - self._parent = parent_session self._ns = namespaced_id(ns, parent_session._ns) # Support nested modules + self._parent = parent_session self.input = ModuleInputs(ns, parent_session.input) self.output = ModuleOutputs(ns, parent_session.output) @@ -73,6 +73,14 @@ def __getattr__(self, attr: str) -> Any: return getattr(self._parent, attr) +class MockModuleSession(ModuleSession): + def __init__(self, ns: str): + self._ns = ns + self._parent = None + self.input = Inputs() + self.output = Outputs(self) + + @add_example() class Module: """ @@ -108,9 +116,9 @@ def ui(self, ns: str, *args: Any, **kwargs: Any) -> TagChildArg: Additional keyword arguments to pass to the module's UI definition. """ - # Support nested modules by adding this namespace to any current namespace. - ns_full = namespaced_id(ns, get_current_namespace()) - with namespace_context(ns_full): + # Create a fake session so that namespaced_id() knows + # what the relevant namespace is + with session_context(MockModuleSession(ns)): return self._ui(*args, **kwargs) def server( @@ -133,11 +141,6 @@ def server( """ mod_sess = ModuleSession(ns, require_active_session(session)) - # N.B. we don't need a `with namespace_context()` here, because when - # reactive primitive like Calc(), Effect(), and RenderFunction() are created, - # they use the session._ns field to add a `with namespace_context()` to the - # the user's function (it has to be done this way since the user functions - # are executed after the server function is executed). with session_context(mod_sess): return self._server( mod_sess.input, mod_sess.output, mod_sess, *args, **kwargs diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index 4fab61aad..ee2ae0cb9 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -18,7 +18,6 @@ from ._core import Context, Dependents, ReactiveWarning from .._docstring import add_example -from .._namespaces import add_namespace from .. import _utils from ..types import MISSING, MISSING_TYPE, SilentException @@ -213,6 +212,12 @@ def __init__( self.__name__ = fn.__name__ self.__doc__ = fn.__doc__ + # The CalcAsync subclass will pass in an async function, but it tells the + # static type checker that it's synchronous. wrap_async() is smart -- if is + # passed an async function, it will not change it. + self._fn: CalcFunctionAsync[T] = _utils.wrap_async(fn) + self._is_async: bool = _utils.is_async_callable(fn) + self._dependents: Dependents = Dependents() self._invalidated: bool = True self._running: bool = False @@ -232,17 +237,6 @@ def __init__( session = get_current_session() self._session = session - # If created within a ModuleSession(), add the namespace_context to the user's - # func so that any UI generated by it will have namespaced ids. - if session is not None and session._ns is not None: - fn = add_namespace(fn, session._ns) - - # The CalcAsync subclass will pass in an async function, but it tells the - # static type checker that it's synchronous. wrap_async() is smart -- if is - # passed an async function, it will not change it. - self._fn: CalcFunctionAsync[T] = _utils.wrap_async(fn) - self._is_async: bool = _utils.is_async_callable(fn) - # Use lists to hold (optional) value and error, instead of Optional[T], # because it makes typing more straightforward. For example if # .get_value() simply returned self._value, self._value had type @@ -426,6 +420,13 @@ def __init__( self.__name__ = fn.__name__ self.__doc__ = fn.__doc__ + # The EffectAsync subclass will pass in an async function, but it tells the + # static type checker that it's synchronous. wrap_async() is smart -- if is + # passed an async function, it will not change it. + self._fn: EffectFunctionAsync = _utils.wrap_async(fn) + # This indicates whether the user's effect function (before wrapping) is async. + self._is_async: bool = _utils.is_async_callable(fn) + self._priority: int = priority self._suspended = suspended self._on_resume: Callable[[], None] = lambda: None @@ -449,17 +450,6 @@ def __init__( if self._session is not None: self._session.on_ended(self._on_session_ended_cb) - # If created within a ModuleSession(), add the namespace_context to the - # user's func so that any UI generated by it will have namespaced ids. - if self._session._ns is not None: - fn = add_namespace(fn, self._session._ns) - - # The EffectAsync subclass will pass in an async function, but it tells the - # static type checker that it's synchronous. wrap_async() is smart -- if is - # passed an async function, it will not change it. - self._fn: EffectFunctionAsync = _utils.wrap_async(fn) - # This indicates whether the user's effect function (before wrapping) is async. - self._is_async: bool = _utils.is_async_callable(fn) # Defer the first running of this until flushReact is called self._create_context().invalidate() diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 759dc10e4..cafb96c52 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -50,7 +50,7 @@ from .._fileupload import FileInfo, FileUploadManager from ..http_staticfiles import FileResponse from ..input_handler import input_handlers -from .._namespaces import namespaced_id, add_namespace +from .._namespaces import namespaced_id from ..reactive import Value, Effect, Effect_, isolate, flush from ..reactive._core import lock from ..types import SafeException, SilentCancelOutputException, SilentException @@ -442,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": self.ns(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() @@ -654,9 +654,6 @@ def _process_ui(self, ui: TagChildArg) -> RenderedDeps: return {"deps": deps, "html": res["html"]} - def ns(self, id: str) -> str: - return namespaced_id(id, self._ns) - # ====================================================================================== # Inputs @@ -751,11 +748,6 @@ def set_fn(fn: render.RenderFunction) -> None: # Get the (possibly namespaced) output id fn_name = namespaced_id(name or fn.__name__, self._ns) - # If created within a ModuleSession(), add the namespace_context to the - # user's func so that any UI generated by it will have namespaced ids. - if self._ns is not None: - fn.__call__ = add_namespace(fn.__call__, 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