reflex 0.6.8a2__py3-none-any.whl → 0.7.0a2__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 (246) hide show
  1. reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +1 -1
  2. reflex/.templates/jinja/web/pages/_app.js.jinja2 +7 -7
  3. reflex/.templates/jinja/web/pages/utils.js.jinja2 +3 -3
  4. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +1 -4
  5. reflex/.templates/web/utils/state.js +65 -36
  6. reflex/__init__.py +4 -17
  7. reflex/__init__.pyi +1 -2
  8. reflex/app.py +249 -116
  9. reflex/app_mixins/lifespan.py +9 -9
  10. reflex/app_mixins/middleware.py +6 -6
  11. reflex/app_module_for_backend.py +3 -7
  12. reflex/base.py +7 -7
  13. reflex/compiler/compiler.py +8 -0
  14. reflex/compiler/utils.py +35 -6
  15. reflex/components/base/app_wrap.pyi +16 -16
  16. reflex/components/base/bare.py +1 -1
  17. reflex/components/base/body.pyi +16 -16
  18. reflex/components/base/document.pyi +76 -76
  19. reflex/components/base/error_boundary.py +2 -1
  20. reflex/components/base/error_boundary.pyi +19 -22
  21. reflex/components/base/fragment.pyi +16 -16
  22. reflex/components/base/head.pyi +31 -31
  23. reflex/components/base/link.pyi +31 -31
  24. reflex/components/base/meta.py +2 -2
  25. reflex/components/base/meta.pyi +61 -61
  26. reflex/components/base/script.pyi +19 -19
  27. reflex/components/base/strict_mode.py +10 -0
  28. reflex/components/base/strict_mode.pyi +57 -0
  29. reflex/components/component.py +38 -77
  30. reflex/components/core/banner.py +159 -4
  31. reflex/components/core/banner.pyi +162 -76
  32. reflex/components/core/breakpoints.py +3 -1
  33. reflex/components/core/client_side_routing.py +1 -1
  34. reflex/components/core/client_side_routing.pyi +32 -32
  35. reflex/components/core/clipboard.pyi +17 -20
  36. reflex/components/core/cond.py +9 -10
  37. reflex/components/core/debounce.py +1 -1
  38. reflex/components/core/debounce.pyi +17 -17
  39. reflex/components/core/foreach.py +28 -3
  40. reflex/components/core/html.py +1 -1
  41. reflex/components/core/html.pyi +16 -16
  42. reflex/components/core/match.py +5 -5
  43. reflex/components/core/sticky.py +160 -0
  44. reflex/components/core/sticky.pyi +449 -0
  45. reflex/components/core/upload.py +2 -2
  46. reflex/components/core/upload.pyi +80 -88
  47. reflex/components/datadisplay/code.py +5 -14
  48. reflex/components/datadisplay/code.pyi +31 -31
  49. reflex/components/datadisplay/dataeditor.py +7 -4
  50. reflex/components/datadisplay/dataeditor.pyi +40 -54
  51. reflex/components/datadisplay/logo.py +13 -8
  52. reflex/components/datadisplay/shiki_code_block.py +14 -9
  53. reflex/components/datadisplay/shiki_code_block.pyi +46 -46
  54. reflex/components/dynamic.py +22 -3
  55. reflex/components/el/constants/reflex.py +1 -1
  56. reflex/components/el/element.py +1 -1
  57. reflex/components/el/element.pyi +16 -16
  58. reflex/components/el/elements/base.pyi +16 -16
  59. reflex/components/el/elements/forms.py +4 -4
  60. reflex/components/el/elements/forms.pyi +224 -258
  61. reflex/components/el/elements/inline.pyi +421 -421
  62. reflex/components/el/elements/media.pyi +376 -376
  63. reflex/components/el/elements/metadata.pyi +91 -91
  64. reflex/components/el/elements/other.pyi +106 -106
  65. reflex/components/el/elements/scripts.pyi +46 -46
  66. reflex/components/el/elements/sectioning.pyi +226 -226
  67. reflex/components/el/elements/tables.pyi +151 -151
  68. reflex/components/el/elements/typography.pyi +226 -226
  69. reflex/components/gridjs/datatable.pyi +31 -31
  70. reflex/components/lucide/icon.py +46 -8
  71. reflex/components/lucide/icon.pyi +85 -31
  72. reflex/components/markdown/markdown.py +10 -8
  73. reflex/components/markdown/markdown.pyi +16 -16
  74. reflex/components/moment/moment.py +2 -2
  75. reflex/components/moment/moment.pyi +17 -19
  76. reflex/components/next/base.pyi +16 -16
  77. reflex/components/next/image.py +16 -4
  78. reflex/components/next/image.pyi +22 -20
  79. reflex/components/next/link.py +1 -1
  80. reflex/components/next/link.pyi +16 -16
  81. reflex/components/next/video.pyi +16 -16
  82. reflex/components/plotly/plotly.py +5 -5
  83. reflex/components/plotly/plotly.pyi +34 -44
  84. reflex/components/props.py +3 -3
  85. reflex/components/radix/__init__.pyi +1 -1
  86. reflex/components/radix/primitives/accordion.py +9 -5
  87. reflex/components/radix/primitives/accordion.pyi +110 -108
  88. reflex/components/radix/primitives/base.pyi +31 -31
  89. reflex/components/radix/primitives/drawer.py +5 -2
  90. reflex/components/radix/primitives/drawer.pyi +179 -187
  91. reflex/components/radix/primitives/form.pyi +160 -172
  92. reflex/components/radix/primitives/progress.py +1 -1
  93. reflex/components/radix/primitives/progress.pyi +76 -76
  94. reflex/components/radix/primitives/slider.py +1 -1
  95. reflex/components/radix/primitives/slider.pyi +78 -82
  96. reflex/components/radix/themes/base.pyi +121 -121
  97. reflex/components/radix/themes/color_mode.py +11 -9
  98. reflex/components/radix/themes/color_mode.pyi +47 -49
  99. reflex/components/radix/themes/components/alert_dialog.py +3 -0
  100. reflex/components/radix/themes/components/alert_dialog.pyi +110 -112
  101. reflex/components/radix/themes/components/aspect_ratio.pyi +16 -16
  102. reflex/components/radix/themes/components/avatar.pyi +16 -16
  103. reflex/components/radix/themes/components/badge.pyi +16 -16
  104. reflex/components/radix/themes/components/button.pyi +16 -16
  105. reflex/components/radix/themes/components/callout.pyi +76 -76
  106. reflex/components/radix/themes/components/card.py +1 -1
  107. reflex/components/radix/themes/components/card.pyi +17 -17
  108. reflex/components/radix/themes/components/checkbox.pyi +49 -55
  109. reflex/components/radix/themes/components/checkbox_cards.pyi +31 -31
  110. reflex/components/radix/themes/components/checkbox_group.pyi +31 -31
  111. reflex/components/radix/themes/components/context_menu.py +5 -0
  112. reflex/components/radix/themes/components/context_menu.pyi +149 -155
  113. reflex/components/radix/themes/components/data_list.pyi +61 -61
  114. reflex/components/radix/themes/components/dialog.py +3 -0
  115. reflex/components/radix/themes/components/dialog.pyi +113 -117
  116. reflex/components/radix/themes/components/dropdown_menu.py +5 -0
  117. reflex/components/radix/themes/components/dropdown_menu.pyi +133 -137
  118. reflex/components/radix/themes/components/hover_card.py +3 -0
  119. reflex/components/radix/themes/components/hover_card.pyi +63 -67
  120. reflex/components/radix/themes/components/icon_button.py +2 -2
  121. reflex/components/radix/themes/components/icon_button.pyi +17 -16
  122. reflex/components/radix/themes/components/inset.pyi +16 -16
  123. reflex/components/radix/themes/components/popover.py +3 -0
  124. reflex/components/radix/themes/components/popover.pyi +68 -70
  125. reflex/components/radix/themes/components/progress.pyi +16 -16
  126. reflex/components/radix/themes/components/radio.pyi +16 -16
  127. reflex/components/radix/themes/components/radio_cards.py +2 -0
  128. reflex/components/radix/themes/components/radio_cards.pyi +32 -34
  129. reflex/components/radix/themes/components/radio_group.py +1 -1
  130. reflex/components/radix/themes/components/radio_group.pyi +62 -64
  131. reflex/components/radix/themes/components/scroll_area.pyi +16 -16
  132. reflex/components/radix/themes/components/segmented_control.pyi +32 -35
  133. reflex/components/radix/themes/components/select.py +4 -0
  134. reflex/components/radix/themes/components/select.pyi +145 -157
  135. reflex/components/radix/themes/components/separator.pyi +16 -16
  136. reflex/components/radix/themes/components/skeleton.py +3 -0
  137. reflex/components/radix/themes/components/skeleton.pyi +16 -16
  138. reflex/components/radix/themes/components/slider.pyi +22 -28
  139. reflex/components/radix/themes/components/spinner.pyi +16 -16
  140. reflex/components/radix/themes/components/switch.pyi +17 -19
  141. reflex/components/radix/themes/components/table.pyi +106 -106
  142. reflex/components/radix/themes/components/tabs.py +3 -0
  143. reflex/components/radix/themes/components/tabs.pyi +78 -82
  144. reflex/components/radix/themes/components/text_area.py +12 -0
  145. reflex/components/radix/themes/components/text_area.pyi +21 -33
  146. reflex/components/radix/themes/components/text_field.py +1 -1
  147. reflex/components/radix/themes/components/text_field.pyi +52 -80
  148. reflex/components/radix/themes/components/tooltip.py +6 -1
  149. reflex/components/radix/themes/components/tooltip.pyi +20 -21
  150. reflex/components/radix/themes/layout/__init__.pyi +1 -1
  151. reflex/components/radix/themes/layout/base.pyi +16 -16
  152. reflex/components/radix/themes/layout/box.pyi +16 -16
  153. reflex/components/radix/themes/layout/center.pyi +16 -16
  154. reflex/components/radix/themes/layout/container.pyi +16 -16
  155. reflex/components/radix/themes/layout/flex.pyi +16 -16
  156. reflex/components/radix/themes/layout/grid.pyi +16 -16
  157. reflex/components/radix/themes/layout/list.py +2 -2
  158. reflex/components/radix/themes/layout/list.pyi +76 -76
  159. reflex/components/radix/themes/layout/section.pyi +16 -16
  160. reflex/components/radix/themes/layout/spacer.pyi +16 -16
  161. reflex/components/radix/themes/layout/stack.py +2 -2
  162. reflex/components/radix/themes/layout/stack.pyi +46 -46
  163. reflex/components/radix/themes/typography/blockquote.pyi +16 -16
  164. reflex/components/radix/themes/typography/code.pyi +16 -16
  165. reflex/components/radix/themes/typography/heading.pyi +16 -16
  166. reflex/components/radix/themes/typography/link.py +1 -1
  167. reflex/components/radix/themes/typography/link.pyi +16 -16
  168. reflex/components/radix/themes/typography/text.py +2 -2
  169. reflex/components/radix/themes/typography/text.pyi +106 -106
  170. reflex/components/react_player/audio.pyi +33 -39
  171. reflex/components/react_player/react_player.py +1 -1
  172. reflex/components/react_player/react_player.pyi +32 -38
  173. reflex/components/react_player/video.pyi +33 -39
  174. reflex/components/recharts/__init__.py +2 -0
  175. reflex/components/recharts/__init__.pyi +2 -0
  176. reflex/components/recharts/cartesian.pyi +282 -282
  177. reflex/components/recharts/charts.py +15 -15
  178. reflex/components/recharts/charts.pyi +164 -164
  179. reflex/components/recharts/general.py +19 -4
  180. reflex/components/recharts/general.pyi +132 -81
  181. reflex/components/recharts/polar.py +2 -2
  182. reflex/components/recharts/polar.pyi +55 -55
  183. reflex/components/recharts/recharts.py +4 -4
  184. reflex/components/recharts/recharts.pyi +31 -31
  185. reflex/components/sonner/toast.py +15 -13
  186. reflex/components/sonner/toast.pyi +22 -22
  187. reflex/components/suneditor/editor.py +6 -4
  188. reflex/components/suneditor/editor.pyi +26 -40
  189. reflex/components/tags/iter_tag.py +3 -3
  190. reflex/components/tags/tag.py +25 -3
  191. reflex/config.py +48 -15
  192. reflex/constants/__init__.py +1 -0
  193. reflex/constants/base.py +4 -1
  194. reflex/constants/compiler.py +5 -2
  195. reflex/constants/config.py +8 -1
  196. reflex/constants/installer.py +9 -9
  197. reflex/constants/style.py +1 -1
  198. reflex/custom_components/custom_components.py +9 -7
  199. reflex/event.py +215 -208
  200. reflex/experimental/__init__.py +19 -11
  201. reflex/experimental/client_state.py +53 -28
  202. reflex/experimental/hooks.py +5 -5
  203. reflex/experimental/layout.py +8 -5
  204. reflex/experimental/layout.pyi +79 -83
  205. reflex/experimental/misc.py +3 -3
  206. reflex/istate/wrappers.py +1 -1
  207. reflex/middleware/hydrate_middleware.py +2 -2
  208. reflex/model.py +11 -6
  209. reflex/page.py +5 -5
  210. reflex/reflex.py +90 -19
  211. reflex/route.py +1 -1
  212. reflex/state.py +358 -401
  213. reflex/style.py +27 -3
  214. reflex/testing.py +29 -23
  215. reflex/utils/build.py +6 -2
  216. reflex/utils/codespaces.py +1 -4
  217. reflex/utils/compat.py +6 -5
  218. reflex/utils/console.py +52 -16
  219. reflex/utils/exceptions.py +89 -26
  220. reflex/utils/exec.py +69 -74
  221. reflex/utils/export.py +6 -1
  222. reflex/utils/format.py +8 -40
  223. reflex/utils/imports.py +2 -2
  224. reflex/utils/lazy_loader.py +7 -1
  225. reflex/utils/path_ops.py +28 -14
  226. reflex/utils/prerequisites.py +326 -67
  227. reflex/utils/processes.py +45 -32
  228. reflex/utils/pyi_generator.py +39 -33
  229. reflex/utils/registry.py +4 -4
  230. reflex/utils/serializers.py +1 -1
  231. reflex/utils/telemetry.py +5 -4
  232. reflex/utils/types.py +42 -18
  233. reflex/vars/base.py +656 -333
  234. reflex/vars/datetime.py +6 -7
  235. reflex/vars/dep_tracking.py +344 -0
  236. reflex/vars/function.py +11 -5
  237. reflex/vars/number.py +31 -43
  238. reflex/vars/object.py +63 -62
  239. reflex/vars/sequence.py +79 -67
  240. {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/METADATA +7 -8
  241. reflex-0.7.0a2.dist-info/RECORD +401 -0
  242. {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/WHEEL +1 -1
  243. reflex/experimental/assets.py +0 -37
  244. reflex-0.6.8a2.dist-info/RECORD +0 -397
  245. {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/LICENSE +0 -0
  246. {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/entry_points.txt +0 -0
reflex/app.py CHANGED
@@ -25,8 +25,8 @@ from typing import (
25
25
  Callable,
26
26
  Coroutine,
27
27
  Dict,
28
- Generic,
29
28
  List,
29
+ MutableMapping,
30
30
  Optional,
31
31
  Set,
32
32
  Type,
@@ -53,22 +53,28 @@ from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
53
53
  from reflex.components.base.app_wrap import AppWrap
54
54
  from reflex.components.base.error_boundary import ErrorBoundary
55
55
  from reflex.components.base.fragment import Fragment
56
+ from reflex.components.base.strict_mode import StrictMode
56
57
  from reflex.components.component import (
57
58
  Component,
58
59
  ComponentStyle,
59
60
  evaluate_style_namespaces,
60
61
  )
61
- from reflex.components.core.banner import connection_pulser, connection_toaster
62
+ from reflex.components.core.banner import (
63
+ backend_disabled,
64
+ connection_pulser,
65
+ connection_toaster,
66
+ )
62
67
  from reflex.components.core.breakpoints import set_breakpoints
63
68
  from reflex.components.core.client_side_routing import (
64
69
  Default404Page,
65
70
  wait_for_client_redirect,
66
71
  )
72
+ from reflex.components.core.sticky import sticky
67
73
  from reflex.components.core.upload import Upload, get_upload_dir
68
74
  from reflex.components.radix import themes
69
75
  from reflex.config import environment, get_config
70
76
  from reflex.event import (
71
- BASE_STATE,
77
+ _EVENT_FIELDS,
72
78
  Event,
73
79
  EventHandler,
74
80
  EventSpec,
@@ -144,7 +150,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
144
150
  position="top-center",
145
151
  id="backend_error",
146
152
  style={"width": "500px"},
147
- ) # type: ignore
153
+ )
148
154
  else:
149
155
  error_message.insert(0, "An error occurred.")
150
156
  return window_alert("\n".join(error_message))
@@ -156,9 +162,12 @@ def default_overlay_component() -> Component:
156
162
  Returns:
157
163
  The default overlay_component, which is a connection_modal.
158
164
  """
165
+ config = get_config()
166
+
159
167
  return Fragment.create(
160
168
  connection_pulser(),
161
169
  connection_toaster(),
170
+ *([backend_disabled()] if config.is_reflex_cloud else []),
162
171
  *codespaces.codespaces_auto_redirect(),
163
172
  )
164
173
 
@@ -185,7 +194,7 @@ class OverlayFragment(Fragment):
185
194
  @dataclasses.dataclass(
186
195
  frozen=True,
187
196
  )
188
- class UnevaluatedPage(Generic[BASE_STATE]):
197
+ class UnevaluatedPage:
189
198
  """An uncompiled page."""
190
199
 
191
200
  component: Union[Component, ComponentCallable]
@@ -193,7 +202,7 @@ class UnevaluatedPage(Generic[BASE_STATE]):
193
202
  title: Union[Var, str, None]
194
203
  description: Union[Var, str, None]
195
204
  image: str
196
- on_load: Union[EventType[[], BASE_STATE], None]
205
+ on_load: Union[EventType[()], None]
197
206
  meta: List[Dict[str, str]]
198
207
 
199
208
 
@@ -250,36 +259,36 @@ class App(MiddlewareMixin, LifespanMixin):
250
259
  # Attributes to add to the html root tag of every page.
251
260
  html_custom_attrs: Optional[Dict[str, str]] = None
252
261
 
253
- # A map from a route to an unevaluated page. PRIVATE.
254
- unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
262
+ # A map from a route to an unevaluated page.
263
+ _unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
255
264
  default_factory=dict
256
265
  )
257
266
 
258
- # A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
259
- pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
267
+ # A map from a page route to the component to render. Users should use `add_page`.
268
+ _pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
260
269
 
261
- # The backend API object. PRIVATE.
262
- api: FastAPI = None # type: ignore
270
+ # The backend API object.
271
+ _api: FastAPI | None = None
263
272
 
264
- # The state class to use for the app. PRIVATE.
265
- state: Optional[Type[BaseState]] = None
273
+ # The state class to use for the app.
274
+ _state: Optional[Type[BaseState]] = None
266
275
 
267
276
  # Class to manage many client states.
268
277
  _state_manager: Optional[StateManager] = None
269
278
 
270
- # Mapping from a route to event handlers to trigger when the page loads. PRIVATE.
271
- load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
279
+ # Mapping from a route to event handlers to trigger when the page loads.
280
+ _load_events: Dict[str, List[IndividualEventType[()]]] = dataclasses.field(
272
281
  default_factory=dict
273
282
  )
274
283
 
275
- # Admin dashboard to view and manage the database. PRIVATE.
284
+ # Admin dashboard to view and manage the database.
276
285
  admin_dash: Optional[AdminDash] = None
277
286
 
278
- # The async server name space. PRIVATE.
279
- event_namespace: Optional[EventNamespace] = None
287
+ # The async server name space.
288
+ _event_namespace: Optional[EventNamespace] = None
280
289
 
281
- # Background tasks that are currently running. PRIVATE.
282
- background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
290
+ # Background tasks that are currently running.
291
+ _background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
283
292
 
284
293
  # Frontend Error Handler Function
285
294
  frontend_exception_handler: Callable[[Exception], None] = (
@@ -291,6 +300,24 @@ class App(MiddlewareMixin, LifespanMixin):
291
300
  [Exception], Union[EventSpec, List[EventSpec], None]
292
301
  ] = default_backend_exception_handler
293
302
 
303
+ @property
304
+ def api(self) -> FastAPI | None:
305
+ """Get the backend api.
306
+
307
+ Returns:
308
+ The backend api.
309
+ """
310
+ return self._api
311
+
312
+ @property
313
+ def event_namespace(self) -> EventNamespace | None:
314
+ """Get the event namespace.
315
+
316
+ Returns:
317
+ The event namespace.
318
+ """
319
+ return self._event_namespace
320
+
294
321
  def __post_init__(self):
295
322
  """Initialize the app.
296
323
 
@@ -310,7 +337,7 @@ class App(MiddlewareMixin, LifespanMixin):
310
337
  set_breakpoints(self.style.pop("breakpoints"))
311
338
 
312
339
  # Set up the API.
313
- self.api = FastAPI(lifespan=self._run_lifespan_tasks)
340
+ self._api = FastAPI(lifespan=self._run_lifespan_tasks)
314
341
  self._add_cors()
315
342
  self._add_default_endpoints()
316
343
 
@@ -333,8 +360,8 @@ class App(MiddlewareMixin, LifespanMixin):
333
360
 
334
361
  def _enable_state(self) -> None:
335
362
  """Enable state for the app."""
336
- if not self.state:
337
- self.state = State
363
+ if not self._state:
364
+ self._state = State
338
365
  self._setup_state()
339
366
 
340
367
  def _setup_state(self) -> None:
@@ -343,13 +370,13 @@ class App(MiddlewareMixin, LifespanMixin):
343
370
  Raises:
344
371
  RuntimeError: If the socket server is invalid.
345
372
  """
346
- if not self.state:
373
+ if not self._state:
347
374
  return
348
375
 
349
376
  config = get_config()
350
377
 
351
378
  # Set up the state manager.
352
- self._state_manager = StateManager.create(state=self.state)
379
+ self._state_manager = StateManager.create(state=self._state)
353
380
 
354
381
  # Set up the Socket.IO AsyncServer.
355
382
  if not self.sio:
@@ -380,12 +407,42 @@ class App(MiddlewareMixin, LifespanMixin):
380
407
  namespace = config.get_event_namespace()
381
408
 
382
409
  # Create the event namespace and attach the main app. Not related to any paths.
383
- self.event_namespace = EventNamespace(namespace, self)
410
+ self._event_namespace = EventNamespace(namespace, self)
384
411
 
385
412
  # Register the event namespace with the socket.
386
413
  self.sio.register_namespace(self.event_namespace)
387
414
  # Mount the socket app with the API.
388
- self.api.mount(str(constants.Endpoint.EVENT), socket_app)
415
+ if self.api:
416
+
417
+ class HeaderMiddleware:
418
+ def __init__(self, app: ASGIApp):
419
+ self.app = app
420
+
421
+ async def __call__(
422
+ self, scope: MutableMapping[str, Any], receive: Any, send: Callable
423
+ ):
424
+ original_send = send
425
+
426
+ async def modified_send(message: dict):
427
+ if message["type"] == "websocket.accept":
428
+ if scope.get("subprotocols"):
429
+ # The following *does* say "subprotocol" instead of "subprotocols", intentionally.
430
+ message["subprotocol"] = scope["subprotocols"][0]
431
+
432
+ headers = dict(message.get("headers", []))
433
+ header_key = b"sec-websocket-protocol"
434
+ if subprotocol := headers.get(header_key):
435
+ message["headers"] = [
436
+ *message.get("headers", []),
437
+ (header_key, subprotocol),
438
+ ]
439
+
440
+ return await original_send(message)
441
+
442
+ return await self.app(scope, receive, modified_send)
443
+
444
+ socket_app_with_headers = HeaderMiddleware(socket_app)
445
+ self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
389
446
 
390
447
  # Check the exception handlers
391
448
  self._validate_exception_handlers()
@@ -396,24 +453,35 @@ class App(MiddlewareMixin, LifespanMixin):
396
453
  Returns:
397
454
  The string representation of the app.
398
455
  """
399
- return f"<App state={self.state.__name__ if self.state else None}>"
456
+ return f"<App state={self._state.__name__ if self._state else None}>"
400
457
 
401
458
  def __call__(self) -> FastAPI:
402
459
  """Run the backend api instance.
403
460
 
461
+ Raises:
462
+ ValueError: If the app has not been initialized.
463
+
404
464
  Returns:
405
465
  The backend api.
406
466
  """
467
+ if not self.api:
468
+ raise ValueError("The app has not been initialized.")
407
469
  return self.api
408
470
 
409
471
  def _add_default_endpoints(self):
410
472
  """Add default api endpoints (ping)."""
411
473
  # To test the server.
474
+ if not self.api:
475
+ return
476
+
412
477
  self.api.get(str(constants.Endpoint.PING))(ping)
413
478
  self.api.get(str(constants.Endpoint.HEALTH))(health)
414
479
 
415
480
  def _add_optional_endpoints(self):
416
481
  """Add optional api endpoints (_upload)."""
482
+ if not self.api:
483
+ return
484
+
417
485
  if Upload.is_used:
418
486
  # To upload files.
419
487
  self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
@@ -431,6 +499,8 @@ class App(MiddlewareMixin, LifespanMixin):
431
499
 
432
500
  def _add_cors(self):
433
501
  """Add CORS middleware to the app."""
502
+ if not self.api:
503
+ return
434
504
  self.api.add_middleware(
435
505
  cors.CORSMiddleware,
436
506
  allow_credentials=True,
@@ -462,14 +532,8 @@ class App(MiddlewareMixin, LifespanMixin):
462
532
 
463
533
  Returns:
464
534
  The generated component.
465
-
466
- Raises:
467
- exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
468
535
  """
469
- try:
470
- return component if isinstance(component, Component) else component()
471
- except exceptions.MatchTypeError:
472
- raise
536
+ return component if isinstance(component, Component) else component()
473
537
 
474
538
  def add_page(
475
539
  self,
@@ -478,7 +542,7 @@ class App(MiddlewareMixin, LifespanMixin):
478
542
  title: str | Var | None = None,
479
543
  description: str | Var | None = None,
480
544
  image: str = constants.DefaultPage.IMAGE,
481
- on_load: EventType[[], BASE_STATE] | None = None,
545
+ on_load: EventType[()] | None = None,
482
546
  meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
483
547
  ):
484
548
  """Add a page to the app.
@@ -526,13 +590,13 @@ class App(MiddlewareMixin, LifespanMixin):
526
590
  # Check if the route given is valid
527
591
  verify_route_validity(route)
528
592
 
529
- if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set():
593
+ if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
530
594
  # when the app is reloaded(typically for app harness tests), we should maintain
531
595
  # the latest render function of a route.This applies typically to decorated pages
532
596
  # since they are only added when app._compile is called.
533
- self.unevaluated_pages.pop(route)
597
+ self._unevaluated_pages.pop(route)
534
598
 
535
- if route in self.unevaluated_pages:
599
+ if route in self._unevaluated_pages:
536
600
  route_name = (
537
601
  f"`{route}` or `/`"
538
602
  if route == constants.PageNames.INDEX_ROUTE
@@ -545,15 +609,15 @@ class App(MiddlewareMixin, LifespanMixin):
545
609
 
546
610
  # Setup dynamic args for the route.
547
611
  # this state assignment is only required for tests using the deprecated state kwarg for App
548
- state = self.state if self.state else State
612
+ state = self._state if self._state else State
549
613
  state.setup_dynamic_args(get_route_args(route))
550
614
 
551
615
  if on_load:
552
- self.load_events[route] = (
616
+ self._load_events[route] = (
553
617
  on_load if isinstance(on_load, list) else [on_load]
554
618
  )
555
619
 
556
- self.unevaluated_pages[route] = UnevaluatedPage(
620
+ self._unevaluated_pages[route] = UnevaluatedPage(
557
621
  component=component,
558
622
  route=route,
559
623
  title=title,
@@ -563,14 +627,15 @@ class App(MiddlewareMixin, LifespanMixin):
563
627
  meta=meta,
564
628
  )
565
629
 
566
- def _compile_page(self, route: str):
630
+ def _compile_page(self, route: str, save_page: bool = True):
567
631
  """Compile a page.
568
632
 
569
633
  Args:
570
634
  route: The route of the page to compile.
635
+ save_page: If True, the compiled page is saved to self._pages.
571
636
  """
572
637
  component, enable_state = compiler.compile_unevaluated_page(
573
- route, self.unevaluated_pages[route], self.state, self.style, self.theme
638
+ route, self._unevaluated_pages[route], self._state, self.style, self.theme
574
639
  )
575
640
 
576
641
  if enable_state:
@@ -578,9 +643,10 @@ class App(MiddlewareMixin, LifespanMixin):
578
643
 
579
644
  # Add the page.
580
645
  self._check_routes_conflict(route)
581
- self.pages[route] = component
646
+ if save_page:
647
+ self._pages[route] = component
582
648
 
583
- def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
649
+ def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
584
650
  """Get the load events for a route.
585
651
 
586
652
  Args:
@@ -592,7 +658,7 @@ class App(MiddlewareMixin, LifespanMixin):
592
658
  route = route.lstrip("/")
593
659
  if route == "":
594
660
  route = constants.PageNames.INDEX_ROUTE
595
- return self.load_events.get(route, [])
661
+ return self._load_events.get(route, [])
596
662
 
597
663
  def _check_routes_conflict(self, new_route: str):
598
664
  """Verify if there is any conflict between the new route and any existing route.
@@ -616,10 +682,13 @@ class App(MiddlewareMixin, LifespanMixin):
616
682
  constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
617
683
  constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
618
684
  )
619
- for route in self.pages:
685
+ for route in self._pages:
620
686
  replaced_route = replace_brackets_with_keywords(route)
621
687
  for rw, r, nr in zip(
622
- replaced_route.split("/"), route.split("/"), new_route.split("/")
688
+ replaced_route.split("/"),
689
+ route.split("/"),
690
+ new_route.split("/"),
691
+ strict=False,
623
692
  ):
624
693
  if rw in segments and r != nr:
625
694
  # If the slugs in the segments of both routes are not the same, then the route is invalid
@@ -639,7 +708,7 @@ class App(MiddlewareMixin, LifespanMixin):
639
708
  title: str = constants.Page404.TITLE,
640
709
  image: str = constants.Page404.IMAGE,
641
710
  description: str = constants.Page404.DESCRIPTION,
642
- on_load: EventType[[], BASE_STATE] | None = None,
711
+ on_load: EventType[()] | None = None,
643
712
  meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
644
713
  ):
645
714
  """Define a custom 404 page for any url having no match.
@@ -650,8 +719,8 @@ class App(MiddlewareMixin, LifespanMixin):
650
719
  Args:
651
720
  component: The component to display at the page.
652
721
  title: The title of the page.
653
- description: The description of the page.
654
722
  image: The image to display on the page.
723
+ description: The description of the page.
655
724
  on_load: The event handler(s) that will be called each time the page load.
656
725
  meta: The metadata of the page.
657
726
  """
@@ -674,6 +743,9 @@ class App(MiddlewareMixin, LifespanMixin):
674
743
  def _setup_admin_dash(self):
675
744
  """Setup the admin dash."""
676
745
  # Get the admin dash.
746
+ if not self.api:
747
+ return
748
+
677
749
  admin_dash = self.admin_dash
678
750
 
679
751
  if admin_dash and admin_dash.models:
@@ -715,7 +787,7 @@ class App(MiddlewareMixin, LifespanMixin):
715
787
  frontend_packages = get_config().frontend_packages
716
788
  _frontend_packages = []
717
789
  for package in frontend_packages:
718
- if package in (get_config().tailwind or {}).get("plugins", []): # type: ignore
790
+ if package in (get_config().tailwind or {}).get("plugins", []):
719
791
  console.warn(
720
792
  f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
721
793
  )
@@ -778,10 +850,10 @@ class App(MiddlewareMixin, LifespanMixin):
778
850
 
779
851
  def _setup_overlay_component(self):
780
852
  """If a State is not used and no overlay_component is specified, do not render the connection modal."""
781
- if self.state is None and self.overlay_component is default_overlay_component:
853
+ if self._state is None and self.overlay_component is default_overlay_component:
782
854
  self.overlay_component = None
783
- for k, component in self.pages.items():
784
- self.pages[k] = self._add_overlay_to_component(component)
855
+ for k, component in self._pages.items():
856
+ self._pages[k] = self._add_overlay_to_component(component)
785
857
 
786
858
  def _add_error_boundary_to_component(self, component: Component) -> Component:
787
859
  if self.error_boundary is None:
@@ -793,14 +865,23 @@ class App(MiddlewareMixin, LifespanMixin):
793
865
 
794
866
  def _setup_error_boundary(self):
795
867
  """If a State is not used and no error_boundary is specified, do not render the error boundary."""
796
- if self.state is None and self.error_boundary is default_error_boundary:
868
+ if self._state is None and self.error_boundary is default_error_boundary:
797
869
  self.error_boundary = None
798
870
 
799
- for k, component in self.pages.items():
871
+ for k, component in self._pages.items():
800
872
  # Skip the 404 page
801
873
  if k == constants.Page404.SLUG:
802
874
  continue
803
- self.pages[k] = self._add_error_boundary_to_component(component)
875
+ self._pages[k] = self._add_error_boundary_to_component(component)
876
+
877
+ def _setup_sticky_badge(self):
878
+ """Add the sticky badge to the app."""
879
+ for k, component in self._pages.items():
880
+ # Would be nice to share single sticky_badge across all pages, but
881
+ # it bungles the StatefulComponent compile step.
882
+ sticky_badge = sticky()
883
+ sticky_badge._add_style_recursive({})
884
+ self._pages[k] = Fragment.create(sticky_badge, component)
804
885
 
805
886
  def _apply_decorated_pages(self):
806
887
  """Add @rx.page decorated pages to the app.
@@ -826,21 +907,27 @@ class App(MiddlewareMixin, LifespanMixin):
826
907
  Raises:
827
908
  VarDependencyError: When a computed var has an invalid dependency.
828
909
  """
829
- if not self.state:
910
+ if not self._state:
830
911
  return
831
912
 
832
913
  if not state:
833
- state = self.state
914
+ state = self._state
834
915
 
835
916
  for var in state.computed_vars.values():
836
917
  if not var._cache:
837
918
  continue
838
919
  deps = var._deps(objclass=state)
839
- for dep in deps:
840
- if dep not in state.vars and dep not in state.backend_vars:
841
- raise exceptions.VarDependencyError(
842
- f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {dep}"
843
- )
920
+ for state_name, dep_set in deps.items():
921
+ state_cls = (
922
+ state.get_root_state().get_class_substate(state_name)
923
+ if state_name != state.get_full_name()
924
+ else state
925
+ )
926
+ for dep in dep_set:
927
+ if dep not in state_cls.vars and dep not in state_cls.backend_vars:
928
+ raise exceptions.VarDependencyError(
929
+ f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {state_name}.{dep}"
930
+ )
844
931
 
845
932
  for substate in state.class_subclasses:
846
933
  self._validate_var_dependencies(substate)
@@ -856,13 +943,13 @@ class App(MiddlewareMixin, LifespanMixin):
856
943
  """
857
944
  from reflex.utils.exceptions import ReflexRuntimeError
858
945
 
859
- self.pages = {}
946
+ self._pages = {}
860
947
 
861
948
  def get_compilation_time() -> str:
862
949
  return str(datetime.now().time()).split(".")[0]
863
950
 
864
951
  # Render a default 404 page if the user didn't supply one
865
- if constants.Page404.SLUG not in self.unevaluated_pages:
952
+ if constants.Page404.SLUG not in self._unevaluated_pages:
866
953
  self.add_page(route=constants.Page404.SLUG)
867
954
 
868
955
  # Fix up the style.
@@ -878,19 +965,23 @@ class App(MiddlewareMixin, LifespanMixin):
878
965
  # If a theme component was provided, wrap the app with it
879
966
  app_wrappers[(20, "Theme")] = self.theme
880
967
 
881
- for route in self.unevaluated_pages:
882
- console.debug(f"Evaluating page: {route}")
883
- self._compile_page(route)
968
+ # Get the env mode.
969
+ config = get_config()
884
970
 
885
- # Add the optional endpoints (_upload)
886
- self._add_optional_endpoints()
971
+ if config.react_strict_mode:
972
+ app_wrappers[(200, "StrictMode")] = StrictMode.create()
887
973
 
888
- if not self._should_compile():
889
- return
974
+ should_compile = self._should_compile()
890
975
 
891
- self._validate_var_dependencies()
892
- self._setup_overlay_component()
893
- self._setup_error_boundary()
976
+ if not should_compile:
977
+ for route in self._unevaluated_pages:
978
+ console.debug(f"Evaluating page: {route}")
979
+ self._compile_page(route, save_page=should_compile)
980
+
981
+ # Add the optional endpoints (_upload)
982
+ self._add_optional_endpoints()
983
+
984
+ return
894
985
 
895
986
  # Create a progress bar.
896
987
  progress = Progress(
@@ -900,18 +991,32 @@ class App(MiddlewareMixin, LifespanMixin):
900
991
  )
901
992
 
902
993
  # try to be somewhat accurate - but still not 100%
903
- adhoc_steps_without_executor = 6
994
+ adhoc_steps_without_executor = 7
904
995
  fixed_pages_within_executor = 5
905
996
  progress.start()
906
997
  task = progress.add_task(
907
998
  f"[{get_compilation_time()}] Compiling:",
908
- total=len(self.pages)
999
+ total=len(self._pages)
1000
+ + (len(self._unevaluated_pages) * 2)
909
1001
  + fixed_pages_within_executor
910
1002
  + adhoc_steps_without_executor,
911
1003
  )
912
1004
 
913
- # Get the env mode.
914
- config = get_config()
1005
+ for route in self._unevaluated_pages:
1006
+ console.debug(f"Evaluating page: {route}")
1007
+ self._compile_page(route, save_page=should_compile)
1008
+ progress.advance(task)
1009
+
1010
+ # Add the optional endpoints (_upload)
1011
+ self._add_optional_endpoints()
1012
+
1013
+ self._validate_var_dependencies()
1014
+ self._setup_overlay_component()
1015
+ self._setup_error_boundary()
1016
+ if config.show_built_with_reflex:
1017
+ self._setup_sticky_badge()
1018
+
1019
+ progress.advance(task)
915
1020
 
916
1021
  # Store the compile results.
917
1022
  compile_results = []
@@ -924,7 +1029,7 @@ class App(MiddlewareMixin, LifespanMixin):
924
1029
 
925
1030
  # This has to happen before compiling stateful components as that
926
1031
  # prevents recursive functions from reaching all components.
927
- for component in self.pages.values():
1032
+ for component in self._pages.values():
928
1033
  # Add component._get_all_imports() to all_imports.
929
1034
  all_imports.update(component._get_all_imports())
930
1035
 
@@ -939,12 +1044,12 @@ class App(MiddlewareMixin, LifespanMixin):
939
1044
  stateful_components_path,
940
1045
  stateful_components_code,
941
1046
  page_components,
942
- ) = compiler.compile_stateful_components(self.pages.values())
1047
+ ) = compiler.compile_stateful_components(self._pages.values())
943
1048
 
944
1049
  progress.advance(task)
945
1050
 
946
1051
  # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
947
- if code_uses_state_contexts(stateful_components_code) and self.state is None:
1052
+ if code_uses_state_contexts(stateful_components_code) and self._state is None:
948
1053
  raise ReflexRuntimeError(
949
1054
  "To access rx.State in frontend components, at least one "
950
1055
  "subclass of rx.State must be defined in the app."
@@ -958,7 +1063,7 @@ class App(MiddlewareMixin, LifespanMixin):
958
1063
  compiler.compile_document_root(
959
1064
  self.head_components,
960
1065
  html_lang=self.html_lang,
961
- html_custom_attrs=self.html_custom_attrs, # type: ignore
1066
+ html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
962
1067
  )
963
1068
  )
964
1069
 
@@ -981,20 +1086,20 @@ class App(MiddlewareMixin, LifespanMixin):
981
1086
  max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
982
1087
  )
983
1088
 
984
- for route, component in zip(self.pages, page_components):
1089
+ for route, component in zip(self._pages, page_components, strict=True):
985
1090
  ExecutorSafeFunctions.COMPONENTS[route] = component
986
1091
 
987
- ExecutorSafeFunctions.STATE = self.state
1092
+ ExecutorSafeFunctions.STATE = self._state
988
1093
 
989
1094
  with executor:
990
1095
  result_futures = []
991
1096
 
992
- def _submit_work(fn, *args, **kwargs):
1097
+ def _submit_work(fn: Callable, *args, **kwargs):
993
1098
  f = executor.submit(fn, *args, **kwargs)
994
1099
  result_futures.append(f)
995
1100
 
996
1101
  # Compile the pre-compiled pages.
997
- for route in self.pages:
1102
+ for route in self._pages:
998
1103
  _submit_work(
999
1104
  ExecutorSafeFunctions.compile_page,
1000
1105
  route,
@@ -1029,7 +1134,7 @@ class App(MiddlewareMixin, LifespanMixin):
1029
1134
 
1030
1135
  # Compile the contexts.
1031
1136
  compile_results.append(
1032
- compiler.compile_contexts(self.state, self.theme),
1137
+ compiler.compile_contexts(self._state, self.theme),
1033
1138
  )
1034
1139
  if self.theme is not None:
1035
1140
  # Fix #2992 by removing the top-level appearance prop
@@ -1151,9 +1256,9 @@ class App(MiddlewareMixin, LifespanMixin):
1151
1256
  )
1152
1257
 
1153
1258
  task = asyncio.create_task(_coro())
1154
- self.background_tasks.add(task)
1259
+ self._background_tasks.add(task)
1155
1260
  # Clean up task from background_tasks set when complete.
1156
- task.add_done_callback(self.background_tasks.discard)
1261
+ task.add_done_callback(self._background_tasks.discard)
1157
1262
  return task
1158
1263
 
1159
1264
  def _validate_exception_handlers(self):
@@ -1163,11 +1268,11 @@ class App(MiddlewareMixin, LifespanMixin):
1163
1268
  ValueError: If the custom exception handlers are invalid.
1164
1269
 
1165
1270
  """
1166
- FRONTEND_ARG_SPEC = {
1271
+ frontend_arg_spec = {
1167
1272
  "exception": Exception,
1168
1273
  }
1169
1274
 
1170
- BACKEND_ARG_SPEC = {
1275
+ backend_arg_spec = {
1171
1276
  "exception": Exception,
1172
1277
  }
1173
1278
 
@@ -1175,9 +1280,10 @@ class App(MiddlewareMixin, LifespanMixin):
1175
1280
  ["frontend", "backend"],
1176
1281
  [self.frontend_exception_handler, self.backend_exception_handler],
1177
1282
  [
1178
- FRONTEND_ARG_SPEC,
1179
- BACKEND_ARG_SPEC,
1283
+ frontend_arg_spec,
1284
+ backend_arg_spec,
1180
1285
  ],
1286
+ strict=True,
1181
1287
  ):
1182
1288
  if hasattr(handler_fn, "__name__"):
1183
1289
  _fn_name = handler_fn.__name__
@@ -1218,7 +1324,7 @@ class App(MiddlewareMixin, LifespanMixin):
1218
1324
  ):
1219
1325
  raise ValueError(
1220
1326
  f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order."
1221
- f"Expected `{required_arg}` as the {required_arg_index+1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
1327
+ f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
1222
1328
  )
1223
1329
 
1224
1330
  if not issubclass(arg_annotations[required_arg], Exception):
@@ -1319,15 +1425,14 @@ async def process(
1319
1425
  if app._process_background(state, event) is not None:
1320
1426
  # `final=True` allows the frontend send more events immediately.
1321
1427
  yield StateUpdate(final=True)
1322
- return
1323
-
1324
- # Process the event synchronously.
1325
- async for update in state._process(event):
1326
- # Postprocess the event.
1327
- update = await app._postprocess(state, event, update)
1328
-
1329
- # Yield the update.
1330
- yield update
1428
+ else:
1429
+ # Process the event synchronously.
1430
+ async for update in state._process(event):
1431
+ # Postprocess the event.
1432
+ update = await app._postprocess(state, event, update)
1433
+
1434
+ # Yield the update.
1435
+ yield update
1331
1436
  except Exception as ex:
1332
1437
  telemetry.send_error(ex, context="backend")
1333
1438
 
@@ -1522,16 +1627,20 @@ class EventNamespace(AsyncNamespace):
1522
1627
  self.sid_to_token = {}
1523
1628
  self.app = app
1524
1629
 
1525
- def on_connect(self, sid, environ):
1630
+ def on_connect(self, sid: str, environ: dict):
1526
1631
  """Event for when the websocket is connected.
1527
1632
 
1528
1633
  Args:
1529
1634
  sid: The Socket.IO session id.
1530
1635
  environ: The request information, including HTTP headers.
1531
1636
  """
1532
- pass
1637
+ subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
1638
+ if subprotocol and subprotocol != constants.Reflex.VERSION:
1639
+ console.warn(
1640
+ f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}."
1641
+ )
1533
1642
 
1534
- def on_disconnect(self, sid):
1643
+ def on_disconnect(self, sid: str):
1535
1644
  """Event for when the websocket disconnects.
1536
1645
 
1537
1646
  Args:
@@ -1553,7 +1662,7 @@ class EventNamespace(AsyncNamespace):
1553
1662
  self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
1554
1663
  )
1555
1664
 
1556
- async def on_event(self, sid, data):
1665
+ async def on_event(self, sid: str, data: Any):
1557
1666
  """Event for receiving front-end websocket events.
1558
1667
 
1559
1668
  Raises:
@@ -1562,12 +1671,36 @@ class EventNamespace(AsyncNamespace):
1562
1671
  Args:
1563
1672
  sid: The Socket.IO session id.
1564
1673
  data: The event data.
1674
+
1675
+ Raises:
1676
+ EventDeserializationError: If the event data is not a dictionary.
1565
1677
  """
1566
1678
  fields = data
1567
- # Get the event.
1568
- event = Event(
1569
- **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
1570
- )
1679
+
1680
+ if isinstance(fields, str):
1681
+ console.warn(
1682
+ "Received event data as a string. This generally should not happen and may indicate a bug."
1683
+ f" Event data: {fields}"
1684
+ )
1685
+ try:
1686
+ fields = json.loads(fields)
1687
+ except json.JSONDecodeError as ex:
1688
+ raise exceptions.EventDeserializationError(
1689
+ f"Failed to deserialize event data: {fields}."
1690
+ ) from ex
1691
+
1692
+ if not isinstance(fields, dict):
1693
+ raise exceptions.EventDeserializationError(
1694
+ f"Event data must be a dictionary, but received {fields} of type {type(fields)}."
1695
+ )
1696
+
1697
+ try:
1698
+ # Get the event.
1699
+ event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
1700
+ except (TypeError, ValueError) as ex:
1701
+ raise exceptions.EventDeserializationError(
1702
+ f"Failed to deserialize event data: {fields}."
1703
+ ) from ex
1571
1704
 
1572
1705
  self.token_to_sid[event.token] = sid
1573
1706
  self.sid_to_token[sid] = event.token
@@ -1596,7 +1729,7 @@ class EventNamespace(AsyncNamespace):
1596
1729
  # Emit the update from processing the event.
1597
1730
  await self.emit_update(update=update, sid=sid)
1598
1731
 
1599
- async def on_ping(self, sid):
1732
+ async def on_ping(self, sid: str):
1600
1733
  """Event for testing the API endpoint.
1601
1734
 
1602
1735
  Args: