eptm_dashboard/.venv/lib/python3.12/site-packages/reflex/experimental/memo.py

1145 lines
37 KiB
Python

"""Experimental memo support for vars and components."""
from __future__ import annotations
import dataclasses
import inspect
from collections.abc import Callable
from copy import copy
from functools import cache, update_wrapper
from typing import Any, get_args, get_origin, get_type_hints
from reflex_base import constants
from reflex_base.components.component import Component
from reflex_base.components.dynamic import bundled_libraries
from reflex_base.components.memoize_helpers import (
MemoizationStrategy,
get_memoization_strategy,
)
from reflex_base.constants.compiler import (
MemoizationDisposition,
MemoizationMode,
SpecialAttributes,
)
from reflex_base.constants.state import CAMEL_CASE_MEMO_MARKER
from reflex_base.utils import format
from reflex_base.utils.imports import ImportVar
from reflex_base.utils.types import safe_issubclass
from reflex_base.vars import VarData
from reflex_base.vars.base import LiteralVar, Var
from reflex_base.vars.function import (
ArgsFunctionOperation,
DestructuredArg,
FunctionStringVar,
FunctionVar,
ReflexCallable,
)
from reflex_base.vars.object import RestProp
from reflex_components_core.base.bare import Bare
from reflex_components_core.base.fragment import Fragment
from reflex.utils import types as type_utils
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class MemoParam:
"""Metadata about a memo parameter."""
name: str
annotation: Any
kind: inspect._ParameterKind
default: Any = inspect.Parameter.empty
js_prop_name: str | None = None
placeholder_name: str = ""
is_children: bool = False
is_rest: bool = False
@dataclasses.dataclass(frozen=True, slots=True)
class ExperimentalMemoDefinition:
"""Base metadata for an experimental memo."""
fn: Callable[..., Any]
python_name: str
params: tuple[MemoParam, ...]
@dataclasses.dataclass(frozen=True, slots=True)
class ExperimentalMemoFunctionDefinition(ExperimentalMemoDefinition):
"""A memo that compiles to a JavaScript function."""
function: ArgsFunctionOperation
imported_var: FunctionVar
@dataclasses.dataclass(frozen=True, slots=True)
class ExperimentalMemoComponentDefinition(ExperimentalMemoDefinition):
"""A memo that compiles to a React component."""
export_name: str
component: Component
# For passthrough wrappers built by the auto-memoize plugin: the
# ``Bare``-wrapped ``{children}`` placeholder used when rendering the memo
# body. The ``component`` keeps its ORIGINAL children so compile-time
# walkers (``Form._get_form_refs`` etc.) can introspect the subtree; the
# compiler swaps to this placeholder only for the JSX render and for
# imports collection, so descendants emit their refs/imports/hooks in the
# page scope rather than being duplicated inside the memo body.
passthrough_hole_child: Component | None = None
class ExperimentalMemoComponent(Component):
"""A rendered instance of an experimental memo component."""
library = f"$/{constants.Dirs.COMPONENTS_PATH}"
_memoization_mode = MemoizationMode(disposition=MemoizationDisposition.NEVER)
def _validate_component_children(self, children: list[Component]) -> None:
"""Skip direct parent/child validation for memo wrapper instances.
Experimental memos wrap an underlying compiled component definition.
The runtime wrapper should not interpose on `_valid_parents` checks for
the authored subtree because the wrapper itself is not the semantic
parent in the user-authored component tree.
Args:
children: The children of the component (ignored).
"""
def _post_init(self, **kwargs):
"""Initialize the experimental memo component.
Args:
**kwargs: The kwargs to pass to the component.
"""
definition = kwargs.pop("memo_definition")
explicit_props = {
param.name
for param in definition.params
if not param.is_children and not param.is_rest
}
component_fields = self.get_fields()
declared_props = {
key: kwargs.pop(key) for key in list(kwargs) if key in explicit_props
}
rest_props = {}
if _get_rest_param(definition.params) is not None:
rest_props = {
key: kwargs.pop(key)
for key in list(kwargs)
if key not in component_fields and not SpecialAttributes.is_special(key)
}
super()._post_init(**kwargs)
props: dict[str, Any] = {}
for key, value in {**declared_props, **rest_props}.items():
camel_cased_key = format.to_camel_case(key)
literal_value = LiteralVar.create(value)
props[camel_cased_key] = literal_value
setattr(self, camel_cased_key, literal_value)
prop_names = tuple(props)
object.__setattr__(self, "get_props", lambda: prop_names)
@cache
def _get_experimental_memo_component_class(
export_name: str,
wrapped_component_type: type[Component] = Component,
) -> type[ExperimentalMemoComponent]:
"""Get the component subclass for an experimental memo export.
Class-level metadata that the compiler reads via ``type(comp)._get_*()``
(notably ``_get_app_wrap_components``, which carries providers like
``UploadFilesProvider`` that must reach the app root) is inherited from
``wrapped_component_type`` so the wrapper is a transparent substitute for
the original in the compile tree.
Args:
export_name: The exported React component name.
wrapped_component_type: The class of the component being memoized.
Defaults to ``Component`` for memos that don't wrap a user
component (e.g. function memos, raw passthroughs).
Returns:
A cached component subclass with the tag set at class definition time.
"""
attrs: dict[str, Any] = {
"__module__": __name__,
"tag": export_name,
# Point each memo at its own per-file module so pages import directly
# from ``$/utils/components/<name>`` rather than through the index.
# Per-file import paths give Vite distinct module boundaries per
# memo, enabling actual code-split by page.
"library": f"$/{constants.Dirs.COMPONENTS_PATH}/{export_name}",
}
if (
wrapped_component_type._get_app_wrap_components
is not Component._get_app_wrap_components
):
attrs["_get_app_wrap_components"] = staticmethod(
wrapped_component_type._get_app_wrap_components
)
return type(
f"ExperimentalMemoComponent_{export_name}",
(ExperimentalMemoComponent,),
attrs,
)
EXPERIMENTAL_MEMOS: dict[str, ExperimentalMemoDefinition] = {}
def _memo_registry_key(definition: ExperimentalMemoDefinition) -> str:
"""Get the registry key for an experimental memo.
Args:
definition: The memo definition.
Returns:
The registry key for the memo.
"""
if isinstance(definition, ExperimentalMemoComponentDefinition):
return definition.export_name
return definition.python_name
def _is_memo_reregistration(
existing: ExperimentalMemoDefinition,
definition: ExperimentalMemoDefinition,
) -> bool:
"""Check whether a memo definition replaces the same memo during reload.
Args:
existing: The currently registered memo definition.
definition: The new memo definition being registered.
Returns:
Whether the new definition should replace the existing one.
"""
return (
type(existing) is type(definition)
and existing.python_name == definition.python_name
and existing.fn.__module__ == definition.fn.__module__
and existing.fn.__qualname__ == definition.fn.__qualname__
)
def _register_memo_definition(definition: ExperimentalMemoDefinition) -> None:
"""Register an experimental memo definition.
Args:
definition: The memo definition to register.
Raises:
ValueError: If another memo already compiles to the same exported name.
"""
key = _memo_registry_key(definition)
if (existing := EXPERIMENTAL_MEMOS.get(key)) is not None and (
not _is_memo_reregistration(existing, definition)
):
msg = (
f"Experimental memo name collision for `{key}`: "
f"`{existing.fn.__module__}.{existing.python_name}` and "
f"`{definition.fn.__module__}.{definition.python_name}` both compile "
"to the same memo name."
)
raise ValueError(msg)
EXPERIMENTAL_MEMOS[key] = definition
def _annotation_inner_type(annotation: Any) -> Any:
"""Unwrap a Var-like annotation to its inner type.
Args:
annotation: The annotation to unwrap.
Returns:
The inner type for the annotation.
"""
if _is_rest_annotation(annotation):
return dict[str, Any]
origin = get_origin(annotation) or annotation
if type_utils.safe_issubclass(origin, Var) and (args := get_args(annotation)):
return args[0]
return Any
def _is_rest_annotation(annotation: Any) -> bool:
"""Check whether an annotation is a RestProp.
Args:
annotation: The annotation to check.
Returns:
Whether the annotation is a RestProp.
"""
origin = get_origin(annotation) or annotation
return isinstance(origin, type) and issubclass(origin, RestProp)
def _is_var_annotation(annotation: Any) -> bool:
"""Check whether an annotation is a Var-like annotation.
Args:
annotation: The annotation to check.
Returns:
Whether the annotation is Var-like.
"""
origin = get_origin(annotation) or annotation
return isinstance(origin, type) and issubclass(origin, Var)
def _is_component_annotation(annotation: Any) -> bool:
"""Check whether an annotation is component-like.
Args:
annotation: The annotation to check.
Returns:
Whether the annotation resolves to Component.
"""
origin = get_origin(annotation) or annotation
return isinstance(origin, type) and (
safe_issubclass(origin, Component)
or bool(
safe_issubclass(origin, Var)
and (args := get_args(annotation))
and safe_issubclass(args[0], Component)
)
)
def _children_annotation_is_valid(annotation: Any) -> bool:
"""Check whether an annotation is valid for children.
Args:
annotation: The annotation to check.
Returns:
Whether the annotation is valid for children.
"""
return _is_var_annotation(annotation) and type_utils.typehint_issubclass(
_annotation_inner_type(annotation), Component
)
def _get_children_param(params: tuple[MemoParam, ...]) -> MemoParam | None:
return next((param for param in params if param.is_children), None)
def _get_rest_param(params: tuple[MemoParam, ...]) -> MemoParam | None:
return next((param for param in params if param.is_rest), None)
def _imported_function_var(name: str, return_type: Any) -> FunctionVar:
"""Create the imported FunctionVar for an experimental memo.
Args:
name: The exported function name.
return_type: The return type of the function.
Returns:
The imported FunctionVar.
"""
return FunctionStringVar.create(
name,
_var_type=ReflexCallable[Any, return_type],
_var_data=VarData(
imports={
f"$/{constants.Dirs.COMPONENTS_PATH}/{name}": [ImportVar(tag=name)]
}
),
)
def _component_import_var(name: str) -> Var:
"""Create the imported component var for an experimental memo component.
Args:
name: The exported component name.
Returns:
The component var.
"""
return Var(
name,
_var_type=type[Component],
_var_data=VarData(
imports={
f"$/{constants.Dirs.COMPONENTS_PATH}/{name}": [ImportVar(tag=name)],
"@emotion/react": [ImportVar(tag="jsx")],
}
),
)
def _validate_var_return_expr(return_expr: Var, func_name: str) -> None:
"""Validate that a var-returning memo can compile safely.
Args:
return_expr: The return expression.
func_name: The function name for error messages.
Raises:
TypeError: If the return expression depends on unsupported features.
"""
var_data = VarData.merge(return_expr._get_all_var_data())
if var_data is None:
return
if var_data.hooks:
msg = (
f"Var-returning `@rx._x.memo` `{func_name}` cannot depend on hooks. "
"Use a component-returning `@rx._x.memo` instead."
)
raise TypeError(msg)
if var_data.components:
msg = (
f"Var-returning `@rx._x.memo` `{func_name}` cannot depend on embedded "
"components, custom code, or dynamic imports. Use a component-returning "
"`@rx._x.memo` instead."
)
raise TypeError(msg)
for lib in dict(var_data.imports):
if not lib:
continue
if lib.startswith((".", "/", "$/", "http")):
continue
if format.format_library_name(lib) in bundled_libraries:
continue
msg = (
f"Var-returning `@rx._x.memo` `{func_name}` cannot import `{lib}` because "
"it is not bundled. Use a component-returning `@rx._x.memo` instead."
)
raise TypeError(msg)
def _rest_placeholder(name: str) -> RestProp:
"""Create the placeholder RestProp.
Args:
name: The JavaScript identifier.
Returns:
The placeholder rest prop.
"""
return RestProp(_js_expr=name, _var_type=dict[str, Any])
def _var_placeholder(name: str, annotation: Any) -> Var:
"""Create a placeholder Var for a memo parameter.
Args:
name: The JavaScript identifier.
annotation: The parameter annotation.
Returns:
The placeholder Var.
"""
return Var(_js_expr=name, _var_type=_annotation_inner_type(annotation)).guess_type()
def _placeholder_for_param(param: MemoParam) -> Var:
"""Create a placeholder var for a parameter.
Args:
param: The parameter metadata.
Returns:
The placeholder var.
"""
if param.is_rest:
return _rest_placeholder(param.placeholder_name)
return _var_placeholder(param.placeholder_name, param.annotation)
def _evaluate_memo_function(
fn: Callable[..., Any],
params: tuple[MemoParam, ...],
) -> Any:
"""Evaluate a memo function with placeholder vars.
Args:
fn: The function to evaluate.
params: The memo parameters.
Returns:
The return value from the function.
"""
positional_args = []
keyword_args = {}
for param in params:
placeholder = _placeholder_for_param(param)
if param.kind in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
):
positional_args.append(placeholder)
else:
keyword_args[param.name] = placeholder
return fn(*positional_args, **keyword_args)
def _normalize_component_return(value: Any) -> Component | None:
"""Normalize a component-like memo return value into a Component.
Args:
value: The value returned from the memo function.
Returns:
The normalized component, or ``None`` if the value is not component-like.
"""
if isinstance(value, Component):
return value
if isinstance(value, Var) and type_utils.typehint_issubclass(
value._var_type, Component
):
return Bare.create(value)
return None
def _lift_rest_props(component: Component) -> Component:
"""Convert RestProp children into special props.
Args:
component: The component tree to rewrite.
Returns:
The rewritten component tree.
"""
special_props = list(component.special_props)
rewritten_children = []
for child in component.children:
if isinstance(child, Bare) and isinstance(child.contents, RestProp):
special_props.append(child.contents)
continue
if isinstance(child, Component):
child = _lift_rest_props(child)
rewritten_children.append(child)
component.children = rewritten_children
component.special_props = special_props
return component
def _analyze_params(
fn: Callable[..., Any],
*,
for_component: bool,
) -> tuple[MemoParam, ...]:
"""Analyze and validate memo parameters.
Args:
fn: The function to analyze.
for_component: Whether the memo returns a component.
Returns:
The analyzed parameters.
Raises:
TypeError: If the function signature is not supported.
"""
signature = inspect.signature(fn)
hints = get_type_hints(fn)
params: list[MemoParam] = []
rest_count = 0
for parameter in signature.parameters.values():
if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
msg = f"`@rx._x.memo` does not support `*args` in `{fn.__name__}`."
raise TypeError(msg)
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
msg = f"`@rx._x.memo` does not support `**kwargs` in `{fn.__name__}`."
raise TypeError(msg)
if parameter.kind is inspect.Parameter.POSITIONAL_ONLY:
msg = (
f"`@rx._x.memo` does not support positional-only parameters in "
f"`{fn.__name__}`."
)
raise TypeError(msg)
annotation = hints.get(parameter.name, parameter.annotation)
if annotation is inspect.Parameter.empty:
msg = (
f"All parameters of `{fn.__name__}` must be annotated as `rx.Var[...]` "
f"or `rx.RestProp`. Missing annotation for `{parameter.name}`."
)
raise TypeError(msg)
is_rest = _is_rest_annotation(annotation)
is_children = parameter.name == "children" and _children_annotation_is_valid(
annotation
)
if parameter.name == "children" and not is_children:
msg = (
f"`children` in `{fn.__name__}` must be annotated as "
"`rx.Var[rx.Component]`."
)
raise TypeError(msg)
if not is_rest and not _is_var_annotation(annotation):
msg = (
f"All parameters of `{fn.__name__}` must be annotated as `rx.Var[...]` "
f"or `rx.RestProp`, got `{annotation}` for `{parameter.name}`."
)
raise TypeError(msg)
if is_rest:
rest_count += 1
if rest_count > 1:
msg = (
f"`@rx._x.memo` only supports one `rx.RestProp` in `{fn.__name__}`."
)
raise TypeError(msg)
js_prop_name = format.to_camel_case(parameter.name)
placeholder_name = (
parameter.name
if is_children or is_rest or not for_component
else js_prop_name + CAMEL_CASE_MEMO_MARKER
)
params.append(
MemoParam(
name=parameter.name,
annotation=annotation,
kind=parameter.kind,
default=parameter.default,
js_prop_name=js_prop_name,
placeholder_name=placeholder_name,
is_children=is_children,
is_rest=is_rest,
)
)
return tuple(params)
def _create_function_definition(
fn: Callable[..., Any],
return_annotation: Any,
) -> ExperimentalMemoFunctionDefinition:
"""Create a definition for a var-returning memo.
Args:
fn: The function to analyze.
return_annotation: The return annotation.
Returns:
The function memo definition.
"""
params = _analyze_params(fn, for_component=False)
return_expr = Var.create(_evaluate_memo_function(fn, params))
_validate_var_return_expr(return_expr, fn.__name__)
children_param = _get_children_param(params)
rest_param = _get_rest_param(params)
if children_param is None and rest_param is None:
function = ArgsFunctionOperation.create(
args_names=tuple(param.placeholder_name for param in params),
return_expr=return_expr,
)
else:
function = ArgsFunctionOperation.create(
args_names=(
DestructuredArg(
fields=tuple(
param.placeholder_name for param in params if not param.is_rest
),
rest=(
rest_param.placeholder_name if rest_param is not None else None
),
),
),
return_expr=return_expr,
)
return ExperimentalMemoFunctionDefinition(
fn=fn,
python_name=fn.__name__,
params=params,
function=function,
imported_var=_imported_function_var(
fn.__name__, _annotation_inner_type(return_annotation)
),
)
def _create_component_definition(
fn: Callable[..., Any],
return_annotation: Any,
) -> ExperimentalMemoComponentDefinition:
"""Create a definition for a component-returning memo.
Args:
fn: The function to analyze.
return_annotation: The return annotation.
Returns:
The component memo definition.
Raises:
TypeError: If the function does not return a component.
"""
params = _analyze_params(fn, for_component=True)
component = _normalize_component_return(_evaluate_memo_function(fn, params))
if component is None:
msg = (
f"Component-returning `@rx._x.memo` `{fn.__name__}` must return an "
"`rx.Component` or `rx.Var[rx.Component]`."
)
raise TypeError(msg)
return ExperimentalMemoComponentDefinition(
fn=fn,
python_name=fn.__name__,
params=params,
export_name=format.to_title_case(fn.__name__),
component=_lift_rest_props(component),
)
def _bind_function_runtime_args(
definition: ExperimentalMemoFunctionDefinition,
*args: Any,
**kwargs: Any,
) -> tuple[Any, ...]:
"""Bind runtime args for a var-returning memo.
Args:
definition: The function memo definition.
*args: Positional arguments.
**kwargs: Keyword arguments.
Returns:
The ordered arguments for the imported FunctionVar.
Raises:
TypeError: If the provided arguments are invalid.
"""
children_param = _get_children_param(definition.params)
rest_param = _get_rest_param(definition.params)
# Validate positional children usage and reserved keywords.
if "children" in kwargs:
msg = f"`{definition.python_name}` only accepts children positionally."
raise TypeError(msg)
if rest_param is not None and rest_param.name in kwargs:
msg = (
f"`{definition.python_name}` captures rest props from extra keyword "
f"arguments. Do not pass `{rest_param.name}=` directly."
)
raise TypeError(msg)
if args and children_param is None:
msg = f"`{definition.python_name}` only accepts keyword props."
raise TypeError(msg)
if any(not _is_component_child(child) for child in args):
msg = (
f"`{definition.python_name}` only accepts positional children that are "
"`rx.Component` or `rx.Var[rx.Component]`."
)
raise TypeError(msg)
# Bind declared props before collecting any rest props.
explicit_params = [
param
for param in definition.params
if not param.is_rest and not param.is_children
]
explicit_values = {}
remaining_props = kwargs.copy()
for param in explicit_params:
if param.name in remaining_props:
explicit_values[param.name] = remaining_props.pop(param.name)
elif param.default is not inspect.Parameter.empty:
explicit_values[param.name] = param.default
else:
msg = f"`{definition.python_name}` is missing required prop `{param.name}`."
raise TypeError(msg)
# Reject unknown props unless a rest prop is declared.
if remaining_props and rest_param is None:
unexpected_prop = next(iter(remaining_props))
msg = (
f"`{definition.python_name}` does not accept prop `{unexpected_prop}`. "
"Only declared props may be passed when no `rx.RestProp` is present."
)
raise TypeError(msg)
# Return ordered explicit args when no packed props object is needed.
if children_param is None and rest_param is None:
return tuple(explicit_values[param.name] for param in explicit_params)
# Build the props object passed to the imported FunctionVar.
children_value: Any | None = None
if children_param is not None:
children_value = args[0] if len(args) == 1 else Fragment.create(*args)
# Convert rest-prop keys to camelCase to match component memo behavior.
camel_cased_remaining_props = {
format.to_camel_case(key): value for key, value in remaining_props.items()
}
bound_props = {}
if children_param is not None:
bound_props[children_param.name] = children_value
bound_props.update(explicit_values)
bound_props.update(camel_cased_remaining_props)
return (bound_props,)
def _is_component_child(value: Any) -> bool:
"""Check whether a value is valid as an experimental memo child.
Args:
value: The value to check.
Returns:
Whether the value is a component child.
"""
return isinstance(value, Component) or (
isinstance(value, Var)
and type_utils.typehint_issubclass(value._var_type, Component)
)
class _ExperimentalMemoFunctionWrapper:
"""Callable wrapper for a var-returning experimental memo."""
def __init__(self, definition: ExperimentalMemoFunctionDefinition):
"""Initialize the wrapper.
Args:
definition: The function memo definition.
"""
self._definition = definition
self._imported_var = definition.imported_var
update_wrapper(self, definition.fn)
def __call__(self, *args: Any, **kwargs: Any) -> Var:
"""Call the wrapped memo and return a var.
Args:
*args: Positional children, if supported.
**kwargs: Explicit props and rest props.
Returns:
The function call var.
"""
return self.call(*args, **kwargs)
def call(self, *args: Any, **kwargs: Any) -> Var:
"""Call the imported memo function.
Args:
*args: Positional children, if supported.
**kwargs: Explicit props and rest props.
Returns:
The function call var.
"""
return self._imported_var.call(
*_bind_function_runtime_args(self._definition, *args, **kwargs)
)
def partial(self, *args: Any, **kwargs: Any) -> FunctionVar:
"""Partially apply the imported memo function.
Args:
*args: Positional children, if supported.
**kwargs: Explicit props and rest props.
Returns:
The partially applied function var.
"""
return self._imported_var.partial(
*_bind_function_runtime_args(self._definition, *args, **kwargs)
)
def _as_var(self) -> FunctionVar:
"""Expose the imported function var.
Returns:
The imported function var.
"""
return self._imported_var
class _ExperimentalMemoComponentWrapper:
"""Callable wrapper for a component-returning experimental memo."""
def __init__(self, definition: ExperimentalMemoComponentDefinition):
"""Initialize the wrapper.
Args:
definition: The component memo definition.
"""
self._definition = definition
self._children_param = _get_children_param(definition.params)
self._rest_param = _get_rest_param(definition.params)
self._explicit_params = [
param
for param in definition.params
if not param.is_children and not param.is_rest
]
update_wrapper(self, definition.fn)
def __call__(self, *children: Any, **props: Any) -> ExperimentalMemoComponent:
"""Call the wrapped memo and return a component.
Args:
*children: Positional children passed to the memo.
**props: Explicit props and rest props.
Returns:
The rendered memo component.
"""
definition = self._definition
rest_param = self._rest_param
# Validate positional children usage and reserved keywords.
if "children" in props:
msg = f"`{definition.python_name}` only accepts children positionally."
raise TypeError(msg)
if rest_param is not None and rest_param.name in props:
msg = (
f"`{definition.python_name}` captures rest props from extra keyword "
f"arguments. Do not pass `{rest_param.name}=` directly."
)
raise TypeError(msg)
if children and self._children_param is None:
msg = f"`{definition.python_name}` only accepts keyword props."
raise TypeError(msg)
if any(not _is_component_child(child) for child in children):
msg = (
f"`{definition.python_name}` only accepts positional children that are "
"`rx.Component` or `rx.Var[rx.Component]`."
)
raise TypeError(msg)
# Bind declared props before collecting any rest props.
explicit_values = {}
remaining_props = props.copy()
for param in self._explicit_params:
if param.name in remaining_props:
explicit_values[param.name] = remaining_props.pop(param.name)
elif param.default is not inspect.Parameter.empty:
explicit_values[param.name] = param.default
else:
msg = f"`{definition.python_name}` is missing required prop `{param.name}`."
raise TypeError(msg)
# Reject unknown props unless a rest prop is declared.
if remaining_props and rest_param is None:
unexpected_prop = next(iter(remaining_props))
msg = (
f"`{definition.python_name}` does not accept prop `{unexpected_prop}`. "
"Only declared props may be passed when no `rx.RestProp` is present."
)
raise TypeError(msg)
# Build the component props passed into the memo wrapper.
return _get_experimental_memo_component_class(
definition.export_name, type(definition.component)
)._create(
children=list(children),
memo_definition=definition,
**explicit_values,
**remaining_props,
)
def _as_var(self) -> Var:
"""Expose the imported component var.
Returns:
The imported component var.
"""
return _component_import_var(self._definition.export_name)
def _create_function_wrapper(
definition: ExperimentalMemoFunctionDefinition,
) -> _ExperimentalMemoFunctionWrapper:
"""Create the Python wrapper for a var-returning memo.
Args:
definition: The function memo definition.
Returns:
The wrapper callable.
"""
return _ExperimentalMemoFunctionWrapper(definition)
def _create_component_wrapper(
definition: ExperimentalMemoComponentDefinition,
) -> _ExperimentalMemoComponentWrapper:
"""Create the Python wrapper for a component-returning memo.
Args:
definition: The component memo definition.
Returns:
The wrapper callable.
"""
return _ExperimentalMemoComponentWrapper(definition)
def create_passthrough_component_memo(
component: Component,
) -> tuple[
Callable[..., ExperimentalMemoComponent],
ExperimentalMemoComponentDefinition,
]:
"""Create an unregistered ``@rx._x.memo``-style passthrough component memo.
This is used by compiler auto-memoization so generated wrappers compile
through the experimental memo pipeline instead of emitting ad-hoc page-local
``React.memo`` declarations.
The exported memo name is derived from ``component._compute_memo_tag()``
after the ``{children}`` hole has been substituted into the wrapped
component's children (passthrough mode), so two call-sites differing only
in their children — whose generated memo bodies are identical — collapse
to one wrapper.
Args:
component: The component to wrap.
Returns:
The callable memo wrapper and its component definition.
"""
# Snapshot-boundary components (see ``is_snapshot_boundary``) own their
# subtree — the ``.children`` slot is internal machinery from the
# subclass's ``.create`` (e.g. the dropzone Div built inside
# ``Upload.create``), not a user content hole. The memoize plugin wraps
# the boundary with no structural children on the page side, so the memo
# body renders the full snapshot rather than a ``{children}``-holed
# template.
render_snapshot = (
get_memoization_strategy(component) is MemoizationStrategy.SNAPSHOT
)
captured_hole_child: list[Component] = []
def passthrough(children: Var[Component]) -> Component:
new_component = copy(component)
if render_snapshot:
return new_component
hole_bare = Bare.create(children)
captured_hole_child.append(hole_bare)
# Substitute the ``{children}`` hole for the original descendants so
# the memo body's hash and JSX both reflect the placeholder, not the
# specific children at any given call site. Original descendants stay
# reachable on the page-level wrapper via the plugin's
# ``_get_all_refs`` delegation back to the source component.
new_component.children = [hole_bare]
# Compile-time walkers that need the real subtree (notably
# ``Form._get_form_refs`` collecting id-based input refs into the
# generated ``handleSubmit`` JS) call ``self._get_all_refs()`` while
# the memo body's hooks are computed. With the hole substituted in,
# that walk would return nothing and the form handler would emit an
# empty ``field_ref_mapping``. Delegate ref collection back to the
# source component so descendants behind the hole remain visible.
object.__setattr__(new_component, "_get_all_refs", component._get_all_refs)
return new_component
# Evaluate once to compute the tag from the rendered memo body shape.
# ``_create_component_definition`` will evaluate again internally; the
# second pass overwrites ``captured_hole_child`` but the captured value
# is identical.
params = _analyze_params(passthrough, for_component=True)
preview = _normalize_component_return(_evaluate_memo_function(passthrough, params))
if preview is None:
msg = (
"`create_passthrough_component_memo` requires a component that "
"normalizes to `rx.Component`."
)
raise TypeError(msg)
tag = preview._compute_memo_tag()
passthrough.__name__ = format.to_snake_case(tag)
passthrough.__qualname__ = passthrough.__name__
passthrough.__module__ = __name__
definition = _create_component_definition(passthrough, Component)
replacements: dict[str, Any] = {}
if definition.export_name != tag:
replacements["export_name"] = tag
if captured_hole_child:
replacements["passthrough_hole_child"] = captured_hole_child[0]
if replacements:
definition = dataclasses.replace(definition, **replacements)
return _create_component_wrapper(definition), definition
def memo(fn: Callable[..., Any]) -> Callable[..., Any]:
"""Create an experimental memo from a function.
Args:
fn: The function to memoize.
Returns:
The wrapped function or component factory.
Raises:
TypeError: If the return type is not supported.
"""
hints = get_type_hints(fn)
return_annotation = hints.get("return", inspect.Signature.empty)
if return_annotation is inspect.Signature.empty:
msg = (
f"`@rx._x.memo` requires a return annotation on `{fn.__name__}`. "
"Use `-> rx.Component` or `-> rx.Var[...]`."
)
raise TypeError(msg)
if _is_component_annotation(return_annotation):
definition = _create_component_definition(fn, return_annotation)
_register_memo_definition(definition)
return _create_component_wrapper(definition)
if _is_var_annotation(return_annotation):
definition = _create_function_definition(fn, return_annotation)
_register_memo_definition(definition)
return _create_function_wrapper(definition)
msg = (
f"`@rx._x.memo` on `{fn.__name__}` must return `rx.Component` or `rx.Var[...]`, "
f"got `{return_annotation}`."
)
raise TypeError(msg)
__all__ = [
"EXPERIMENTAL_MEMOS",
"ExperimentalMemoComponent",
"ExperimentalMemoComponentDefinition",
"ExperimentalMemoDefinition",
"ExperimentalMemoFunctionDefinition",
"create_passthrough_component_memo",
"memo",
]