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.
- reflex/.templates/jinja/web/pages/_app.js.jinja2 +1 -1
- reflex/.templates/jinja/web/styles/styles.css.jinja2 +1 -0
- reflex/.templates/web/app/routes.js +3 -3
- reflex/.templates/web/utils/client_side_routing.js +1 -1
- reflex/.templates/web/utils/state.js +110 -52
- reflex/__init__.pyi +327 -188
- reflex/app.py +50 -48
- reflex/compiler/compiler.py +6 -2
- reflex/compiler/utils.py +32 -14
- reflex/components/__init__.pyi +34 -15
- reflex/components/base/__init__.pyi +30 -19
- reflex/components/base/app_wrap.pyi +2 -3
- reflex/components/base/body.pyi +2 -3
- reflex/components/base/document.pyi +7 -13
- reflex/components/base/error_boundary.pyi +2 -3
- reflex/components/base/fragment.pyi +2 -3
- reflex/components/base/link.pyi +3 -5
- reflex/components/base/meta.py +4 -15
- reflex/components/base/meta.pyi +14 -18
- reflex/components/base/script.pyi +2 -3
- reflex/components/base/strict_mode.pyi +2 -3
- reflex/components/core/__init__.pyi +77 -38
- reflex/components/core/auto_scroll.pyi +2 -3
- reflex/components/core/banner.pyi +8 -14
- reflex/components/core/client_side_routing.pyi +2 -3
- reflex/components/core/clipboard.pyi +2 -3
- reflex/components/core/debounce.pyi +2 -3
- reflex/components/core/foreach.py +2 -2
- reflex/components/core/helmet.pyi +2 -3
- reflex/components/core/html.pyi +2 -3
- reflex/components/core/match.py +3 -3
- reflex/components/core/sticky.pyi +4 -7
- reflex/components/core/upload.py +2 -1
- reflex/components/core/upload.pyi +5 -9
- reflex/components/datadisplay/__init__.pyi +13 -7
- reflex/components/datadisplay/code.py +12 -7
- reflex/components/datadisplay/code.pyi +2 -3
- reflex/components/datadisplay/dataeditor.pyi +33 -11
- reflex/components/datadisplay/shiki_code_block.py +5 -3
- reflex/components/datadisplay/shiki_code_block.pyi +3 -5
- reflex/components/el/__init__.pyi +506 -246
- reflex/components/el/element.pyi +2 -3
- reflex/components/el/elements/__init__.pyi +504 -245
- reflex/components/el/elements/base.pyi +2 -3
- reflex/components/el/elements/forms.pyi +77 -49
- reflex/components/el/elements/inline.pyi +29 -57
- reflex/components/el/elements/media.pyi +26 -51
- reflex/components/el/elements/metadata.pyi +7 -13
- reflex/components/el/elements/other.pyi +8 -15
- reflex/components/el/elements/scripts.pyi +4 -7
- reflex/components/el/elements/sectioning.pyi +16 -31
- reflex/components/el/elements/tables.pyi +11 -21
- reflex/components/el/elements/typography.pyi +16 -31
- reflex/components/gridjs/datatable.pyi +3 -5
- reflex/components/lucide/icon.pyi +4 -7
- reflex/components/markdown/markdown.py +5 -3
- reflex/components/markdown/markdown.pyi +2 -3
- reflex/components/moment/moment.py +1 -1
- reflex/components/moment/moment.pyi +2 -3
- reflex/components/plotly/plotly.py +12 -6
- reflex/components/plotly/plotly.pyi +31 -39
- reflex/components/radix/__init__.pyi +123 -65
- reflex/components/radix/primitives/__init__.pyi +6 -4
- reflex/components/radix/primitives/accordion.pyi +8 -15
- reflex/components/radix/primitives/base.pyi +3 -5
- reflex/components/radix/primitives/drawer.pyi +11 -21
- reflex/components/radix/primitives/form.pyi +22 -22
- reflex/components/radix/primitives/progress.pyi +5 -9
- reflex/components/radix/primitives/slider.pyi +6 -11
- reflex/components/radix/themes/__init__.pyi +5 -6
- reflex/components/radix/themes/base.pyi +9 -17
- reflex/components/radix/themes/color_mode.py +5 -6
- reflex/components/radix/themes/color_mode.pyi +4 -7
- reflex/components/radix/themes/components/__init__.pyi +75 -38
- reflex/components/radix/themes/components/alert_dialog.pyi +8 -15
- reflex/components/radix/themes/components/aspect_ratio.pyi +2 -3
- reflex/components/radix/themes/components/avatar.pyi +2 -3
- reflex/components/radix/themes/components/badge.pyi +2 -3
- reflex/components/radix/themes/components/button.pyi +2 -3
- reflex/components/radix/themes/components/callout.pyi +5 -9
- reflex/components/radix/themes/components/card.pyi +2 -3
- reflex/components/radix/themes/components/checkbox.pyi +3 -5
- reflex/components/radix/themes/components/checkbox_cards.pyi +3 -5
- reflex/components/radix/themes/components/checkbox_group.pyi +3 -5
- reflex/components/radix/themes/components/context_menu.pyi +14 -27
- reflex/components/radix/themes/components/data_list.pyi +5 -9
- reflex/components/radix/themes/components/dialog.pyi +7 -13
- reflex/components/radix/themes/components/dropdown_menu.pyi +9 -17
- reflex/components/radix/themes/components/hover_card.pyi +4 -7
- reflex/components/radix/themes/components/icon_button.pyi +2 -3
- reflex/components/radix/themes/components/inset.pyi +2 -3
- reflex/components/radix/themes/components/popover.pyi +5 -9
- reflex/components/radix/themes/components/progress.pyi +2 -3
- reflex/components/radix/themes/components/radio.pyi +2 -3
- reflex/components/radix/themes/components/radio_cards.pyi +3 -5
- reflex/components/radix/themes/components/radio_group.pyi +4 -7
- reflex/components/radix/themes/components/scroll_area.pyi +2 -3
- reflex/components/radix/themes/components/segmented_control.pyi +3 -5
- reflex/components/radix/themes/components/select.pyi +9 -17
- reflex/components/radix/themes/components/separator.pyi +2 -3
- reflex/components/radix/themes/components/skeleton.pyi +2 -3
- reflex/components/radix/themes/components/slider.pyi +12 -5
- reflex/components/radix/themes/components/spinner.pyi +2 -3
- reflex/components/radix/themes/components/switch.pyi +2 -3
- reflex/components/radix/themes/components/table.pyi +8 -15
- reflex/components/radix/themes/components/tabs.pyi +5 -9
- reflex/components/radix/themes/components/text_area.pyi +10 -5
- reflex/components/radix/themes/components/text_field.pyi +19 -9
- reflex/components/radix/themes/components/tooltip.pyi +2 -3
- reflex/components/radix/themes/layout/__init__.pyi +27 -14
- reflex/components/radix/themes/layout/base.pyi +2 -3
- reflex/components/radix/themes/layout/box.pyi +2 -3
- reflex/components/radix/themes/layout/center.pyi +2 -3
- reflex/components/radix/themes/layout/container.pyi +2 -3
- reflex/components/radix/themes/layout/flex.pyi +2 -3
- reflex/components/radix/themes/layout/grid.pyi +2 -3
- reflex/components/radix/themes/layout/list.pyi +5 -9
- reflex/components/radix/themes/layout/section.pyi +2 -3
- reflex/components/radix/themes/layout/spacer.pyi +2 -3
- reflex/components/radix/themes/layout/stack.pyi +4 -7
- reflex/components/radix/themes/typography/__init__.pyi +7 -5
- reflex/components/radix/themes/typography/blockquote.pyi +2 -3
- reflex/components/radix/themes/typography/code.pyi +2 -3
- reflex/components/radix/themes/typography/heading.pyi +2 -3
- reflex/components/radix/themes/typography/link.pyi +3 -5
- reflex/components/radix/themes/typography/text.pyi +7 -13
- reflex/components/react_player/audio.pyi +5 -4
- reflex/components/react_player/react_player.pyi +2 -3
- reflex/components/react_player/video.pyi +5 -4
- reflex/components/recharts/__init__.pyi +208 -100
- reflex/components/recharts/cartesian.py +8 -7
- reflex/components/recharts/cartesian.pyi +25 -48
- reflex/components/recharts/charts.pyi +13 -25
- reflex/components/recharts/general.pyi +7 -13
- reflex/components/recharts/polar.pyi +7 -13
- reflex/components/recharts/recharts.py +2 -2
- reflex/components/recharts/recharts.pyi +3 -5
- reflex/components/sonner/toast.py +1 -1
- reflex/components/sonner/toast.pyi +2 -3
- reflex/constants/installer.py +7 -8
- reflex/constants/route.py +13 -6
- reflex/istate/__init__.py +69 -0
- reflex/istate/manager.py +1 -0
- reflex/plugins/shared_tailwind.py +58 -1
- reflex/plugins/tailwind_v3.py +4 -4
- reflex/plugins/tailwind_v4.py +6 -5
- reflex/route.py +159 -71
- reflex/state.py +38 -16
- reflex/testing.py +2 -1
- reflex/utils/exec.py +18 -13
- reflex/utils/format.py +1 -5
- reflex/utils/imports.py +5 -12
- reflex/utils/misc.py +40 -0
- reflex/utils/prerequisites.py +7 -12
- reflex/utils/processes.py +8 -41
- reflex/utils/pyi_generator.py +23 -40
- reflex/utils/telemetry.py +0 -15
- reflex/utils/types.py +1 -1
- {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/METADATA +3 -3
- {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/RECORD +163 -163
- {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/WHEEL +0 -0
- {reflex-0.8.0a4.dist-info → reflex-0.8.0a6.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
20
|
-
|
|
21
|
-
if
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
81
|
-
"""
|
|
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
|
-
>>>
|
|
85
|
-
'
|
|
86
|
-
>>>
|
|
87
|
-
'
|
|
88
|
-
>>>
|
|
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
|
-
|
|
114
|
+
input_string: String to replace.
|
|
93
115
|
|
|
94
116
|
Returns:
|
|
95
|
-
|
|
117
|
+
new string containing keywords.
|
|
96
118
|
"""
|
|
97
|
-
|
|
98
|
-
return
|
|
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
|
|
102
|
-
"""
|
|
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
|
-
|
|
140
|
+
keyworded_route: The route with keywords.
|
|
106
141
|
|
|
107
142
|
Returns:
|
|
108
|
-
|
|
143
|
+
A tuple containing the counts of double catchall segments,
|
|
144
|
+
double segments, and single segments in the route.
|
|
109
145
|
"""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -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
|
-
|
|
355
|
-
|
|
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 =
|
|
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(
|
|
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
|
|