[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/src/pythonnative/component.py [Back]  [Original]

"""The ``@component`` decorator and the [`Component`][pythonnative.Component] type.

A component is a plain Python function that takes props and returns a
[`Node`][pythonnative.element.Node]: an [`Element`][pythonnative.Element],
a list of elements, or ``None``. Decorating it with
[`@component`][pythonnative.component.component] turns it into a
[`Component`][pythonnative.Component]: a callable that *describes* a
render (it returns an ``Element`` whose ``type`` is the component)
instead of performing one. The reconciler invokes the function body
later, with hook state installed.

The decorator preserves the function's signature for type checkers via
:class:`typing.ParamSpec`, so ``Greeting(nme="x")`` is a static error
and editors autocomplete props from the function definition.

Children
--------

Children are positional. A component that accepts children declares a
``*children: pn.Node`` parameter, exactly like the built-in containers:

```python
@pn.component
def Card(*children: pn.Node, title: str = "") -> pn.Node:
    return pn.Column(pn.Text(title, style=pn.style(bold=True)), *children)

Card(pn.Text("body"), title="Hello")
```

Positional arguments to a component *without* ``*children`` bind to its
positional parameters, so ``Greeting("World")`` works for
``def Greeting(name: str)``.

Keys
----

``key`` identifies an element, not a prop, so a component's signature
can't declare it. Key a component's element with
[`Element.with_key`][pythonnative.element.Element.with_key], which type-checks
at every call site:

```python
pn.Column(*(Row(item).with_key(item.id) for item in items))
```

Passing ``key=`` to a user component raises ``TypeError``; built-in
factories accept ``key=`` directly.
"""

from __future__ import annotations

import functools
import inspect
from typing import Any, Awaitable, Callable, Dict, Generic, Mapping, Optional, ParamSpec, Union, overload

from . import diagnostics
from .element import Element, Node

__all__ = ["Component", "RenderFn", "component", "memo", "is_component"]

P = ParamSpec("P")

RenderFn = Callable[P, Union[Node, Awaitable[Node]]]
"""A component body: returns a [`Node`][pythonnative.element.Node], or awaits one when ``async def``."""


def _hook_signature(fn: Callable[..., Any]) -> Optional[tuple[str, ...]]:
    """Fast Refresh's hook fingerprint of ``fn``, or ``None`` without the refresh module.

    Release bundles omit ``pythonnative.refresh`` (and ship bytecode, which
    has no source to fingerprint), so the import is lazy, optional, and
    attempted once.
    """
    global _refresh_signature
    if _refresh_signature is None:
        try:
            from .refresh import hook_signature
        except ImportError:
            _refresh_signature = False
        else:
            _refresh_signature = hook_signature
    return _refresh_signature(fn) if callable(_refresh_signature) else None


_refresh_signature: Union[None, bool, Callable[[Callable[..., Any]], Optional[tuple[str, ...]]]] = None


class Component(Generic[P]):
    """A render-function wrapped by [`@component`][pythonnative.component.component].

    Calling a ``Component`` does not run the function; it returns an
    [`Element`][pythonnative.Element] describing the call so the
    reconciler can mount it, preserve its hook state across renders,
    and re-run it when its state or props change.

    Attributes:
        fn: The original render function.
        display_name: Name shown in diagnostics and dev tooling
            (defaults to ``fn.__name__``).
        memoized: Whether [`memo`][pythonnative.memo] was applied, in
            which case the reconciler skips re-rendering this component
            when its props are shallowly equal to the previous render.
        accepts_children: Whether ``fn`` declares ``*children``.
    """

    __slots__ = (
        "fn",
        "display_name",
        "memoized",
        "props_equal",
        "accepts_children",
        "_signature",
        "_keyed_builtin",
        "_is_async",
        "refresh_signature",
        "__wrapped__",
        # ``functools.update_wrapper`` copies ``__module__`` and
        # ``__qualname__`` from the render function so Fast Refresh can
        # find a component's replacement by module and name; those two
        # names can't be slots (they conflict with the class attributes),
        # so instances keep a ``__dict__`` for them.
        "__dict__",
    )

    def __init__(self, fn: RenderFn[P], *, display_name: Optional[str] = None, _keyed_builtin: bool = False) -> None:
        if isinstance(fn, Component):
            raise TypeError(f"{fn!r} is already a component; remove the duplicate @component")
        sig = inspect.signature(fn)
        accepts_children = False
        for name, param in sig.parameters.items():
            if param.kind is inspect.Parameter.VAR_POSITIONAL:
                accepts_children = True
            if name == "key" and not _keyed_builtin:
                raise TypeError(
                    f"{getattr(fn, '__qualname__', fn)!r} declares a 'key' parameter, but 'key' names an element's "
                    "identity, not a prop. Rename the parameter; key a rendered element with .with_key(...)."
                )
        self.refresh_signature = _hook_signature(fn)
        self.fn = fn
        self.display_name: str = display_name or str(getattr(fn, "__name__", "Component"))
        self.memoized = False
        self.props_equal: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], bool]] = None
        self.accepts_children = accepts_children
        self._signature = sig
        self._keyed_builtin = _keyed_builtin
        self._is_async = inspect.iscoroutinefunction(fn)
        self.__wrapped__ = fn
        functools.update_wrapper(self, fn, updated=())

    # ------------------------------------------------------------------
    # Element construction
    # ------------------------------------------------------------------

    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Element:
        """Describe a render of this component with the given props."""
        key: Optional[str] = None
        if "key" in kwargs:
            if not self._keyed_builtin:
                raise TypeError(
                    f"{self.display_name}() got key=...; components don't take a key prop. "
                    f"Use {self.display_name}(...).with_key(key) instead."
                )
            raw = kwargs.pop("key")
            key = None if raw is None else str(raw)
        bound = self._signature.bind(*args, **kwargs)
        bound.apply_defaults()
        children: tuple[Any, ...] = ()
        props: Dict[str, Any] = {}
        for name, value in bound.arguments.items():
            kind = self._signature.parameters[name].kind
            if kind is inspect.Parameter.VAR_POSITIONAL:
                children = value
            else:
                props[name] = value
        props.pop("key", None)
        if diagnostics.is_dev() and not self._keyed_builtin:
            from .prop_checks import check_props

            check_props(self, props)
        return Element(self, props, children, key=key)

    # ------------------------------------------------------------------
    # Rendering (used by the reconciler)
    # ------------------------------------------------------------------

    def render(self, element: Element) -> Any:
        """Invoke the render function for ``element``.

        Children stored on the element are passed positionally; props
        are passed by keyword. Returns whatever the function returned
        (an element, a list, ``None``, or a coroutine for ``async def``
        bodies).
        """
        arguments = dict(element.props)
        for name, param in self._signature.parameters.items():
            if param.kind is inspect.Parameter.VAR_POSITIONAL:
                arguments[name] = element.children
        bound = inspect.BoundArguments(self._signature, arguments)
        return self.fn(*bound.args, **bound.kwargs)

    @property
    def is_async(self) -> bool:
        """Whether the render function is an ``async def``."""
        return self._is_async

    def __repr__(self) -> str:
        flags = " memo" if self.memoized else ""
        return f""

    def __get__(self, instance: Any, owner: Any = None) -> "Component[P]":
        # Components stored on classes (rare) should not bind ``self``.
        return self


def component(fn: RenderFn[P]) -> Component[P]:
    """Turn a render function into a [`Component`][pythonnative.Component].

    The decorated function may use hooks (``use_state``, ``use_effect``,
    etc.) and returns an [`Element`][pythonnative.Element] tree, a list
    of elements, or ``None``. Each call site creates an independent
    component instance with its own hook state.

    Args:
        fn: The render function. May be ``async def``; the body is then
            driven by the reconciler and suspends on pending awaits (see
            [`Suspense`][pythonnative.Suspense]).

    Returns:
        A ``Component`` whose call signature mirrors ``fn``.

    Example:
        ```python
        import pythonnative as pn

        @pn.component
        def Greeting(name: str = "World"):
            return pn.Text(f"Hello, {name}!")
        ```
    """
    return Component(fn)


@overload
def memo(
    target: Component[P], *, equal: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], bool]] = None
) -> Component[P]: ...


@overload
def memo(
    target: None = None, *, equal: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], bool]] = None
) -> Callable[[Component[P]], Component[P]]: ...


def memo(
    target: Optional[Component[P]] = None,
    *,
    equal: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], bool]] = None,
) -> Any:
    """Skip a component's render when its props haven't changed.

    Apply on top of ``@component``. When the reconciler re-renders the
    parent tree, a memoized child is skipped (its previously-rendered
    subtree is reused) iff its props and children are equal to the
    previous render and none of its own state setters fired. Props are
    compared shallowly by default: callables by identity, everything
    else by ``==``.

    Pair with [`use_callback`][pythonnative.use_callback] when passing
    callbacks as props, otherwise a fresh closure defeats the memo.

    Args:
        target: A [`Component`][pythonnative.Component].
        equal: Optional custom comparator ``(old_props, new_props) ->
            bool`` replacing the shallow comparison.

    Returns:
        The same component, marked for memoization (or a decorator when
        called with keyword arguments only).

    Example:
        ```python
        @pn.memo
        @pn.component
        def ExpensiveRow(label: str):
            ...

        @pn.memo(equal=lambda a, b: a["id"] == b["id"])
        @pn.component
        def Row(id: int, extra: dict):
            ...
        ```
    """

    def apply(comp: Component[P]) -> Component[P]:
        if not isinstance(comp, Component):
            raise TypeError("@memo must wrap a @component; write @memo above @component")
        comp.memoized = True
        comp.props_equal = equal
        return comp

    if target is None:
        return apply
    return apply(target)


def builtin_component(fn: RenderFn[P]) -> Component[P]:
    """Framework-internal ``@component`` whose signature may declare ``key``.

    Built-in factories implemented as components (``FlatList``,
    ``SectionList``) keep a typed ``key=`` parameter like every other
    built-in factory. The key becomes the element's identity and never
    reaches the render function.
    """
    return Component(fn, _keyed_builtin=True)


def is_component(obj: Any) -> bool:
    """Return whether ``obj`` is a [`Component`][pythonnative.Component]."""
    return isinstance(obj, Component)

Web Proxy Viewer  |  New URL  |  Original Page