"""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)