match.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """rx.match."""
  2. import textwrap
  3. from typing import Any, List, cast
  4. from typing_extensions import Unpack
  5. from reflex.components.base import Fragment
  6. from reflex.components.component import BaseComponent, Component, MemoizationLeaf
  7. from reflex.utils import types
  8. from reflex.utils.exceptions import MatchTypeError
  9. from reflex.vars.base import VAR_TYPE, Var
  10. from reflex.vars.number import MatchOperation
  11. CASE_TYPE = tuple[Unpack[tuple[Any, ...]], Var[VAR_TYPE] | VAR_TYPE]
  12. class Match(MemoizationLeaf):
  13. """Match cases based on a condition."""
  14. # The condition to determine which case to match.
  15. cond: Var[Any]
  16. # The list of match cases to be matched.
  17. match_cases: List[Any] = []
  18. # The catchall case to match.
  19. default: Any
  20. @classmethod
  21. def create(
  22. cls,
  23. cond: Any,
  24. *cases: Unpack[
  25. tuple[Unpack[tuple[CASE_TYPE[VAR_TYPE], ...]], Var[VAR_TYPE] | VAR_TYPE]
  26. ],
  27. ) -> Var[VAR_TYPE]:
  28. """Create a Match Component.
  29. Args:
  30. cond: The condition to determine which case to match.
  31. cases: This list of cases to match.
  32. Returns:
  33. The match component.
  34. Raises:
  35. ValueError: When a default case is not provided for cases with Var return types.
  36. """
  37. default = None
  38. if len([case for case in cases if not isinstance(case, tuple)]) > 1:
  39. raise ValueError("rx.match can only have one default case.")
  40. if not cases:
  41. raise ValueError("rx.match should have at least one case.")
  42. # Get the default case which should be the last non-tuple arg
  43. if not isinstance(cases[-1], tuple):
  44. default = cases[-1]
  45. actual_cases = cases[:-1]
  46. else:
  47. actual_cases = cast(tuple[CASE_TYPE[VAR_TYPE], ...], cases)
  48. cls._process_match_cases(actual_cases)
  49. cls._validate_return_types(actual_cases)
  50. if default is None and any(
  51. not (
  52. isinstance((return_type := case[-1]), Component)
  53. or (
  54. isinstance(return_type, Var)
  55. and types.typehint_issubclass(return_type._var_type, Component)
  56. )
  57. )
  58. for case in actual_cases
  59. ):
  60. raise ValueError(
  61. "For cases with return types as Vars, a default case must be provided"
  62. )
  63. elif default is None:
  64. default = Fragment.create()
  65. default = cast(Var[VAR_TYPE] | VAR_TYPE, default)
  66. return cls._create_match_cond_var_or_component(
  67. cond,
  68. actual_cases,
  69. default,
  70. )
  71. @classmethod
  72. def _process_match_cases(cls, cases: tuple[CASE_TYPE[VAR_TYPE], ...]):
  73. """Process the individual match cases.
  74. Args:
  75. cases: The match cases.
  76. Returns:
  77. The processed match cases.
  78. Raises:
  79. ValueError: If the default case is not the last case or the tuple elements are less than 2.
  80. """
  81. for case in cases:
  82. if not isinstance(case, tuple):
  83. raise ValueError(
  84. "rx.match should have tuples of cases and a default case as the last argument."
  85. )
  86. # There should be at least two elements in a case tuple(a condition and return value)
  87. if len(case) < 2:
  88. raise ValueError(
  89. "A case tuple should have at least a match case element and a return value."
  90. )
  91. @classmethod
  92. def _validate_return_types(
  93. cls, match_cases: tuple[CASE_TYPE[VAR_TYPE], ...]
  94. ) -> None:
  95. """Validate that match cases have the same return types.
  96. Args:
  97. match_cases: The match cases.
  98. Raises:
  99. MatchTypeError: If the return types of cases are different.
  100. """
  101. first_case_return = match_cases[0][-1]
  102. return_type = type(first_case_return)
  103. if types._isinstance(first_case_return, BaseComponent):
  104. return_type = BaseComponent
  105. elif types._isinstance(first_case_return, Var):
  106. return_type = Var
  107. for index, case in enumerate(match_cases):
  108. if not (
  109. types._issubclass(type(case[-1]), return_type)
  110. or (
  111. isinstance(case[-1], Var)
  112. and types.typehint_issubclass(case[-1]._var_type, return_type)
  113. )
  114. ):
  115. raise MatchTypeError(
  116. f"Match cases should have the same return types. Case {index} with return "
  117. f"value `{case[-1]._js_expr if isinstance(case[-1], Var) else textwrap.shorten(str(case[-1]), width=250)}`"
  118. f" of type {(type(case[-1]) if not isinstance(case[-1], Var) else case[-1]._var_type)!r} is not {return_type}"
  119. )
  120. @classmethod
  121. def _create_match_cond_var_or_component(
  122. cls,
  123. match_cond_var: Var,
  124. match_cases: tuple[CASE_TYPE[VAR_TYPE], ...],
  125. default: VAR_TYPE | Var[VAR_TYPE],
  126. ) -> Var[VAR_TYPE]:
  127. """Create and return the match condition var or component.
  128. Args:
  129. match_cond_var: The match condition.
  130. match_cases: The list of match cases.
  131. default: The default case.
  132. Returns:
  133. The match component wrapped in a fragment or the match var.
  134. Raises:
  135. ValueError: If the return types are not vars when creating a match var for Var types.
  136. """
  137. return MatchOperation.create(match_cond_var, match_cases, default)
  138. match = Match.create