foreach.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """Create a list of components from an iterable."""
  2. from __future__ import annotations
  3. import functools
  4. import inspect
  5. from collections.abc import Callable, Iterable
  6. from typing import Any
  7. from reflex.components.base.fragment import Fragment
  8. from reflex.components.component import Component
  9. from reflex.components.core.cond import cond
  10. from reflex.components.tags import IterTag
  11. from reflex.constants import MemoizationMode
  12. from reflex.state import ComponentState
  13. from reflex.utils import types
  14. from reflex.utils.exceptions import UntypedVarError
  15. from reflex.vars.base import LiteralVar, Var
  16. class ForeachVarError(TypeError):
  17. """Raised when the iterable type is Any."""
  18. class ForeachRenderError(TypeError):
  19. """Raised when there is an error with the foreach render function."""
  20. class Foreach(Component):
  21. """A component that takes in an iterable and a render function and renders a list of components."""
  22. _memoization_mode = MemoizationMode(recursive=False)
  23. # The iterable to create components from.
  24. iterable: Var[Iterable]
  25. # A function from the render args to the component.
  26. render_fn: Callable = Fragment.create
  27. @classmethod
  28. def create(
  29. cls,
  30. iterable: Var[Iterable] | Iterable,
  31. render_fn: Callable,
  32. ) -> Foreach:
  33. """Create a foreach component.
  34. Args:
  35. iterable: The iterable to create components from.
  36. render_fn: A function from the render args to the component.
  37. Returns:
  38. The foreach component.
  39. Raises:
  40. ForeachVarError: If the iterable is of type Any.
  41. TypeError: If the render function is a ComponentState.
  42. UntypedVarError: If the iterable is of type Any without a type annotation.
  43. # noqa: DAR401 with_traceback
  44. # noqa: DAR402 UntypedVarError
  45. """
  46. from reflex.vars import ArrayVar, ObjectVar, StringVar
  47. iterable = (
  48. LiteralVar.create(iterable).guess_type()
  49. if not isinstance(iterable, Var)
  50. else iterable.guess_type()
  51. )
  52. if iterable._var_type == Any:
  53. raise ForeachVarError(
  54. f"Could not foreach over var `{iterable!s}` of type Any. "
  55. "(If you are trying to foreach over a state var, add a type annotation to the var). "
  56. "See https://reflex.dev/docs/library/dynamic-rendering/foreach/"
  57. )
  58. if (
  59. hasattr(render_fn, "__qualname__")
  60. and render_fn.__qualname__ == ComponentState.create.__qualname__
  61. ):
  62. raise TypeError(
  63. "Using a ComponentState as `render_fn` inside `rx.foreach` is not supported yet."
  64. )
  65. if isinstance(iterable, ObjectVar):
  66. iterable = iterable.entries()
  67. if isinstance(iterable, StringVar):
  68. iterable = iterable.split()
  69. if not isinstance(iterable, ArrayVar):
  70. raise ForeachVarError(
  71. f"Could not foreach over var `{iterable!s}` of type {iterable._var_type}. "
  72. "See https://reflex.dev/docs/library/dynamic-rendering/foreach/"
  73. )
  74. if types.is_optional(iterable._var_type):
  75. iterable = cond(iterable, iterable, [])
  76. component = cls._create(
  77. children=[],
  78. iterable=iterable,
  79. render_fn=render_fn,
  80. )
  81. try:
  82. # Keep a ref to a rendered component to determine correct imports/hooks/styles.
  83. component.children = [component._render().render_component()]
  84. except UntypedVarError as e:
  85. raise UntypedVarError(
  86. iterable,
  87. "foreach",
  88. "https://reflex.dev/docs/library/dynamic-rendering/foreach/",
  89. ).with_traceback(e.__traceback__) from None
  90. return component
  91. def _render(self) -> IterTag:
  92. props = {}
  93. render_sig = inspect.signature(self.render_fn)
  94. params = list(render_sig.parameters.values())
  95. # Validate the render function signature.
  96. if len(params) == 0 or len(params) > 2:
  97. raise ForeachRenderError(
  98. "Expected 1 or 2 parameters in foreach render function, got "
  99. f"{[p.name for p in params]}. See "
  100. "https://reflex.dev/docs/library/dynamic-rendering/foreach/"
  101. )
  102. if len(params) >= 1:
  103. # Determine the arg var name based on the params accepted by render_fn.
  104. props["arg_var_name"] = params[0].name
  105. if len(params) == 2:
  106. # Determine the index var name based on the params accepted by render_fn.
  107. props["index_var_name"] = params[1].name
  108. else:
  109. render_fn = self.render_fn
  110. # Otherwise, use a deterministic index, based on the render function bytecode.
  111. code_hash = (
  112. hash(
  113. getattr(
  114. render_fn,
  115. "__code__",
  116. (
  117. repr(self.render_fn)
  118. if not isinstance(render_fn, functools.partial)
  119. else render_fn.func.__code__
  120. ),
  121. )
  122. )
  123. .to_bytes(
  124. length=8,
  125. byteorder="big",
  126. signed=True,
  127. )
  128. .hex()
  129. )
  130. props["index_var_name"] = f"index_{code_hash}"
  131. return IterTag(
  132. iterable=self.iterable,
  133. render_fn=self.render_fn,
  134. children=self.children,
  135. **props,
  136. )
  137. def render(self):
  138. """Render the component.
  139. Returns:
  140. The dictionary for template of component.
  141. """
  142. tag = self._render()
  143. return dict(
  144. tag,
  145. iterable_state=str(tag.iterable),
  146. arg_name=tag.arg_var_name,
  147. arg_index=tag.get_index_var_arg(),
  148. )
  149. foreach = Foreach.create