reflex 0.8.0a4__py3-none-any.whl → 0.8.0a6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

Files changed (163) hide show
  1. reflex/.templates/jinja/web/pages/_app.js.jinja2 +1 -1
  2. reflex/.templates/jinja/web/styles/styles.css.jinja2 +1 -0
  3. reflex/.templates/web/app/routes.js +3 -3
  4. reflex/.templates/web/utils/client_side_routing.js +1 -1
  5. reflex/.templates/web/utils/state.js +110 -52
  6. reflex/__init__.pyi +327 -188
  7. reflex/app.py +50 -48
  8. reflex/compiler/compiler.py +6 -2
  9. reflex/compiler/utils.py +32 -14
  10. reflex/components/__init__.pyi +34 -15
  11. reflex/components/base/__init__.pyi +30 -19
  12. reflex/components/base/app_wrap.pyi +2 -3
  13. reflex/components/base/body.pyi +2 -3
  14. reflex/components/base/document.pyi +7 -13
  15. reflex/components/base/error_boundary.pyi +2 -3
  16. reflex/components/base/fragment.pyi +2 -3
  17. reflex/components/base/link.pyi +3 -5
  18. reflex/components/base/meta.py +4 -15
  19. reflex/components/base/meta.pyi +14 -18
  20. reflex/components/base/script.pyi +2 -3
  21. reflex/components/base/strict_mode.pyi +2 -3
  22. reflex/components/core/__init__.pyi +77 -38
  23. reflex/components/core/auto_scroll.pyi +2 -3
  24. reflex/components/core/banner.pyi +8 -14
  25. reflex/components/core/client_side_routing.pyi +2 -3
  26. reflex/components/core/clipboard.pyi +2 -3
  27. reflex/components/core/debounce.pyi +2 -3
  28. reflex/components/core/foreach.py +2 -2
  29. reflex/components/core/helmet.pyi +2 -3
  30. reflex/components/core/html.pyi +2 -3
  31. reflex/components/core/match.py +3 -3
  32. reflex/components/core/sticky.pyi +4 -7
  33. reflex/components/core/upload.py +2 -1
  34. reflex/components/core/upload.pyi +5 -9
  35. reflex/components/datadisplay/__init__.pyi +13 -7
  36. reflex/components/datadisplay/code.py +12 -7
  37. reflex/components/datadisplay/code.pyi +2 -3
  38. reflex/components/datadisplay/dataeditor.pyi +33 -11
  39. reflex/components/datadisplay/shiki_code_block.py +5 -3
  40. reflex/components/datadisplay/shiki_code_block.pyi +3 -5
  41. reflex/components/el/__init__.pyi +506 -246
  42. reflex/components/el/element.pyi +2 -3
  43. reflex/components/el/elements/__init__.pyi +504 -245
  44. reflex/components/el/elements/base.pyi +2 -3
  45. reflex/components/el/elements/forms.pyi +77 -49
  46. reflex/components/el/elements/inline.pyi +29 -57
  47. reflex/components/el/elements/media.pyi +26 -51
  48. reflex/components/el/elements/metadata.pyi +7 -13
  49. reflex/components/el/elements/other.pyi +8 -15
  50. reflex/components/el/elements/scripts.pyi +4 -7
  51. reflex/components/el/elements/sectioning.pyi +16 -31
  52. reflex/components/el/elements/tables.pyi +11 -21
  53. reflex/components/el/elements/typography.pyi +16 -31
  54. reflex/components/gridjs/datatable.pyi +3 -5
  55. reflex/components/lucide/icon.pyi +4 -7
  56. reflex/components/markdown/markdown.py +5 -3
  57. reflex/components/markdown/markdown.pyi +2 -3
  58. reflex/components/moment/moment.py +1 -1
  59. reflex/components/moment/moment.pyi +2 -3
  60. reflex/components/plotly/plotly.py +12 -6
  61. reflex/components/plotly/plotly.pyi +31 -39
  62. reflex/components/radix/__init__.pyi +123 -65
  63. reflex/components/radix/primitives/__init__.pyi +6 -4
  64. reflex/components/radix/primitives/accordion.pyi +8 -15
  65. reflex/components/radix/primitives/base.pyi +3 -5
  66. reflex/components/radix/primitives/drawer.pyi +11 -21
  67. reflex/components/radix/primitives/form.pyi +22 -22
  68. reflex/components/radix/primitives/progress.pyi +5 -9
  69. reflex/components/radix/primitives/slider.pyi +6 -11
  70. reflex/components/radix/themes/__init__.pyi +5 -6
  71. reflex/components/radix/themes/base.pyi +9 -17
  72. reflex/components/radix/themes/color_mode.py +5 -6
  73. reflex/components/radix/themes/color_mode.pyi +4 -7
  74. reflex/components/radix/themes/components/__init__.pyi +75 -38
  75. reflex/components/radix/themes/components/alert_dialog.pyi +8 -15
  76. reflex/components/radix/themes/components/aspect_ratio.pyi +2 -3
  77. reflex/components/radix/themes/components/avatar.pyi +2 -3
  78. reflex/components/radix/themes/components/badge.pyi +2 -3
  79. reflex/components/radix/themes/components/button.pyi +2 -3
  80. reflex/components/radix/themes/components/callout.pyi +5 -9
  81. reflex/components/radix/themes/components/card.pyi +2 -3
  82. reflex/components/radix/themes/components/checkbox.pyi +3 -5
  83. reflex/components/radix/themes/components/checkbox_cards.pyi +3 -5
  84. reflex/components/radix/themes/components/checkbox_group.pyi +3 -5
  85. reflex/components/radix/themes/components/context_menu.pyi +14 -27
  86. reflex/components/radix/themes/components/data_list.pyi +5 -9
  87. reflex/components/radix/themes/components/dialog.pyi +7 -13
  88. reflex/components/radix/themes/components/dropdown_menu.pyi +9 -17
  89. reflex/components/radix/themes/components/hover_card.pyi +4 -7
  90. reflex/components/radix/themes/components/icon_button.pyi +2 -3
  91. reflex/components/radix/themes/components/inset.pyi +2 -3
  92. reflex/components/radix/themes/components/popover.pyi +5 -9
  93. reflex/components/radix/themes/components/progress.pyi +2 -3
  94. reflex/components/radix/themes/components/radio.pyi +2 -3
  95. reflex/components/radix/themes/components/radio_cards.pyi +3 -5
  96. reflex/components/radix/themes/components/radio_group.pyi +4 -7
  97. reflex/components/radix/themes/components/scroll_area.pyi +2 -3
  98. reflex/components/radix/themes/components/segmented_control.pyi +3 -5
  99. reflex/components/radix/themes/components/select.pyi +9 -17
  100. reflex/components/radix/themes/components/separator.pyi +2 -3
  101. reflex/components/radix/themes/components/skeleton.pyi +2 -3
  102. reflex/components/radix/themes/components/slider.pyi +12 -5
  103. reflex/components/radix/themes/components/spinner.pyi +2 -3
  104. reflex/components/radix/themes/components/switch.pyi +2 -3
  105. reflex/components/radix/themes/components/table.pyi +8 -15
  106. reflex/components/radix/themes/components/tabs.pyi +5 -9
  107. reflex/components/radix/themes/components/text_area.pyi +10 -5
  108. reflex/components/radix/themes/components/text_field.pyi +19 -9
  109. reflex/components/radix/themes/components/tooltip.pyi +2 -3
  110. reflex/components/radix/themes/layout/__init__.pyi +27 -14
  111. reflex/components/radix/themes/layout/base.pyi +2 -3
  112. reflex/components/radix/themes/layout/box.pyi +2 -3
  113. reflex/components/radix/themes/layout/center.pyi +2 -3
  114. reflex/components/radix/themes/layout/container.pyi +2 -3
  115. reflex/components/radix/themes/layout/flex.pyi +2 -3
  116. reflex/components/radix/themes/layout/grid.pyi +2 -3
  117. reflex/components/radix/themes/layout/list.pyi +5 -9
  118. reflex/components/radix/themes/layout/section.pyi +2 -3
  119. reflex/components/radix/themes/layout/spacer.pyi +2 -3
  120. reflex/components/radix/themes/layout/stack.pyi +4 -7
  121. reflex/components/radix/themes/typography/__init__.pyi +7 -5
  122. reflex/components/radix/themes/typography/blockquote.pyi +2 -3
  123. reflex/components/radix/themes/typography/code.pyi +2 -3
  124. reflex/components/radix/themes/typography/heading.pyi +2 -3
  125. reflex/components/radix/themes/typography/link.pyi +3 -5
  126. reflex/components/radix/themes/typography/text.pyi +7 -13
  127. reflex/components/react_player/audio.pyi +5 -4
  128. reflex/components/react_player/react_player.pyi +2 -3
  129. reflex/components/react_player/video.pyi +5 -4
  130. reflex/components/recharts/__init__.pyi +208 -100
  131. reflex/components/recharts/cartesian.py +8 -7
  132. reflex/components/recharts/cartesian.pyi +25 -48
  133. reflex/components/recharts/charts.pyi +13 -25
  134. reflex/components/recharts/general.pyi +7 -13
  135. reflex/components/recharts/polar.pyi +7 -13
  136. reflex/components/recharts/recharts.py +2 -2
  137. reflex/components/recharts/recharts.pyi +3 -5
  138. reflex/components/sonner/toast.py +1 -1
  139. reflex/components/sonner/toast.pyi +2 -3
  140. reflex/constants/installer.py +7 -8
  141. reflex/constants/route.py +13 -6
  142. reflex/istate/__init__.py +69 -0
  143. reflex/istate/manager.py +1 -0
  144. reflex/plugins/shared_tailwind.py +58 -1
  145. reflex/plugins/tailwind_v3.py +4 -4
  146. reflex/plugins/tailwind_v4.py +6 -5
  147. reflex/route.py +159 -71
  148. reflex/state.py +38 -16
  149. reflex/testing.py +2 -1
  150. reflex/utils/exec.py +18 -13
  151. reflex/utils/format.py +1 -5
  152. reflex/utils/imports.py +5 -12
  153. reflex/utils/misc.py +40 -0
  154. reflex/utils/prerequisites.py +7 -12
  155. reflex/utils/processes.py +8 -41
  156. reflex/utils/pyi_generator.py +23 -40
  157. reflex/utils/telemetry.py +0 -15
  158. reflex/utils/types.py +1 -1
  159. {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/METADATA +3 -3
  160. {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/RECORD +163 -163
  161. {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/WHEEL +0 -0
  162. {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/entry_points.txt +0 -0
  163. {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/licenses/LICENSE +0 -0
reflex/route.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
+ from collections.abc import Callable
6
7
 
7
8
  from reflex import constants
8
9
 
@@ -16,13 +17,39 @@ def verify_route_validity(route: str) -> None:
16
17
  Raises:
17
18
  ValueError: If the route is invalid.
18
19
  """
19
- pattern = catchall_in_route(route)
20
- if pattern:
21
- if pattern != "[[...splat]]":
22
- msg = f"Catchall pattern `{pattern}` is not valid. Only `[[...splat]]` is allowed."
20
+ route_parts = route.removeprefix("/").split("/")
21
+ for i, part in enumerate(route_parts):
22
+ if constants.RouteRegex.SLUG.fullmatch(part):
23
+ continue
24
+ if not part.startswith("[") or not part.endswith("]"):
25
+ msg = (
26
+ f"Route part `{part}` is not valid. Reflex only supports "
27
+ "alphabetic characters, underscores, and hyphens in route parts. "
28
+ )
23
29
  raise ValueError(msg)
24
- if not route.endswith(pattern):
25
- msg = f"Catchall pattern `{pattern}` must be at the end of the route."
30
+ if part.startswith(("[[...", "[...")):
31
+ if part != constants.RouteRegex.SPLAT_CATCHALL:
32
+ msg = f"Catchall pattern `{part}` is not valid. Only `{constants.RouteRegex.SPLAT_CATCHALL}` is allowed."
33
+ raise ValueError(msg)
34
+ if i != len(route_parts) - 1:
35
+ msg = f"Catchall pattern `{part}` must be at the end of the route."
36
+ raise ValueError(msg)
37
+ continue
38
+ if part.startswith("[["):
39
+ if constants.RouteRegex.OPTIONAL_ARG.fullmatch(part):
40
+ continue
41
+ msg = (
42
+ f"Route part `{part}` with optional argument is not valid. "
43
+ "Reflex only supports optional arguments that start with an alphabetic character or underscore, "
44
+ "followed by alphanumeric characters or underscores."
45
+ )
46
+ raise ValueError(msg)
47
+ if not constants.RouteRegex.ARG.fullmatch(part):
48
+ msg = (
49
+ f"Route part `{part}` with argument is not valid. "
50
+ "Reflex only supports argument names that start with an alphabetic character or underscore, "
51
+ "followed by alphanumeric characters or underscores."
52
+ )
26
53
  raise ValueError(msg)
27
54
 
28
55
 
@@ -37,98 +64,159 @@ def get_route_args(route: str) -> dict[str, str]:
37
64
  """
38
65
  args = {}
39
66
 
40
- def add_route_arg(match: re.Match[str], type_: str):
41
- """Add arg from regex search result.
42
-
43
- Args:
44
- match: Result of a regex search
45
- type_: The assigned type for this arg
46
-
47
- Raises:
48
- ValueError: If the route is invalid.
49
- """
50
- arg_name = match.groups()[0]
67
+ def _add_route_arg(arg_name: str, type_: str):
51
68
  if arg_name in args:
52
- msg = f"Arg name [{arg_name}] is used more than once in this URL"
69
+ msg = (
70
+ f"Arg name `{arg_name}` is used more than once in the route `{route}`."
71
+ )
53
72
  raise ValueError(msg)
54
73
  args[arg_name] = type_
55
74
 
56
75
  # Regex to check for route args.
57
- check = constants.RouteRegex.ARG
58
- check_strict_catchall = constants.RouteRegex.STRICT_CATCHALL
59
- check_opt_catchall = constants.RouteRegex.OPT_CATCHALL
76
+ argument_regex = constants.RouteRegex.ARG
77
+ optional_argument_regex = constants.RouteRegex.OPTIONAL_ARG
60
78
 
61
79
  # Iterate over the route parts and check for route args.
62
80
  for part in route.split("/"):
63
- match_opt = check_opt_catchall.match(part)
64
- if match_opt:
65
- add_route_arg(match_opt, constants.RouteArgType.LIST)
81
+ if part == constants.RouteRegex.SPLAT_CATCHALL:
82
+ _add_route_arg("splat", constants.RouteArgType.LIST)
66
83
  break
67
84
 
68
- match_strict = check_strict_catchall.match(part)
69
- if match_strict:
70
- add_route_arg(match_strict, constants.RouteArgType.LIST)
71
- break
85
+ optional_argument = optional_argument_regex.match(part)
86
+ if optional_argument:
87
+ _add_route_arg(optional_argument.group(1), constants.RouteArgType.SINGLE)
88
+ continue
89
+
90
+ argument = argument_regex.match(part)
91
+ if argument:
92
+ _add_route_arg(argument.group(1), constants.RouteArgType.SINGLE)
93
+ continue
72
94
 
73
- match = check.match(part)
74
- if match:
75
- # Add the route arg to the list.
76
- add_route_arg(match, constants.RouteArgType.SINGLE)
77
95
  return args
78
96
 
79
97
 
80
- def catchall_in_route(route: str) -> str:
81
- """Extract the catchall part from a route.
98
+ def replace_brackets_with_keywords(input_string: str) -> str:
99
+ """Replace brackets and everything inside it in a string with a keyword.
82
100
 
83
101
  Example:
84
- >>> catchall_in_route("/posts/[...slug]")
85
- '[...slug]'
86
- >>> catchall_in_route("/posts/[[...slug]]")
87
- '[[...slug]]'
88
- >>> catchall_in_route("/posts/[slug]")
89
- ''
102
+ >>> replace_brackets_with_keywords("/posts")
103
+ '/posts'
104
+ >>> replace_brackets_with_keywords("/posts/[slug]")
105
+ '/posts/__SINGLE_SEGMENT__'
106
+ >>> replace_brackets_with_keywords("/posts/[slug]/comments")
107
+ '/posts/__SINGLE_SEGMENT__/comments'
108
+ >>> replace_brackets_with_keywords("/posts/[[slug]]")
109
+ '/posts/__DOUBLE_SEGMENT__'
110
+ >>> replace_brackets_with_keywords("/posts/[[...splat]]")
111
+ '/posts/__DOUBLE_CATCHALL_SEGMENT__'
90
112
 
91
113
  Args:
92
- route: the route from which to extract
114
+ input_string: String to replace.
93
115
 
94
116
  Returns:
95
- str: the catchall part of the URI
117
+ new string containing keywords.
96
118
  """
97
- match_ = constants.RouteRegex.CATCHALL.search(route)
98
- return match_.group() if match_ else ""
119
+ # Replace [<slug>] with __SINGLE_SEGMENT__
120
+ return constants.RouteRegex.ARG.sub(
121
+ constants.RouteRegex.SINGLE_SEGMENT,
122
+ # Replace [[slug]] with __DOUBLE_SEGMENT__
123
+ constants.RouteRegex.OPTIONAL_ARG.sub(
124
+ constants.RouteRegex.DOUBLE_SEGMENT,
125
+ # Replace [[...splat]] with __DOUBLE_CATCHALL_SEGMENT__
126
+ input_string.replace(
127
+ constants.RouteRegex.SPLAT_CATCHALL,
128
+ constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
129
+ ),
130
+ ),
131
+ )
99
132
 
100
133
 
101
- def replace_brackets_with_keywords(input_string: str) -> str:
102
- """Replace brackets and everything inside it in a string with a keyword.
134
+ def route_specifity(keyworded_route: str) -> tuple[int, int, int]:
135
+ """Get the specificity of a route with keywords.
136
+
137
+ The smaller the number, the more specific the route is.
103
138
 
104
139
  Args:
105
- input_string: String to replace.
140
+ keyworded_route: The route with keywords.
106
141
 
107
142
  Returns:
108
- new string containing keywords.
143
+ A tuple containing the counts of double catchall segments,
144
+ double segments, and single segments in the route.
109
145
  """
110
- # /posts -> /post
111
- # /posts/[slug] -> /posts/__SINGLE_SEGMENT__
112
- # /posts/[slug]/comments -> /posts/__SINGLE_SEGMENT__/comments
113
- # /posts/[[slug]] -> /posts/__DOUBLE_SEGMENT__
114
- # / posts/[[...slug2]]-> /posts/__DOUBLE_CATCHALL_SEGMENT__
115
- # /posts/[...slug3]-> /posts/__SINGLE_CATCHALL_SEGMENT__
116
-
117
- # Replace [[...<slug>]] with __DOUBLE_CATCHALL_SEGMENT__
118
- output_string = re.sub(
119
- r"\[\[\.\.\..+?\]\]",
120
- constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
121
- input_string,
122
- )
123
- # Replace [...<slug>] with __SINGLE_CATCHALL_SEGMENT__
124
- output_string = re.sub(
125
- r"\[\.\.\..+?\]",
126
- constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
127
- output_string,
146
+ return (
147
+ keyworded_route.count(constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT),
148
+ keyworded_route.count(constants.RouteRegex.DOUBLE_SEGMENT),
149
+ keyworded_route.count(constants.RouteRegex.SINGLE_SEGMENT),
128
150
  )
129
- # Replace [[<slug>]] with __DOUBLE_SEGMENT__
130
- output_string = re.sub(
131
- r"\[\[.+?\]\]", constants.RouteRegex.DOUBLE_SEGMENT, output_string
151
+
152
+
153
+ def get_route_regex(keyworded_route: str) -> re.Pattern:
154
+ """Get the regex pattern for a route with keywords.
155
+
156
+ Args:
157
+ keyworded_route: The route with keywords.
158
+
159
+ Returns:
160
+ A compiled regex pattern for the route.
161
+ """
162
+ if keyworded_route == "index":
163
+ return re.compile(re.escape("/"))
164
+ path_parts = keyworded_route.split("/")
165
+ regex_parts = []
166
+ for part in path_parts:
167
+ if part == constants.RouteRegex.SINGLE_SEGMENT:
168
+ # Match a single segment (/slug)
169
+ regex_parts.append(r"/[^/]*")
170
+ elif part == constants.RouteRegex.DOUBLE_SEGMENT:
171
+ # Match a single optional segment (/slug or nothing)
172
+ regex_parts.append(r"(/[^/]+)?")
173
+ elif part == constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT:
174
+ regex_parts.append(".*")
175
+ else:
176
+ regex_parts.append(re.escape("/" + part))
177
+ # Join the regex parts and compile the regex
178
+ regex_pattern = "".join(regex_parts)
179
+ regex_pattern = f"^{regex_pattern}/?$"
180
+ return re.compile(regex_pattern)
181
+
182
+
183
+ def get_router(routes: list[str]) -> Callable[[str], str | None]:
184
+ """Get a function that computes the route for a given path.
185
+
186
+ Args:
187
+ routes: A list of routes to match against.
188
+
189
+ Returns:
190
+ A function that takes a path and returns the first matching route,
191
+ or None if no match is found.
192
+ """
193
+ keyworded_routes = {
194
+ replace_brackets_with_keywords(route): route for route in routes
195
+ }
196
+ sorted_routes_by_specifity = sorted(
197
+ keyworded_routes.items(),
198
+ key=lambda item: route_specifity(item[0]),
132
199
  )
133
- # Replace [<slug>] with __SINGLE_SEGMENT__
134
- return re.sub(r"\[.+?\]", constants.RouteRegex.SINGLE_SEGMENT, output_string)
200
+ regexed_routes = [
201
+ (get_route_regex(keyworded_route), original_route)
202
+ for keyworded_route, original_route in sorted_routes_by_specifity
203
+ ]
204
+
205
+ def get_route(path: str) -> str | None:
206
+ """Get the first matching route for a given path.
207
+
208
+ Args:
209
+ path: The path to match against the routes.
210
+
211
+ Returns:
212
+ The first matching route, or None if no match is found.
213
+ """
214
+ path = "/" + path.removeprefix("/").removesuffix("/")
215
+ if path == "/index":
216
+ path = "/"
217
+ for regex, original_route in regexed_routes:
218
+ if regex.fullmatch(path):
219
+ return original_route
220
+ return None
221
+
222
+ return get_route
reflex/state.py CHANGED
@@ -37,6 +37,7 @@ from reflex.event import (
37
37
  EventSpec,
38
38
  fix_events,
39
39
  )
40
+ from reflex.istate import HANDLED_PICKLE_ERRORS, debug_failed_pickles
40
41
  from reflex.istate.data import RouterData
41
42
  from reflex.istate.proxy import ImmutableMutableProxy as ImmutableMutableProxy
42
43
  from reflex.istate.proxy import MutableProxy, StateProxy
@@ -85,14 +86,6 @@ if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
85
86
  # Only warn about each state class size once.
86
87
  _WARNED_ABOUT_STATE_SIZE: set[str] = set()
87
88
 
88
- # Errors caught during pickling of state
89
- HANDLED_PICKLE_ERRORS = (
90
- pickle.PicklingError,
91
- AttributeError,
92
- IndexError,
93
- TypeError,
94
- ValueError,
95
- )
96
89
 
97
90
  # For BaseState.get_var_value
98
91
  VAR_TYPE = TypeVar("VAR_TYPE")
@@ -2053,11 +2046,28 @@ class BaseState(EvenMoreBasicBaseState):
2053
2046
 
2054
2047
  Returns:
2055
2048
  The value of the field.
2049
+
2050
+ Raises:
2051
+ TypeError: If the key is not a string or MutableProxy.
2056
2052
  """
2057
- value = getattr(self, key)
2058
- if isinstance(value, MutableProxy):
2059
- return value.__wrapped__
2060
- return value
2053
+ if isinstance(key, MutableProxy):
2054
+ # Legacy behavior from v0.7.14: handle non-string keys with deprecation warning
2055
+ from reflex.utils import console
2056
+
2057
+ console.deprecate(
2058
+ feature_name="Non-string keys in get_value",
2059
+ reason="Passing non-string keys to get_value is deprecated and will no longer be supported",
2060
+ deprecation_version="0.8.0",
2061
+ removal_version="0.9.0",
2062
+ )
2063
+
2064
+ return key.__wrapped__
2065
+
2066
+ if isinstance(key, str):
2067
+ return getattr(self, key)
2068
+
2069
+ msg = f"Invalid key type: {type(key)}. Expected str."
2070
+ raise TypeError(msg)
2061
2071
 
2062
2072
  def dict(
2063
2073
  self, include_computed: bool = True, initial: bool = False, **kwargs
@@ -2234,11 +2244,16 @@ class BaseState(EvenMoreBasicBaseState):
2234
2244
 
2235
2245
  Raises:
2236
2246
  StateSerializationError: If the state cannot be serialized.
2247
+
2248
+ # noqa: DAR401: e
2249
+ # noqa: DAR402: StateSerializationError
2237
2250
  """
2238
2251
  payload = b""
2239
2252
  error = ""
2253
+ self_schema = self._to_schema()
2254
+ pickle_function = pickle.dumps
2240
2255
  try:
2241
- payload = pickle.dumps((self._to_schema(), self))
2256
+ payload = pickle.dumps((self_schema, self))
2242
2257
  except HANDLED_PICKLE_ERRORS as og_pickle_error:
2243
2258
  error = (
2244
2259
  f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
@@ -2247,7 +2262,8 @@ class BaseState(EvenMoreBasicBaseState):
2247
2262
  try:
2248
2263
  import dill
2249
2264
 
2250
- payload = dill.dumps((self._to_schema(), self))
2265
+ pickle_function = dill.dumps
2266
+ payload = dill.dumps((self_schema, self))
2251
2267
  except ImportError:
2252
2268
  error += (
2253
2269
  f"Pickle error: {og_pickle_error}. "
@@ -2255,13 +2271,19 @@ class BaseState(EvenMoreBasicBaseState):
2255
2271
  )
2256
2272
  except HANDLED_PICKLE_ERRORS as ex:
2257
2273
  error += f"Dill was also unable to pickle the state: {ex}"
2258
- console.warn(error)
2259
2274
 
2260
2275
  if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
2261
2276
  self._check_state_size(len(payload))
2262
2277
 
2263
2278
  if not payload:
2264
- raise StateSerializationError(error)
2279
+ e = StateSerializationError(error)
2280
+ if sys.version_info >= (3, 11):
2281
+ try:
2282
+ debug_failed_pickles(self, pickle_function)
2283
+ except HANDLED_PICKLE_ERRORS as ex:
2284
+ for note in ex.__notes__:
2285
+ e.add_note(note)
2286
+ raise e
2265
2287
 
2266
2288
  return payload
2267
2289
 
reflex/testing.py CHANGED
@@ -24,7 +24,6 @@ from http.server import SimpleHTTPRequestHandler
24
24
  from pathlib import Path
25
25
  from typing import TYPE_CHECKING, Any, Literal, TypeVar
26
26
 
27
- import psutil
28
27
  import uvicorn
29
28
 
30
29
  import reflex
@@ -478,6 +477,8 @@ class AppHarness:
478
477
 
479
478
  def stop(self) -> None:
480
479
  """Stop the frontend and backend servers."""
480
+ import psutil
481
+
481
482
  # Quit browsers first to avoid any lingering events being sent during shutdown.
482
483
  for driver in self._frontends:
483
484
  driver.quit()
reflex/utils/exec.py CHANGED
@@ -15,14 +15,13 @@ from pathlib import Path
15
15
  from typing import Any, NamedTuple, TypedDict
16
16
  from urllib.parse import urljoin
17
17
 
18
- import psutil
19
-
20
18
  from reflex import constants
21
19
  from reflex.config import get_config
22
20
  from reflex.constants.base import LogLevel
23
21
  from reflex.environment import environment
24
22
  from reflex.utils import console, path_ops
25
23
  from reflex.utils.decorator import once
24
+ from reflex.utils.misc import get_module_path
26
25
  from reflex.utils.prerequisites import get_web_dir
27
26
 
28
27
  # For uvicorn windows bug fix (#2335)
@@ -130,12 +129,16 @@ def get_different_packages(
130
129
  def kill(proc_pid: int):
131
130
  """Kills a process and all its child processes.
132
131
 
132
+ Requires the `psutil` library to be installed.
133
+
133
134
  Args:
134
- proc_pid (int): The process ID of the process to be killed.
135
+ proc_pid: The process ID of the process to be killed.
135
136
 
136
137
  Example:
137
138
  >>> kill(1234)
138
139
  """
140
+ import psutil
141
+
139
142
  process = psutil.Process(proc_pid)
140
143
  for proc in process.children(recursive=True):
141
144
  proc.kill()
@@ -323,15 +326,12 @@ def get_app_file() -> Path:
323
326
  if current_working_dir not in sys.path:
324
327
  # Add the current working directory to sys.path
325
328
  sys.path.insert(0, current_working_dir)
326
- module_spec = importlib.util.find_spec(get_app_module())
327
- if module_spec is None:
328
- msg = f"Module {get_app_module()} not found. Make sure the module is installed."
329
+ app_module = get_app_module()
330
+ module_path = get_module_path(app_module)
331
+ if module_path is None:
332
+ msg = f"Module {app_module} not found. Make sure the module is installed."
329
333
  raise ImportError(msg)
330
- file_name = module_spec.origin
331
- if file_name is None:
332
- msg = f"Module {get_app_module()} not found. Make sure the module is installed."
333
- raise ImportError(msg)
334
- return Path(file_name).resolve()
334
+ return module_path
335
335
 
336
336
 
337
337
  def get_app_instance_from_file() -> str:
@@ -367,6 +367,9 @@ def run_backend(
367
367
 
368
368
  # Run the backend in development mode.
369
369
  if should_use_granian():
370
+ # We import reflex app because this lets granian cache the module
371
+ import reflex.app # noqa: F401
372
+
370
373
  run_granian_backend(host, port, loglevel)
371
374
  else:
372
375
  run_uvicorn_backend(host, port, loglevel)
@@ -396,8 +399,10 @@ def get_reload_paths() -> Sequence[Path]:
396
399
  """
397
400
  config = get_config()
398
401
  reload_paths = [Path.cwd()]
399
- if (spec := importlib.util.find_spec(config.module)) is not None and spec.origin:
400
- module_path = Path(spec.origin).resolve().parent
402
+ app_module = config.module
403
+ module_path = get_module_path(app_module)
404
+ if module_path is not None:
405
+ module_path = module_path.parent
401
406
 
402
407
  while module_path.parent.name and _has_child_file(module_path, "__init__.py"):
403
408
  if _has_child_file(module_path, "rxconfig.py"):
reflex/utils/format.py CHANGED
@@ -310,20 +310,16 @@ def format_var(var: Var) -> str:
310
310
  return str(var)
311
311
 
312
312
 
313
- def format_route(route: str, format_case: bool = True) -> str:
313
+ def format_route(route: str) -> str:
314
314
  """Format the given route.
315
315
 
316
316
  Args:
317
317
  route: The route to format.
318
- format_case: whether to format case to kebab case.
319
318
 
320
319
  Returns:
321
320
  The formatted route.
322
321
  """
323
322
  route = route.strip("/")
324
- # Strip the route and format casing.
325
- if format_case:
326
- route = to_kebab_case(route)
327
323
 
328
324
  # If the route is empty, return the index route.
329
325
  if route == "":
reflex/utils/imports.py CHANGED
@@ -52,19 +52,12 @@ def parse_imports(
52
52
  Returns:
53
53
  The parsed import dict.
54
54
  """
55
-
56
- def _make_list(
57
- value: ImmutableImportTypes,
58
- ) -> list[str | ImportVar] | list[ImportVar]:
59
- if isinstance(value, (str, ImportVar)):
60
- return [value]
61
- return list(value)
62
-
63
55
  return {
64
- package: [
65
- ImportVar(tag=tag) if isinstance(tag, str) else tag
66
- for tag in _make_list(maybe_tags)
67
- ]
56
+ package: [maybe_tags]
57
+ if isinstance(maybe_tags, ImportVar)
58
+ else [ImportVar(tag=maybe_tags)]
59
+ if isinstance(maybe_tags, str)
60
+ else [ImportVar(tag=tag) if isinstance(tag, str) else tag for tag in maybe_tags]
68
61
  for package, maybe_tags in imports.items()
69
62
  }
70
63
 
reflex/utils/misc.py CHANGED
@@ -9,6 +9,46 @@ from pathlib import Path
9
9
  from typing import Any
10
10
 
11
11
 
12
+ def get_module_path(module_name: str) -> Path | None:
13
+ """Check if a module exists and return its path.
14
+
15
+ This function searches for a module by navigating through the module hierarchy
16
+ in each path of sys.path, checking for both .py files and packages with __init__.py.
17
+
18
+ Args:
19
+ module_name: The name of the module to search for (e.g., "package.submodule").
20
+
21
+ Returns:
22
+ The path to the module file if found, None otherwise.
23
+ """
24
+ parts = module_name.split(".")
25
+
26
+ # Check each path in sys.path
27
+ for path in sys.path:
28
+ current_path = Path(path)
29
+
30
+ # Navigate through the module hierarchy
31
+ for i, part in enumerate(parts):
32
+ potential_file = current_path / (part + ".py")
33
+ potential_dir = current_path / part
34
+ potential_init = current_path / part / "__init__.py"
35
+
36
+ if potential_file.is_file():
37
+ # We encountered a file, but we can't continue deeper
38
+ if i == len(parts) - 1:
39
+ return potential_file
40
+ return None # Can't continue deeper
41
+ if potential_dir.is_dir() and potential_init.is_file():
42
+ # It's a package, so we can continue deeper
43
+ current_path = potential_dir
44
+ else:
45
+ break # Path doesn't exist, break out of the loop
46
+ else:
47
+ return current_path / "__init__.py" # Made it through all parts
48
+
49
+ return None
50
+
51
+
12
52
  async def run_in_thread(func: Callable) -> Any:
13
53
  """Run a function in a separate thread.
14
54
 
@@ -40,6 +40,7 @@ from reflex.config import Config, get_config
40
40
  from reflex.environment import environment
41
41
  from reflex.utils import console, net, path_ops, processes, redir
42
42
  from reflex.utils.exceptions import SystemPackageMissingError
43
+ from reflex.utils.misc import get_module_path
43
44
  from reflex.utils.registry import get_npm_registry
44
45
 
45
46
  if typing.TYPE_CHECKING:
@@ -115,7 +116,7 @@ def check_latest_package_version(package_name: str):
115
116
  # Get the latest version from PyPI
116
117
  current_version = importlib.metadata.version(package_name)
117
118
  url = f"https://pypi.org/pypi/{package_name}/json"
118
- response = net.get(url)
119
+ response = net.get(url, timeout=2)
119
120
  latest_version = response.json()["info"]["version"]
120
121
  console.debug(f"Latest version of {package_name}: {latest_version}")
121
122
  if get_or_set_last_reflex_version_check_datetime():
@@ -348,14 +349,11 @@ def _check_app_name(config: Config):
348
349
  )
349
350
  raise RuntimeError(msg)
350
351
 
351
- from reflex.utils.misc import with_cwd_in_syspath
352
+ from reflex.utils.misc import get_module_path, with_cwd_in_syspath
352
353
 
353
354
  with with_cwd_in_syspath():
354
- try:
355
- mod_spec = importlib.util.find_spec(config.module)
356
- except ModuleNotFoundError:
357
- mod_spec = None
358
- if mod_spec is None:
355
+ module_path = get_module_path(config.module)
356
+ if module_path is None:
359
357
  msg = f"Module {config.module} not found. "
360
358
  if config.app_module_import is not None:
361
359
  msg += f"Ensure app_module_import='{config.app_module_import}' in rxconfig.py matches your folder structure."
@@ -740,14 +738,11 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
740
738
  sys.path.insert(0, str(Path.cwd()))
741
739
 
742
740
  config = get_config()
743
- module_path = importlib.util.find_spec(config.module)
741
+ module_path = get_module_path(config.module)
744
742
  if module_path is None:
745
743
  console.error(f"Could not find module {config.module}.")
746
744
  raise click.exceptions.Exit(1)
747
745
 
748
- if not module_path.origin:
749
- console.error(f"Could not find origin for module {config.module}.")
750
- raise click.exceptions.Exit(1)
751
746
  console.info(f"Renaming app directory to {new_app_name}.")
752
747
  process_directory(
753
748
  Path.cwd(),
@@ -756,7 +751,7 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
756
751
  exclude_dirs=[constants.Dirs.WEB, constants.Dirs.APP_ASSETS],
757
752
  )
758
753
 
759
- rename_path_up_tree(Path(module_path.origin), config.app_name, new_app_name)
754
+ rename_path_up_tree(module_path, config.app_name, new_app_name)
760
755
 
761
756
  console.success(f"App directory renamed to [bold]{new_app_name}[/bold].")
762
757