match.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. """rx.match."""
  2. from __future__ import annotations
  3. import textwrap
  4. from typing import Any, Dict, List, Optional, Tuple, Union
  5. from reflex.components.base import Fragment
  6. from reflex.components.component import BaseComponent, Component, MemoizationLeaf
  7. from reflex.components.core.colors import Color
  8. from reflex.components.tags import MatchTag, Tag
  9. from reflex.style import Style
  10. from reflex.utils import format, imports, types
  11. from reflex.utils.exceptions import MatchTypeError
  12. from reflex.vars import BaseVar, Var, VarData
  13. class Match(MemoizationLeaf):
  14. """Match cases based on a condition."""
  15. # The condition to determine which case to match.
  16. cond: Var[Any]
  17. # The list of match cases to be matched.
  18. match_cases: List[Any] = []
  19. # The catchall case to match.
  20. default: Any
  21. @classmethod
  22. def create(cls, cond: Any, *cases) -> Union[Component, BaseVar]:
  23. """Create a Match Component.
  24. Args:
  25. cond: The condition to determine which case to match.
  26. cases: This list of cases to match.
  27. Returns:
  28. The match component.
  29. Raises:
  30. ValueError: When a default case is not provided for cases with Var return types.
  31. """
  32. match_cond_var = cls._create_condition_var(cond)
  33. cases, default = cls._process_cases(list(cases))
  34. match_cases = cls._process_match_cases(cases)
  35. cls._validate_return_types(match_cases)
  36. if default is None and types._issubclass(type(match_cases[0][-1]), BaseVar):
  37. raise ValueError(
  38. "For cases with return types as Vars, a default case must be provided"
  39. )
  40. return cls._create_match_cond_var_or_component(
  41. match_cond_var, match_cases, default
  42. )
  43. @classmethod
  44. def _create_condition_var(cls, cond: Any) -> BaseVar:
  45. """Convert the condition to a Var.
  46. Args:
  47. cond: The condition.
  48. Returns:
  49. The condition as a base var
  50. Raises:
  51. ValueError: If the condition is not provided.
  52. """
  53. match_cond_var = Var.create(cond, _var_is_string=isinstance(cond, str))
  54. if match_cond_var is None:
  55. raise ValueError("The condition must be set")
  56. return match_cond_var # type: ignore
  57. @classmethod
  58. def _process_cases(
  59. cls, cases: List
  60. ) -> Tuple[List, Optional[Union[BaseVar, BaseComponent]]]:
  61. """Process the list of match cases and the catchall default case.
  62. Args:
  63. cases: The list of match cases.
  64. Returns:
  65. The default case and the list of match case tuples.
  66. Raises:
  67. ValueError: If there are multiple default cases.
  68. """
  69. default = None
  70. if len([case for case in cases if not isinstance(case, tuple)]) > 1:
  71. raise ValueError("rx.match can only have one default case.")
  72. # Get the default case which should be the last non-tuple arg
  73. if not isinstance(cases[-1], tuple):
  74. default = cases.pop()
  75. default = (
  76. cls._create_case_var_with_var_data(default)
  77. if not isinstance(default, BaseComponent)
  78. else default
  79. )
  80. return cases, default # type: ignore
  81. @classmethod
  82. def _create_case_var_with_var_data(cls, case_element):
  83. """Convert a case element into a Var.If the case
  84. is a Style type, we extract the var data and merge it with the
  85. newly created Var.
  86. Args:
  87. case_element: The case element.
  88. Returns:
  89. The case element Var.
  90. """
  91. _var_data = case_element._var_data if isinstance(case_element, Style) else None # type: ignore
  92. case_element = Var.create(
  93. case_element,
  94. _var_is_string=isinstance(case_element, (str, Color)),
  95. )
  96. if _var_data is not None:
  97. case_element._var_data = VarData.merge(case_element._var_data, _var_data) # type: ignore
  98. return case_element
  99. @classmethod
  100. def _process_match_cases(cls, cases: List) -> List[List[BaseVar]]:
  101. """Process the individual match cases.
  102. Args:
  103. cases: The match cases.
  104. Returns:
  105. The processed match cases.
  106. Raises:
  107. ValueError: If the default case is not the last case or the tuple elements are less than 2.
  108. """
  109. match_cases = []
  110. for case in cases:
  111. if not isinstance(case, tuple):
  112. raise ValueError(
  113. "rx.match should have tuples of cases and a default case as the last argument."
  114. )
  115. # There should be at least two elements in a case tuple(a condition and return value)
  116. if len(case) < 2:
  117. raise ValueError(
  118. "A case tuple should have at least a match case element and a return value."
  119. )
  120. case_list = []
  121. for element in case:
  122. # convert all non component element to vars.
  123. el = (
  124. cls._create_case_var_with_var_data(element)
  125. if not isinstance(element, BaseComponent)
  126. else element
  127. )
  128. if not isinstance(el, (BaseVar, BaseComponent)):
  129. raise ValueError("Case element must be a var or component")
  130. case_list.append(el)
  131. match_cases.append(case_list)
  132. return match_cases
  133. @classmethod
  134. def _validate_return_types(cls, match_cases: List[List[BaseVar]]) -> None:
  135. """Validate that match cases have the same return types.
  136. Args:
  137. match_cases: The match cases.
  138. Raises:
  139. MatchTypeError: If the return types of cases are different.
  140. """
  141. first_case_return = match_cases[0][-1]
  142. return_type = type(first_case_return)
  143. if types._isinstance(first_case_return, BaseComponent):
  144. return_type = BaseComponent
  145. elif types._isinstance(first_case_return, BaseVar):
  146. return_type = BaseVar
  147. for index, case in enumerate(match_cases):
  148. if not types._issubclass(type(case[-1]), return_type):
  149. raise MatchTypeError(
  150. f"Match cases should have the same return types. Case {index} with return "
  151. f"value `{case[-1]._var_name if isinstance(case[-1], BaseVar) else textwrap.shorten(str(case[-1]), width=250)}`"
  152. f" of type {type(case[-1])!r} is not {return_type}"
  153. )
  154. @classmethod
  155. def _create_match_cond_var_or_component(
  156. cls,
  157. match_cond_var: Var,
  158. match_cases: List[List[BaseVar]],
  159. default: Optional[Union[BaseVar, BaseComponent]],
  160. ) -> Union[Component, BaseVar]:
  161. """Create and return the match condition var or component.
  162. Args:
  163. match_cond_var: The match condition.
  164. match_cases: The list of match cases.
  165. default: The default case.
  166. Returns:
  167. The match component wrapped in a fragment or the match var.
  168. Raises:
  169. ValueError: If the return types are not vars when creating a match var for Var types.
  170. """
  171. if default is None and types._issubclass(
  172. type(match_cases[0][-1]), BaseComponent
  173. ):
  174. default = Fragment.create()
  175. if types._issubclass(type(match_cases[0][-1]), BaseComponent):
  176. return Fragment.create(
  177. cls(
  178. cond=match_cond_var,
  179. match_cases=match_cases,
  180. default=default,
  181. children=[case[-1] for case in match_cases] + [default], # type: ignore
  182. )
  183. )
  184. # Validate the match cases (as well as the default case) to have Var return types.
  185. if any(
  186. case for case in match_cases if not types._isinstance(case[-1], BaseVar)
  187. ) or not types._isinstance(default, BaseVar):
  188. raise ValueError("Return types of match cases should be Vars.")
  189. # match cases and default should all be Vars at this point.
  190. # Retrieve var data of every var in the match cases and default.
  191. var_data = [
  192. *[el._var_data for case in match_cases for el in case],
  193. default._var_data, # type: ignore
  194. ]
  195. return match_cond_var._replace(
  196. _var_name=format.format_match(
  197. cond=match_cond_var._var_name_unwrapped,
  198. match_cases=match_cases, # type: ignore
  199. default=default, # type: ignore
  200. ),
  201. _var_type=default._var_type, # type: ignore
  202. _var_is_local=False,
  203. _var_full_name_needs_state_prefix=False,
  204. _var_is_string=False,
  205. merge_var_data=VarData.merge(*var_data),
  206. )
  207. def _render(self) -> Tag:
  208. return MatchTag(
  209. cond=self.cond, match_cases=self.match_cases, default=self.default
  210. )
  211. def render(self) -> Dict:
  212. """Render the component.
  213. Returns:
  214. The dictionary for template of component.
  215. """
  216. tag = self._render()
  217. tag.name = "match"
  218. return dict(tag)
  219. def _get_imports_list(self) -> list[imports.ImportVar]:
  220. return [
  221. *super()._get_imports_list(),
  222. *getattr(self.cond._var_data, "imports", []),
  223. ]
  224. def _apply_theme(self, theme: Component):
  225. """Apply the theme to this component.
  226. Args:
  227. theme: The theme to apply.
  228. """
  229. # apply theme to return components.
  230. for match_case in self.match_cases:
  231. if isinstance(match_case[-1], Component):
  232. match_case[-1].apply_theme(theme)
  233. # apply theme to default component
  234. if isinstance(self.default, Component):
  235. self.default.apply_theme(theme)