reflex/reflex/components/core/foreach.py
Masen Furer d767dc5fc7
[REF-2802] Foreach should respect modifications to children (#3263)
* Unit tests for add_style and component styles with foreach

The styles should be correctly applied for components that are rendered as part
of a foreach.

* [REF-2802] Foreach should respect modifications to children

Components are mutable, and there is logic that depends on walking through the
component tree and making modifications to components along the way. These
modifications _must_ be respected by foreach for consistency.

Modifications necessary to fix the bug:

* Change the hash function in `_render` to get a hash over the render_fn's
  `__code__` object. This way we get a stable hash without having to call the
  render function with bogus values.
* Call the render function once during `create` and save the result as a child
  of the Foreach component (tree walks will modify this instance).
* Directly render the original (and possibly modified) child component instead
  of calling the render_fn again and creating a new component instance at
  render time.

Additional changes because they're nice:

* Deprecate passing `**props` to `rx.foreach`. No one should have been
  doing this anyway, because it just does not work in any reasonable way.
* Raise `ForeachVarError` when the iterable type is Any
* Raise `ForeachRenderError` when the render function does not take 1 or 2 args.
* Link to the foreach component docs when either of those errors are hit.
* Change the `iterable` arg in `create` to accept `Var[Iterable] | Iterable`
  for better typing support (and remove some type: ignore comments)
* Simplify `_render` and `render` methods -- remove unused and potentially
  confusing code.

* Fixup: `to_bytes` requires `byteorder` arg before py3.11
2024-05-09 15:04:26 -07:00

131 lines
4.2 KiB
Python

"""Create a list of components from an iterable."""
from __future__ import annotations
import inspect
from typing import Any, Callable, Iterable
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.tags import IterTag
from reflex.constants import MemoizationMode
from reflex.utils import console
from reflex.vars import Var
class ForeachVarError(TypeError):
"""Raised when the iterable type is Any."""
class ForeachRenderError(TypeError):
"""Raised when there is an error with the foreach render function."""
class Foreach(Component):
"""A component that takes in an iterable and a render function and renders a list of components."""
_memoization_mode = MemoizationMode(recursive=False)
# The iterable to create components from.
iterable: Var[Iterable]
# A function from the render args to the component.
render_fn: Callable = Fragment.create
@classmethod
def create(
cls,
iterable: Var[Iterable] | Iterable,
render_fn: Callable,
**props,
) -> Foreach:
"""Create a foreach component.
Args:
iterable: The iterable to create components from.
render_fn: A function from the render args to the component.
**props: The attributes to pass to each child component (deprecated).
Returns:
The foreach component.
Raises:
ForeachVarError: If the iterable is of type Any.
"""
if props:
console.deprecate(
feature_name="Passing props to rx.foreach",
reason="it does not have the intended effect and may be confusing",
deprecation_version="0.5.0",
removal_version="0.6.0",
)
iterable = Var.create_safe(iterable)
if iterable._var_type == Any:
raise ForeachVarError(
f"Could not foreach over var `{iterable._var_full_name}` of type Any. "
"(If you are trying to foreach over a state var, add a type annotation to the var). "
"See https://reflex.dev/docs/library/layout/foreach/"
)
component = cls(
iterable=iterable,
render_fn=render_fn,
)
# Keep a ref to a rendered component to determine correct imports/hooks/styles.
component.children = [component._render().render_component()]
return component
def _render(self) -> IterTag:
props = {}
render_sig = inspect.signature(self.render_fn)
params = list(render_sig.parameters.values())
# Validate the render function signature.
if len(params) == 0 or len(params) > 2:
raise ForeachRenderError(
"Expected 1 or 2 parameters in foreach render function, got "
f"{[p.name for p in params]}. See https://reflex.dev/docs/library/layout/foreach/"
)
if len(params) >= 1:
# Determine the arg var name based on the params accepted by render_fn.
props["arg_var_name"] = params[0].name
if len(params) == 2:
# Determine the index var name based on the params accepted by render_fn.
props["index_var_name"] = params[1].name
else:
# Otherwise, use a deterministic index, based on the render function bytecode.
code_hash = (
hash(self.render_fn.__code__)
.to_bytes(
length=8,
byteorder="big",
signed=True,
)
.hex()
)
props["index_var_name"] = f"index_{code_hash}"
return IterTag(
iterable=self.iterable,
render_fn=self.render_fn,
children=self.children,
**props,
)
def render(self):
"""Render the component.
Returns:
The dictionary for template of component.
"""
tag = self._render()
return dict(
tag,
iterable_state=tag.iterable._var_full_name,
arg_name=tag.arg_var_name,
arg_index=tag.get_index_var_arg(),
iterable_type=tag.iterable._var_type.mro()[0].__name__,
)