from __future__ import annotations import re from contextlib import contextmanager from contextvars import ContextVar, Token from typing import Generator, Pattern, Union, overload class ResolvedId(str): def __call__(self, id: Id) -> ResolvedId: if isinstance(id, ResolvedId): return id validate_id(id) if self == "": return ResolvedId(id) else: return ResolvedId(self + "-" + id) Root: ResolvedId = ResolvedId("") Id = Union[str, ResolvedId] def current_namespace() -> ResolvedId: return _current_namespace.get() or _default_namespace def resolve_id(id: Id) -> ResolvedId: """ Resolve an ID, possibly with a module namespace. Parameters ---------- Args id: An ID. Returns An ID (if in a module, this will contain a namespace prefix). """ curr_ns = current_namespace() return curr_ns(id) @overload def resolve_id_or_none(id: None) -> None: ... @overload def resolve_id_or_none(id: Id) -> ResolvedId: ... # Do not export this method from `shiny`. Let developers handle it themselves. def resolve_id_or_none(id: Id | None) -> ResolvedId | None: """ Resolve an ID, possibly with a module namespace. With `None` support. This method should only be used if `None` values are allowed. If not, use `resolve_id(id=)`. Parameters ---------- Args id: An ID or `None`. Returns If `id=None`, then `None`. Otherwise, an ID (if in a module, this will contain a namespace prefix). """ if id is None: return None return resolve_id(id) # \w is a large set for unicode patterns, that's fine; we mostly want to avoid some # special characters like space, comma, period, and especially dash re_valid_id: Pattern[str] = re.compile("^\\.?\\w+$") def validate_id(id: str) -> None: if not isinstance(id, str): raise ValueError("`id` must be a single string") if id == "": raise ValueError("`id` must be a non-empty string") if not re_valid_id.match(id): raise ValueError( f"The string '{id}' is not a valid id; only letters, numbers, and " "underscore are permitted" ) _current_namespace: ContextVar[ResolvedId | None] = ContextVar( "current_namespace", default=None ) _default_namespace: ResolvedId = Root @contextmanager def namespace_context(id: Id | None) -> Generator[None, None, None]: namespace = resolve_id(id) if id else Root token: Token[ResolvedId | None] = _current_namespace.set(namespace) try: yield finally: _current_namespace.reset(token)