reflex 0.5.10a3__py3-none-any.whl → 0.6.0__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 (303) hide show
  1. reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +2 -2
  2. reflex/.templates/jinja/web/pages/_app.js.jinja2 +1 -1
  3. reflex/.templates/jinja/web/pages/utils.js.jinja2 +4 -4
  4. reflex/.templates/jinja/web/utils/context.js.jinja2 +1 -1
  5. reflex/.templates/jinja/web/utils/theme.js.jinja2 +1 -1
  6. reflex/.templates/web/utils/state.js +3 -1
  7. reflex/__init__.py +10 -3
  8. reflex/__init__.pyi +3 -2
  9. reflex/app.py +47 -11
  10. reflex/app_module_for_backend.py +1 -1
  11. reflex/base.py +3 -2
  12. reflex/compiler/compiler.py +5 -5
  13. reflex/compiler/utils.py +5 -3
  14. reflex/components/base/app_wrap.py +2 -4
  15. reflex/components/base/app_wrap.pyi +16 -26
  16. reflex/components/base/bare.py +6 -4
  17. reflex/components/base/body.pyi +16 -26
  18. reflex/components/base/document.pyi +76 -126
  19. reflex/components/base/error_boundary.py +9 -8
  20. reflex/components/base/error_boundary.pyi +18 -30
  21. reflex/components/base/fragment.pyi +16 -26
  22. reflex/components/base/head.pyi +31 -51
  23. reflex/components/base/link.py +1 -1
  24. reflex/components/base/link.pyi +31 -51
  25. reflex/components/base/meta.pyi +61 -101
  26. reflex/components/base/script.py +2 -2
  27. reflex/components/base/script.pyi +20 -36
  28. reflex/components/component.py +107 -130
  29. reflex/components/core/banner.py +61 -66
  30. reflex/components/core/banner.pyi +129 -230
  31. reflex/components/core/client_side_routing.py +2 -2
  32. reflex/components/core/client_side_routing.pyi +31 -51
  33. reflex/components/core/clipboard.py +4 -3
  34. reflex/components/core/clipboard.pyi +19 -31
  35. reflex/components/core/cond.py +21 -44
  36. reflex/components/core/debounce.py +7 -9
  37. reflex/components/core/debounce.pyi +19 -31
  38. reflex/components/core/foreach.py +4 -14
  39. reflex/components/core/html.py +1 -1
  40. reflex/components/core/html.pyi +34 -44
  41. reflex/components/core/match.py +36 -43
  42. reflex/components/core/upload.py +27 -26
  43. reflex/components/core/upload.pyi +81 -116
  44. reflex/components/datadisplay/code.py +55 -29
  45. reflex/components/datadisplay/code.pyi +303 -410
  46. reflex/components/datadisplay/dataeditor.py +13 -9
  47. reflex/components/datadisplay/dataeditor.pyi +39 -51
  48. reflex/components/el/__init__.py +0 -1
  49. reflex/components/el/__init__.pyi +0 -11
  50. reflex/components/el/element.pyi +16 -26
  51. reflex/components/el/elements/__init__.py +1 -7
  52. reflex/components/el/elements/__init__.pyi +1 -15
  53. reflex/components/el/elements/base.py +1 -1
  54. reflex/components/el/elements/base.pyi +33 -43
  55. reflex/components/el/elements/forms.py +26 -33
  56. reflex/components/el/elements/forms.pyi +542 -694
  57. reflex/components/el/elements/inline.py +1 -1
  58. reflex/components/el/elements/inline.pyi +909 -1189
  59. reflex/components/el/elements/media.py +1 -22
  60. reflex/components/el/elements/media.pyi +765 -975
  61. reflex/components/el/elements/metadata.py +3 -5
  62. reflex/components/el/elements/metadata.pyi +175 -235
  63. reflex/components/el/elements/other.py +1 -1
  64. reflex/components/el/elements/other.pyi +228 -298
  65. reflex/components/el/elements/scripts.py +1 -1
  66. reflex/components/el/elements/scripts.pyi +106 -136
  67. reflex/components/el/elements/sectioning.py +0 -2
  68. reflex/components/el/elements/sectioning.pyi +481 -631
  69. reflex/components/el/elements/tables.py +1 -1
  70. reflex/components/el/elements/tables.pyi +341 -441
  71. reflex/components/el/elements/typography.py +1 -1
  72. reflex/components/el/elements/typography.pyi +491 -641
  73. reflex/components/gridjs/datatable.py +9 -13
  74. reflex/components/gridjs/datatable.pyi +33 -53
  75. reflex/components/lucide/icon.py +3 -127
  76. reflex/components/lucide/icon.pyi +31 -160
  77. reflex/components/markdown/markdown.py +32 -42
  78. reflex/components/markdown/markdown.pyi +28 -41
  79. reflex/components/moment/moment.py +13 -12
  80. reflex/components/moment/moment.pyi +22 -33
  81. reflex/components/next/base.pyi +16 -26
  82. reflex/components/next/image.py +1 -5
  83. reflex/components/next/image.pyi +21 -35
  84. reflex/components/next/link.py +1 -1
  85. reflex/components/next/link.pyi +16 -26
  86. reflex/components/next/video.py +1 -1
  87. reflex/components/next/video.pyi +16 -26
  88. reflex/components/plotly/plotly.py +17 -30
  89. reflex/components/plotly/plotly.pyi +38 -52
  90. reflex/components/props.py +21 -10
  91. reflex/components/radix/__init__.pyi +2 -1
  92. reflex/components/radix/primitives/accordion.py +6 -7
  93. reflex/components/radix/primitives/accordion.pyi +415 -485
  94. reflex/components/radix/primitives/base.py +1 -1
  95. reflex/components/radix/primitives/base.pyi +31 -51
  96. reflex/components/radix/primitives/drawer.py +1 -1
  97. reflex/components/radix/primitives/drawer.pyi +162 -262
  98. reflex/components/radix/primitives/form.py +1 -1
  99. reflex/components/radix/primitives/form.pyi +247 -353
  100. reflex/components/radix/primitives/progress.py +1 -1
  101. reflex/components/radix/primitives/progress.pyi +226 -276
  102. reflex/components/radix/primitives/slider.py +1 -1
  103. reflex/components/radix/primitives/slider.pyi +82 -132
  104. reflex/components/radix/themes/base.py +8 -25
  105. reflex/components/radix/themes/base.pyi +171 -242
  106. reflex/components/radix/themes/color_mode.py +11 -20
  107. reflex/components/radix/themes/color_mode.pyi +198 -231
  108. reflex/components/radix/themes/components/__init__.pyi +1 -0
  109. reflex/components/radix/themes/components/alert_dialog.py +1 -1
  110. reflex/components/radix/themes/components/alert_dialog.pyi +129 -199
  111. reflex/components/radix/themes/components/aspect_ratio.py +1 -1
  112. reflex/components/radix/themes/components/aspect_ratio.pyi +16 -26
  113. reflex/components/radix/themes/components/avatar.py +1 -1
  114. reflex/components/radix/themes/components/avatar.pyi +69 -79
  115. reflex/components/radix/themes/components/badge.py +1 -1
  116. reflex/components/radix/themes/components/badge.pyi +87 -97
  117. reflex/components/radix/themes/components/button.py +1 -1
  118. reflex/components/radix/themes/components/button.pyi +97 -107
  119. reflex/components/radix/themes/components/callout.py +1 -1
  120. reflex/components/radix/themes/components/callout.pyi +317 -367
  121. reflex/components/radix/themes/components/card.py +1 -1
  122. reflex/components/radix/themes/components/card.pyi +37 -47
  123. reflex/components/radix/themes/components/checkbox.py +2 -4
  124. reflex/components/radix/themes/components/checkbox.pyi +205 -241
  125. reflex/components/radix/themes/components/checkbox_cards.py +1 -1
  126. reflex/components/radix/themes/components/checkbox_cards.pyi +92 -112
  127. reflex/components/radix/themes/components/checkbox_group.py +1 -1
  128. reflex/components/radix/themes/components/checkbox_group.pyi +84 -104
  129. reflex/components/radix/themes/components/context_menu.py +1 -1
  130. reflex/components/radix/themes/components/context_menu.pyi +230 -310
  131. reflex/components/radix/themes/components/data_list.py +6 -1
  132. reflex/components/radix/themes/components/data_list.pyi +131 -166
  133. reflex/components/radix/themes/components/dialog.py +1 -1
  134. reflex/components/radix/themes/components/dialog.pyi +132 -202
  135. reflex/components/radix/themes/components/dropdown_menu.py +1 -1
  136. reflex/components/radix/themes/components/dropdown_menu.pyi +241 -323
  137. reflex/components/radix/themes/components/hover_card.py +1 -1
  138. reflex/components/radix/themes/components/hover_card.pyi +86 -126
  139. reflex/components/radix/themes/components/icon_button.py +1 -1
  140. reflex/components/radix/themes/components/icon_button.pyi +97 -107
  141. reflex/components/radix/themes/components/inset.py +1 -1
  142. reflex/components/radix/themes/components/inset.pyi +46 -56
  143. reflex/components/radix/themes/components/popover.py +1 -1
  144. reflex/components/radix/themes/components/popover.pyi +91 -131
  145. reflex/components/radix/themes/components/progress.py +1 -1
  146. reflex/components/radix/themes/components/progress.pyi +70 -80
  147. reflex/components/radix/themes/components/radio.py +1 -1
  148. reflex/components/radix/themes/components/radio.pyi +68 -78
  149. reflex/components/radix/themes/components/radio_cards.py +1 -1
  150. reflex/components/radix/themes/components/radio_cards.pyi +96 -116
  151. reflex/components/radix/themes/components/radio_group.py +32 -31
  152. reflex/components/radix/themes/components/radio_group.pyi +230 -266
  153. reflex/components/radix/themes/components/scroll_area.py +1 -1
  154. reflex/components/radix/themes/components/scroll_area.pyi +20 -30
  155. reflex/components/radix/themes/components/segmented_control.py +1 -1
  156. reflex/components/radix/themes/components/segmented_control.pyi +88 -110
  157. reflex/components/radix/themes/components/select.py +1 -1
  158. reflex/components/radix/themes/components/select.pyi +365 -461
  159. reflex/components/radix/themes/components/separator.py +2 -4
  160. reflex/components/radix/themes/components/separator.pyi +68 -78
  161. reflex/components/radix/themes/components/skeleton.py +1 -1
  162. reflex/components/radix/themes/components/skeleton.pyi +22 -32
  163. reflex/components/radix/themes/components/slider.py +1 -1
  164. reflex/components/radix/themes/components/slider.pyi +74 -86
  165. reflex/components/radix/themes/components/spinner.py +1 -1
  166. reflex/components/radix/themes/components/spinner.pyi +18 -28
  167. reflex/components/radix/themes/components/switch.py +1 -1
  168. reflex/components/radix/themes/components/switch.pyi +70 -82
  169. reflex/components/radix/themes/components/table.py +1 -1
  170. reflex/components/radix/themes/components/table.pyi +254 -324
  171. reflex/components/radix/themes/components/tabs.py +1 -1
  172. reflex/components/radix/themes/components/tabs.pyi +134 -188
  173. reflex/components/radix/themes/components/text_area.py +1 -1
  174. reflex/components/radix/themes/components/text_area.pyi +95 -109
  175. reflex/components/radix/themes/components/text_field.py +1 -80
  176. reflex/components/radix/themes/components/text_field.pyi +245 -290
  177. reflex/components/radix/themes/components/tooltip.py +1 -1
  178. reflex/components/radix/themes/components/tooltip.pyi +25 -35
  179. reflex/components/radix/themes/layout/__init__.pyi +1 -0
  180. reflex/components/radix/themes/layout/base.py +1 -1
  181. reflex/components/radix/themes/layout/base.pyi +55 -65
  182. reflex/components/radix/themes/layout/box.pyi +33 -43
  183. reflex/components/radix/themes/layout/center.pyi +55 -65
  184. reflex/components/radix/themes/layout/container.py +2 -4
  185. reflex/components/radix/themes/layout/container.pyi +35 -45
  186. reflex/components/radix/themes/layout/flex.py +1 -1
  187. reflex/components/radix/themes/layout/flex.pyi +55 -65
  188. reflex/components/radix/themes/layout/grid.py +1 -1
  189. reflex/components/radix/themes/layout/grid.pyi +63 -73
  190. reflex/components/radix/themes/layout/list.py +1 -1
  191. reflex/components/radix/themes/layout/list.pyi +188 -238
  192. reflex/components/radix/themes/layout/section.py +2 -4
  193. reflex/components/radix/themes/layout/section.pyi +35 -45
  194. reflex/components/radix/themes/layout/spacer.pyi +55 -65
  195. reflex/components/radix/themes/layout/stack.py +1 -1
  196. reflex/components/radix/themes/layout/stack.pyi +125 -155
  197. reflex/components/radix/themes/typography/blockquote.py +1 -1
  198. reflex/components/radix/themes/typography/blockquote.pyi +88 -98
  199. reflex/components/radix/themes/typography/code.py +1 -1
  200. reflex/components/radix/themes/typography/code.pyi +89 -99
  201. reflex/components/radix/themes/typography/heading.py +1 -1
  202. reflex/components/radix/themes/typography/heading.pyi +95 -105
  203. reflex/components/radix/themes/typography/link.py +1 -1
  204. reflex/components/radix/themes/typography/link.pyi +101 -111
  205. reflex/components/radix/themes/typography/text.py +1 -1
  206. reflex/components/radix/themes/typography/text.pyi +494 -564
  207. reflex/components/react_player/audio.pyi +32 -58
  208. reflex/components/react_player/react_player.py +1 -1
  209. reflex/components/react_player/react_player.pyi +32 -58
  210. reflex/components/react_player/video.pyi +32 -58
  211. reflex/components/recharts/cartesian.py +22 -19
  212. reflex/components/recharts/cartesian.pyi +658 -840
  213. reflex/components/recharts/charts.py +3 -3
  214. reflex/components/recharts/charts.pyi +238 -342
  215. reflex/components/recharts/general.py +8 -8
  216. reflex/components/recharts/general.pyi +175 -225
  217. reflex/components/recharts/polar.py +11 -11
  218. reflex/components/recharts/polar.pyi +135 -171
  219. reflex/components/recharts/recharts.pyi +31 -51
  220. reflex/components/sonner/toast.py +27 -31
  221. reflex/components/sonner/toast.pyi +36 -45
  222. reflex/components/suneditor/editor.py +1 -1
  223. reflex/components/suneditor/editor.pyi +54 -76
  224. reflex/components/tags/cond_tag.py +6 -4
  225. reflex/components/tags/iter_tag.py +37 -25
  226. reflex/components/tags/match_tag.py +6 -4
  227. reflex/components/tags/tag.py +43 -28
  228. reflex/constants/base.py +3 -1
  229. reflex/constants/event.py +1 -0
  230. reflex/custom_components/custom_components.py +3 -1
  231. reflex/event.py +166 -108
  232. reflex/experimental/__init__.py +25 -6
  233. reflex/experimental/client_state.py +34 -57
  234. reflex/experimental/hooks.py +12 -17
  235. reflex/experimental/layout.py +4 -4
  236. reflex/experimental/layout.pyi +130 -180
  237. reflex/middleware/hydrate_middleware.py +2 -0
  238. reflex/middleware/middleware.py +3 -3
  239. reflex/model.py +22 -0
  240. reflex/reflex.py +4 -0
  241. reflex/state.py +491 -110
  242. reflex/style.py +56 -39
  243. reflex/testing.py +8 -3
  244. reflex/utils/exceptions.py +32 -0
  245. reflex/utils/exec.py +0 -14
  246. reflex/utils/format.py +80 -209
  247. reflex/utils/imports.py +16 -73
  248. reflex/utils/net.py +43 -0
  249. reflex/utils/path_ops.py +13 -1
  250. reflex/utils/prerequisites.py +81 -41
  251. reflex/utils/pyi_generator.py +12 -6
  252. reflex/utils/serializers.py +13 -41
  253. reflex/utils/telemetry.py +3 -2
  254. reflex/utils/types.py +47 -7
  255. reflex/{experimental/vars → vars}/__init__.py +6 -3
  256. reflex/vars/base.py +2563 -0
  257. reflex/vars/function.py +196 -0
  258. reflex/vars/number.py +1158 -0
  259. reflex/vars/object.py +562 -0
  260. reflex/vars/sequence.py +1604 -0
  261. {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/METADATA +6 -9
  262. reflex-0.6.0.dist-info/RECORD +382 -0
  263. reflex/.templates/apps/demo/.gitignore +0 -4
  264. reflex/.templates/apps/demo/assets/favicon.ico +0 -0
  265. reflex/.templates/apps/demo/assets/github.svg +0 -10
  266. reflex/.templates/apps/demo/assets/icon.svg +0 -37
  267. reflex/.templates/apps/demo/assets/logo.svg +0 -68
  268. reflex/.templates/apps/demo/assets/paneleft.svg +0 -13
  269. reflex/.templates/apps/demo/code/__init__.py +0 -1
  270. reflex/.templates/apps/demo/code/demo.py +0 -127
  271. reflex/.templates/apps/demo/code/pages/__init__.py +0 -7
  272. reflex/.templates/apps/demo/code/pages/chatapp.py +0 -31
  273. reflex/.templates/apps/demo/code/pages/datatable.py +0 -360
  274. reflex/.templates/apps/demo/code/pages/forms.py +0 -257
  275. reflex/.templates/apps/demo/code/pages/graphing.py +0 -253
  276. reflex/.templates/apps/demo/code/pages/home.py +0 -56
  277. reflex/.templates/apps/demo/code/sidebar.py +0 -178
  278. reflex/.templates/apps/demo/code/state.py +0 -22
  279. reflex/.templates/apps/demo/code/states/form_state.py +0 -40
  280. reflex/.templates/apps/demo/code/states/pie_state.py +0 -47
  281. reflex/.templates/apps/demo/code/styles.py +0 -68
  282. reflex/.templates/apps/demo/code/webui/__init__.py +0 -0
  283. reflex/.templates/apps/demo/code/webui/components/__init__.py +0 -4
  284. reflex/.templates/apps/demo/code/webui/components/chat.py +0 -118
  285. reflex/.templates/apps/demo/code/webui/components/loading_icon.py +0 -19
  286. reflex/.templates/apps/demo/code/webui/components/modal.py +0 -56
  287. reflex/.templates/apps/demo/code/webui/components/navbar.py +0 -70
  288. reflex/.templates/apps/demo/code/webui/components/sidebar.py +0 -66
  289. reflex/.templates/apps/demo/code/webui/state.py +0 -146
  290. reflex/.templates/apps/demo/code/webui/styles.py +0 -88
  291. reflex/.templates/web/components/reflex/chakra_color_mode_provider.js +0 -36
  292. reflex/experimental/vars/base.py +0 -583
  293. reflex/experimental/vars/function.py +0 -290
  294. reflex/experimental/vars/number.py +0 -1458
  295. reflex/experimental/vars/object.py +0 -804
  296. reflex/experimental/vars/sequence.py +0 -1764
  297. reflex/utils/watch.py +0 -96
  298. reflex/vars.py +0 -2604
  299. reflex/vars.pyi +0 -218
  300. reflex-0.5.10a3.dist-info/RECORD +0 -413
  301. {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/LICENSE +0 -0
  302. {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/WHEEL +0 -0
  303. {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/entry_points.txt +0 -0
reflex/state.py CHANGED
@@ -5,12 +5,14 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import contextlib
7
7
  import copy
8
+ import dataclasses
8
9
  import functools
9
10
  import inspect
10
11
  import os
11
12
  import uuid
12
13
  from abc import ABC, abstractmethod
13
14
  from collections import defaultdict
15
+ from pathlib import Path
14
16
  from types import FunctionType, MethodType
15
17
  from typing import (
16
18
  TYPE_CHECKING,
@@ -23,6 +25,7 @@ from typing import (
23
25
  Optional,
24
26
  Sequence,
25
27
  Set,
28
+ Tuple,
26
29
  Type,
27
30
  Union,
28
31
  cast,
@@ -32,6 +35,13 @@ import dill
32
35
  from sqlalchemy.orm import DeclarativeBase
33
36
 
34
37
  from reflex.config import get_config
38
+ from reflex.vars.base import (
39
+ ComputedVar,
40
+ DynamicRouteVar,
41
+ Var,
42
+ computed_var,
43
+ is_computed_var,
44
+ )
35
45
 
36
46
  try:
37
47
  import pydantic.v1 as pydantic
@@ -51,12 +61,19 @@ from reflex.event import (
51
61
  EventSpec,
52
62
  fix_events,
53
63
  )
54
- from reflex.utils import console, format, prerequisites, types
55
- from reflex.utils.exceptions import ImmutableStateError, LockExpiredError
64
+ from reflex.utils import console, format, path_ops, prerequisites, types
65
+ from reflex.utils.exceptions import (
66
+ ComputedVarShadowsBaseVars,
67
+ ComputedVarShadowsStateVar,
68
+ DynamicRouteArgShadowsStateVar,
69
+ EventHandlerShadowsBuiltInStateMethod,
70
+ ImmutableStateError,
71
+ LockExpiredError,
72
+ )
56
73
  from reflex.utils.exec import is_testing_env
57
74
  from reflex.utils.serializers import SerializedType, serialize, serializer
58
75
  from reflex.utils.types import override
59
- from reflex.vars import BaseVar, ComputedVar, Var, computed_var
76
+ from reflex.vars import VarData
60
77
 
61
78
  if TYPE_CHECKING:
62
79
  from reflex.components.component import Component
@@ -70,13 +87,15 @@ var = computed_var
70
87
  TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb
71
88
 
72
89
 
73
- class HeaderData(Base):
90
+ @dataclasses.dataclass(frozen=True)
91
+ class HeaderData:
74
92
  """An object containing headers data."""
75
93
 
76
94
  host: str = ""
77
95
  origin: str = ""
78
96
  upgrade: str = ""
79
97
  connection: str = ""
98
+ cookie: str = ""
80
99
  pragma: str = ""
81
100
  cache_control: str = ""
82
101
  user_agent: str = ""
@@ -92,13 +111,16 @@ class HeaderData(Base):
92
111
  Args:
93
112
  router_data: the router_data dict.
94
113
  """
95
- super().__init__()
96
114
  if router_data:
97
115
  for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items():
98
- setattr(self, format.to_snake_case(k), v)
116
+ object.__setattr__(self, format.to_snake_case(k), v)
117
+ else:
118
+ for k in dataclasses.fields(self):
119
+ object.__setattr__(self, k.name, "")
99
120
 
100
121
 
101
- class PageData(Base):
122
+ @dataclasses.dataclass(frozen=True)
123
+ class PageData:
102
124
  """An object containing page data."""
103
125
 
104
126
  host: str = "" # repeated with self.headers.origin (remove or keep the duplicate?)
@@ -106,7 +128,7 @@ class PageData(Base):
106
128
  raw_path: str = ""
107
129
  full_path: str = ""
108
130
  full_raw_path: str = ""
109
- params: dict = {}
131
+ params: dict = dataclasses.field(default_factory=dict)
110
132
 
111
133
  def __init__(self, router_data: Optional[dict] = None):
112
134
  """Initalize the PageData object based on router_data.
@@ -114,17 +136,34 @@ class PageData(Base):
114
136
  Args:
115
137
  router_data: the router_data dict.
116
138
  """
117
- super().__init__()
118
139
  if router_data:
119
- self.host = router_data.get(constants.RouteVar.HEADERS, {}).get("origin")
120
- self.path = router_data.get(constants.RouteVar.PATH, "")
121
- self.raw_path = router_data.get(constants.RouteVar.ORIGIN, "")
122
- self.full_path = f"{self.host}{self.path}"
123
- self.full_raw_path = f"{self.host}{self.raw_path}"
124
- self.params = router_data.get(constants.RouteVar.QUERY, {})
140
+ object.__setattr__(
141
+ self,
142
+ "host",
143
+ router_data.get(constants.RouteVar.HEADERS, {}).get("origin", ""),
144
+ )
145
+ object.__setattr__(
146
+ self, "path", router_data.get(constants.RouteVar.PATH, "")
147
+ )
148
+ object.__setattr__(
149
+ self, "raw_path", router_data.get(constants.RouteVar.ORIGIN, "")
150
+ )
151
+ object.__setattr__(self, "full_path", f"{self.host}{self.path}")
152
+ object.__setattr__(self, "full_raw_path", f"{self.host}{self.raw_path}")
153
+ object.__setattr__(
154
+ self, "params", router_data.get(constants.RouteVar.QUERY, {})
155
+ )
156
+ else:
157
+ object.__setattr__(self, "host", "")
158
+ object.__setattr__(self, "path", "")
159
+ object.__setattr__(self, "raw_path", "")
160
+ object.__setattr__(self, "full_path", "")
161
+ object.__setattr__(self, "full_raw_path", "")
162
+ object.__setattr__(self, "params", {})
125
163
 
126
164
 
127
- class SessionData(Base):
165
+ @dataclasses.dataclass(frozen=True, init=False)
166
+ class SessionData:
128
167
  """An object containing session data."""
129
168
 
130
169
  client_token: str = ""
@@ -137,19 +176,24 @@ class SessionData(Base):
137
176
  Args:
138
177
  router_data: the router_data dict.
139
178
  """
140
- super().__init__()
141
179
  if router_data:
142
- self.client_token = router_data.get(constants.RouteVar.CLIENT_TOKEN, "")
143
- self.client_ip = router_data.get(constants.RouteVar.CLIENT_IP, "")
144
- self.session_id = router_data.get(constants.RouteVar.SESSION_ID, "")
180
+ client_token = router_data.get(constants.RouteVar.CLIENT_TOKEN, "")
181
+ client_ip = router_data.get(constants.RouteVar.CLIENT_IP, "")
182
+ session_id = router_data.get(constants.RouteVar.SESSION_ID, "")
183
+ else:
184
+ client_token = client_ip = session_id = ""
185
+ object.__setattr__(self, "client_token", client_token)
186
+ object.__setattr__(self, "client_ip", client_ip)
187
+ object.__setattr__(self, "session_id", session_id)
145
188
 
146
189
 
147
- class RouterData(Base):
190
+ @dataclasses.dataclass(frozen=True, init=False)
191
+ class RouterData:
148
192
  """An object containing RouterData."""
149
193
 
150
- session: SessionData = SessionData()
151
- headers: HeaderData = HeaderData()
152
- page: PageData = PageData()
194
+ session: SessionData = dataclasses.field(default_factory=SessionData)
195
+ headers: HeaderData = dataclasses.field(default_factory=HeaderData)
196
+ page: PageData = dataclasses.field(default_factory=PageData)
153
197
 
154
198
  def __init__(self, router_data: Optional[dict] = None):
155
199
  """Initialize the RouterData object.
@@ -157,10 +201,9 @@ class RouterData(Base):
157
201
  Args:
158
202
  router_data: the router_data dict.
159
203
  """
160
- super().__init__()
161
- self.session = SessionData(router_data)
162
- self.headers = HeaderData(router_data)
163
- self.page = PageData(router_data)
204
+ object.__setattr__(self, "session", SessionData(router_data))
205
+ object.__setattr__(self, "headers", HeaderData(router_data))
206
+ object.__setattr__(self, "page", PageData(router_data))
164
207
 
165
208
 
166
209
  def _no_chain_background_task(
@@ -236,10 +279,11 @@ def _split_substate_key(substate_key: str) -> tuple[str, str]:
236
279
  return token, state_name
237
280
 
238
281
 
282
+ @dataclasses.dataclass(frozen=True, init=False)
239
283
  class EventHandlerSetVar(EventHandler):
240
284
  """A special event handler to wrap setvar functionality."""
241
285
 
242
- state_cls: Type[BaseState]
286
+ state_cls: Type[BaseState] = dataclasses.field(init=False)
243
287
 
244
288
  def __init__(self, state_cls: Type[BaseState]):
245
289
  """Initialize the EventHandlerSetVar.
@@ -250,8 +294,8 @@ class EventHandlerSetVar(EventHandler):
250
294
  super().__init__(
251
295
  fn=type(self).setvar,
252
296
  state_full_name=state_cls.get_full_name(),
253
- state_cls=state_cls, # type: ignore
254
297
  )
298
+ object.__setattr__(self, "state_cls", state_cls)
255
299
 
256
300
  def setvar(self, var_name: str, value: Any):
257
301
  """Set the state variable to the value of the event.
@@ -299,7 +343,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
299
343
  vars: ClassVar[Dict[str, Var]] = {}
300
344
 
301
345
  # The base vars of the class.
302
- base_vars: ClassVar[Dict[str, BaseVar]] = {}
346
+ base_vars: ClassVar[Dict[str, Var]] = {}
303
347
 
304
348
  # The computed vars of the class.
305
349
  computed_vars: ClassVar[Dict[str, ComputedVar]] = {}
@@ -424,8 +468,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
424
468
  return [
425
469
  v
426
470
  for mixin in cls._mixins() + [cls]
427
- for v in mixin.__dict__.values()
428
- if isinstance(v, ComputedVar)
471
+ for name, v in mixin.__dict__.items()
472
+ if is_computed_var(v) and name not in cls.inherited_vars
429
473
  ]
430
474
 
431
475
  @classmethod
@@ -469,7 +513,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
469
513
  cls._check_overridden_methods()
470
514
 
471
515
  # Computed vars should not shadow builtin state props.
472
- cls._check_overriden_basevars()
516
+ cls._check_overridden_basevars()
473
517
 
474
518
  # Reset subclass tracking for this class.
475
519
  cls.class_subclasses = set()
@@ -487,26 +531,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
487
531
  if cls.get_name() in set(
488
532
  c.get_name() for c in parent_state.class_subclasses
489
533
  ):
490
- if is_testing_env():
491
- # Clear existing subclass with same name when app is reloaded via
492
- # utils.prerequisites.get_app(reload=True)
493
- parent_state.class_subclasses = set(
494
- c
495
- for c in parent_state.class_subclasses
496
- if c.get_name() != cls.get_name()
497
- )
498
- else:
499
- # During normal operation, subclasses cannot have the same name, even if they are
500
- # defined in different modules.
501
- raise StateValueError(
502
- f"The substate class '{cls.get_name()}' has been defined multiple times. "
503
- "Shadowing substate classes is not allowed."
504
- )
534
+ # This should not happen, since we have added module prefix to state names in #3214
535
+ raise StateValueError(
536
+ f"The substate class '{cls.get_name()}' has been defined multiple times. "
537
+ "Shadowing substate classes is not allowed."
538
+ )
505
539
  # Track this new subclass in the parent state's subclasses set.
506
540
  parent_state.class_subclasses.add(cls)
507
541
 
508
542
  # Get computed vars.
509
543
  computed_vars = cls._get_computed_vars()
544
+ cls._check_overridden_computed_vars()
510
545
 
511
546
  new_backend_vars = {
512
547
  name: value
@@ -521,13 +556,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
521
556
 
522
557
  # Set the base and computed vars.
523
558
  cls.base_vars = {
524
- f.name: BaseVar(_var_name=f.name, _var_type=f.outer_type_)._var_set_state(
525
- cls
526
- )
559
+ f.name: Var(
560
+ _js_expr=format.format_state_name(cls.get_full_name()) + "." + f.name,
561
+ _var_type=f.outer_type_,
562
+ _var_data=VarData.from_state(cls),
563
+ ).guess_type()
527
564
  for f in cls.get_fields().values()
528
565
  if f.name not in cls.get_skip_vars()
529
566
  }
530
- cls.computed_vars = {v._var_name: v._var_set_state(cls) for v in computed_vars}
567
+ cls.computed_vars = {
568
+ v._js_expr: v._replace(merge_var_data=VarData.from_state(cls))
569
+ for v in computed_vars
570
+ }
531
571
  cls.vars = {
532
572
  **cls.inherited_vars,
533
573
  **cls.base_vars,
@@ -548,15 +588,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
548
588
 
549
589
  for mixin in cls._mixins():
550
590
  for name, value in mixin.__dict__.items():
551
- if isinstance(value, ComputedVar):
591
+ if name in cls.inherited_vars:
592
+ continue
593
+ if is_computed_var(value):
552
594
  fget = cls._copy_fn(value.fget)
553
- newcv = value._replace(fget=fget)
595
+ newcv = value._replace(fget=fget, _var_data=VarData.from_state(cls))
554
596
  # cleanup refs to mixin cls in var_data
555
- newcv._var_data = None
556
- newcv._var_set_state(cls)
557
597
  setattr(cls, name, newcv)
558
- cls.computed_vars[newcv._var_name] = newcv
559
- cls.vars[newcv._var_name] = newcv
598
+ cls.computed_vars[newcv._js_expr] = newcv
599
+ cls.vars[newcv._js_expr] = newcv
560
600
  continue
561
601
  if types.is_backend_base_variable(name, mixin):
562
602
  cls.backend_vars[name] = copy.deepcopy(value)
@@ -579,6 +619,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
579
619
  cls.event_handlers[name] = handler
580
620
  setattr(cls, name, handler)
581
621
 
622
+ # Initialize per-class var dependency tracking.
623
+ cls._computed_var_dependencies = defaultdict(set)
624
+ cls._substate_var_dependencies = defaultdict(set)
582
625
  cls._init_var_dependency_dicts()
583
626
 
584
627
  @staticmethod
@@ -648,10 +691,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
648
691
  Additional updates tracking dicts for vars and substates that always
649
692
  need to be recomputed.
650
693
  """
651
- # Initialize per-class var dependency tracking.
652
- cls._computed_var_dependencies = defaultdict(set)
653
- cls._substate_var_dependencies = defaultdict(set)
654
-
655
694
  inherited_vars = set(cls.inherited_vars).union(
656
695
  set(cls.inherited_backend_vars),
657
696
  )
@@ -697,7 +736,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
697
736
  """Check for shadow methods and raise error if any.
698
737
 
699
738
  Raises:
700
- NameError: When an event handler shadows an inbuilt state method.
739
+ EventHandlerShadowsBuiltInStateMethod: When an event handler shadows an inbuilt state method.
701
740
  """
702
741
  overridden_methods = set()
703
742
  state_base_functions = cls._get_base_functions()
@@ -711,21 +750,37 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
711
750
  overridden_methods.add(method.__name__)
712
751
 
713
752
  for method_name in overridden_methods:
714
- raise NameError(
753
+ raise EventHandlerShadowsBuiltInStateMethod(
715
754
  f"The event handler name `{method_name}` shadows a builtin State method; use a different name instead"
716
755
  )
717
756
 
718
757
  @classmethod
719
- def _check_overriden_basevars(cls):
758
+ def _check_overridden_basevars(cls):
720
759
  """Check for shadow base vars and raise error if any.
721
760
 
722
761
  Raises:
723
- NameError: When a computed var shadows a base var.
762
+ ComputedVarShadowsBaseVars: When a computed var shadows a base var.
724
763
  """
725
764
  for computed_var_ in cls._get_computed_vars():
726
- if computed_var_._var_name in cls.__annotations__:
727
- raise NameError(
728
- f"The computed var name `{computed_var_._var_name}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
765
+ if computed_var_._js_expr in cls.__annotations__:
766
+ raise ComputedVarShadowsBaseVars(
767
+ f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
768
+ )
769
+
770
+ @classmethod
771
+ def _check_overridden_computed_vars(cls) -> None:
772
+ """Check for shadow computed vars and raise error if any.
773
+
774
+ Raises:
775
+ ComputedVarShadowsStateVar: When a computed var shadows another.
776
+ """
777
+ for name, cv in cls.__dict__.items():
778
+ if not is_computed_var(cv):
779
+ continue
780
+ name = cv._js_expr
781
+ if name in cls.inherited_vars or name in cls.inherited_backend_vars:
782
+ raise ComputedVarShadowsStateVar(
783
+ f"The computed var name `{cv._js_expr}` shadows a var in {cls.__module__}.{cls.__name__}; use a different name instead"
729
784
  )
730
785
 
731
786
  @classmethod
@@ -847,7 +902,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
847
902
  return getattr(substate, name)
848
903
 
849
904
  @classmethod
850
- def _init_var(cls, prop: BaseVar):
905
+ def _init_var(cls, prop: Var):
851
906
  """Initialize a variable.
852
907
 
853
908
  Args:
@@ -863,7 +918,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
863
918
  "State vars must be primitive Python types, "
864
919
  "Plotly figures, Pandas dataframes, "
865
920
  "or subclasses of rx.Base. "
866
- f'Found var "{prop._var_name}" with type {prop._var_type}.'
921
+ f'Found var "{prop._js_expr}" with type {prop._var_type}.'
867
922
  )
868
923
  cls._set_var(prop)
869
924
  cls._create_setter(prop)
@@ -890,8 +945,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
890
945
  )
891
946
 
892
947
  # create the variable based on name and type
893
- var = BaseVar(_var_name=name, _var_type=type_)
894
- var._var_set_state(cls)
948
+ var = Var(
949
+ _js_expr=format.format_state_name(cls.get_full_name()) + "." + name,
950
+ _var_type=type_,
951
+ _var_data=VarData.from_state(cls),
952
+ ).guess_type()
895
953
 
896
954
  # add the pydantic field dynamically (must be done before _init_var)
897
955
  cls.add_field(var, default_value)
@@ -910,13 +968,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
910
968
  cls._init_var_dependency_dicts()
911
969
 
912
970
  @classmethod
913
- def _set_var(cls, prop: BaseVar):
971
+ def _set_var(cls, prop: Var):
914
972
  """Set the var as a class member.
915
973
 
916
974
  Args:
917
975
  prop: The var instance to set.
918
976
  """
919
- setattr(cls, prop._var_name, prop)
977
+ acutal_var_name = (
978
+ prop._js_expr if "." not in prop._js_expr else prop._js_expr.split(".")[-1]
979
+ )
980
+ setattr(cls, acutal_var_name, prop)
920
981
 
921
982
  @classmethod
922
983
  def _create_event_handler(cls, fn):
@@ -936,7 +997,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
936
997
  cls.setvar = cls.event_handlers["setvar"] = EventHandlerSetVar(state_cls=cls)
937
998
 
938
999
  @classmethod
939
- def _create_setter(cls, prop: BaseVar):
1000
+ def _create_setter(cls, prop: Var):
940
1001
  """Create a setter for the var.
941
1002
 
942
1003
  Args:
@@ -949,14 +1010,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
949
1010
  setattr(cls, setter_name, event_handler)
950
1011
 
951
1012
  @classmethod
952
- def _set_default_value(cls, prop: BaseVar):
1013
+ def _set_default_value(cls, prop: Var):
953
1014
  """Set the default value for the var.
954
1015
 
955
1016
  Args:
956
1017
  prop: The var to set the default value for.
957
1018
  """
958
1019
  # Get the pydantic field for the var.
959
- field = cls.get_fields()[prop._var_name]
1020
+ if "." in prop._js_expr:
1021
+ field = cls.get_fields()[prop._js_expr.split(".")[-1]]
1022
+ else:
1023
+ field = cls.get_fields()[prop._js_expr]
960
1024
  if field.required:
961
1025
  default_value = prop.get_default_value()
962
1026
  if default_value is not None:
@@ -968,7 +1032,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
968
1032
  and not types.is_optional(prop._var_type)
969
1033
  ):
970
1034
  # Ensure frontend uses null coalescing when accessing.
971
- prop._var_type = Optional[prop._var_type]
1035
+ object.__setattr__(prop, "_var_type", Optional[prop._var_type])
972
1036
 
973
1037
  @staticmethod
974
1038
  def _get_base_functions() -> dict[str, FunctionType]:
@@ -983,6 +1047,27 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
983
1047
  if not func[0].startswith("__")
984
1048
  }
985
1049
 
1050
+ @classmethod
1051
+ def _update_substate_inherited_vars(cls, vars_to_add: dict[str, Var]):
1052
+ """Update the inherited vars of substates recursively when new vars are added.
1053
+
1054
+ Also updates the var dependency tracking dicts after adding vars.
1055
+
1056
+ Args:
1057
+ vars_to_add: names to Var instances to add to substates
1058
+ """
1059
+ for substate_class in cls.class_subclasses:
1060
+ for name, var in vars_to_add.items():
1061
+ if types.is_backend_base_variable(name, cls):
1062
+ substate_class.backend_vars.setdefault(name, var)
1063
+ substate_class.inherited_backend_vars.setdefault(name, var)
1064
+ else:
1065
+ substate_class.vars.setdefault(name, var)
1066
+ substate_class.inherited_vars.setdefault(name, var)
1067
+ substate_class._update_substate_inherited_vars(vars_to_add)
1068
+ # Reinitialize dependency tracking dicts.
1069
+ cls._init_var_dependency_dicts()
1070
+
986
1071
  @classmethod
987
1072
  def setup_dynamic_args(cls, args: dict[str, str]):
988
1073
  """Set up args for easy access in renderer.
@@ -990,21 +1075,24 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
990
1075
  Args:
991
1076
  args: a dict of args
992
1077
  """
1078
+ if not args:
1079
+ return
1080
+
1081
+ cls._check_overwritten_dynamic_args(list(args.keys()))
993
1082
 
994
1083
  def argsingle_factory(param):
995
- @ComputedVar
996
1084
  def inner_func(self) -> str:
997
1085
  return self.router.page.params.get(param, "")
998
1086
 
999
1087
  return inner_func
1000
1088
 
1001
1089
  def arglist_factory(param):
1002
- @ComputedVar
1003
- def inner_func(self) -> List:
1090
+ def inner_func(self) -> List[str]:
1004
1091
  return self.router.page.params.get(param, [])
1005
1092
 
1006
1093
  return inner_func
1007
1094
 
1095
+ dynamic_vars = {}
1008
1096
  for param, value in args.items():
1009
1097
  if value == constants.RouteArgType.SINGLE:
1010
1098
  func = argsingle_factory(param)
@@ -1012,13 +1100,39 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1012
1100
  func = arglist_factory(param)
1013
1101
  else:
1014
1102
  continue
1015
- # to allow passing as a prop
1016
- func._var_name = param
1017
- cls.vars[param] = cls.computed_vars[param] = func._var_set_state(cls) # type: ignore
1018
- setattr(cls, param, func)
1103
+ dynamic_vars[param] = DynamicRouteVar(
1104
+ fget=func,
1105
+ cache=True,
1106
+ _js_expr=param,
1107
+ _var_data=VarData.from_state(cls),
1108
+ )
1109
+ setattr(cls, param, dynamic_vars[param])
1110
+
1111
+ # Update tracking dicts.
1112
+ cls.computed_vars.update(dynamic_vars)
1113
+ cls.vars.update(dynamic_vars)
1114
+ cls._update_substate_inherited_vars(dynamic_vars)
1019
1115
 
1020
- # Reinitialize dependency tracking dicts.
1021
- cls._init_var_dependency_dicts()
1116
+ @classmethod
1117
+ def _check_overwritten_dynamic_args(cls, args: list[str]):
1118
+ """Check if dynamic args are shadowing existing vars. Recursively checks all child states.
1119
+
1120
+ Args:
1121
+ args: a dict of args
1122
+
1123
+ Raises:
1124
+ DynamicRouteArgShadowsStateVar: If a dynamic arg is shadowing an existing var.
1125
+ """
1126
+ for arg in args:
1127
+ if (
1128
+ arg in cls.computed_vars
1129
+ and not isinstance(cls.computed_vars[arg], DynamicRouteVar)
1130
+ ) or arg in cls.base_vars:
1131
+ raise DynamicRouteArgShadowsStateVar(
1132
+ f"Dynamic route arg '{arg}' is shadowing an existing var in {cls.__module__}.{cls.__name__}"
1133
+ )
1134
+ for substate in cls.get_substates():
1135
+ substate._check_overwritten_dynamic_args(args)
1022
1136
 
1023
1137
  def __getattribute__(self, name: str) -> Any:
1024
1138
  """Get the state var.
@@ -1765,16 +1879,21 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1765
1879
  self.dirty_vars.update(self._always_dirty_computed_vars)
1766
1880
  self._mark_dirty()
1767
1881
 
1882
+ def dictify(value: Any):
1883
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
1884
+ return dataclasses.asdict(value)
1885
+ return value
1886
+
1768
1887
  base_vars = {
1769
- prop_name: self.get_value(getattr(self, prop_name))
1888
+ prop_name: dictify(self.get_value(getattr(self, prop_name)))
1770
1889
  for prop_name in self.base_vars
1771
1890
  }
1772
- if initial:
1891
+ if initial and include_computed:
1773
1892
  computed_vars = {
1774
1893
  # Include initial computed vars.
1775
1894
  prop_name: (
1776
1895
  cv._initial_value
1777
- if isinstance(cv, ComputedVar)
1896
+ if is_computed_var(cv)
1778
1897
  and not isinstance(cv._initial_value, types.Unset)
1779
1898
  else self.get_value(getattr(self, prop_name))
1780
1899
  )
@@ -1846,9 +1965,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1846
1965
  return state
1847
1966
 
1848
1967
 
1849
- EventHandlerSetVar.update_forward_refs()
1850
-
1851
-
1852
1968
  class State(BaseState):
1853
1969
  """The app Base State."""
1854
1970
 
@@ -2272,25 +2388,37 @@ class StateProxy(wrapt.ObjectProxy):
2272
2388
  Returns:
2273
2389
  The state update.
2274
2390
  """
2391
+ original_mutable = self._self_mutable
2275
2392
  self._self_mutable = True
2276
2393
  try:
2277
2394
  return self.__wrapped__._as_state_update(*args, **kwargs)
2278
2395
  finally:
2279
- self._self_mutable = False
2396
+ self._self_mutable = original_mutable
2280
2397
 
2281
2398
 
2282
- class StateUpdate(Base):
2399
+ @dataclasses.dataclass(
2400
+ frozen=True,
2401
+ )
2402
+ class StateUpdate:
2283
2403
  """A state update sent to the frontend."""
2284
2404
 
2285
2405
  # The state delta.
2286
- delta: Delta = {}
2406
+ delta: Delta = dataclasses.field(default_factory=dict)
2287
2407
 
2288
2408
  # Events to be added to the event queue.
2289
- events: List[Event] = []
2409
+ events: List[Event] = dataclasses.field(default_factory=list)
2290
2410
 
2291
2411
  # Whether this is the final state update for the event.
2292
2412
  final: bool = True
2293
2413
 
2414
+ def json(self) -> str:
2415
+ """Convert the state update to a JSON string.
2416
+
2417
+ Returns:
2418
+ The state update as a JSON string.
2419
+ """
2420
+ return format.json_dumps(dataclasses.asdict(self))
2421
+
2294
2422
 
2295
2423
  class StateManager(Base, ABC):
2296
2424
  """A class to manage many client states."""
@@ -2318,7 +2446,7 @@ class StateManager(Base, ABC):
2318
2446
  token_expiration=config.redis_token_expiration,
2319
2447
  lock_expiration=config.redis_lock_expiration,
2320
2448
  )
2321
- return StateManagerMemory(state=state)
2449
+ return StateManagerDisk(state=state)
2322
2450
 
2323
2451
  @abstractmethod
2324
2452
  async def get_state(self, token: str) -> BaseState:
@@ -2425,6 +2553,266 @@ class StateManagerMemory(StateManager):
2425
2553
  await self.set_state(token, state)
2426
2554
 
2427
2555
 
2556
+ def _default_token_expiration() -> int:
2557
+ """Get the default token expiration time.
2558
+
2559
+ Returns:
2560
+ The default token expiration time.
2561
+ """
2562
+ return get_config().redis_token_expiration
2563
+
2564
+
2565
+ def _serialize_type(type_: Any) -> str:
2566
+ """Serialize a type.
2567
+
2568
+ Args:
2569
+ type_: The type to serialize.
2570
+
2571
+ Returns:
2572
+ The serialized type.
2573
+ """
2574
+ if not inspect.isclass(type_):
2575
+ return f"{type_}"
2576
+ return f"{type_.__module__}.{type_.__qualname__}"
2577
+
2578
+
2579
+ def state_to_schema(
2580
+ state: BaseState,
2581
+ ) -> List[
2582
+ Tuple[
2583
+ str,
2584
+ str,
2585
+ Any,
2586
+ Union[bool, None],
2587
+ ]
2588
+ ]:
2589
+ """Convert a state to a schema.
2590
+
2591
+ Args:
2592
+ state: The state to convert to a schema.
2593
+
2594
+ Returns:
2595
+ The schema.
2596
+ """
2597
+ return list(
2598
+ sorted(
2599
+ (
2600
+ field_name,
2601
+ model_field.name,
2602
+ _serialize_type(model_field.type_),
2603
+ (
2604
+ model_field.required
2605
+ if isinstance(model_field.required, bool)
2606
+ else None
2607
+ ),
2608
+ )
2609
+ for field_name, model_field in state.__fields__.items()
2610
+ )
2611
+ )
2612
+
2613
+
2614
+ def reset_disk_state_manager():
2615
+ """Reset the disk state manager."""
2616
+ states_directory = prerequisites.get_web_dir() / constants.Dirs.STATES
2617
+ if states_directory.exists():
2618
+ for path in states_directory.iterdir():
2619
+ path.unlink()
2620
+
2621
+
2622
+ class StateManagerDisk(StateManager):
2623
+ """A state manager that stores states in memory."""
2624
+
2625
+ # The mapping of client ids to states.
2626
+ states: Dict[str, BaseState] = {}
2627
+
2628
+ # The mutex ensures the dict of mutexes is updated exclusively
2629
+ _state_manager_lock = asyncio.Lock()
2630
+
2631
+ # The dict of mutexes for each client
2632
+ _states_locks: Dict[str, asyncio.Lock] = pydantic.PrivateAttr({})
2633
+
2634
+ # The token expiration time (s).
2635
+ token_expiration: int = pydantic.Field(default_factory=_default_token_expiration)
2636
+
2637
+ class Config:
2638
+ """The Pydantic config."""
2639
+
2640
+ fields = {
2641
+ "_states_locks": {"exclude": True},
2642
+ }
2643
+ keep_untouched = (functools.cached_property,)
2644
+
2645
+ def __init__(self, state: Type[BaseState]):
2646
+ """Create a new state manager.
2647
+
2648
+ Args:
2649
+ state: The state class to use.
2650
+ """
2651
+ super().__init__(state=state)
2652
+
2653
+ path_ops.mkdir(self.states_directory)
2654
+
2655
+ self._purge_expired_states()
2656
+
2657
+ @functools.cached_property
2658
+ def states_directory(self) -> Path:
2659
+ """Get the states directory.
2660
+
2661
+ Returns:
2662
+ The states directory.
2663
+ """
2664
+ return prerequisites.get_web_dir() / constants.Dirs.STATES
2665
+
2666
+ def _purge_expired_states(self):
2667
+ """Purge expired states from the disk."""
2668
+ import time
2669
+
2670
+ for path in path_ops.ls(self.states_directory):
2671
+ # check path is a pickle file
2672
+ if path.suffix != ".pkl":
2673
+ continue
2674
+
2675
+ # load last edited field from file
2676
+ last_edited = path.stat().st_mtime
2677
+
2678
+ # check if the file is older than the token expiration time
2679
+ if time.time() - last_edited > self.token_expiration:
2680
+ # remove the file
2681
+ path.unlink()
2682
+
2683
+ def token_path(self, token: str) -> Path:
2684
+ """Get the path for a token.
2685
+
2686
+ Args:
2687
+ token: The token to get the path for.
2688
+
2689
+ Returns:
2690
+ The path for the token.
2691
+ """
2692
+ return (self.states_directory / f"{token}.pkl").absolute()
2693
+
2694
+ async def load_state(self, token: str, root_state: BaseState) -> BaseState:
2695
+ """Load a state object based on the provided token.
2696
+
2697
+ Args:
2698
+ token: The token used to identify the state object.
2699
+ root_state: The root state object.
2700
+
2701
+ Returns:
2702
+ The loaded state object.
2703
+ """
2704
+ if token in self.states:
2705
+ return self.states[token]
2706
+
2707
+ client_token, substate_address = _split_substate_key(token)
2708
+
2709
+ token_path = self.token_path(token)
2710
+
2711
+ if token_path.exists():
2712
+ try:
2713
+ with token_path.open(mode="rb") as file:
2714
+ (substate_schema, substate) = dill.load(file)
2715
+ if substate_schema == state_to_schema(substate):
2716
+ await self.populate_substates(client_token, substate, root_state)
2717
+ return substate
2718
+ except Exception:
2719
+ pass
2720
+
2721
+ return root_state.get_substate(substate_address.split(".")[1:])
2722
+
2723
+ async def populate_substates(
2724
+ self, client_token: str, state: BaseState, root_state: BaseState
2725
+ ):
2726
+ """Populate the substates of a state object.
2727
+
2728
+ Args:
2729
+ client_token: The client token.
2730
+ state: The state object to populate.
2731
+ root_state: The root state object.
2732
+ """
2733
+ for substate in state.get_substates():
2734
+ substate_token = _substate_key(client_token, substate)
2735
+
2736
+ substate = await self.load_state(substate_token, root_state)
2737
+
2738
+ state.substates[substate.get_name()] = substate
2739
+ substate.parent_state = state
2740
+
2741
+ @override
2742
+ async def get_state(
2743
+ self,
2744
+ token: str,
2745
+ ) -> BaseState:
2746
+ """Get the state for a token.
2747
+
2748
+ Args:
2749
+ token: The token to get the state for.
2750
+
2751
+ Returns:
2752
+ The state for the token.
2753
+ """
2754
+ client_token, substate_address = _split_substate_key(token)
2755
+
2756
+ root_state_token = _substate_key(client_token, substate_address.split(".")[0])
2757
+
2758
+ return await self.load_state(
2759
+ root_state_token, self.state(_reflex_internal_init=True)
2760
+ )
2761
+
2762
+ async def set_state_for_substate(self, client_token: str, substate: BaseState):
2763
+ """Set the state for a substate.
2764
+
2765
+ Args:
2766
+ client_token: The client token.
2767
+ substate: The substate to set.
2768
+ """
2769
+ substate_token = _substate_key(client_token, substate)
2770
+
2771
+ self.states[substate_token] = substate
2772
+
2773
+ state_dilled = dill.dumps((state_to_schema(substate), substate))
2774
+ if not self.states_directory.exists():
2775
+ self.states_directory.mkdir(parents=True, exist_ok=True)
2776
+ self.token_path(substate_token).write_bytes(state_dilled)
2777
+
2778
+ for substate_substate in substate.substates.values():
2779
+ await self.set_state_for_substate(client_token, substate_substate)
2780
+
2781
+ @override
2782
+ async def set_state(self, token: str, state: BaseState):
2783
+ """Set the state for a token.
2784
+
2785
+ Args:
2786
+ token: The token to set the state for.
2787
+ state: The state to set.
2788
+ """
2789
+ client_token, substate = _split_substate_key(token)
2790
+ await self.set_state_for_substate(client_token, state)
2791
+
2792
+ @override
2793
+ @contextlib.asynccontextmanager
2794
+ async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
2795
+ """Modify the state for a token while holding exclusive lock.
2796
+
2797
+ Args:
2798
+ token: The token to modify the state for.
2799
+
2800
+ Yields:
2801
+ The state for the token.
2802
+ """
2803
+ # Memory state manager ignores the substate suffix and always returns the top-level state.
2804
+ client_token, substate = _split_substate_key(token)
2805
+ if client_token not in self._states_locks:
2806
+ async with self._state_manager_lock:
2807
+ if client_token not in self._states_locks:
2808
+ self._states_locks[client_token] = asyncio.Lock()
2809
+
2810
+ async with self._states_locks[client_token]:
2811
+ state = await self.get_state(token)
2812
+ yield state
2813
+ await self.set_state(token, state)
2814
+
2815
+
2428
2816
  # Workaround https://github.com/cloudpipe/cloudpickle/issues/408 for dynamic pydantic classes
2429
2817
  if not isinstance(State.validate.__func__, FunctionType):
2430
2818
  cython_function_or_method = type(State.validate.__func__)
@@ -2453,15 +2841,6 @@ def _default_lock_expiration() -> int:
2453
2841
  return get_config().redis_lock_expiration
2454
2842
 
2455
2843
 
2456
- def _default_token_expiration() -> int:
2457
- """Get the default token expiration time.
2458
-
2459
- Returns:
2460
- The default token expiration time.
2461
- """
2462
- return get_config().redis_token_expiration
2463
-
2464
-
2465
2844
  class StateManagerRedis(StateManager):
2466
2845
  """A state manager that stores states in redis."""
2467
2846
 
@@ -3340,5 +3719,7 @@ def reload_state_module(
3340
3719
  if subclass.__module__ == module and module is not None:
3341
3720
  state.class_subclasses.remove(subclass)
3342
3721
  state._always_dirty_substates.discard(subclass.get_name())
3343
- state._init_var_dependency_dicts()
3722
+ state._computed_var_dependencies = defaultdict(set)
3723
+ state._substate_var_dependencies = defaultdict(set)
3724
+ state._init_var_dependency_dicts()
3344
3725
  state.get_class_substate.cache_clear()