reflex 0.7.12a1__py3-none-any.whl → 0.7.13__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 (55) hide show
  1. reflex/.templates/jinja/app/rxconfig.py.jinja2 +1 -0
  2. reflex/.templates/web/postcss.config.js +0 -1
  3. reflex/.templates/web/utils/state.js +1 -1
  4. reflex/__init__.py +1 -0
  5. reflex/__init__.pyi +1 -0
  6. reflex/app.py +101 -29
  7. reflex/compiler/compiler.py +11 -62
  8. reflex/compiler/templates.py +12 -3
  9. reflex/compiler/utils.py +20 -4
  10. reflex/components/component.py +366 -88
  11. reflex/components/core/helmet.pyi +66 -0
  12. reflex/components/datadisplay/code.py +1 -1
  13. reflex/components/datadisplay/shiki_code_block.py +97 -86
  14. reflex/components/datadisplay/shiki_code_block.pyi +4 -2
  15. reflex/components/el/elements/forms.py +1 -1
  16. reflex/components/lucide/icon.py +2 -1
  17. reflex/components/lucide/icon.pyi +1 -0
  18. reflex/components/plotly/plotly.py +2 -2
  19. reflex/components/plotly/plotly.pyi +2 -3
  20. reflex/components/radix/primitives/accordion.py +1 -1
  21. reflex/components/radix/primitives/drawer.py +1 -1
  22. reflex/components/radix/primitives/form.py +1 -1
  23. reflex/components/radix/themes/base.py +4 -11
  24. reflex/components/radix/themes/components/icon_button.py +2 -2
  25. reflex/components/radix/themes/components/text_field.py +3 -0
  26. reflex/components/radix/themes/components/text_field.pyi +2 -0
  27. reflex/components/radix/themes/layout/list.py +1 -1
  28. reflex/components/tags/iter_tag.py +3 -5
  29. reflex/config.py +57 -7
  30. reflex/constants/__init__.py +0 -2
  31. reflex/event.py +154 -93
  32. reflex/experimental/client_state.py +3 -1
  33. reflex/plugins/__init__.py +7 -0
  34. reflex/plugins/base.py +101 -0
  35. reflex/plugins/tailwind_v3.py +255 -0
  36. reflex/plugins/tailwind_v4.py +258 -0
  37. reflex/state.py +24 -3
  38. reflex/utils/build.py +1 -1
  39. reflex/utils/console.py +1 -1
  40. reflex/utils/exec.py +23 -0
  41. reflex/utils/path_ops.py +26 -6
  42. reflex/utils/prerequisites.py +21 -90
  43. reflex/utils/pyi_generator.py +12 -2
  44. reflex/utils/types.py +15 -1
  45. reflex/vars/base.py +59 -4
  46. reflex/vars/object.py +8 -0
  47. {reflex-0.7.12a1.dist-info → reflex-0.7.13.dist-info}/METADATA +2 -2
  48. {reflex-0.7.12a1.dist-info → reflex-0.7.13.dist-info}/RECORD +52 -50
  49. scripts/hatch_build.py +17 -0
  50. reflex/.templates/jinja/web/tailwind.config.js.jinja2 +0 -66
  51. reflex/.templates/web/styles/tailwind.css +0 -6
  52. reflex/constants/style.py +0 -16
  53. {reflex-0.7.12a1.dist-info → reflex-0.7.13.dist-info}/WHEEL +0 -0
  54. {reflex-0.7.12a1.dist-info → reflex-0.7.13.dist-info}/entry_points.txt +0 -0
  55. {reflex-0.7.12a1.dist-info → reflex-0.7.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,255 @@
1
+ """Base class for all plugins."""
2
+
3
+ from pathlib import Path
4
+ from types import SimpleNamespace
5
+
6
+ from reflex.constants.base import Dirs
7
+ from reflex.constants.compiler import Ext, PageNames
8
+ from reflex.plugins.base import Plugin as PluginBase
9
+ from reflex.utils.decorator import once
10
+
11
+
12
+ class Constants(SimpleNamespace):
13
+ """Tailwind constants."""
14
+
15
+ # The Tailwindcss version
16
+ VERSION = "tailwindcss@3.4.17"
17
+ # The Tailwind config.
18
+ CONFIG = "tailwind.config.js"
19
+ # Default Tailwind content paths
20
+ CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
21
+ # Relative tailwind style path to root stylesheet in Dirs.STYLES.
22
+ ROOT_STYLE_PATH = "./tailwind.css"
23
+
24
+ # Content of the style content.
25
+ ROOT_STYLE_CONTENT = """
26
+ @import "tailwindcss/base";
27
+
28
+ @import url('{radix_url}');
29
+
30
+ @tailwind components;
31
+ @tailwind utilities;
32
+ """
33
+
34
+ # The default tailwind css.
35
+ TAILWIND_CSS = "@import url('./tailwind.css');"
36
+
37
+
38
+ @once
39
+ def tailwind_config_js_template():
40
+ """Get the Tailwind config template.
41
+
42
+ Returns:
43
+ The Tailwind config template.
44
+ """
45
+ from reflex.compiler.templates import from_string
46
+
47
+ source = r"""
48
+ {# Helper macro to render JS objects and arrays #}
49
+ {% macro render_js(val, indent=2, level=0) -%}
50
+ {%- set space = ' ' * (indent * level) -%}
51
+ {%- set next_space = ' ' * (indent * (level + 1)) -%}
52
+
53
+ {%- if val is mapping -%}
54
+ {
55
+ {%- for k, v in val.items() %}
56
+ {{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
57
+ {%- endfor %}
58
+ {{ space }}}
59
+ {%- elif val is iterable and val is not string -%}
60
+ [
61
+ {%- for item in val %}
62
+ {{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
63
+ {%- endfor %}
64
+ {{ space }}]
65
+ {%- else -%}
66
+ {{ val | tojson }}
67
+ {%- endif -%}
68
+ {%- endmacro %}
69
+
70
+ {# Extract destructured imports from plugin dicts only #}
71
+ {%- set imports = [] %}
72
+ {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
73
+ {%- set _ = imports.append(plugin.import) %}
74
+ {%- endfor %}
75
+
76
+ /** @type {import('tailwindcss').Config} */
77
+ {%- for imp in imports %}
78
+ const { {{ imp.name }} } = require({{ imp.from | tojson }});
79
+ {%- endfor %}
80
+
81
+ module.exports = {
82
+ content: {{ render_js(content) }},
83
+ theme: {{ render_js(theme) }},
84
+ {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
85
+ {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
86
+ {% if important is defined %}important: {{ important | tojson }},{% endif %}
87
+ {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
88
+ {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
89
+ {% if presets is defined %}
90
+ presets: [
91
+ {% for preset in presets %}
92
+ require({{ preset | tojson }}){{ "," if not loop.last }}
93
+ {% endfor %}
94
+ ],
95
+ {% endif %}
96
+ plugins: [
97
+ {% for plugin in plugins %}
98
+ {% if plugin is mapping %}
99
+ {% if plugin.call is defined %}
100
+ {{ plugin.call }}(
101
+ {%- if plugin.args is defined -%}
102
+ {{ render_js(plugin.args) }}
103
+ {%- endif -%}
104
+ ){{ "," if not loop.last }}
105
+ {% else %}
106
+ require({{ plugin.name | tojson }}){{ "," if not loop.last }}
107
+ {% endif %}
108
+ {% else %}
109
+ require({{ plugin | tojson }}){{ "," if not loop.last }}
110
+ {% endif %}
111
+ {% endfor %}
112
+ ]
113
+ };
114
+ """
115
+
116
+ return from_string(source)
117
+
118
+
119
+ def compile_config(
120
+ config: dict,
121
+ ):
122
+ """Compile the Tailwind config.
123
+
124
+ Args:
125
+ config: The Tailwind config.
126
+
127
+ Returns:
128
+ The compiled Tailwind config.
129
+ """
130
+ return Constants.CONFIG, tailwind_config_js_template().render(
131
+ **config,
132
+ )
133
+
134
+
135
+ def compile_root_style():
136
+ """Compile the Tailwind root style.
137
+
138
+ Returns:
139
+ The compiled Tailwind root style.
140
+ """
141
+ from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
142
+
143
+ return str(
144
+ Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH
145
+ ), Constants.ROOT_STYLE_CONTENT.format(
146
+ radix_url=RADIX_THEMES_STYLESHEET,
147
+ )
148
+
149
+
150
+ def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None:
151
+ return next(
152
+ (i for i, line in enumerate(haystack) if needle in line),
153
+ None,
154
+ )
155
+
156
+
157
+ def add_tailwind_to_postcss_config(postcss_file_content: str) -> str:
158
+ """Add tailwind to the postcss config.
159
+
160
+ Args:
161
+ postcss_file_content: The content of the postcss config file.
162
+
163
+ Returns:
164
+ The modified postcss config file content.
165
+ """
166
+ from reflex.constants import Dirs
167
+
168
+ postcss_file_lines = postcss_file_content.splitlines()
169
+
170
+ if _index_of_element_that_has(postcss_file_lines, "tailwindcss") is not None:
171
+ return postcss_file_content
172
+
173
+ line_with_postcss_plugins = _index_of_element_that_has(
174
+ postcss_file_lines, "plugins"
175
+ )
176
+ if not line_with_postcss_plugins:
177
+ print( # noqa: T201
178
+ f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
179
+ "Please make sure the file exists and is valid."
180
+ )
181
+ return postcss_file_content
182
+
183
+ postcss_import_line = _index_of_element_that_has(
184
+ postcss_file_lines, '"postcss-import"'
185
+ )
186
+ postcss_file_lines.insert(
187
+ (postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {},"
188
+ )
189
+
190
+ return "\n".join(postcss_file_lines)
191
+
192
+
193
+ def add_tailwind_to_css_file(css_file_content: str) -> str:
194
+ """Add tailwind to the css file.
195
+
196
+ Args:
197
+ css_file_content: The content of the css file.
198
+
199
+ Returns:
200
+ The modified css file content.
201
+ """
202
+ from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
203
+
204
+ if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content:
205
+ return css_file_content
206
+ if RADIX_THEMES_STYLESHEET not in css_file_content:
207
+ print( # noqa: T201
208
+ f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. "
209
+ "Please make sure the file exists and is valid."
210
+ )
211
+ return css_file_content
212
+ return css_file_content.replace(
213
+ f"@import url('{RADIX_THEMES_STYLESHEET}');",
214
+ Constants.TAILWIND_CSS,
215
+ )
216
+
217
+
218
+ class Plugin(PluginBase):
219
+ """Plugin for Tailwind CSS."""
220
+
221
+ def get_frontend_development_dependencies(self, **context) -> list[str]:
222
+ """Get the packages required by the plugin.
223
+
224
+ Args:
225
+ **context: The context for the plugin.
226
+
227
+ Returns:
228
+ A list of packages required by the plugin.
229
+ """
230
+ from reflex.config import get_config
231
+
232
+ config = get_config()
233
+ return [
234
+ plugin if isinstance(plugin, str) else plugin.get("name")
235
+ for plugin in (config.tailwind or {}).get("plugins", [])
236
+ ] + [Constants.VERSION]
237
+
238
+ def pre_compile(self, **context):
239
+ """Pre-compile the plugin.
240
+
241
+ Args:
242
+ context: The context for the plugin.
243
+ """
244
+ from reflex.config import get_config
245
+
246
+ config = get_config().tailwind or {}
247
+
248
+ config["content"] = config.get("content", Constants.CONTENT)
249
+ context["add_save_task"](compile_config, config)
250
+ context["add_save_task"](compile_root_style)
251
+ context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
252
+ context["add_modify_task"](
253
+ str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
254
+ add_tailwind_to_css_file,
255
+ )
@@ -0,0 +1,258 @@
1
+ """Base class for all plugins."""
2
+
3
+ from pathlib import Path
4
+ from types import SimpleNamespace
5
+
6
+ from reflex.constants.base import Dirs
7
+ from reflex.constants.compiler import Ext, PageNames
8
+ from reflex.plugins.base import Plugin as PluginBase
9
+ from reflex.utils.decorator import once
10
+
11
+
12
+ class Constants(SimpleNamespace):
13
+ """Tailwind constants."""
14
+
15
+ # The Tailwindcss version
16
+ VERSION = "tailwindcss@4.1.7"
17
+ # The Tailwind config.
18
+ CONFIG = "tailwind.config.js"
19
+ # Default Tailwind content paths
20
+ CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
21
+ # Relative tailwind style path to root stylesheet in Dirs.STYLES.
22
+ ROOT_STYLE_PATH = "./tailwind.css"
23
+
24
+ # Content of the style content.
25
+ ROOT_STYLE_CONTENT = """@layer theme, base, components, utilities;
26
+ @config "../tailwind.config.js";
27
+ @import "tailwindcss/theme.css" layer(theme);
28
+ @import "tailwindcss/preflight.css" layer(base);
29
+ @import "{radix_url}" layer(components);
30
+ @import "tailwindcss/utilities.css" layer(utilities);
31
+ """
32
+
33
+ # The default tailwind css.
34
+ TAILWIND_CSS = "@import url('./tailwind.css');"
35
+
36
+
37
+ @once
38
+ def tailwind_config_js_template():
39
+ """Get the Tailwind config template.
40
+
41
+ Returns:
42
+ The Tailwind config template.
43
+ """
44
+ from reflex.compiler.templates import from_string
45
+
46
+ source = r"""
47
+ {# Helper macro to render JS objects and arrays #}
48
+ {% macro render_js(val, indent=2, level=0) -%}
49
+ {%- set space = ' ' * (indent * level) -%}
50
+ {%- set next_space = ' ' * (indent * (level + 1)) -%}
51
+
52
+ {%- if val is mapping -%}
53
+ {
54
+ {%- for k, v in val.items() %}
55
+ {{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
56
+ {%- endfor %}
57
+ {{ space }}}
58
+ {%- elif val is iterable and val is not string -%}
59
+ [
60
+ {%- for item in val %}
61
+ {{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
62
+ {%- endfor %}
63
+ {{ space }}]
64
+ {%- else -%}
65
+ {{ val | tojson }}
66
+ {%- endif -%}
67
+ {%- endmacro %}
68
+
69
+ {# Extract destructured imports from plugin dicts only #}
70
+ {%- set imports = [] %}
71
+ {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
72
+ {%- set _ = imports.append(plugin.import) %}
73
+ {%- endfor %}
74
+
75
+ /** @type {import('tailwindcss').Config} */
76
+ {%- for imp in imports %}
77
+ const { {{ imp.name }} } = require({{ imp.from | tojson }});
78
+ {%- endfor %}
79
+
80
+ module.exports = {
81
+ content: {{ render_js(content) }},
82
+ theme: {{ render_js(theme) }},
83
+ {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
84
+ {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
85
+ {% if important is defined %}important: {{ important | tojson }},{% endif %}
86
+ {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
87
+ {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
88
+ {% if presets is defined %}
89
+ presets: [
90
+ {% for preset in presets %}
91
+ require({{ preset | tojson }}){{ "," if not loop.last }}
92
+ {% endfor %}
93
+ ],
94
+ {% endif %}
95
+ plugins: [
96
+ {% for plugin in plugins %}
97
+ {% if plugin is mapping %}
98
+ {% if plugin.call is defined %}
99
+ {{ plugin.call }}(
100
+ {%- if plugin.args is defined -%}
101
+ {{ render_js(plugin.args) }}
102
+ {%- endif -%}
103
+ ){{ "," if not loop.last }}
104
+ {% else %}
105
+ require({{ plugin.name | tojson }}){{ "," if not loop.last }}
106
+ {% endif %}
107
+ {% else %}
108
+ require({{ plugin | tojson }}){{ "," if not loop.last }}
109
+ {% endif %}
110
+ {% endfor %}
111
+ ]
112
+ };
113
+ """
114
+
115
+ return from_string(source)
116
+
117
+
118
+ def compile_config(
119
+ config: dict,
120
+ ):
121
+ """Compile the Tailwind config.
122
+
123
+ Args:
124
+ config: The Tailwind config.
125
+
126
+ Returns:
127
+ The compiled Tailwind config.
128
+ """
129
+ return Constants.CONFIG, tailwind_config_js_template().render(
130
+ **config,
131
+ )
132
+
133
+
134
+ def compile_root_style():
135
+ """Compile the Tailwind root style.
136
+
137
+ Returns:
138
+ The compiled Tailwind root style.
139
+ """
140
+ from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
141
+
142
+ return str(
143
+ Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH
144
+ ), Constants.ROOT_STYLE_CONTENT.format(
145
+ radix_url=RADIX_THEMES_STYLESHEET,
146
+ )
147
+
148
+
149
+ def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None:
150
+ return next(
151
+ (i for i, line in enumerate(haystack) if needle in line),
152
+ None,
153
+ )
154
+
155
+
156
+ def add_tailwind_to_postcss_config(postcss_file_content: str) -> str:
157
+ """Add tailwind to the postcss config.
158
+
159
+ Args:
160
+ postcss_file_content: The content of the postcss config file.
161
+
162
+ Returns:
163
+ The modified postcss config file content.
164
+ """
165
+ from reflex.constants import Dirs
166
+
167
+ postcss_file_lines = postcss_file_content.splitlines()
168
+
169
+ line_with_postcss_plugins = _index_of_element_that_has(
170
+ postcss_file_lines, "plugins"
171
+ )
172
+ if not line_with_postcss_plugins:
173
+ print( # noqa: T201
174
+ f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
175
+ "Please make sure the file exists and is valid."
176
+ )
177
+ return postcss_file_content
178
+
179
+ plugins_to_remove = ['"postcss-import"', "tailwindcss", "autoprefixer"]
180
+ plugins_to_add = ['"@tailwindcss/postcss"']
181
+
182
+ for plugin in plugins_to_remove:
183
+ plugin_index = _index_of_element_that_has(postcss_file_lines, plugin)
184
+ if plugin_index is not None:
185
+ postcss_file_lines.pop(plugin_index)
186
+
187
+ for plugin in plugins_to_add[::-1]:
188
+ if not _index_of_element_that_has(postcss_file_lines, plugin):
189
+ postcss_file_lines.insert(
190
+ line_with_postcss_plugins + 1, f" {plugin}: {{}},"
191
+ )
192
+
193
+ return "\n".join(postcss_file_lines)
194
+
195
+
196
+ def add_tailwind_to_css_file(css_file_content: str) -> str:
197
+ """Add tailwind to the css file.
198
+
199
+ Args:
200
+ css_file_content: The content of the css file.
201
+
202
+ Returns:
203
+ The modified css file content.
204
+ """
205
+ from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
206
+
207
+ if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content:
208
+ return css_file_content
209
+ if RADIX_THEMES_STYLESHEET not in css_file_content:
210
+ print( # noqa: T201
211
+ f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. "
212
+ "Please make sure the file exists and is valid."
213
+ )
214
+ return css_file_content
215
+ return css_file_content.replace(
216
+ f"@import url('{RADIX_THEMES_STYLESHEET}');",
217
+ Constants.TAILWIND_CSS,
218
+ )
219
+
220
+
221
+ class Plugin(PluginBase):
222
+ """Plugin for Tailwind CSS."""
223
+
224
+ def get_frontend_development_dependencies(self, **context) -> list[str]:
225
+ """Get the packages required by the plugin.
226
+
227
+ Args:
228
+ **context: The context for the plugin.
229
+
230
+ Returns:
231
+ A list of packages required by the plugin.
232
+ """
233
+ from reflex.config import get_config
234
+
235
+ config = get_config()
236
+ return [
237
+ plugin if isinstance(plugin, str) else plugin.get("name")
238
+ for plugin in (config.tailwind or {}).get("plugins", [])
239
+ ] + [Constants.VERSION, "@tailwindcss/postcss@4.1.7"]
240
+
241
+ def pre_compile(self, **context):
242
+ """Pre-compile the plugin.
243
+
244
+ Args:
245
+ context: The context for the plugin.
246
+ """
247
+ from reflex.config import get_config
248
+
249
+ config = get_config().tailwind or {}
250
+
251
+ config["content"] = config.get("content", Constants.CONTENT)
252
+ context["add_save_task"](compile_config, config)
253
+ context["add_save_task"](compile_root_style)
254
+ context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
255
+ context["add_modify_task"](
256
+ str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
257
+ add_tailwind_to_css_file,
258
+ )
reflex/state.py CHANGED
@@ -1003,6 +1003,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1003
1003
  raise ValueError(f"Invalid path: {path}")
1004
1004
  return getattr(substate, name)
1005
1005
 
1006
+ @classmethod
1007
+ def is_user_defined(cls) -> bool:
1008
+ """Check if the state is user-defined.
1009
+
1010
+ Returns:
1011
+ True if the state is user-defined, False otherwise.
1012
+ """
1013
+ return (
1014
+ not cls.__module__.startswith("reflex.")
1015
+ or cls.__module__ == "reflex.istate.dynamic"
1016
+ )
1017
+
1006
1018
  @classmethod
1007
1019
  def _init_var(cls, prop: Var):
1008
1020
  """Initialize a variable.
@@ -1024,7 +1036,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1024
1036
  f'Found var "{prop._js_expr}" with type {prop._var_type}.'
1025
1037
  )
1026
1038
  cls._set_var(prop)
1027
- if get_config().state_auto_setters:
1039
+ if cls.is_user_defined() and get_config().state_auto_setters:
1028
1040
  cls._create_setter(prop)
1029
1041
  cls._set_default_value(prop)
1030
1042
 
@@ -2330,6 +2342,15 @@ class State(BaseState):
2330
2342
  # The hydrated bool.
2331
2343
  is_hydrated: bool = False
2332
2344
 
2345
+ @event
2346
+ def set_is_hydrated(self, value: bool) -> None:
2347
+ """Set the hydrated state.
2348
+
2349
+ Args:
2350
+ value: The hydrated state.
2351
+ """
2352
+ self.is_hydrated = value
2353
+
2333
2354
 
2334
2355
  T = TypeVar("T", bound=BaseState)
2335
2356
 
@@ -2424,7 +2445,7 @@ class OnLoadInternalState(State):
2424
2445
  This is a separate substate to avoid deserializing the entire state tree for every page navigation.
2425
2446
  """
2426
2447
 
2427
- def on_load_internal(self) -> list[Event | EventSpec] | None:
2448
+ def on_load_internal(self) -> list[Event | EventSpec | event.EventCallback] | None:
2428
2449
  """Queue on_load handlers for the current page.
2429
2450
 
2430
2451
  Returns:
@@ -2444,7 +2465,7 @@ class OnLoadInternalState(State):
2444
2465
  self.router.session.client_token,
2445
2466
  router_data=self.router_data,
2446
2467
  ),
2447
- State.set_is_hydrated(True), # pyright: ignore [reportAttributeAccessIssue]
2468
+ State.set_is_hydrated(True),
2448
2469
  ]
2449
2470
 
2450
2471
 
reflex/utils/build.py CHANGED
@@ -230,7 +230,7 @@ def setup_frontend(
230
230
  """
231
231
  # Create the assets dir if it doesn't exist.
232
232
  path_ops.mkdir(constants.Dirs.APP_ASSETS)
233
- path_ops.cp(
233
+ path_ops.copy_tree(
234
234
  src=str(root / constants.Dirs.APP_ASSETS),
235
235
  dest=str(root / prerequisites.get_web_dir() / constants.Dirs.PUBLIC),
236
236
  ignore=tuple(f"*.{ext}" for ext in constants.Reflex.STYLESHEETS_SUPPORTED),
reflex/utils/console.py CHANGED
@@ -65,7 +65,7 @@ def set_log_level(log_level: LogLevel | None):
65
65
  global _LOG_LEVEL
66
66
  if log_level != _LOG_LEVEL:
67
67
  # Set the loglevel persistenly for subprocesses.
68
- os.environ["LOGLEVEL"] = log_level.value
68
+ os.environ["REFLEX_LOGLEVEL"] = log_level.value
69
69
  _LOG_LEVEL = log_level
70
70
 
71
71
 
reflex/utils/exec.py CHANGED
@@ -374,6 +374,23 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel):
374
374
  )
375
375
 
376
376
 
377
+ HOTRELOAD_IGNORE_EXTENSIONS = (
378
+ "txt",
379
+ "toml",
380
+ "sqlite",
381
+ "yaml",
382
+ "yml",
383
+ "json",
384
+ "sh",
385
+ "bash",
386
+ )
387
+
388
+ HOTRELOAD_IGNORE_PATTERNS = (
389
+ *[rf"^.*\.{ext}$" for ext in HOTRELOAD_IGNORE_EXTENSIONS],
390
+ r"^[^\.]*$", # Ignore files without an extension
391
+ )
392
+
393
+
377
394
  def run_granian_backend(host: str, port: int, loglevel: LogLevel):
378
395
  """Run the backend in development mode using Granian.
379
396
 
@@ -384,6 +401,11 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
384
401
  """
385
402
  console.debug("Using Granian for backend")
386
403
 
404
+ if environment.REFLEX_STRICT_HOT_RELOAD.get():
405
+ import multiprocessing
406
+
407
+ multiprocessing.set_start_method("spawn", force=True)
408
+
387
409
  from granian.constants import Interfaces
388
410
  from granian.log import LogLevels
389
411
  from granian.server import MPServer as Granian
@@ -398,6 +420,7 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
398
420
  reload=True,
399
421
  reload_paths=get_reload_paths(),
400
422
  reload_ignore_worker_failure=True,
423
+ reload_ignore_patterns=HOTRELOAD_IGNORE_PATTERNS,
401
424
  reload_tick=100,
402
425
  workers_kill_timeout=2,
403
426
  ).serve()
reflex/utils/path_ops.py CHANGED
@@ -42,6 +42,31 @@ def rm(path: str | Path):
42
42
  path.unlink()
43
43
 
44
44
 
45
+ def copy_tree(
46
+ src: str | Path,
47
+ dest: str | Path,
48
+ ignore: tuple[str, ...] | None = None,
49
+ ):
50
+ """Copy a directory tree.
51
+
52
+ Args:
53
+ src: The path to the source directory.
54
+ dest: The path to the destination directory.
55
+ ignore: Ignoring files and directories that match one of the glob-style patterns provided
56
+ """
57
+ src = Path(src)
58
+ dest = Path(dest)
59
+ if dest.exists():
60
+ for item in dest.iterdir():
61
+ rm(item)
62
+ shutil.copytree(
63
+ src,
64
+ dest,
65
+ ignore=shutil.ignore_patterns(*ignore) if ignore is not None else ignore,
66
+ dirs_exist_ok=True,
67
+ )
68
+
69
+
45
70
  def cp(
46
71
  src: str | Path,
47
72
  dest: str | Path,
@@ -65,12 +90,7 @@ def cp(
65
90
  if not overwrite and dest.exists():
66
91
  return False
67
92
  if src.is_dir():
68
- rm(dest)
69
- shutil.copytree(
70
- src,
71
- dest,
72
- ignore=shutil.ignore_patterns(*ignore) if ignore is not None else ignore,
73
- )
93
+ copy_tree(src, dest, ignore)
74
94
  else:
75
95
  shutil.copyfile(src, dest)
76
96
  return True