reflex 0.8.11a1__py3-none-any.whl → 0.8.12__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.

@@ -478,8 +478,8 @@ export const queueEvents = async (
478
478
  * @param params The params object from React Router
479
479
  */
480
480
  export const processEvent = async (socket, navigate, params) => {
481
- // Only proceed if the socket is up and no event in the queue uses state, otherwise we throw the event into the void
482
- if (!socket && isStateful()) {
481
+ // Only proceed if the socket is up or no event in the queue uses state, otherwise we throw the event into the void
482
+ if (isStateful() && !(socket && socket.connected)) {
483
483
  return;
484
484
  }
485
485
 
@@ -531,6 +531,7 @@ export const connect = async (
531
531
  ) => {
532
532
  // Get backend URL object from the endpoint.
533
533
  const endpoint = getBackendURL(EVENTURL);
534
+ const on_hydrated_queue = [];
534
535
 
535
536
  // Create the socket.
536
537
  socket.current = io(endpoint.href, {
@@ -552,7 +553,17 @@ export const connect = async (
552
553
 
553
554
  function checkVisibility() {
554
555
  if (document.visibilityState === "visible") {
555
- if (!socket.current.connected) {
556
+ if (!socket.current) {
557
+ connect(
558
+ socket,
559
+ dispatch,
560
+ transports,
561
+ setConnectErrors,
562
+ client_storage,
563
+ navigate,
564
+ params,
565
+ );
566
+ } else if (!socket.current.connected) {
556
567
  console.log("Socket is disconnected, attempting to reconnect ");
557
568
  socket.current.connect();
558
569
  } else {
@@ -576,11 +587,15 @@ export const connect = async (
576
587
  };
577
588
 
578
589
  // Once the socket is open, hydrate the page.
579
- socket.current.on("connect", () => {
590
+ socket.current.on("connect", async () => {
580
591
  setConnectErrors([]);
581
592
  window.addEventListener("pagehide", pagehideHandler);
582
593
  window.addEventListener("beforeunload", disconnectTrigger);
583
594
  window.addEventListener("unload", disconnectTrigger);
595
+ // Drain any initial events from the queue.
596
+ while (event_queue.length > 0 && !event_processing) {
597
+ await processEvent(socket.current, navigate, () => params.current);
598
+ }
584
599
  });
585
600
 
586
601
  socket.current.on("connect_error", (error) => {
@@ -589,6 +604,7 @@ export const connect = async (
589
604
 
590
605
  // When the socket disconnects reset the event_processing flag
591
606
  socket.current.on("disconnect", () => {
607
+ socket.current = null; // allow reconnect to occur automatically
592
608
  event_processing = false;
593
609
  window.removeEventListener("unload", disconnectTrigger);
594
610
  window.removeEventListener("beforeunload", disconnectTrigger);
@@ -599,6 +615,14 @@ export const connect = async (
599
615
  socket.current.on("event", async (update) => {
600
616
  for (const substate in update.delta) {
601
617
  dispatch[substate](update.delta[substate]);
618
+ // handle events waiting for `is_hydrated`
619
+ if (
620
+ substate === state_name &&
621
+ update.delta[substate]?.is_hydrated_rx_state_
622
+ ) {
623
+ queueEvents(on_hydrated_queue, socket, false, navigate, params);
624
+ on_hydrated_queue.length = 0;
625
+ }
602
626
  }
603
627
  applyClientStorageDelta(client_storage, update.delta);
604
628
  event_processing = !update.final;
@@ -608,7 +632,8 @@ export const connect = async (
608
632
  });
609
633
  socket.current.on("reload", async (event) => {
610
634
  event_processing = false;
611
- queueEvents([...initialEvents(), event], socket, true, navigate, params);
635
+ on_hydrated_queue.push(event);
636
+ queueEvents(initialEvents(), socket, true, navigate, params);
612
637
  });
613
638
  socket.current.on("new_token", async (new_token) => {
614
639
  token = new_token;
@@ -770,10 +795,32 @@ export const useEventLoop = (
770
795
  }
771
796
  }, [paramsR]);
772
797
 
798
+ const ensureSocketConnected = useCallback(async () => {
799
+ // only use websockets if state is present and backend is not disabled (reflex cloud).
800
+ if (
801
+ Object.keys(initialState).length > 1 &&
802
+ !isBackendDisabled() &&
803
+ !socket.current
804
+ ) {
805
+ // Initialize the websocket connection.
806
+ await connect(
807
+ socket,
808
+ dispatch,
809
+ ["websocket"],
810
+ setConnectErrors,
811
+ client_storage,
812
+ navigate,
813
+ () => params.current,
814
+ );
815
+ }
816
+ }, [socket, dispatch, setConnectErrors, client_storage, navigate, params]);
817
+
773
818
  // Function to add new events to the event queue.
774
819
  const addEvents = useCallback((events, args, event_actions) => {
775
820
  const _events = events.filter((e) => e !== undefined && e !== null);
776
821
 
822
+ ensureSocketConnected();
823
+
777
824
  if (!(args instanceof Array)) {
778
825
  args = [args];
779
826
  }
@@ -866,21 +913,8 @@ export const useEventLoop = (
866
913
 
867
914
  // Handle socket connect/disconnect.
868
915
  useEffect(() => {
869
- // only use websockets if state is present and backend is not disabled (reflex cloud).
870
- if (Object.keys(initialState).length > 1 && !isBackendDisabled()) {
871
- // Initialize the websocket connection.
872
- if (!socket.current) {
873
- connect(
874
- socket,
875
- dispatch,
876
- ["websocket"],
877
- setConnectErrors,
878
- client_storage,
879
- navigate,
880
- () => params.current,
881
- );
882
- }
883
- }
916
+ // Initialize the websocket connection.
917
+ ensureSocketConnected();
884
918
 
885
919
  // Cleanup function.
886
920
  return () => {
@@ -893,12 +927,13 @@ export const useEventLoop = (
893
927
  // Main event loop.
894
928
  useEffect(() => {
895
929
  // Skip if the backend is disabled
896
- if (isBackendDisabled()) {
930
+ if (isBackendDisabled() || !socket.current || !socket.current.connected) {
897
931
  return;
898
932
  }
899
933
  (async () => {
900
934
  // Process all outstanding events.
901
935
  while (event_queue.length > 0 && !event_processing) {
936
+ await ensureSocketConnected();
902
937
  await processEvent(socket.current, navigate, () => params.current);
903
938
  }
904
939
  })();
reflex/app.py CHANGED
@@ -97,6 +97,7 @@ from reflex.state import (
97
97
  State,
98
98
  StateManager,
99
99
  StateUpdate,
100
+ _split_substate_key,
100
101
  _substate_key,
101
102
  all_base_state_classes,
102
103
  code_uses_state_contexts,
@@ -1559,7 +1560,7 @@ class App(MiddlewareMixin, LifespanMixin):
1559
1560
  state._clean()
1560
1561
  await self.event_namespace.emit_update(
1561
1562
  update=StateUpdate(delta=delta),
1562
- sid=state.router.session.session_id,
1563
+ token=token,
1563
1564
  )
1564
1565
 
1565
1566
  def _process_background(
@@ -1599,7 +1600,7 @@ class App(MiddlewareMixin, LifespanMixin):
1599
1600
  # Send the update to the client.
1600
1601
  await self.event_namespace.emit_update(
1601
1602
  update=update,
1602
- sid=state.router.session.session_id,
1603
+ token=event.token,
1603
1604
  )
1604
1605
 
1605
1606
  task = asyncio.create_task(
@@ -2061,20 +2062,19 @@ class EventNamespace(AsyncNamespace):
2061
2062
  and console.error(f"Token cleanup error: {t.exception()}")
2062
2063
  )
2063
2064
 
2064
- async def emit_update(self, update: StateUpdate, sid: str) -> None:
2065
+ async def emit_update(self, update: StateUpdate, token: str) -> None:
2065
2066
  """Emit an update to the client.
2066
2067
 
2067
2068
  Args:
2068
2069
  update: The state update to send.
2069
- sid: The Socket.IO session id.
2070
+ token: The client token (tab) associated with the event.
2070
2071
  """
2071
- if not sid:
2072
+ client_token, _ = _split_substate_key(token)
2073
+ sid = self.token_to_sid.get(client_token)
2074
+ if sid is None:
2072
2075
  # If the sid is None, we are not connected to a client. Prevent sending
2073
2076
  # updates to all clients.
2074
- return
2075
- token = self.sid_to_token.get(sid)
2076
- if token is None:
2077
- console.warn(f"Attempting to send delta to disconnected websocket {sid}")
2077
+ console.warn(f"Attempting to send delta to disconnected client {token!r}")
2078
2078
  return
2079
2079
  # Creating a task prevents the update from being blocked behind other coroutines.
2080
2080
  await asyncio.create_task(
@@ -2165,7 +2165,7 @@ class EventNamespace(AsyncNamespace):
2165
2165
  # Process the events.
2166
2166
  async for update in updates_gen:
2167
2167
  # Emit the update from processing the event.
2168
- await self.emit_update(update=update, sid=sid)
2168
+ await self.emit_update(update=update, token=event.token)
2169
2169
 
2170
2170
  async def on_ping(self, sid: str):
2171
2171
  """Event for testing the API endpoint.
@@ -2189,3 +2189,10 @@ class EventNamespace(AsyncNamespace):
2189
2189
  if new_token:
2190
2190
  # Duplicate detected, emit new token to client
2191
2191
  await self.emit("new_token", new_token, to=sid)
2192
+
2193
+ # Update client state to apply new sid/token for running background tasks.
2194
+ async with self.app.modify_state(
2195
+ _substate_key(new_token or token, self.app.state_manager.state)
2196
+ ) as state:
2197
+ state.router_data[constants.RouteVar.SESSION_ID] = sid
2198
+ state.router = RouterData.from_router_data(state.router_data)
@@ -60,6 +60,17 @@ class LifespanMixin(AppMixin):
60
60
  for task in running_tasks:
61
61
  console.debug(f"Canceling lifespan task: {task}")
62
62
  task.cancel(msg="lifespan_cleanup")
63
+ # Disassociate sid / token pairings so they can be reconnected properly.
64
+ try:
65
+ event_namespace = self.event_namespace # pyright: ignore[reportAttributeAccessIssue]
66
+ except AttributeError:
67
+ pass
68
+ else:
69
+ try:
70
+ if event_namespace:
71
+ await event_namespace._token_manager.disconnect_all()
72
+ except Exception as e:
73
+ console.error(f"Error during lifespan cleanup: {e}")
63
74
 
64
75
  def register_lifespan_task(self, task: Callable | asyncio.Task, **task_kwargs):
65
76
  """Register a task to run during the lifespan of the app.
@@ -621,7 +621,10 @@ def purge_web_pages_dir():
621
621
  return
622
622
 
623
623
  # Empty out the web pages directory.
624
- utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["routes.js"])
624
+ utils.empty_dir(
625
+ get_web_dir() / constants.Dirs.PAGES,
626
+ keep_files=["routes.js", "entry.client.js"],
627
+ )
625
628
 
626
629
 
627
630
  if TYPE_CHECKING:
@@ -86,7 +86,7 @@ class _RenderUtils:
86
86
  children_rendered = "".join(
87
87
  [_RenderUtils.render(child) for child in component.get("children", [])]
88
88
  )
89
- return f"{component['iterable_state']}.map(({component['arg_name']},{component['arg_index']})=>({children_rendered}))"
89
+ return f"Array.prototype.map.call({component['iterable_state']} ?? [],(({component['arg_name']},{component['arg_index']})=>({children_rendered})))"
90
90
 
91
91
  @staticmethod
92
92
  def render_match_tag(component: Any) -> str:
@@ -17,6 +17,7 @@ from reflex.components.component import (
17
17
  from reflex.components.core.cond import cond
18
18
  from reflex.components.el.elements.forms import Input
19
19
  from reflex.components.radix.themes.layout.box import Box
20
+ from reflex.components.sonner.toast import toast
20
21
  from reflex.constants import Dirs
21
22
  from reflex.constants.compiler import Hooks, Imports
22
23
  from reflex.environment import environment
@@ -36,7 +37,8 @@ from reflex.utils.imports import ImportVar
36
37
  from reflex.vars import VarData
37
38
  from reflex.vars.base import Var, get_unique_variable_name
38
39
  from reflex.vars.function import FunctionVar
39
- from reflex.vars.sequence import LiteralStringVar
40
+ from reflex.vars.object import ObjectVar
41
+ from reflex.vars.sequence import ArrayVar, LiteralStringVar
40
42
 
41
43
  DEFAULT_UPLOAD_ID: str = "default"
42
44
 
@@ -178,6 +180,34 @@ def _on_drop_spec(files: Var) -> tuple[Var[Any]]:
178
180
  return (files,)
179
181
 
180
182
 
183
+ def _default_drop_rejected(rejected_files: ArrayVar[list[dict[str, Any]]]) -> EventSpec:
184
+ """Event handler for showing a toast with rejected file info.
185
+
186
+ Args:
187
+ rejected_files: The files that were rejected.
188
+
189
+ Returns:
190
+ An event spec that shows a toast with the rejected file info when triggered.
191
+ """
192
+
193
+ def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str:
194
+ rf = rf.to(ObjectVar, dict[str, dict[str, Any]])
195
+ file = rf["file"].to(ObjectVar, dict[str, Any])
196
+ errors = rf["errors"].to(ArrayVar, list[dict[str, Any]])
197
+ return (
198
+ f"{file['path']}: {errors.foreach(lambda err: err['message']).join(', ')}"
199
+ )
200
+
201
+ return toast.error(
202
+ title="Files not Accepted",
203
+ description=rejected_files.to(ArrayVar)
204
+ .foreach(_format_rejected_file_record)
205
+ .join("\n\n"),
206
+ close_button=True,
207
+ style={"white_space": "pre-line"},
208
+ )
209
+
210
+
181
211
  class UploadFilesProvider(Component):
182
212
  """AppWrap component that provides a dict of selected files by ID via useContext."""
183
213
 
@@ -191,6 +221,9 @@ class GhostUpload(Fragment):
191
221
  # Fired when files are dropped.
192
222
  on_drop: EventHandler[_on_drop_spec]
193
223
 
224
+ # Fired when dropped files do not meet the specified criteria.
225
+ on_drop_rejected: EventHandler[_on_drop_spec]
226
+
194
227
 
195
228
  class Upload(MemoizationLeaf):
196
229
  """A file upload component."""
@@ -234,6 +267,9 @@ class Upload(MemoizationLeaf):
234
267
  # Fired when files are dropped.
235
268
  on_drop: EventHandler[_on_drop_spec]
236
269
 
270
+ # Fired when dropped files do not meet the specified criteria.
271
+ on_drop_rejected: EventHandler[_on_drop_spec]
272
+
237
273
  # Style rules to apply when actively dragging.
238
274
  drag_active_style: Style | None = field(default=None, is_javascript_property=False)
239
275
 
@@ -295,6 +331,10 @@ class Upload(MemoizationLeaf):
295
331
  on_drop[ix] = event
296
332
  upload_props["on_drop"] = on_drop
297
333
 
334
+ if upload_props.get("on_drop_rejected") is None:
335
+ # If on_drop_rejected is not provided, show an error toast.
336
+ upload_props["on_drop_rejected"] = _default_drop_rejected
337
+
298
338
  input_props_unique_name = get_unique_variable_name()
299
339
  root_props_unique_name = get_unique_variable_name()
300
340
  is_drag_active_unique_name = get_unique_variable_name()
@@ -313,22 +353,22 @@ class Upload(MemoizationLeaf):
313
353
  ),
314
354
  )
315
355
 
316
- event_var, callback_str = StatefulComponent._get_memoized_event_triggers(
317
- GhostUpload.create(on_drop=upload_props["on_drop"])
318
- )["on_drop"]
319
-
320
- upload_props["on_drop"] = event_var
356
+ event_triggers = StatefulComponent._get_memoized_event_triggers(
357
+ GhostUpload.create(
358
+ on_drop=upload_props["on_drop"],
359
+ on_drop_rejected=upload_props["on_drop_rejected"],
360
+ )
361
+ )
362
+ callback_hooks = []
363
+ for trigger_name, (event_var, callback_str) in event_triggers.items():
364
+ upload_props[trigger_name] = event_var
365
+ callback_hooks.append(callback_str)
321
366
 
322
367
  upload_props = {
323
368
  format.to_camel_case(key): value for key, value in upload_props.items()
324
369
  }
325
370
 
326
- use_dropzone_arguments = Var.create(
327
- {
328
- "onDrop": event_var,
329
- **upload_props,
330
- }
331
- )
371
+ use_dropzone_arguments = Var.create(upload_props)
332
372
 
333
373
  left_side = (
334
374
  "const { "
@@ -344,11 +384,10 @@ class Upload(MemoizationLeaf):
344
384
  imports=Imports.EVENTS,
345
385
  hooks={Hooks.EVENTS: None},
346
386
  ),
347
- event_var._get_all_var_data(),
348
387
  use_dropzone_arguments._get_all_var_data(),
349
388
  VarData(
350
389
  hooks={
351
- callback_str: None,
390
+ **dict.fromkeys(callback_hooks, None),
352
391
  f"{left_side} = {right_side};": None,
353
392
  },
354
393
  imports={
@@ -108,6 +108,7 @@ class GhostUpload(Fragment):
108
108
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
109
109
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
110
110
  on_drop: EventType[()] | EventType[Any] | None = None,
111
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
111
112
  on_focus: EventType[()] | None = None,
112
113
  on_mount: EventType[()] | None = None,
113
114
  on_mouse_down: EventType[()] | None = None,
@@ -127,6 +128,7 @@ class GhostUpload(Fragment):
127
128
  Args:
128
129
  *children: The children of the component.
129
130
  on_drop: Fired when files are dropped.
131
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
130
132
  style: The style of the component.
131
133
  key: A unique key for the component.
132
134
  id: The id for the component.
@@ -171,6 +173,7 @@ class Upload(MemoizationLeaf):
171
173
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
172
174
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
173
175
  on_drop: EventType[()] | EventType[Any] | None = None,
176
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
174
177
  on_focus: EventType[()] | None = None,
175
178
  on_mount: EventType[()] | None = None,
176
179
  on_mouse_down: EventType[()] | None = None,
@@ -199,6 +202,7 @@ class Upload(MemoizationLeaf):
199
202
  no_drag: Whether to disable drag and drop.
200
203
  no_keyboard: Whether to disable using the space/enter keys to upload.
201
204
  on_drop: Fired when files are dropped.
205
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
202
206
  drag_active_style: Style rules to apply when actively dragging.
203
207
  style: The style of the component.
204
208
  key: A unique key for the component.
@@ -242,6 +246,7 @@ class StyledUpload(Upload):
242
246
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
243
247
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
244
248
  on_drop: EventType[()] | EventType[Any] | None = None,
249
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
245
250
  on_focus: EventType[()] | None = None,
246
251
  on_mount: EventType[()] | None = None,
247
252
  on_mouse_down: EventType[()] | None = None,
@@ -270,6 +275,7 @@ class StyledUpload(Upload):
270
275
  no_drag: Whether to disable drag and drop.
271
276
  no_keyboard: Whether to disable using the space/enter keys to upload.
272
277
  on_drop: Fired when files are dropped.
278
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
273
279
  drag_active_style: Style rules to apply when actively dragging.
274
280
  style: The style of the component.
275
281
  key: A unique key for the component.
@@ -314,6 +320,7 @@ class UploadNamespace(ComponentNamespace):
314
320
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
315
321
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
316
322
  on_drop: EventType[()] | EventType[Any] | None = None,
323
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
317
324
  on_focus: EventType[()] | None = None,
318
325
  on_mount: EventType[()] | None = None,
319
326
  on_mouse_down: EventType[()] | None = None,
@@ -342,6 +349,7 @@ class UploadNamespace(ComponentNamespace):
342
349
  no_drag: Whether to disable drag and drop.
343
350
  no_keyboard: Whether to disable using the space/enter keys to upload.
344
351
  on_drop: Fired when files are dropped.
352
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
345
353
  drag_active_style: Style rules to apply when actively dragging.
346
354
  style: The style of the component.
347
355
  key: A unique key for the component.
@@ -6,7 +6,7 @@ from reflex.utils.imports import ImportVar
6
6
  from reflex.vars.base import LiteralVar, Var
7
7
  from reflex.vars.sequence import LiteralStringVar, StringVar
8
8
 
9
- LUCIDE_LIBRARY = "lucide-react@0.543.0"
9
+ LUCIDE_LIBRARY = "lucide-react@0.544.0"
10
10
 
11
11
 
12
12
  class LucideIconComponent(Component):
@@ -674,6 +674,7 @@ LUCIDE_ICON_LIST = [
674
674
  "eraser",
675
675
  "ethernet_port",
676
676
  "euro",
677
+ "ev_charger",
677
678
  "expand",
678
679
  "external_link",
679
680
  "eye_closed",
@@ -11,7 +11,7 @@ from reflex.components.core.breakpoints import Breakpoints
11
11
  from reflex.event import EventType, PointerEventInfo
12
12
  from reflex.vars.base import Var
13
13
 
14
- LUCIDE_LIBRARY = "lucide-react@0.543.0"
14
+ LUCIDE_LIBRARY = "lucide-react@0.544.0"
15
15
 
16
16
  class LucideIconComponent(Component):
17
17
  @classmethod
@@ -739,6 +739,7 @@ LUCIDE_ICON_LIST = [
739
739
  "eraser",
740
740
  "ethernet_port",
741
741
  "euro",
742
+ "ev_charger",
742
743
  "expand",
743
744
  "external_link",
744
745
  "eye_closed",
@@ -14,7 +14,7 @@ class Bun(SimpleNamespace):
14
14
  """Bun constants."""
15
15
 
16
16
  # The Bun version.
17
- VERSION = "1.2.21"
17
+ VERSION = "1.2.22"
18
18
 
19
19
  # Min Bun Version
20
20
  MIN_VERSION = "1.2.17"
@@ -75,7 +75,7 @@ fetch-retries=0
75
75
 
76
76
 
77
77
  def _determine_react_router_version() -> str:
78
- default_version = "7.8.2"
78
+ default_version = "7.9.1"
79
79
  if (version := os.getenv("REACT_ROUTER_VERSION")) and version != default_version:
80
80
  from reflex.utils import console
81
81
 
@@ -143,11 +143,11 @@ class PackageJson(SimpleNamespace):
143
143
  "postcss-import": "16.1.1",
144
144
  "@react-router/dev": _react_router_version,
145
145
  "@react-router/fs-routes": _react_router_version,
146
- "vite": "npm:rolldown-vite@7.1.8",
146
+ "vite": "npm:rolldown-vite@7.1.9",
147
147
  }
148
148
  OVERRIDES = {
149
149
  # This should always match the `react` version in DEPENDENCIES for recharts compatibility.
150
150
  "react-is": _react_version,
151
151
  "cookie": "1.0.2",
152
- "vite": "npm:rolldown-vite@7.1.8",
152
+ "vite": "npm:rolldown-vite@7.1.9",
153
153
  }
reflex/event.py CHANGED
@@ -1782,7 +1782,7 @@ def call_event_fn(
1782
1782
  from reflex.event import EventHandler, EventSpec
1783
1783
  from reflex.utils.exceptions import EventHandlerValueError
1784
1784
 
1785
- parsed_args, event_annotations = parse_args_spec(arg_spec)
1785
+ parsed_args, _ = parse_args_spec(arg_spec)
1786
1786
 
1787
1787
  parameters = inspect.signature(fn).parameters
1788
1788
 
reflex/istate/manager.py CHANGED
@@ -355,7 +355,7 @@ class StateManagerDisk(StateManager):
355
355
  token: The token to set the state for.
356
356
  state: The state to set.
357
357
  """
358
- client_token, substate = _split_substate_key(token)
358
+ client_token, _ = _split_substate_key(token)
359
359
  await self.set_state_for_substate(client_token, state)
360
360
 
361
361
  @override
@@ -370,7 +370,7 @@ class StateManagerDisk(StateManager):
370
370
  The state for the token.
371
371
  """
372
372
  # Memory state manager ignores the substate suffix and always returns the top-level state.
373
- client_token, substate = _split_substate_key(token)
373
+ client_token, _ = _split_substate_key(token)
374
374
  if client_token not in self._states_locks:
375
375
  async with self._state_manager_lock:
376
376
  if client_token not in self._states_locks:
reflex/istate/proxy.py CHANGED
@@ -71,10 +71,15 @@ class StateProxy(wrapt.ObjectProxy):
71
71
  state_instance: The state instance to proxy.
72
72
  parent_state_proxy: The parent state proxy, for linked mutability and context tracking.
73
73
  """
74
+ from reflex.state import _substate_key
75
+
74
76
  super().__init__(state_instance)
75
- # compile is not relevant to backend logic
76
77
  self._self_app = prerequisites.get_and_validate_app().app
77
78
  self._self_substate_path = tuple(state_instance.get_full_name().split("."))
79
+ self._self_substate_token = _substate_key(
80
+ state_instance.router.session.client_token,
81
+ self._self_substate_path,
82
+ )
78
83
  self._self_actx = None
79
84
  self._self_mutable = False
80
85
  self._self_actx_lock = asyncio.Lock()
@@ -127,16 +132,9 @@ class StateProxy(wrapt.ObjectProxy):
127
132
  msg = "The state is already mutable. Do not nest `async with self` blocks."
128
133
  raise ImmutableStateError(msg)
129
134
 
130
- from reflex.state import _substate_key
131
-
132
135
  await self._self_actx_lock.acquire()
133
136
  self._self_actx_lock_holder = current_task
134
- self._self_actx = self._self_app.modify_state(
135
- token=_substate_key(
136
- self.__wrapped__.router.session.client_token,
137
- self._self_substate_path,
138
- )
139
- )
137
+ self._self_actx = self._self_app.modify_state(token=self._self_substate_token)
140
138
  mutable_state = await self._self_actx.__aenter__()
141
139
  super().__setattr__(
142
140
  "__wrapped__", mutable_state.get_substate(self._self_substate_path)
@@ -378,17 +376,6 @@ class MutableProxy(wrapt.ObjectProxy):
378
376
  pydantic.BaseModel.__dict__
379
377
  )
380
378
 
381
- # These types will be wrapped in MutableProxy
382
- __mutable_types__ = (
383
- list,
384
- dict,
385
- set,
386
- Base,
387
- DeclarativeBase,
388
- BaseModelV2,
389
- BaseModelV1,
390
- )
391
-
392
379
  # Dynamically generated classes for tracking dataclass mutations.
393
380
  __dataclass_proxies__: dict[type, type] = {}
394
381
 
@@ -469,20 +456,6 @@ class MutableProxy(wrapt.ObjectProxy):
469
456
  return wrapped(*args, **(kwargs or {}))
470
457
  return None
471
458
 
472
- @classmethod
473
- def _is_mutable_type(cls, value: Any) -> bool:
474
- """Check if a value is of a mutable type and should be wrapped.
475
-
476
- Args:
477
- value: The value to check.
478
-
479
- Returns:
480
- Whether the value is of a mutable type.
481
- """
482
- return isinstance(value, cls.__mutable_types__) or (
483
- dataclasses.is_dataclass(value) and not isinstance(value, Var)
484
- )
485
-
486
459
  @staticmethod
487
460
  def _is_called_from_dataclasses_internal() -> bool:
488
461
  """Check if the current function is called from dataclasses helper.
@@ -514,7 +487,7 @@ class MutableProxy(wrapt.ObjectProxy):
514
487
  if self._is_called_from_dataclasses_internal():
515
488
  return value
516
489
  # Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
517
- if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
490
+ if is_mutable_type(type(value)) and not isinstance(value, MutableProxy):
518
491
  base_cls = globals()[self.__base_proxy__]
519
492
  return base_cls(
520
493
  wrapped=value,
@@ -575,7 +548,7 @@ class MutableProxy(wrapt.ObjectProxy):
575
548
  self._wrap_recursive_decorator,
576
549
  )
577
550
 
578
- if self._is_mutable_type(value) and __name not in (
551
+ if is_mutable_type(type(value)) and __name not in (
579
552
  "__wrapped__",
580
553
  "_self_state",
581
554
  "__dict__",
@@ -764,3 +737,30 @@ class ImmutableMutableProxy(MutableProxy):
764
737
  return super()._mark_dirty(
765
738
  wrapped=wrapped, instance=instance, args=args, kwargs=kwargs
766
739
  )
740
+
741
+
742
+ # These types will be wrapped in MutableProxy
743
+ MUTABLE_TYPES = (
744
+ list,
745
+ dict,
746
+ set,
747
+ Base,
748
+ DeclarativeBase,
749
+ BaseModelV2,
750
+ BaseModelV1,
751
+ )
752
+
753
+
754
+ @functools.lru_cache(maxsize=1024)
755
+ def is_mutable_type(type_: type) -> bool:
756
+ """Check if a type is mutable and should be wrapped.
757
+
758
+ Args:
759
+ type_: The type to check.
760
+
761
+ Returns:
762
+ Whether the type is mutable and should be wrapped.
763
+ """
764
+ return issubclass(type_, MUTABLE_TYPES) or (
765
+ dataclasses.is_dataclass(type_) and not issubclass(type_, Var)
766
+ )