"""Base helpers for SDK command namespaces. Each namespace (``b.tabs``, ``b.dom``, ...) is a thin object bound to its :class:`~browser_cli.BrowserCLI` client. Namespaces hold no state of their own; they delegate to the client's shared infrastructure (command dispatch, the multi-browser helpers, and the ``Tab``/``Group`` factories). """ from __future__ import annotations from collections.abc import Callable from functools import wraps from typing import Any, TypeVar, cast F = TypeVar("F", bound=Callable) _MISSING = object() def _clone_default(value): if isinstance(value, (dict, list, set)): return value.copy() return value def sdk_command( name: str, args: Callable | None = None, *, default=_MISSING, field: str | None = None, fallback=_MISSING, mapper: Callable | None = None, return_result: bool = True, ): """Decorate a namespace method as a browser wire command. This keeps the public method signature/docstring on the normal Python method, while moving repetitive command-dispatch plumbing into one place. The wrapped function body is intentionally unused; it exists only for signature, docs, and type hints. """ def decorator(func: F) -> F: @wraps(func) def wrapper(self, *method_args, **method_kwargs): payload = args(self, *method_args, **method_kwargs) if args is not None else {} result = self.command(name, payload) if not return_result: return None if mapper is not None: return mapper(self, result, *method_args, **method_kwargs) if field is not None: if fallback is _MISSING: return self.field(result, field, default) return self.field(result, field, default, fallback=fallback) if default is not _MISSING and not result: return _clone_default(default) return result setattr(wrapper, "_browser_cli_command", name) return cast(F, wrapper) return decorator class Namespace: """A group of related SDK methods, bound to a BrowserCLI client.""" def __init__(self, client: Any): self._c = client def command(self, name: str, args: dict | None = None): """Dispatch a browser command through the owning client.""" return self._c.dispatch(name, args) def tab_from(self, data: dict): """Build a bound Tab from a raw command response dict.""" return self._c.tab_from(data) def group_from(self, data: dict): """Build a bound Group from a raw command response dict.""" return self._c.group_from(data) def tab_from_target(self, data: dict, target): """Build a bound Tab for a multi-browser target.""" return self._c.tab_from_target(data, target) def group_from_target(self, data: dict, target): """Build a bound Group for a multi-browser target.""" return self._c.group_from_target(data, target) def tag_browser(self, item: dict, target): """Annotate a raw dict with its browser in multi-browser mode.""" return self._c.tag_browser(item, target) def multi_list(self, name: str, args: dict | None, mapper: Callable): """Run a list command with multi-browser fan-out support.""" return self._c.multi_list(name, args, mapper) def multi_count(self, name: str, args: dict | None = None): """Run a count command with multi-browser fan-out support.""" return self._c.multi_count(name, args) def apply_tab_filter(self, filter_fn: Callable): """Apply a Python-side tab filter using client semantics.""" return self._c.apply_tab_filter(filter_fn) def toggle_tab(self, name: str, tab_id: int | None): """Run a tab toggle command and return the affected tab ID.""" return self._c.toggle_tab(name, tab_id) def require_tab(self, data, error: str): """Convert a tab-like response into a bound Tab or raise a clean error.""" return self._c.require_tab(data, error) def field(self, result, key, default=None, *, fallback=_MISSING): """Read a field from command output using the client's SDK semantics.""" if fallback is _MISSING: return self._c.field(result, key, default) return self._c.field(result, key, default, fallback=fallback)