reflex 0.8.15a1__py3-none-any.whl → 0.8.16a1__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 (139) hide show
  1. reflex/.templates/web/utils/state.js +68 -8
  2. reflex/app.py +45 -51
  3. reflex/app_mixins/lifespan.py +12 -5
  4. reflex/base.py +1 -0
  5. reflex/compiler/compiler.py +4 -6
  6. reflex/compiler/templates.py +25 -31
  7. reflex/compiler/utils.py +6 -5
  8. reflex/components/base/body.pyi +1 -195
  9. reflex/components/base/link.pyi +1 -407
  10. reflex/components/base/meta.pyi +1 -405
  11. reflex/components/base/script.pyi +1 -237
  12. reflex/components/component.py +41 -46
  13. reflex/components/core/auto_scroll.pyi +1 -195
  14. reflex/components/core/banner.pyi +1 -391
  15. reflex/components/core/breakpoints.py +14 -18
  16. reflex/components/core/html.pyi +1 -197
  17. reflex/components/core/match.py +2 -2
  18. reflex/components/core/sticky.py +11 -15
  19. reflex/components/core/sticky.pyi +0 -788
  20. reflex/components/core/upload.py +1 -3
  21. reflex/components/datadisplay/code.pyi +1 -0
  22. reflex/components/datadisplay/dataeditor.py +4 -6
  23. reflex/components/datadisplay/shiki_code_block.py +106 -110
  24. reflex/components/dynamic.py +4 -6
  25. reflex/components/el/elements/__init__.py +5 -7
  26. reflex/components/el/elements/__init__.pyi +5 -7
  27. reflex/components/el/elements/base.py +1 -1
  28. reflex/components/el/elements/base.pyi +1 -195
  29. reflex/components/el/elements/forms.py +7 -9
  30. reflex/components/el/elements/forms.pyi +12 -3112
  31. reflex/components/el/elements/inline.pyi +0 -5481
  32. reflex/components/el/elements/media.pyi +0 -10280
  33. reflex/components/el/elements/metadata.pyi +1 -835
  34. reflex/components/el/elements/other.pyi +1 -1365
  35. reflex/components/el/elements/scripts.pyi +1 -625
  36. reflex/components/el/elements/sectioning.pyi +1 -2911
  37. reflex/components/el/elements/tables.pyi +1 -1973
  38. reflex/components/el/elements/typography.pyi +1 -3125
  39. reflex/components/lucide/icon.py +4 -4
  40. reflex/components/lucide/icon.pyi +0 -4
  41. reflex/components/markdown/markdown.py +15 -19
  42. reflex/components/markdown/markdown.pyi +1 -0
  43. reflex/components/moment/moment.pyi +0 -49
  44. reflex/components/props.py +3 -3
  45. reflex/components/radix/primitives/accordion.py +4 -6
  46. reflex/components/radix/primitives/accordion.pyi +0 -14
  47. reflex/components/radix/primitives/base.pyi +0 -5
  48. reflex/components/radix/primitives/dialog.py +2 -0
  49. reflex/components/radix/primitives/dialog.pyi +1 -233
  50. reflex/components/radix/primitives/drawer.pyi +0 -18
  51. reflex/components/radix/primitives/form.pyi +30 -632
  52. reflex/components/radix/primitives/progress.pyi +0 -10
  53. reflex/components/radix/primitives/slider.pyi +0 -10
  54. reflex/components/radix/themes/color_mode.pyi +1 -284
  55. reflex/components/radix/themes/components/alert_dialog.pyi +0 -207
  56. reflex/components/radix/themes/components/aspect_ratio.pyi +0 -2
  57. reflex/components/radix/themes/components/avatar.pyi +0 -80
  58. reflex/components/radix/themes/components/badge.pyi +1 -270
  59. reflex/components/radix/themes/components/button.pyi +1 -274
  60. reflex/components/radix/themes/components/callout.pyi +0 -1197
  61. reflex/components/radix/themes/components/card.pyi +1 -209
  62. reflex/components/radix/themes/components/checkbox.pyi +0 -261
  63. reflex/components/radix/themes/components/checkbox_cards.pyi +1 -96
  64. reflex/components/radix/themes/components/checkbox_group.pyi +1 -80
  65. reflex/components/radix/themes/components/context_menu.pyi +13 -321
  66. reflex/components/radix/themes/components/data_list.pyi +1 -107
  67. reflex/components/radix/themes/components/dialog.pyi +1 -210
  68. reflex/components/radix/themes/components/dropdown_menu.pyi +0 -209
  69. reflex/components/radix/themes/components/hover_card.pyi +1 -246
  70. reflex/components/radix/themes/components/icon_button.pyi +1 -195
  71. reflex/components/radix/themes/components/inset.pyi +0 -252
  72. reflex/components/radix/themes/components/popover.pyi +1 -234
  73. reflex/components/radix/themes/components/progress.pyi +1 -84
  74. reflex/components/radix/themes/components/radio.pyi +1 -72
  75. reflex/components/radix/themes/components/radio_cards.pyi +1 -123
  76. reflex/components/radix/themes/components/scroll_area.pyi +1 -11
  77. reflex/components/radix/themes/components/select.pyi +1 -376
  78. reflex/components/radix/themes/components/separator.pyi +0 -77
  79. reflex/components/radix/themes/components/skeleton.pyi +0 -30
  80. reflex/components/radix/themes/components/slider.py +3 -5
  81. reflex/components/radix/themes/components/spinner.pyi +0 -5
  82. reflex/components/radix/themes/components/switch.pyi +0 -89
  83. reflex/components/radix/themes/components/table.pyi +0 -1453
  84. reflex/components/radix/themes/components/text_area.pyi +7 -282
  85. reflex/components/radix/themes/components/text_field.pyi +6 -392
  86. reflex/components/radix/themes/components/tooltip.pyi +0 -42
  87. reflex/components/radix/themes/layout/box.pyi +1 -195
  88. reflex/components/radix/themes/layout/center.pyi +0 -194
  89. reflex/components/radix/themes/layout/container.pyi +0 -178
  90. reflex/components/radix/themes/layout/flex.pyi +0 -194
  91. reflex/components/radix/themes/layout/grid.pyi +0 -194
  92. reflex/components/radix/themes/layout/list.pyi +0 -978
  93. reflex/components/radix/themes/layout/section.pyi +0 -194
  94. reflex/components/radix/themes/layout/spacer.pyi +0 -194
  95. reflex/components/radix/themes/layout/stack.pyi +0 -582
  96. reflex/components/radix/themes/typography/blockquote.pyi +0 -196
  97. reflex/components/radix/themes/typography/code.pyi +0 -194
  98. reflex/components/radix/themes/typography/heading.pyi +0 -194
  99. reflex/components/radix/themes/typography/link.pyi +0 -237
  100. reflex/components/radix/themes/typography/text.pyi +0 -1360
  101. reflex/components/react_router/dom.pyi +0 -237
  102. reflex/components/recharts/cartesian.py +12 -18
  103. reflex/components/recharts/general.py +12 -18
  104. reflex/constants/installer.py +5 -5
  105. reflex/custom_components/custom_components.py +6 -5
  106. reflex/environment.py +30 -7
  107. reflex/event.py +14 -12
  108. reflex/experimental/client_state.py +11 -12
  109. reflex/istate/data.py +8 -10
  110. reflex/istate/manager/__init__.py +3 -0
  111. reflex/istate/manager/disk.py +151 -5
  112. reflex/model.py +1 -1
  113. reflex/plugins/_screenshot.py +2 -2
  114. reflex/plugins/shared_tailwind.py +9 -14
  115. reflex/reflex.py +7 -9
  116. reflex/state.py +30 -37
  117. reflex/style.py +6 -6
  118. reflex/testing.py +54 -30
  119. reflex/utils/codespaces.py +31 -2
  120. reflex/utils/compat.py +1 -0
  121. reflex/utils/decorator.py +3 -3
  122. reflex/utils/format.py +18 -22
  123. reflex/utils/prerequisites.py +1 -1
  124. reflex/utils/pyi_generator.py +51 -57
  125. reflex/utils/serializers.py +1 -1
  126. reflex/utils/telemetry.py +1 -1
  127. reflex/utils/templates.py +4 -4
  128. reflex/utils/types.py +11 -4
  129. reflex/vars/base.py +26 -29
  130. reflex/vars/color.py +6 -8
  131. reflex/vars/dep_tracking.py +5 -3
  132. reflex/vars/function.py +3 -3
  133. reflex/vars/object.py +9 -13
  134. reflex/vars/sequence.py +18 -24
  135. {reflex-0.8.15a1.dist-info → reflex-0.8.16a1.dist-info}/METADATA +1 -1
  136. {reflex-0.8.15a1.dist-info → reflex-0.8.16a1.dist-info}/RECORD +139 -139
  137. {reflex-0.8.15a1.dist-info → reflex-0.8.16a1.dist-info}/WHEEL +0 -0
  138. {reflex-0.8.15a1.dist-info → reflex-0.8.16a1.dist-info}/entry_points.txt +0 -0
  139. {reflex-0.8.15a1.dist-info → reflex-0.8.16a1.dist-info}/licenses/LICENSE +0 -0
@@ -4,15 +4,27 @@ import asyncio
4
4
  import contextlib
5
5
  import dataclasses
6
6
  import functools
7
+ import time
7
8
  from collections.abc import AsyncIterator
8
9
  from hashlib import md5
9
10
  from pathlib import Path
10
11
 
11
12
  from typing_extensions import override
12
13
 
14
+ from reflex.environment import environment
13
15
  from reflex.istate.manager import StateManager, _default_token_expiration
14
16
  from reflex.state import BaseState, _split_substate_key, _substate_key
15
- from reflex.utils import path_ops, prerequisites
17
+ from reflex.utils import console, path_ops, prerequisites
18
+ from reflex.utils.misc import run_in_thread
19
+
20
+
21
+ @dataclasses.dataclass(frozen=True)
22
+ class QueueItem:
23
+ """An item in the write queue."""
24
+
25
+ token: str
26
+ state: BaseState
27
+ timestamp: float
16
28
 
17
29
 
18
30
  @dataclasses.dataclass
@@ -34,6 +46,22 @@ class StateManagerDisk(StateManager):
34
46
  # The token expiration time (s).
35
47
  token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
36
48
 
49
+ # Last time a token was touched.
50
+ _token_last_touched: dict[str, float] = dataclasses.field(
51
+ default_factory=dict,
52
+ init=False,
53
+ )
54
+
55
+ # Pending writes
56
+ _write_queue: dict[str, QueueItem] = dataclasses.field(
57
+ default_factory=dict,
58
+ init=False,
59
+ )
60
+ _write_queue_task: asyncio.Task | None = None
61
+ _write_debounce_seconds: float = dataclasses.field(
62
+ default=environment.REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS.get()
63
+ )
64
+
37
65
  def __post_init__(self):
38
66
  """Create a new state manager."""
39
67
  path_ops.mkdir(self.states_directory)
@@ -51,8 +79,6 @@ class StateManagerDisk(StateManager):
51
79
 
52
80
  def _purge_expired_states(self):
53
81
  """Purge expired states from the disk."""
54
- import time
55
-
56
82
  for path in path_ops.ls(self.states_directory):
57
83
  # check path is a pickle file
58
84
  if path.suffix != ".pkl":
@@ -137,6 +163,7 @@ class StateManagerDisk(StateManager):
137
163
  The state for the token.
138
164
  """
139
165
  client_token = _split_substate_key(token)[0]
166
+ self._token_last_touched[client_token] = time.time()
140
167
  root_state = self.states.get(client_token)
141
168
  if root_state is not None:
142
169
  # Retrieved state from memory.
@@ -170,11 +197,109 @@ class StateManagerDisk(StateManager):
170
197
  if pickle_state:
171
198
  if not self.states_directory.exists():
172
199
  self.states_directory.mkdir(parents=True, exist_ok=True)
173
- self.token_path(substate_token).write_bytes(pickle_state)
200
+ await run_in_thread(
201
+ lambda: self.token_path(substate_token).write_bytes(pickle_state),
202
+ )
174
203
 
175
204
  for substate_substate in substate.substates.values():
176
205
  await self.set_state_for_substate(client_token, substate_substate)
177
206
 
207
+ async def _process_write_queue_delay(self):
208
+ """Wait for the debounce period before processing the write queue again."""
209
+ now = time.time()
210
+ if self._write_queue:
211
+ # There are still items in the queue, schedule another run.
212
+ next_write_in = max(
213
+ 0,
214
+ min(
215
+ self._write_debounce_seconds - (now - item.timestamp)
216
+ for item in self._write_queue.values()
217
+ ),
218
+ )
219
+ await asyncio.sleep(next_write_in)
220
+ elif self._write_debounce_seconds > 0:
221
+ # No items left, wait a bit before checking again.
222
+ await asyncio.sleep(self._write_debounce_seconds)
223
+ else:
224
+ # Debounce is disabled, so sleep until the next token expiration.
225
+ oldest_token_last_touch = min(
226
+ self._token_last_touched.values(), default=now
227
+ )
228
+ next_expiration_in = self.token_expiration - (now - oldest_token_last_touch)
229
+ await asyncio.sleep(next_expiration_in)
230
+
231
+ async def _process_write_queue(self):
232
+ """Long running task that checks for states to write to disk.
233
+
234
+ Raises:
235
+ asyncio.CancelledError: When the task is cancelled.
236
+ """
237
+ while True:
238
+ try:
239
+ now = time.time()
240
+ # sort the _write_queue by oldest timestamp and exclude items younger than debounce time
241
+ items_to_write = sorted(
242
+ (
243
+ item
244
+ for item in self._write_queue.values()
245
+ if now - item.timestamp >= self._write_debounce_seconds
246
+ ),
247
+ key=lambda item: item.timestamp,
248
+ )
249
+ for item in items_to_write:
250
+ token = item.token
251
+ client_token, _ = _split_substate_key(token)
252
+ await self.set_state_for_substate(
253
+ client_token, self._write_queue.pop(token).state
254
+ )
255
+ # Check for expired states to purge.
256
+ for token, last_touched in list(self._token_last_touched.items()):
257
+ if now - last_touched > self.token_expiration:
258
+ self._token_last_touched.pop(token)
259
+ self.states.pop(token, None)
260
+ await run_in_thread(self._purge_expired_states)
261
+ await self._process_write_queue_delay()
262
+ except asyncio.CancelledError: # noqa: PERF203
263
+ await self._flush_write_queue()
264
+ raise
265
+ except Exception as e:
266
+ console.error(f"Error processing write queue: {e!r}")
267
+ if e.args == ("cannot schedule new futures after shutdown",):
268
+ # Event loop is shutdown, nothing else we can really do...
269
+ return
270
+ await self._process_write_queue_delay()
271
+
272
+ async def _flush_write_queue(self):
273
+ """Flush any remaining items in the write queue to disk."""
274
+ outstanding_items = list(self._write_queue.values())
275
+ n_outstanding_items = len(outstanding_items)
276
+ self._write_queue.clear()
277
+ # When the task is cancelled, write all remaining items to disk.
278
+ console.debug(
279
+ f"StateManagerDisk._flush_write_queue: writing {n_outstanding_items} remaining items to disk"
280
+ )
281
+ for item in outstanding_items:
282
+ token = item.token
283
+ client_token, _ = _split_substate_key(token)
284
+ await self.set_state_for_substate(
285
+ client_token,
286
+ item.state,
287
+ )
288
+ console.debug(
289
+ f"StateManagerDisk._flush_write_queue: Finished writing {n_outstanding_items} items"
290
+ )
291
+
292
+ async def _schedule_process_write_queue(self):
293
+ """Schedule the write queue processing task if not already running."""
294
+ if self._write_queue_task is None or self._write_queue_task.done():
295
+ async with self._state_manager_lock:
296
+ if self._write_queue_task is None or self._write_queue_task.done():
297
+ self._write_queue_task = asyncio.create_task(
298
+ self._process_write_queue(),
299
+ name="StateManagerDisk|WriteQueueProcessor",
300
+ )
301
+ await asyncio.sleep(0) # Yield to allow the task to start.
302
+
178
303
  @override
179
304
  async def set_state(self, token: str, state: BaseState):
180
305
  """Set the state for a token.
@@ -184,7 +309,19 @@ class StateManagerDisk(StateManager):
184
309
  state: The state to set.
185
310
  """
186
311
  client_token, _ = _split_substate_key(token)
187
- await self.set_state_for_substate(client_token, state)
312
+ if self._write_debounce_seconds > 0:
313
+ # Deferred write to reduce disk IO overhead.
314
+ if client_token not in self._write_queue:
315
+ self._write_queue[client_token] = QueueItem(
316
+ token=client_token,
317
+ state=state,
318
+ timestamp=time.time(),
319
+ )
320
+ else:
321
+ # Immediate write to disk.
322
+ await self.set_state_for_substate(client_token, state)
323
+ # Ensure the processing task is scheduled to handle expirations and any deferred writes.
324
+ await self._schedule_process_write_queue()
188
325
 
189
326
  @override
190
327
  @contextlib.asynccontextmanager
@@ -208,3 +345,12 @@ class StateManagerDisk(StateManager):
208
345
  state = await self.get_state(token)
209
346
  yield state
210
347
  await self.set_state(token, state)
348
+
349
+ async def close(self):
350
+ """Close the state manager, flushing any pending writes to disk."""
351
+ async with self._state_manager_lock:
352
+ if self._write_queue_task:
353
+ self._write_queue_task.cancel()
354
+ with contextlib.suppress(asyncio.CancelledError):
355
+ await self._write_queue_task
356
+ self._write_queue_task = None
reflex/model.py CHANGED
@@ -299,7 +299,7 @@ if find_spec("sqlmodel") and find_spec("sqlalchemy") and find_spec("pydantic"):
299
299
  # Format output with message
300
300
  return f" [{status_icon}] {current}{head_marker}, {message}"
301
301
 
302
- async def get_db_status() -> dict[str, bool]:
302
+ def get_db_status() -> dict[str, bool]:
303
303
  """Checks the status of the database connection.
304
304
 
305
305
  Attempts to connect to the database and execute a simple query to verify connectivity.
@@ -68,7 +68,7 @@ class ScreenshotPlugin(BasePlugin):
68
68
  if not app._api:
69
69
  return
70
70
 
71
- async def active_connections(_request: "Request") -> "Response":
71
+ def active_connections(_request: "Request") -> "Response":
72
72
  from starlette.responses import JSONResponse
73
73
 
74
74
  if not app.event_namespace:
@@ -122,7 +122,7 @@ class ScreenshotPlugin(BasePlugin):
122
122
  while found_new:
123
123
  found_new = False
124
124
 
125
- for state in all_states:
125
+ for state in list(all_states):
126
126
  for substate in state.substates.values():
127
127
  substate._was_touched = True
128
128
 
@@ -112,12 +112,9 @@ def tailwind_config_js_template(
112
112
  ]
113
113
 
114
114
  # Generate import statements for destructured imports
115
- import_lines = "\n".join(
116
- [
117
- f"import {{ {imp['name']} }} from {json.dumps(imp['from'])};"
118
- for imp in imports
119
- ]
120
- )
115
+ import_lines = "\n".join([
116
+ f"import {{ {imp['name']} }} from {json.dumps(imp['from'])};" for imp in imports
117
+ ])
121
118
 
122
119
  # Generate plugin imports
123
120
  plugin_imports = []
@@ -131,12 +128,10 @@ def tailwind_config_js_template(
131
128
 
132
129
  plugin_imports_lines = "\n".join(plugin_imports)
133
130
 
134
- presets_imports_lines = "\n".join(
135
- [
136
- f"import preset{i} from {json.dumps(preset)};"
137
- for i, preset in enumerate(presets, 1)
138
- ]
139
- )
131
+ presets_imports_lines = "\n".join([
132
+ f"import preset{i} from {json.dumps(preset)};"
133
+ for i, preset in enumerate(presets, 1)
134
+ ])
140
135
 
141
136
  # Generate plugin array
142
137
  plugin_list = []
@@ -159,8 +154,8 @@ def tailwind_config_js_template(
159
154
  {presets_imports_lines}
160
155
 
161
156
  export default {{
162
- content: {json.dumps(content if content else default_content)},
163
- theme: {json.dumps(theme if theme else {})},
157
+ content: {json.dumps(content or default_content)},
158
+ theme: {json.dumps(theme or {})},
164
159
  {f"darkMode: {json.dumps(dark_mode)}," if dark_mode is not None else ""}
165
160
  {f"corePlugins: {json.dumps(core_plugins)}," if core_plugins is not None else ""}
166
161
  {f"importants: {json.dumps(important)}," if important is not None else ""}
reflex/reflex.py CHANGED
@@ -269,15 +269,13 @@ def _run(
269
269
 
270
270
  # In prod mode, run the backend on a separate thread.
271
271
  if backend and env == constants.Env.PROD:
272
- commands.append(
273
- (
274
- backend_cmd,
275
- backend_host,
276
- backend_port,
277
- config.loglevel.subprocess_level(),
278
- frontend,
279
- )
280
- )
272
+ commands.append((
273
+ backend_cmd,
274
+ backend_host,
275
+ backend_port,
276
+ config.loglevel.subprocess_level(),
277
+ frontend,
278
+ ))
281
279
 
282
280
  if single_port:
283
281
  setup_frontend(Path.cwd())
reflex/state.py CHANGED
@@ -112,13 +112,13 @@ def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Call
112
112
  )
113
113
  if inspect.iscoroutinefunction(fn):
114
114
 
115
- async def _no_chain_background_task_co(*args, **kwargs):
115
+ async def _no_chain_background_task_co(*args, **kwargs): # noqa: RUF029
116
116
  raise RuntimeError(message)
117
117
 
118
118
  return _no_chain_background_task_co
119
119
  if inspect.isasyncgenfunction(fn):
120
120
 
121
- async def _no_chain_background_task_gen(*args, **kwargs):
121
+ async def _no_chain_background_task_gen(*args, **kwargs): # noqa: RUF029
122
122
  yield
123
123
  raise RuntimeError(message)
124
124
 
@@ -187,14 +187,12 @@ class EventHandlerSetVar(EventHandler):
187
187
  Returns:
188
188
  The hash of the event handler.
189
189
  """
190
- return hash(
191
- (
192
- tuple(self.event_actions.items()),
193
- self.fn,
194
- self.state_full_name,
195
- self.state_cls,
196
- )
197
- )
190
+ return hash((
191
+ tuple(self.event_actions.items()),
192
+ self.fn,
193
+ self.state_full_name,
194
+ self.state_cls,
195
+ ))
198
196
 
199
197
  def setvar(self, var_name: str, value: Any):
200
198
  """Set the state variable to the value of the event.
@@ -529,14 +527,12 @@ class BaseState(EvenMoreBasicBaseState):
529
527
  if types.is_backend_base_variable(name, cls)
530
528
  }
531
529
  # Add annotated backend vars that may not have a default value.
532
- new_backend_vars.update(
533
- {
534
- name: cls._get_var_default(name, annotation_value)
535
- for name, annotation_value in cls._get_type_hints().items()
536
- if name not in new_backend_vars
537
- and types.is_backend_base_variable(name, cls)
538
- }
539
- )
530
+ new_backend_vars.update({
531
+ name: cls._get_var_default(name, annotation_value)
532
+ for name, annotation_value in cls._get_type_hints().items()
533
+ if name not in new_backend_vars
534
+ and types.is_backend_base_variable(name, cls)
535
+ })
540
536
 
541
537
  cls.backend_vars = {
542
538
  **cls.inherited_backend_vars,
@@ -794,9 +790,10 @@ class BaseState(EvenMoreBasicBaseState):
794
790
  parent_state = defining_state_cls.get_parent_state()
795
791
  if parent_state is not None:
796
792
  defining_state_cls = parent_state
797
- defining_state_cls._var_dependencies.setdefault(dvar, set()).add(
798
- (cls.get_full_name(), cvar_name)
799
- )
793
+ defining_state_cls._var_dependencies.setdefault(dvar, set()).add((
794
+ cls.get_full_name(),
795
+ cvar_name,
796
+ ))
800
797
  defining_state_cls._potentially_dirty_states.add(
801
798
  cls.get_full_name()
802
799
  )
@@ -1143,7 +1140,7 @@ class BaseState(EvenMoreBasicBaseState):
1143
1140
  from reflex.config import get_config
1144
1141
 
1145
1142
  config = get_config()
1146
- _create_event_handler_kwargs = {}
1143
+ create_event_handler_kwargs = {}
1147
1144
 
1148
1145
  if config.state_auto_setters is None:
1149
1146
 
@@ -1159,14 +1156,14 @@ class BaseState(EvenMoreBasicBaseState):
1159
1156
  )
1160
1157
  return super().__call__(*args, **kwargs)
1161
1158
 
1162
- _create_event_handler_kwargs["event_handler_cls"] = (
1159
+ create_event_handler_kwargs["event_handler_cls"] = (
1163
1160
  EventHandlerDeprecatedSetter
1164
1161
  )
1165
1162
 
1166
1163
  setter_name = Var._get_setter_name_for_name(name)
1167
1164
  if setter_name not in cls.__dict__:
1168
1165
  event_handler = cls._create_event_handler(
1169
- prop._get_setter(name), **_create_event_handler_kwargs
1166
+ prop._get_setter(name), **create_event_handler_kwargs
1170
1167
  )
1171
1168
  cls.event_handlers[setter_name] = event_handler
1172
1169
  setattr(cls, setter_name, event_handler)
@@ -1524,12 +1521,10 @@ class BaseState(EvenMoreBasicBaseState):
1524
1521
  return {
1525
1522
  cls.get_class_substate(substate_name)
1526
1523
  for substate_name in cls._always_dirty_substates
1527
- }.union(
1528
- {
1529
- cls.get_root_state().get_class_substate(substate_name)
1530
- for substate_name in cls._potentially_dirty_states
1531
- }
1532
- )
1524
+ }.union({
1525
+ cls.get_root_state().get_class_substate(substate_name)
1526
+ for substate_name in cls._potentially_dirty_states
1527
+ })
1533
1528
 
1534
1529
  def _get_root_state(self) -> BaseState:
1535
1530
  """Get the root state of the state tree.
@@ -1858,13 +1853,11 @@ class BaseState(EvenMoreBasicBaseState):
1858
1853
  ):
1859
1854
  if issubclass(hinted_args, Model):
1860
1855
  # Remove non-fields from the payload
1861
- payload[arg] = hinted_args(
1862
- **{
1863
- key: value
1864
- for key, value in value.items()
1865
- if key in hinted_args.__fields__
1866
- }
1867
- )
1856
+ payload[arg] = hinted_args(**{
1857
+ key: value
1858
+ for key, value in value.items()
1859
+ if key in hinted_args.__fields__
1860
+ })
1868
1861
  elif dataclasses.is_dataclass(hinted_args):
1869
1862
  payload[arg] = hinted_args(**value)
1870
1863
  elif find_spec("pydantic"):
reflex/style.py CHANGED
@@ -289,11 +289,11 @@ class Style(dict[str, Any]):
289
289
  value: The value to set.
290
290
  """
291
291
  # Create a Var to collapse VarData encoded in f-string.
292
- _var = LiteralVar.create(value)
293
- if _var is not None:
292
+ var = LiteralVar.create(value)
293
+ if var is not None:
294
294
  # Carry the imports/hooks when setting a Var as a value.
295
295
  self._var_data = VarData.merge(
296
- getattr(self, "_var_data", None), _var._get_all_var_data()
296
+ getattr(self, "_var_data", None), var._get_all_var_data()
297
297
  )
298
298
  super().__setitem__(key, value)
299
299
 
@@ -348,7 +348,7 @@ def format_as_emotion(style_dict: dict[str, Any]) -> Style | None:
348
348
  Returns:
349
349
  The emotion style dict.
350
350
  """
351
- _var_data = style_dict._var_data if isinstance(style_dict, Style) else None
351
+ var_data = style_dict._var_data if isinstance(style_dict, Style) else None
352
352
 
353
353
  emotion_style = Style()
354
354
 
@@ -381,8 +381,8 @@ def format_as_emotion(style_dict: dict[str, Any]) -> Style | None:
381
381
  else:
382
382
  emotion_style[key] = value
383
383
  if emotion_style:
384
- if _var_data is not None:
385
- emotion_style._var_data = VarData.merge(emotion_style._var_data, _var_data)
384
+ if var_data is not None:
385
+ emotion_style._var_data = VarData.merge(emotion_style._var_data, var_data)
386
386
  return emotion_style
387
387
  return None
388
388
 
reflex/testing.py CHANGED
@@ -28,10 +28,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar
28
28
  import uvicorn
29
29
 
30
30
  import reflex
31
- import reflex.environment
32
31
  import reflex.reflex
33
32
  import reflex.utils.build
34
- import reflex.utils.exec
35
33
  import reflex.utils.format
36
34
  import reflex.utils.prerequisites
37
35
  import reflex.utils.processes
@@ -41,7 +39,12 @@ from reflex.environment import environment
41
39
  from reflex.istate.manager.disk import StateManagerDisk
42
40
  from reflex.istate.manager.memory import StateManagerMemory
43
41
  from reflex.istate.manager.redis import StateManagerRedis
44
- from reflex.state import BaseState, StateManager, reload_state_module
42
+ from reflex.state import (
43
+ BaseState,
44
+ StateManager,
45
+ _split_substate_key,
46
+ reload_state_module,
47
+ )
45
48
  from reflex.utils import console, js_runtimes
46
49
  from reflex.utils.export import export
47
50
  from reflex.utils.token_manager import TokenManager
@@ -246,14 +249,12 @@ class AppHarness:
246
249
  if isinstance(self.app_source, functools.partial):
247
250
  self.app_source = self.app_source.func
248
251
  # get the source from a function or module object
249
- source_code = "\n".join(
250
- [
251
- "\n".join(
252
- self.get_app_global_source(k, v) for k, v in app_globals.items()
253
- ),
254
- self._get_source_from_app_source(self.app_source),
255
- ]
256
- )
252
+ source_code = "\n".join([
253
+ "\n".join([
254
+ self.get_app_global_source(k, v) for k, v in app_globals.items()
255
+ ]),
256
+ self._get_source_from_app_source(self.app_source),
257
+ ])
257
258
  get_config().loglevel = reflex.constants.LogLevel.INFO
258
259
  with chdir(self.app_path):
259
260
  reflex.reflex._init(
@@ -279,15 +280,16 @@ class AppHarness:
279
280
  )
280
281
  )
281
282
  self.app_asgi = self.app_instance()
282
- if self.app_instance and isinstance(
283
- self.app_instance._state_manager, StateManagerRedis
284
- ):
283
+ if self.app_instance and self.app_instance._state_manager is not None:
285
284
  if self.app_instance._state is None:
286
285
  msg = "State is not set."
287
286
  raise RuntimeError(msg)
288
- # Create our own redis connection for testing.
289
- self.state_manager = StateManagerRedis.create(self.app_instance._state)
290
- else:
287
+ if isinstance(self.app_instance._state_manager, StateManagerRedis):
288
+ # Create our own redis connection for testing.
289
+ self.state_manager = StateManagerRedis.create(self.app_instance._state)
290
+ elif isinstance(self.app_instance._state_manager, StateManagerDisk):
291
+ self.state_manager = StateManagerDisk.create(self.app_instance._state)
292
+ if self.state_manager is None:
291
293
  self.state_manager = (
292
294
  self.app_instance._state_manager if self.app_instance else None
293
295
  )
@@ -305,8 +307,9 @@ class AppHarness:
305
307
 
306
308
  async def _shutdown(*args, **kwargs) -> None:
307
309
  # ensure redis is closed before event loop
308
- if self.app_instance is not None and isinstance(
309
- self.app_instance._state_manager, StateManagerRedis
310
+ if (
311
+ self.app_instance is not None
312
+ and self.app_instance._state_manager is not None
310
313
  ):
311
314
  with contextlib.suppress(ValueError):
312
315
  await self.app_instance._state_manager.close()
@@ -358,6 +361,12 @@ class AppHarness:
358
361
  Raises:
359
362
  RuntimeError: when the state manager cannot be reset
360
363
  """
364
+ if (
365
+ self.app_instance is not None
366
+ and self.app_instance._state_manager is not None
367
+ ):
368
+ with contextlib.suppress(RuntimeError):
369
+ await self.app_instance._state_manager.close()
361
370
  if (
362
371
  self.app_instance is not None
363
372
  and isinstance(
@@ -366,8 +375,6 @@ class AppHarness:
366
375
  )
367
376
  and self.app_instance._state is not None
368
377
  ):
369
- with contextlib.suppress(RuntimeError):
370
- await self.app_instance._state_manager.close()
371
378
  self.app_instance._state_manager = StateManagerRedis.create(
372
379
  state=self.app_instance._state,
373
380
  )
@@ -713,11 +720,20 @@ class AppHarness:
713
720
  if self.state_manager is None:
714
721
  msg = "state_manager is not set."
715
722
  raise RuntimeError(msg)
723
+ if self.app_instance is not None and isinstance(
724
+ self.app_instance.state_manager, StateManagerDisk
725
+ ):
726
+ # Song and dance to convince the instance's state manager to flush
727
+ # (we can't directly await the _other_ loop's Future)
728
+ await self.app_instance.state_manager._flush_write_queue()
729
+ if isinstance(self.state_manager, StateManagerDisk):
730
+ # Force reload the latest state from disk.
731
+ client_token, _ = _split_substate_key(token)
732
+ self.state_manager.states.pop(client_token, None)
716
733
  try:
717
734
  return await self.state_manager.get_state(token)
718
735
  finally:
719
- if isinstance(self.state_manager, StateManagerRedis):
720
- await self.state_manager.close()
736
+ await self.state_manager.close()
721
737
 
722
738
  async def set_state(self, token: str, **kwargs) -> None:
723
739
  """Set the state associated with the given token.
@@ -738,8 +754,13 @@ class AppHarness:
738
754
  try:
739
755
  await self.state_manager.set_state(token, state)
740
756
  finally:
741
- if isinstance(self.state_manager, StateManagerRedis):
742
- await self.state_manager.close()
757
+ if self.app_instance is not None and isinstance(
758
+ self.app_instance.state_manager, StateManagerDisk
759
+ ):
760
+ # Clear the token from the backend's cache so it will be reloaded.
761
+ client_token, _ = _split_substate_key(token)
762
+ self.app_instance.state_manager.states.pop(client_token, None)
763
+ await self.state_manager.close()
743
764
 
744
765
  @contextlib.asynccontextmanager
745
766
  async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
@@ -761,17 +782,20 @@ class AppHarness:
761
782
  msg = "App is not running."
762
783
  raise RuntimeError(msg)
763
784
  app_state_manager = self.app_instance.state_manager
764
- if isinstance(self.state_manager, StateManagerRedis):
785
+ if isinstance(self.state_manager, (StateManagerRedis, StateManagerDisk)):
765
786
  # Temporarily replace the app's state manager with our own, since
766
- # the redis connection is on the backend_thread event loop
787
+ # the redis/disk connection is on the backend_thread event loop
767
788
  self.app_instance._state_manager = self.state_manager
768
789
  try:
769
790
  async with self.app_instance.modify_state(token) as state:
770
791
  yield state
771
792
  finally:
772
- if isinstance(self.state_manager, StateManagerRedis):
773
- self.app_instance._state_manager = app_state_manager
774
- await self.state_manager.close()
793
+ if isinstance(app_state_manager, StateManagerDisk):
794
+ # Clear the token from the cache so it will be reloaded.
795
+ client_token, _ = _split_substate_key(token)
796
+ app_state_manager.states.pop(client_token, None)
797
+ await self.state_manager.close()
798
+ self.app_instance._state_manager = app_state_manager
775
799
 
776
800
  def token_manager(self) -> TokenManager:
777
801
  """Get the token manager for the app instance.