foreach.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """Create a list of components from an iterable."""
  2. from __future__ import annotations
  3. import inspect
  4. from typing import Any, Callable, Iterable
  5. from reflex.components.base.fragment import Fragment
  6. from reflex.components.component import Component
  7. from reflex.components.tags import IterTag
  8. from reflex.constants import MemoizationMode
  9. from reflex.state import ComponentState
  10. from reflex.utils import console
  11. from reflex.vars import Var
  12. class ForeachVarError(TypeError):
  13. """Raised when the iterable type is Any."""
  14. class ForeachRenderError(TypeError):
  15. """Raised when there is an error with the foreach render function."""
  16. class Foreach(Component):
  17. """A component that takes in an iterable and a render function and renders a list of components."""
  18. _memoization_mode = MemoizationMode(recursive=False)
  19. # The iterable to create components from.
  20. iterable: Var[Iterable]
  21. # A function from the render args to the component.
  22. render_fn: Callable = Fragment.create
  23. @classmethod
  24. def create(
  25. cls,
  26. iterable: Var[Iterable] | Iterable,
  27. render_fn: Callable,
  28. **props,
  29. ) -> Foreach:
  30. """Create a foreach component.
  31. Args:
  32. iterable: The iterable to create components from.
  33. render_fn: A function from the render args to the component.
  34. **props: The attributes to pass to each child component (deprecated).
  35. Returns:
  36. The foreach component.
  37. Raises:
  38. ForeachVarError: If the iterable is of type Any.
  39. TypeError: If the render function is a ComponentState.
  40. """
  41. if props:
  42. console.deprecate(
  43. feature_name="Passing props to rx.foreach",
  44. reason="it does not have the intended effect and may be confusing",
  45. deprecation_version="0.5.0",
  46. removal_version="0.6.0",
  47. )
  48. iterable = Var.create_safe(iterable)
  49. if iterable._var_type == Any:
  50. raise ForeachVarError(
  51. f"Could not foreach over var `{iterable._var_full_name}` of type Any. "
  52. "(If you are trying to foreach over a state var, add a type annotation to the var). "
  53. "See https://reflex.dev/docs/library/layout/foreach/"
  54. )
  55. if (
  56. hasattr(render_fn, "__qualname__")
  57. and render_fn.__qualname__ == ComponentState.create.__qualname__
  58. ):
  59. raise TypeError(
  60. "Using a ComponentState as `render_fn` inside `rx.foreach` is not supported yet."
  61. )
  62. component = cls(
  63. iterable=iterable,
  64. render_fn=render_fn,
  65. )
  66. # Keep a ref to a rendered component to determine correct imports/hooks/styles.
  67. component.children = [component._render().render_component()]
  68. return component
  69. def _render(self) -> IterTag:
  70. props = {}
  71. render_sig = inspect.signature(self.render_fn)
  72. params = list(render_sig.parameters.values())
  73. # Validate the render function signature.
  74. if len(params) == 0 or len(params) > 2:
  75. raise ForeachRenderError(
  76. "Expected 1 or 2 parameters in foreach render function, got "
  77. f"{[p.name for p in params]}. See https://reflex.dev/docs/library/layout/foreach/"
  78. )
  79. if len(params) >= 1:
  80. # Determine the arg var name based on the params accepted by render_fn.
  81. props["arg_var_name"] = params[0].name
  82. if len(params) == 2:
  83. # Determine the index var name based on the params accepted by render_fn.
  84. props["index_var_name"] = params[1].name
  85. else:
  86. # Otherwise, use a deterministic index, based on the render function bytecode.
  87. code_hash = (
  88. hash(self.render_fn.__code__)
  89. .to_bytes(
  90. length=8,
  91. byteorder="big",
  92. signed=True,
  93. )
  94. .hex()
  95. )
  96. props["index_var_name"] = f"index_{code_hash}"
  97. return IterTag(
  98. iterable=self.iterable,
  99. render_fn=self.render_fn,
  100. children=self.children,
  101. **props,
  102. )
  103. def render(self):
  104. """Render the component.
  105. Returns:
  106. The dictionary for template of component.
  107. """
  108. tag = self._render()
  109. return dict(
  110. tag,
  111. iterable_state=tag.iterable._var_full_name,
  112. arg_name=tag.arg_var_name,
  113. arg_index=tag.get_index_var_arg(),
  114. iterable_type=tag.iterable._var_type.mro()[0].__name__,
  115. )