reflex 0.8.0a3__py3-none-any.whl → 0.8.0a5__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/styles/styles.css.jinja2 +1 -0
- reflex/.templates/web/app/routes.js +3 -3
- reflex/.templates/web/styles/__reflex_style_reset.css +399 -0
- reflex/.templates/web/utils/client_side_routing.js +1 -1
- reflex/.templates/web/utils/state.js +32 -21
- reflex/.templates/web/vite.config.js +6 -0
- reflex/app.py +50 -46
- reflex/compiler/compiler.py +26 -10
- reflex/compiler/utils.py +4 -2
- reflex/components/base/meta.py +4 -15
- reflex/components/core/foreach.py +2 -2
- reflex/components/core/match.py +3 -3
- reflex/components/core/upload.py +2 -1
- reflex/components/datadisplay/code.py +12 -7
- reflex/components/datadisplay/shiki_code_block.py +5 -3
- reflex/components/markdown/markdown.py +5 -3
- reflex/components/plotly/plotly.py +12 -6
- reflex/components/radix/themes/color_mode.py +5 -6
- reflex/components/recharts/cartesian.py +9 -2
- reflex/constants/compiler.py +7 -0
- reflex/constants/route.py +13 -6
- reflex/environment.py +6 -4
- reflex/route.py +159 -71
- reflex/state.py +21 -4
- reflex/utils/exec.py +10 -10
- reflex/utils/format.py +1 -5
- reflex/utils/misc.py +40 -0
- reflex/utils/prerequisites.py +6 -11
- reflex/utils/pyi_generator.py +23 -40
- reflex/utils/types.py +1 -1
- {reflex-0.8.0a3.dist-info → reflex-0.8.0a5.dist-info}/METADATA +2 -2
- {reflex-0.8.0a3.dist-info → reflex-0.8.0a5.dist-info}/RECORD +35 -34
- {reflex-0.8.0a3.dist-info → reflex-0.8.0a5.dist-info}/WHEEL +0 -0
- {reflex-0.8.0a3.dist-info → reflex-0.8.0a5.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.0a3.dist-info → reflex-0.8.0a5.dist-info}/licenses/LICENSE +0 -0
reflex/environment.py
CHANGED
|
@@ -69,7 +69,7 @@ def interpret_boolean_env(value: str, field_name: str) -> bool:
|
|
|
69
69
|
return True
|
|
70
70
|
if value.lower() in false_values:
|
|
71
71
|
return False
|
|
72
|
-
msg = f"Invalid boolean value: {value} for {field_name}"
|
|
72
|
+
msg = f"Invalid boolean value: {value!r} for {field_name}"
|
|
73
73
|
raise EnvironmentVarValueError(msg)
|
|
74
74
|
|
|
75
75
|
|
|
@@ -89,7 +89,7 @@ def interpret_int_env(value: str, field_name: str) -> int:
|
|
|
89
89
|
try:
|
|
90
90
|
return int(value)
|
|
91
91
|
except ValueError as ve:
|
|
92
|
-
msg = f"Invalid integer value: {value} for {field_name}"
|
|
92
|
+
msg = f"Invalid integer value: {value!r} for {field_name}"
|
|
93
93
|
raise EnvironmentVarValueError(msg) from ve
|
|
94
94
|
|
|
95
95
|
|
|
@@ -108,7 +108,7 @@ def interpret_existing_path_env(value: str, field_name: str) -> ExistingPath:
|
|
|
108
108
|
"""
|
|
109
109
|
path = Path(value)
|
|
110
110
|
if not path.exists():
|
|
111
|
-
msg = f"Path does not exist: {path} for {field_name}"
|
|
111
|
+
msg = f"Path does not exist: {path!r} for {field_name}"
|
|
112
112
|
raise EnvironmentVarValueError(msg)
|
|
113
113
|
return path
|
|
114
114
|
|
|
@@ -143,7 +143,7 @@ def interpret_enum_env(value: str, field_type: GenericType, field_name: str) ->
|
|
|
143
143
|
try:
|
|
144
144
|
return field_type(value)
|
|
145
145
|
except ValueError as ve:
|
|
146
|
-
msg = f"Invalid enum value: {value} for {field_name}"
|
|
146
|
+
msg = f"Invalid enum value: {value!r} for {field_name}"
|
|
147
147
|
raise EnvironmentVarValueError(msg) from ve
|
|
148
148
|
|
|
149
149
|
|
|
@@ -169,6 +169,8 @@ def interpret_env_var_value(
|
|
|
169
169
|
msg = f"Union types are not supported for environment variables: {field_name}."
|
|
170
170
|
raise ValueError(msg)
|
|
171
171
|
|
|
172
|
+
value = value.strip()
|
|
173
|
+
|
|
172
174
|
if field_type is bool:
|
|
173
175
|
return interpret_boolean_env(value, field_name)
|
|
174
176
|
if field_type is str:
|
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
|
@@ -2053,11 +2053,28 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
2053
2053
|
|
|
2054
2054
|
Returns:
|
|
2055
2055
|
The value of the field.
|
|
2056
|
+
|
|
2057
|
+
Raises:
|
|
2058
|
+
TypeError: If the key is not a string or MutableProxy.
|
|
2056
2059
|
"""
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2060
|
+
if isinstance(key, MutableProxy):
|
|
2061
|
+
# Legacy behavior from v0.7.14: handle non-string keys with deprecation warning
|
|
2062
|
+
from reflex.utils import console
|
|
2063
|
+
|
|
2064
|
+
console.deprecate(
|
|
2065
|
+
feature_name="Non-string keys in get_value",
|
|
2066
|
+
reason="Passing non-string keys to get_value is deprecated and will no longer be supported",
|
|
2067
|
+
deprecation_version="0.8.0",
|
|
2068
|
+
removal_version="0.9.0",
|
|
2069
|
+
)
|
|
2070
|
+
|
|
2071
|
+
return key.__wrapped__
|
|
2072
|
+
|
|
2073
|
+
if isinstance(key, str):
|
|
2074
|
+
return getattr(self, key)
|
|
2075
|
+
|
|
2076
|
+
msg = f"Invalid key type: {type(key)}. Expected str."
|
|
2077
|
+
raise TypeError(msg)
|
|
2061
2078
|
|
|
2062
2079
|
def dict(
|
|
2063
2080
|
self, include_computed: bool = True, initial: bool = False, **kwargs
|
reflex/utils/exec.py
CHANGED
|
@@ -23,6 +23,7 @@ from reflex.constants.base import LogLevel
|
|
|
23
23
|
from reflex.environment import environment
|
|
24
24
|
from reflex.utils import console, path_ops
|
|
25
25
|
from reflex.utils.decorator import once
|
|
26
|
+
from reflex.utils.misc import get_module_path
|
|
26
27
|
from reflex.utils.prerequisites import get_web_dir
|
|
27
28
|
|
|
28
29
|
# For uvicorn windows bug fix (#2335)
|
|
@@ -323,15 +324,12 @@ def get_app_file() -> Path:
|
|
|
323
324
|
if current_working_dir not in sys.path:
|
|
324
325
|
# Add the current working directory to sys.path
|
|
325
326
|
sys.path.insert(0, current_working_dir)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
app_module = get_app_module()
|
|
328
|
+
module_path = get_module_path(app_module)
|
|
329
|
+
if module_path is None:
|
|
330
|
+
msg = f"Module {app_module} not found. Make sure the module is installed."
|
|
329
331
|
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()
|
|
332
|
+
return module_path
|
|
335
333
|
|
|
336
334
|
|
|
337
335
|
def get_app_instance_from_file() -> str:
|
|
@@ -396,8 +394,10 @@ def get_reload_paths() -> Sequence[Path]:
|
|
|
396
394
|
"""
|
|
397
395
|
config = get_config()
|
|
398
396
|
reload_paths = [Path.cwd()]
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
app_module = config.module
|
|
398
|
+
module_path = get_module_path(app_module)
|
|
399
|
+
if module_path is not None:
|
|
400
|
+
module_path = module_path.parent
|
|
401
401
|
|
|
402
402
|
while module_path.parent.name and _has_child_file(module_path, "__init__.py"):
|
|
403
403
|
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/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:
|
|
@@ -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
|
|
reflex/utils/pyi_generator.py
CHANGED
|
@@ -13,7 +13,6 @@ import subprocess
|
|
|
13
13
|
import sys
|
|
14
14
|
import typing
|
|
15
15
|
from collections.abc import Callable, Iterable, Sequence
|
|
16
|
-
from fileinput import FileInput
|
|
17
16
|
from hashlib import md5
|
|
18
17
|
from inspect import getfullargspec
|
|
19
18
|
from itertools import chain
|
|
@@ -67,7 +66,6 @@ OVERWRITE_TYPES = {
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
DEFAULT_TYPING_IMPORTS = {
|
|
70
|
-
"overload",
|
|
71
69
|
"Any",
|
|
72
70
|
"Callable",
|
|
73
71
|
"Dict",
|
|
@@ -677,10 +675,7 @@ def _generate_component_create_functiondef(
|
|
|
677
675
|
value=ast.Constant(value=Ellipsis),
|
|
678
676
|
),
|
|
679
677
|
],
|
|
680
|
-
decorator_list=
|
|
681
|
-
ast.Name(id="overload"),
|
|
682
|
-
*decorator_list,
|
|
683
|
-
],
|
|
678
|
+
decorator_list=list(decorator_list),
|
|
684
679
|
lineno=lineno,
|
|
685
680
|
returns=ast.Constant(value=clz.__name__),
|
|
686
681
|
)
|
|
@@ -896,7 +891,7 @@ class StubGenerator(ast.NodeTransformer):
|
|
|
896
891
|
The modified ImportFrom node.
|
|
897
892
|
"""
|
|
898
893
|
if node.module == "__future__":
|
|
899
|
-
return None # ignore __future__ imports
|
|
894
|
+
return None # ignore __future__ imports: https://docs.astral.sh/ruff/rules/future-annotations-in-stub/
|
|
900
895
|
return self.visit_Import(node)
|
|
901
896
|
|
|
902
897
|
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
|
|
@@ -1109,9 +1104,10 @@ class PyiGenerator:
|
|
|
1109
1104
|
|
|
1110
1105
|
def _get_init_lazy_imports(self, mod: tuple | ModuleType, new_tree: ast.AST):
|
|
1111
1106
|
# retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present.
|
|
1112
|
-
sub_mods = getattr(mod, "_SUBMODULES", None)
|
|
1113
|
-
sub_mod_attrs
|
|
1114
|
-
|
|
1107
|
+
sub_mods: set[str] | None = getattr(mod, "_SUBMODULES", None)
|
|
1108
|
+
sub_mod_attrs: dict[str, list[str | tuple[str, str]]] | None = getattr(
|
|
1109
|
+
mod, "_SUBMOD_ATTRS", None
|
|
1110
|
+
)
|
|
1115
1111
|
|
|
1116
1112
|
if not sub_mods and not sub_mod_attrs:
|
|
1117
1113
|
return None
|
|
@@ -1119,31 +1115,34 @@ class PyiGenerator:
|
|
|
1119
1115
|
sub_mod_attrs_imports = []
|
|
1120
1116
|
|
|
1121
1117
|
if sub_mods:
|
|
1122
|
-
sub_mods_imports = [
|
|
1123
|
-
f"from . import {mod} as {mod}" for mod in sorted(sub_mods)
|
|
1124
|
-
]
|
|
1118
|
+
sub_mods_imports = [f"from . import {mod}" for mod in sorted(sub_mods)]
|
|
1125
1119
|
sub_mods_imports.append("")
|
|
1126
1120
|
|
|
1127
1121
|
if sub_mod_attrs:
|
|
1128
|
-
|
|
1129
|
-
|
|
1122
|
+
flattened_sub_mod_attrs = {
|
|
1123
|
+
imported: module
|
|
1124
|
+
for module, attrs in sub_mod_attrs.items()
|
|
1125
|
+
for imported in attrs
|
|
1130
1126
|
}
|
|
1131
1127
|
# construct the import statement and handle special cases for aliases
|
|
1132
1128
|
sub_mod_attrs_imports = [
|
|
1133
|
-
f"from .{
|
|
1129
|
+
f"from .{module} import "
|
|
1134
1130
|
+ (
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1131
|
+
(
|
|
1132
|
+
(imported[0] + " as " + imported[1])
|
|
1133
|
+
if imported[0] != imported[1]
|
|
1134
|
+
else imported[0]
|
|
1135
|
+
)
|
|
1136
|
+
if isinstance(imported, tuple)
|
|
1137
|
+
else imported
|
|
1140
1138
|
)
|
|
1141
|
-
for
|
|
1139
|
+
for imported, module in flattened_sub_mod_attrs.items()
|
|
1142
1140
|
]
|
|
1143
1141
|
sub_mod_attrs_imports.append("")
|
|
1144
1142
|
|
|
1145
1143
|
text = "\n" + "\n".join([*sub_mods_imports, *sub_mod_attrs_imports])
|
|
1146
|
-
text += ast.unparse(new_tree) + "\n"
|
|
1144
|
+
text += ast.unparse(new_tree) + "\n\n"
|
|
1145
|
+
text += f"__all__ = {getattr(mod, '__all__', [])!r}\n"
|
|
1147
1146
|
return text
|
|
1148
1147
|
|
|
1149
1148
|
def _scan_file(self, module_path: Path) -> tuple[str, str] | None:
|
|
@@ -1258,10 +1257,7 @@ class PyiGenerator:
|
|
|
1258
1257
|
if file_paths:
|
|
1259
1258
|
subprocess.run(["ruff", "format", *file_paths])
|
|
1260
1259
|
subprocess.run(["ruff", "check", "--fix", *file_paths])
|
|
1261
|
-
|
|
1262
|
-
# For some reason, we need to format the __init__.pyi files again after fixing...
|
|
1263
|
-
init_files = [f for f in file_paths if "/__init__.pyi" in f]
|
|
1264
|
-
subprocess.run(["ruff", "format", *init_files])
|
|
1260
|
+
subprocess.run(["ruff", "format", *file_paths])
|
|
1265
1261
|
|
|
1266
1262
|
if use_json:
|
|
1267
1263
|
if file_paths and changed_files is None:
|
|
@@ -1327,19 +1323,6 @@ class PyiGenerator:
|
|
|
1327
1323
|
json.dumps(pyi_hashes, indent=2, sort_keys=True) + "\n"
|
|
1328
1324
|
)
|
|
1329
1325
|
|
|
1330
|
-
# Post-process the generated pyi files to add hacky type: ignore comments
|
|
1331
|
-
for file_path in file_paths:
|
|
1332
|
-
with FileInput(file_path, inplace=True) as f:
|
|
1333
|
-
for line in f:
|
|
1334
|
-
# Hack due to ast not supporting comments in the tree.
|
|
1335
|
-
if (
|
|
1336
|
-
"def create(" in line
|
|
1337
|
-
or "Var[Figure]" in line
|
|
1338
|
-
or "Var[Template]" in line
|
|
1339
|
-
):
|
|
1340
|
-
line = line.rstrip() + " # type: ignore\n"
|
|
1341
|
-
print(line, end="") # noqa: T201
|
|
1342
|
-
|
|
1343
1326
|
|
|
1344
1327
|
if __name__ == "__main__":
|
|
1345
1328
|
import argparse
|
reflex/utils/types.py
CHANGED
|
@@ -495,7 +495,7 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
|
|
|
495
495
|
return list[
|
|
496
496
|
get_attribute_access_type(
|
|
497
497
|
attr.target_class,
|
|
498
|
-
attr.remote_attr.key, #
|
|
498
|
+
attr.remote_attr.key, # pyright: ignore [reportAttributeAccessIssue]
|
|
499
499
|
)
|
|
500
500
|
]
|
|
501
501
|
elif isinstance(cls, type) and not is_generic_alias(cls) and issubclass(cls, Model):
|