reflex 0.5.10a3__py3-none-any.whl → 0.6.0a1__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 (237) hide show
  1. reflex/.templates/jinja/web/pages/utils.js.jinja2 +4 -4
  2. reflex/.templates/jinja/web/utils/context.js.jinja2 +1 -1
  3. reflex/.templates/jinja/web/utils/theme.js.jinja2 +1 -1
  4. reflex/__init__.py +3 -2
  5. reflex/__init__.pyi +2 -2
  6. reflex/app.py +43 -9
  7. reflex/base.py +3 -2
  8. reflex/compiler/compiler.py +6 -6
  9. reflex/compiler/utils.py +5 -3
  10. reflex/components/base/app_wrap.py +2 -4
  11. reflex/components/base/app_wrap.pyi +17 -17
  12. reflex/components/base/bare.py +7 -4
  13. reflex/components/base/body.pyi +17 -17
  14. reflex/components/base/document.pyi +81 -81
  15. reflex/components/base/error_boundary.py +10 -8
  16. reflex/components/base/error_boundary.pyi +20 -19
  17. reflex/components/base/fragment.pyi +17 -17
  18. reflex/components/base/head.pyi +33 -33
  19. reflex/components/base/link.pyi +34 -33
  20. reflex/components/base/meta.pyi +65 -65
  21. reflex/components/base/script.py +2 -1
  22. reflex/components/base/script.pyi +21 -20
  23. reflex/components/component.py +116 -145
  24. reflex/components/core/banner.py +59 -60
  25. reflex/components/core/banner.pyi +86 -150
  26. reflex/components/core/client_side_routing.py +2 -1
  27. reflex/components/core/client_side_routing.pyi +34 -33
  28. reflex/components/core/clipboard.py +2 -2
  29. reflex/components/core/clipboard.pyi +19 -18
  30. reflex/components/core/cond.py +21 -44
  31. reflex/components/core/debounce.py +6 -8
  32. reflex/components/core/debounce.pyi +19 -18
  33. reflex/components/core/foreach.py +5 -14
  34. reflex/components/core/html.pyi +18 -17
  35. reflex/components/core/match.py +36 -43
  36. reflex/components/core/upload.py +32 -25
  37. reflex/components/core/upload.pyi +84 -73
  38. reflex/components/datadisplay/code.py +55 -28
  39. reflex/components/datadisplay/code.pyi +20 -17
  40. reflex/components/datadisplay/dataeditor.py +17 -11
  41. reflex/components/datadisplay/dataeditor.pyi +34 -33
  42. reflex/components/el/__init__.py +0 -1
  43. reflex/components/el/__init__.pyi +0 -11
  44. reflex/components/el/element.pyi +17 -17
  45. reflex/components/el/elements/__init__.py +1 -7
  46. reflex/components/el/elements/__init__.pyi +1 -15
  47. reflex/components/el/elements/base.pyi +18 -17
  48. reflex/components/el/elements/forms.py +24 -31
  49. reflex/components/el/elements/forms.pyi +237 -236
  50. reflex/components/el/elements/inline.pyi +450 -449
  51. reflex/components/el/elements/media.py +0 -21
  52. reflex/components/el/elements/media.pyi +338 -337
  53. reflex/components/el/elements/metadata.py +3 -2
  54. reflex/components/el/elements/metadata.pyi +98 -97
  55. reflex/components/el/elements/other.pyi +114 -113
  56. reflex/components/el/elements/scripts.pyi +50 -49
  57. reflex/components/el/elements/sectioning.pyi +242 -241
  58. reflex/components/el/elements/tables.pyi +162 -161
  59. reflex/components/el/elements/typography.pyi +242 -241
  60. reflex/components/gridjs/datatable.py +13 -14
  61. reflex/components/gridjs/datatable.pyi +34 -33
  62. reflex/components/lucide/icon.py +2 -126
  63. reflex/components/lucide/icon.pyi +34 -142
  64. reflex/components/markdown/markdown.py +30 -35
  65. reflex/components/markdown/markdown.pyi +29 -32
  66. reflex/components/moment/moment.pyi +19 -18
  67. reflex/components/next/base.pyi +17 -17
  68. reflex/components/next/image.py +0 -4
  69. reflex/components/next/image.pyi +20 -19
  70. reflex/components/next/link.pyi +18 -17
  71. reflex/components/next/video.pyi +18 -17
  72. reflex/components/plotly/plotly.py +16 -28
  73. reflex/components/plotly/plotly.pyi +36 -35
  74. reflex/components/props.py +21 -10
  75. reflex/components/radix/__init__.pyi +1 -1
  76. reflex/components/radix/primitives/__init__.pyi +0 -1
  77. reflex/components/radix/primitives/accordion.py +7 -8
  78. reflex/components/radix/primitives/accordion.pyi +117 -116
  79. reflex/components/radix/primitives/base.pyi +34 -33
  80. reflex/components/radix/primitives/drawer.pyi +169 -168
  81. reflex/components/radix/primitives/form.pyi +168 -167
  82. reflex/components/radix/primitives/progress.pyi +82 -81
  83. reflex/components/radix/primitives/slider.pyi +84 -83
  84. reflex/components/radix/themes/base.py +8 -4
  85. reflex/components/radix/themes/base.pyi +114 -113
  86. reflex/components/radix/themes/color_mode.py +12 -21
  87. reflex/components/radix/themes/color_mode.pyi +67 -67
  88. reflex/components/radix/themes/components/__init__.pyi +1 -0
  89. reflex/components/radix/themes/components/alert_dialog.pyi +118 -117
  90. reflex/components/radix/themes/components/aspect_ratio.pyi +18 -17
  91. reflex/components/radix/themes/components/avatar.pyi +18 -17
  92. reflex/components/radix/themes/components/badge.pyi +18 -17
  93. reflex/components/radix/themes/components/button.pyi +18 -17
  94. reflex/components/radix/themes/components/callout.pyi +82 -81
  95. reflex/components/radix/themes/components/card.pyi +18 -17
  96. reflex/components/radix/themes/components/checkbox.py +2 -3
  97. reflex/components/radix/themes/components/checkbox.pyi +53 -52
  98. reflex/components/radix/themes/components/checkbox_cards.pyi +34 -33
  99. reflex/components/radix/themes/components/checkbox_group.pyi +34 -33
  100. reflex/components/radix/themes/components/context_menu.pyi +140 -139
  101. reflex/components/radix/themes/components/data_list.py +5 -0
  102. reflex/components/radix/themes/components/data_list.pyi +71 -65
  103. reflex/components/radix/themes/components/dialog.pyi +121 -120
  104. reflex/components/radix/themes/components/dropdown_menu.pyi +142 -141
  105. reflex/components/radix/themes/components/hover_card.pyi +68 -67
  106. reflex/components/radix/themes/components/icon_button.py +2 -1
  107. reflex/components/radix/themes/components/icon_button.pyi +18 -17
  108. reflex/components/radix/themes/components/inset.pyi +18 -17
  109. reflex/components/radix/themes/components/popover.pyi +73 -72
  110. reflex/components/radix/themes/components/progress.pyi +18 -17
  111. reflex/components/radix/themes/components/radio.pyi +18 -17
  112. reflex/components/radix/themes/components/radio_cards.pyi +35 -34
  113. reflex/components/radix/themes/components/radio_group.py +35 -31
  114. reflex/components/radix/themes/components/radio_group.pyi +73 -66
  115. reflex/components/radix/themes/components/scroll_area.pyi +18 -17
  116. reflex/components/radix/themes/components/segmented_control.pyi +35 -34
  117. reflex/components/radix/themes/components/select.py +2 -1
  118. reflex/components/radix/themes/components/select.pyi +155 -154
  119. reflex/components/radix/themes/components/separator.py +2 -3
  120. reflex/components/radix/themes/components/separator.pyi +18 -17
  121. reflex/components/radix/themes/components/skeleton.pyi +18 -17
  122. reflex/components/radix/themes/components/slider.py +2 -1
  123. reflex/components/radix/themes/components/slider.pyi +20 -19
  124. reflex/components/radix/themes/components/spinner.pyi +18 -17
  125. reflex/components/radix/themes/components/switch.pyi +19 -18
  126. reflex/components/radix/themes/components/table.pyi +114 -113
  127. reflex/components/radix/themes/components/tabs.pyi +84 -83
  128. reflex/components/radix/themes/components/text_area.pyi +21 -20
  129. reflex/components/radix/themes/components/text_field.py +0 -79
  130. reflex/components/radix/themes/components/text_field.pyi +57 -63
  131. reflex/components/radix/themes/components/tooltip.pyi +21 -20
  132. reflex/components/radix/themes/layout/base.pyi +18 -17
  133. reflex/components/radix/themes/layout/box.pyi +18 -17
  134. reflex/components/radix/themes/layout/center.pyi +18 -17
  135. reflex/components/radix/themes/layout/container.py +2 -3
  136. reflex/components/radix/themes/layout/container.pyi +18 -17
  137. reflex/components/radix/themes/layout/flex.pyi +18 -17
  138. reflex/components/radix/themes/layout/grid.pyi +18 -17
  139. reflex/components/radix/themes/layout/list.py +5 -4
  140. reflex/components/radix/themes/layout/list.pyi +86 -85
  141. reflex/components/radix/themes/layout/section.py +2 -3
  142. reflex/components/radix/themes/layout/section.pyi +18 -17
  143. reflex/components/radix/themes/layout/spacer.pyi +18 -17
  144. reflex/components/radix/themes/layout/stack.pyi +50 -49
  145. reflex/components/radix/themes/typography/blockquote.pyi +18 -17
  146. reflex/components/radix/themes/typography/code.pyi +18 -17
  147. reflex/components/radix/themes/typography/heading.pyi +18 -17
  148. reflex/components/radix/themes/typography/link.pyi +18 -17
  149. reflex/components/radix/themes/typography/text.pyi +114 -113
  150. reflex/components/react_player/audio.pyi +34 -33
  151. reflex/components/react_player/react_player.pyi +34 -33
  152. reflex/components/react_player/video.pyi +34 -33
  153. reflex/components/recharts/cartesian.py +23 -19
  154. reflex/components/recharts/cartesian.pyi +297 -296
  155. reflex/components/recharts/charts.py +6 -5
  156. reflex/components/recharts/charts.pyi +179 -178
  157. reflex/components/recharts/general.py +8 -7
  158. reflex/components/recharts/general.pyi +82 -81
  159. reflex/components/recharts/polar.py +14 -13
  160. reflex/components/recharts/polar.pyi +76 -75
  161. reflex/components/recharts/recharts.pyi +33 -33
  162. reflex/components/sonner/toast.py +30 -33
  163. reflex/components/sonner/toast.pyi +27 -25
  164. reflex/components/suneditor/editor.py +2 -1
  165. reflex/components/suneditor/editor.pyi +27 -26
  166. reflex/components/tags/iter_tag.py +16 -16
  167. reflex/components/tags/tag.py +8 -10
  168. reflex/constants/base.py +3 -1
  169. reflex/constants/event.py +1 -0
  170. reflex/event.py +89 -79
  171. reflex/experimental/__init__.py +25 -6
  172. reflex/experimental/client_state.py +34 -58
  173. reflex/experimental/hooks.py +13 -18
  174. reflex/experimental/layout.py +5 -5
  175. reflex/experimental/layout.pyi +84 -83
  176. reflex/{experimental/vars → ivars}/__init__.py +0 -1
  177. reflex/ivars/base.py +2180 -0
  178. reflex/ivars/function.py +200 -0
  179. reflex/ivars/number.py +1137 -0
  180. reflex/ivars/object.py +564 -0
  181. reflex/ivars/sequence.py +1601 -0
  182. reflex/model.py +22 -0
  183. reflex/reflex.py +4 -0
  184. reflex/state.py +388 -73
  185. reflex/style.py +52 -34
  186. reflex/testing.py +8 -3
  187. reflex/utils/exceptions.py +12 -0
  188. reflex/utils/exec.py +0 -14
  189. reflex/utils/format.py +74 -223
  190. reflex/utils/net.py +43 -0
  191. reflex/utils/path_ops.py +13 -1
  192. reflex/utils/prerequisites.py +46 -26
  193. reflex/utils/pyi_generator.py +5 -4
  194. reflex/utils/serializers.py +13 -31
  195. reflex/utils/types.py +44 -9
  196. reflex/vars.py +127 -2230
  197. {reflex-0.5.10a3.dist-info → reflex-0.6.0a1.dist-info}/METADATA +4 -6
  198. reflex-0.6.0a1.dist-info/RECORD +384 -0
  199. reflex/.templates/apps/demo/.gitignore +0 -4
  200. reflex/.templates/apps/demo/assets/favicon.ico +0 -0
  201. reflex/.templates/apps/demo/assets/github.svg +0 -10
  202. reflex/.templates/apps/demo/assets/icon.svg +0 -37
  203. reflex/.templates/apps/demo/assets/logo.svg +0 -68
  204. reflex/.templates/apps/demo/assets/paneleft.svg +0 -13
  205. reflex/.templates/apps/demo/code/__init__.py +0 -1
  206. reflex/.templates/apps/demo/code/demo.py +0 -127
  207. reflex/.templates/apps/demo/code/pages/__init__.py +0 -7
  208. reflex/.templates/apps/demo/code/pages/chatapp.py +0 -31
  209. reflex/.templates/apps/demo/code/pages/datatable.py +0 -360
  210. reflex/.templates/apps/demo/code/pages/forms.py +0 -257
  211. reflex/.templates/apps/demo/code/pages/graphing.py +0 -253
  212. reflex/.templates/apps/demo/code/pages/home.py +0 -56
  213. reflex/.templates/apps/demo/code/sidebar.py +0 -178
  214. reflex/.templates/apps/demo/code/state.py +0 -22
  215. reflex/.templates/apps/demo/code/states/form_state.py +0 -40
  216. reflex/.templates/apps/demo/code/states/pie_state.py +0 -47
  217. reflex/.templates/apps/demo/code/styles.py +0 -68
  218. reflex/.templates/apps/demo/code/webui/__init__.py +0 -0
  219. reflex/.templates/apps/demo/code/webui/components/__init__.py +0 -4
  220. reflex/.templates/apps/demo/code/webui/components/chat.py +0 -118
  221. reflex/.templates/apps/demo/code/webui/components/loading_icon.py +0 -19
  222. reflex/.templates/apps/demo/code/webui/components/modal.py +0 -56
  223. reflex/.templates/apps/demo/code/webui/components/navbar.py +0 -70
  224. reflex/.templates/apps/demo/code/webui/components/sidebar.py +0 -66
  225. reflex/.templates/apps/demo/code/webui/state.py +0 -146
  226. reflex/.templates/apps/demo/code/webui/styles.py +0 -88
  227. reflex/experimental/vars/base.py +0 -583
  228. reflex/experimental/vars/function.py +0 -290
  229. reflex/experimental/vars/number.py +0 -1458
  230. reflex/experimental/vars/object.py +0 -804
  231. reflex/experimental/vars/sequence.py +0 -1764
  232. reflex/utils/watch.py +0 -96
  233. reflex/vars.pyi +0 -218
  234. reflex-0.5.10a3.dist-info/RECORD +0 -413
  235. {reflex-0.5.10a3.dist-info → reflex-0.6.0a1.dist-info}/LICENSE +0 -0
  236. {reflex-0.5.10a3.dist-info → reflex-0.6.0a1.dist-info}/WHEEL +0 -0
  237. {reflex-0.5.10a3.dist-info → reflex-0.6.0a1.dist-info}/entry_points.txt +0 -0
reflex/state.py CHANGED
@@ -11,6 +11,7 @@ import os
11
11
  import uuid
12
12
  from abc import ABC, abstractmethod
13
13
  from collections import defaultdict
14
+ from pathlib import Path
14
15
  from types import FunctionType, MethodType
15
16
  from typing import (
16
17
  TYPE_CHECKING,
@@ -23,6 +24,7 @@ from typing import (
23
24
  Optional,
24
25
  Sequence,
25
26
  Set,
27
+ Tuple,
26
28
  Type,
27
29
  Union,
28
30
  cast,
@@ -32,6 +34,13 @@ import dill
32
34
  from sqlalchemy.orm import DeclarativeBase
33
35
 
34
36
  from reflex.config import get_config
37
+ from reflex.ivars.base import (
38
+ DynamicRouteVar,
39
+ ImmutableComputedVar,
40
+ ImmutableVar,
41
+ immutable_computed_var,
42
+ is_computed_var,
43
+ )
35
44
 
36
45
  try:
37
46
  import pydantic.v1 as pydantic
@@ -51,19 +60,23 @@ from reflex.event import (
51
60
  EventSpec,
52
61
  fix_events,
53
62
  )
54
- from reflex.utils import console, format, prerequisites, types
55
- from reflex.utils.exceptions import ImmutableStateError, LockExpiredError
63
+ from reflex.utils import console, format, path_ops, prerequisites, types
64
+ from reflex.utils.exceptions import (
65
+ DynamicRouteArgShadowsStateVar,
66
+ ImmutableStateError,
67
+ LockExpiredError,
68
+ )
56
69
  from reflex.utils.exec import is_testing_env
57
70
  from reflex.utils.serializers import SerializedType, serialize, serializer
58
71
  from reflex.utils.types import override
59
- from reflex.vars import BaseVar, ComputedVar, Var, computed_var
72
+ from reflex.vars import VarData
60
73
 
61
74
  if TYPE_CHECKING:
62
75
  from reflex.components.component import Component
63
76
 
64
77
 
65
78
  Delta = Dict[str, Any]
66
- var = computed_var
79
+ var = immutable_computed_var
67
80
 
68
81
 
69
82
  # If the state is this large, it's considered a performance issue.
@@ -296,16 +309,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
296
309
  """The state of the app."""
297
310
 
298
311
  # A map from the var name to the var.
299
- vars: ClassVar[Dict[str, Var]] = {}
312
+ vars: ClassVar[Dict[str, ImmutableVar]] = {}
300
313
 
301
314
  # The base vars of the class.
302
- base_vars: ClassVar[Dict[str, BaseVar]] = {}
315
+ base_vars: ClassVar[Dict[str, ImmutableVar]] = {}
303
316
 
304
317
  # The computed vars of the class.
305
- computed_vars: ClassVar[Dict[str, ComputedVar]] = {}
318
+ computed_vars: ClassVar[Dict[str, ImmutableComputedVar]] = {}
306
319
 
307
320
  # Vars inherited by the parent state.
308
- inherited_vars: ClassVar[Dict[str, Var]] = {}
321
+ inherited_vars: ClassVar[Dict[str, ImmutableVar]] = {}
309
322
 
310
323
  # Backend base vars that are never sent to the client.
311
324
  backend_vars: ClassVar[Dict[str, Any]] = {}
@@ -415,7 +428,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
415
428
  return f"{self.__class__.__name__}({self.dict()})"
416
429
 
417
430
  @classmethod
418
- def _get_computed_vars(cls) -> list[ComputedVar]:
431
+ def _get_computed_vars(cls) -> list[ImmutableComputedVar]:
419
432
  """Helper function to get all computed vars of a instance.
420
433
 
421
434
  Returns:
@@ -424,8 +437,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
424
437
  return [
425
438
  v
426
439
  for mixin in cls._mixins() + [cls]
427
- for v in mixin.__dict__.values()
428
- if isinstance(v, ComputedVar)
440
+ for name, v in mixin.__dict__.items()
441
+ if is_computed_var(v) and name not in cls.inherited_vars
429
442
  ]
430
443
 
431
444
  @classmethod
@@ -469,7 +482,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
469
482
  cls._check_overridden_methods()
470
483
 
471
484
  # Computed vars should not shadow builtin state props.
472
- cls._check_overriden_basevars()
485
+ cls._check_overridden_basevars()
473
486
 
474
487
  # Reset subclass tracking for this class.
475
488
  cls.class_subclasses = set()
@@ -487,26 +500,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
487
500
  if cls.get_name() in set(
488
501
  c.get_name() for c in parent_state.class_subclasses
489
502
  ):
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
- )
503
+ # This should not happen, since we have added module prefix to state names in #3214
504
+ raise StateValueError(
505
+ f"The substate class '{cls.get_name()}' has been defined multiple times. "
506
+ "Shadowing substate classes is not allowed."
507
+ )
505
508
  # Track this new subclass in the parent state's subclasses set.
506
509
  parent_state.class_subclasses.add(cls)
507
510
 
508
511
  # Get computed vars.
509
512
  computed_vars = cls._get_computed_vars()
513
+ cls._check_overridden_computed_vars()
510
514
 
511
515
  new_backend_vars = {
512
516
  name: value
@@ -521,13 +525,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
521
525
 
522
526
  # Set the base and computed vars.
523
527
  cls.base_vars = {
524
- f.name: BaseVar(_var_name=f.name, _var_type=f.outer_type_)._var_set_state(
525
- cls
526
- )
528
+ f.name: ImmutableVar(
529
+ _var_name=format.format_state_name(cls.get_full_name()) + "." + f.name,
530
+ _var_type=f.outer_type_,
531
+ _var_data=VarData.from_state(cls),
532
+ ).guess_type()
527
533
  for f in cls.get_fields().values()
528
534
  if f.name not in cls.get_skip_vars()
529
535
  }
530
- cls.computed_vars = {v._var_name: v._var_set_state(cls) for v in computed_vars}
536
+ cls.computed_vars = {
537
+ v._var_name: v._replace(merge_var_data=VarData.from_state(cls))
538
+ for v in computed_vars
539
+ }
531
540
  cls.vars = {
532
541
  **cls.inherited_vars,
533
542
  **cls.base_vars,
@@ -548,12 +557,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
548
557
 
549
558
  for mixin in cls._mixins():
550
559
  for name, value in mixin.__dict__.items():
551
- if isinstance(value, ComputedVar):
560
+ if name in cls.inherited_vars:
561
+ continue
562
+ if is_computed_var(value):
552
563
  fget = cls._copy_fn(value.fget)
553
- newcv = value._replace(fget=fget)
564
+ newcv = value._replace(fget=fget, _var_data=VarData.from_state(cls))
554
565
  # cleanup refs to mixin cls in var_data
555
- newcv._var_data = None
556
- newcv._var_set_state(cls)
557
566
  setattr(cls, name, newcv)
558
567
  cls.computed_vars[newcv._var_name] = newcv
559
568
  cls.vars[newcv._var_name] = newcv
@@ -579,6 +588,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
579
588
  cls.event_handlers[name] = handler
580
589
  setattr(cls, name, handler)
581
590
 
591
+ # Initialize per-class var dependency tracking.
592
+ cls._computed_var_dependencies = defaultdict(set)
593
+ cls._substate_var_dependencies = defaultdict(set)
582
594
  cls._init_var_dependency_dicts()
583
595
 
584
596
  @staticmethod
@@ -648,10 +660,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
648
660
  Additional updates tracking dicts for vars and substates that always
649
661
  need to be recomputed.
650
662
  """
651
- # Initialize per-class var dependency tracking.
652
- cls._computed_var_dependencies = defaultdict(set)
653
- cls._substate_var_dependencies = defaultdict(set)
654
-
655
663
  inherited_vars = set(cls.inherited_vars).union(
656
664
  set(cls.inherited_backend_vars),
657
665
  )
@@ -716,7 +724,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
716
724
  )
717
725
 
718
726
  @classmethod
719
- def _check_overriden_basevars(cls):
727
+ def _check_overridden_basevars(cls):
720
728
  """Check for shadow base vars and raise error if any.
721
729
 
722
730
  Raises:
@@ -728,6 +736,22 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
728
736
  f"The computed var name `{computed_var_._var_name}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
729
737
  )
730
738
 
739
+ @classmethod
740
+ def _check_overridden_computed_vars(cls) -> None:
741
+ """Check for shadow computed vars and raise error if any.
742
+
743
+ Raises:
744
+ NameError: When a computed var shadows another.
745
+ """
746
+ for name, cv in cls.__dict__.items():
747
+ if not is_computed_var(cv):
748
+ continue
749
+ name = cv._var_name
750
+ if name in cls.inherited_vars or name in cls.inherited_backend_vars:
751
+ raise NameError(
752
+ f"The computed var name `{cv._var_name}` shadows a var in {cls.__module__}.{cls.__name__}; use a different name instead"
753
+ )
754
+
731
755
  @classmethod
732
756
  def get_skip_vars(cls) -> set[str]:
733
757
  """Get the vars to skip when serializing.
@@ -847,7 +871,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
847
871
  return getattr(substate, name)
848
872
 
849
873
  @classmethod
850
- def _init_var(cls, prop: BaseVar):
874
+ def _init_var(cls, prop: ImmutableVar):
851
875
  """Initialize a variable.
852
876
 
853
877
  Args:
@@ -890,8 +914,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
890
914
  )
891
915
 
892
916
  # create the variable based on name and type
893
- var = BaseVar(_var_name=name, _var_type=type_)
894
- var._var_set_state(cls)
917
+ var = ImmutableVar(
918
+ _var_name=format.format_state_name(cls.get_full_name()) + "." + name,
919
+ _var_type=type_,
920
+ _var_data=VarData.from_state(cls),
921
+ ).guess_type()
895
922
 
896
923
  # add the pydantic field dynamically (must be done before _init_var)
897
924
  cls.add_field(var, default_value)
@@ -910,13 +937,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
910
937
  cls._init_var_dependency_dicts()
911
938
 
912
939
  @classmethod
913
- def _set_var(cls, prop: BaseVar):
940
+ def _set_var(cls, prop: ImmutableVar):
914
941
  """Set the var as a class member.
915
942
 
916
943
  Args:
917
944
  prop: The var instance to set.
918
945
  """
919
- setattr(cls, prop._var_name, prop)
946
+ acutal_var_name = (
947
+ prop._var_name
948
+ if "." not in prop._var_name
949
+ else prop._var_name.split(".")[-1]
950
+ )
951
+ setattr(cls, acutal_var_name, prop)
920
952
 
921
953
  @classmethod
922
954
  def _create_event_handler(cls, fn):
@@ -936,7 +968,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
936
968
  cls.setvar = cls.event_handlers["setvar"] = EventHandlerSetVar(state_cls=cls)
937
969
 
938
970
  @classmethod
939
- def _create_setter(cls, prop: BaseVar):
971
+ def _create_setter(cls, prop: ImmutableVar):
940
972
  """Create a setter for the var.
941
973
 
942
974
  Args:
@@ -949,14 +981,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
949
981
  setattr(cls, setter_name, event_handler)
950
982
 
951
983
  @classmethod
952
- def _set_default_value(cls, prop: BaseVar):
984
+ def _set_default_value(cls, prop: ImmutableVar):
953
985
  """Set the default value for the var.
954
986
 
955
987
  Args:
956
988
  prop: The var to set the default value for.
957
989
  """
958
990
  # Get the pydantic field for the var.
959
- field = cls.get_fields()[prop._var_name]
991
+ if "." in prop._var_name:
992
+ field = cls.get_fields()[prop._var_name.split(".")[-1]]
993
+ else:
994
+ field = cls.get_fields()[prop._var_name]
960
995
  if field.required:
961
996
  default_value = prop.get_default_value()
962
997
  if default_value is not None:
@@ -968,7 +1003,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
968
1003
  and not types.is_optional(prop._var_type)
969
1004
  ):
970
1005
  # Ensure frontend uses null coalescing when accessing.
971
- prop._var_type = Optional[prop._var_type]
1006
+ object.__setattr__(prop, "_var_type", Optional[prop._var_type])
972
1007
 
973
1008
  @staticmethod
974
1009
  def _get_base_functions() -> dict[str, FunctionType]:
@@ -990,20 +1025,22 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
990
1025
  Args:
991
1026
  args: a dict of args
992
1027
  """
1028
+ if not args:
1029
+ return
1030
+
1031
+ cls._check_overwritten_dynamic_args(list(args.keys()))
993
1032
 
994
1033
  def argsingle_factory(param):
995
- @ComputedVar
996
1034
  def inner_func(self) -> str:
997
1035
  return self.router.page.params.get(param, "")
998
1036
 
999
- return inner_func
1037
+ return DynamicRouteVar(fget=inner_func, cache=True)
1000
1038
 
1001
1039
  def arglist_factory(param):
1002
- @ComputedVar
1003
- def inner_func(self) -> List:
1040
+ def inner_func(self) -> List[str]:
1004
1041
  return self.router.page.params.get(param, [])
1005
1042
 
1006
- return inner_func
1043
+ return DynamicRouteVar(fget=inner_func, cache=True)
1007
1044
 
1008
1045
  for param, value in args.items():
1009
1046
  if value == constants.RouteArgType.SINGLE:
@@ -1012,13 +1049,37 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1012
1049
  func = arglist_factory(param)
1013
1050
  else:
1014
1051
  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
1052
+ # to allow passing as a prop, evade python frozen rules (bad practice)
1053
+ object.__setattr__(func, "_var_name", param)
1054
+ # cls.vars[param] = cls.computed_vars[param] = func._var_set_state(cls) # type: ignore
1055
+ cls.vars[param] = cls.computed_vars[param] = func._replace(
1056
+ _var_data=VarData.from_state(cls)
1057
+ )
1018
1058
  setattr(cls, param, func)
1019
1059
 
1020
- # Reinitialize dependency tracking dicts.
1021
- cls._init_var_dependency_dicts()
1060
+ # Reinitialize dependency tracking dicts.
1061
+ cls._init_var_dependency_dicts()
1062
+
1063
+ @classmethod
1064
+ def _check_overwritten_dynamic_args(cls, args: list[str]):
1065
+ """Check if dynamic args are shadowing existing vars. Recursively checks all child states.
1066
+
1067
+ Args:
1068
+ args: a dict of args
1069
+
1070
+ Raises:
1071
+ DynamicRouteArgShadowsStateVar: If a dynamic arg is shadowing an existing var.
1072
+ """
1073
+ for arg in args:
1074
+ if (
1075
+ arg in cls.computed_vars
1076
+ and not isinstance(cls.computed_vars[arg], DynamicRouteVar)
1077
+ ) or arg in cls.base_vars:
1078
+ raise DynamicRouteArgShadowsStateVar(
1079
+ f"Dynamic route arg '{arg}' is shadowing an existing var in {cls.__module__}.{cls.__name__}"
1080
+ )
1081
+ for substate in cls.get_substates():
1082
+ substate._check_overwritten_dynamic_args(args)
1022
1083
 
1023
1084
  def __getattribute__(self, name: str) -> Any:
1024
1085
  """Get the state var.
@@ -1769,12 +1830,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1769
1830
  prop_name: self.get_value(getattr(self, prop_name))
1770
1831
  for prop_name in self.base_vars
1771
1832
  }
1772
- if initial:
1833
+ if initial and include_computed:
1773
1834
  computed_vars = {
1774
1835
  # Include initial computed vars.
1775
1836
  prop_name: (
1776
1837
  cv._initial_value
1777
- if isinstance(cv, ComputedVar)
1838
+ if is_computed_var(cv)
1778
1839
  and not isinstance(cv._initial_value, types.Unset)
1779
1840
  else self.get_value(getattr(self, prop_name))
1780
1841
  )
@@ -2272,11 +2333,12 @@ class StateProxy(wrapt.ObjectProxy):
2272
2333
  Returns:
2273
2334
  The state update.
2274
2335
  """
2336
+ original_mutable = self._self_mutable
2275
2337
  self._self_mutable = True
2276
2338
  try:
2277
2339
  return self.__wrapped__._as_state_update(*args, **kwargs)
2278
2340
  finally:
2279
- self._self_mutable = False
2341
+ self._self_mutable = original_mutable
2280
2342
 
2281
2343
 
2282
2344
  class StateUpdate(Base):
@@ -2318,7 +2380,7 @@ class StateManager(Base, ABC):
2318
2380
  token_expiration=config.redis_token_expiration,
2319
2381
  lock_expiration=config.redis_lock_expiration,
2320
2382
  )
2321
- return StateManagerMemory(state=state)
2383
+ return StateManagerDisk(state=state)
2322
2384
 
2323
2385
  @abstractmethod
2324
2386
  async def get_state(self, token: str) -> BaseState:
@@ -2425,6 +2487,266 @@ class StateManagerMemory(StateManager):
2425
2487
  await self.set_state(token, state)
2426
2488
 
2427
2489
 
2490
+ def _default_token_expiration() -> int:
2491
+ """Get the default token expiration time.
2492
+
2493
+ Returns:
2494
+ The default token expiration time.
2495
+ """
2496
+ return get_config().redis_token_expiration
2497
+
2498
+
2499
+ def _serialize_type(type_: Any) -> str:
2500
+ """Serialize a type.
2501
+
2502
+ Args:
2503
+ type_: The type to serialize.
2504
+
2505
+ Returns:
2506
+ The serialized type.
2507
+ """
2508
+ if not inspect.isclass(type_):
2509
+ return f"{type_}"
2510
+ return f"{type_.__module__}.{type_.__qualname__}"
2511
+
2512
+
2513
+ def state_to_schema(
2514
+ state: BaseState,
2515
+ ) -> List[
2516
+ Tuple[
2517
+ str,
2518
+ str,
2519
+ Any,
2520
+ Union[bool, None],
2521
+ ]
2522
+ ]:
2523
+ """Convert a state to a schema.
2524
+
2525
+ Args:
2526
+ state: The state to convert to a schema.
2527
+
2528
+ Returns:
2529
+ The schema.
2530
+ """
2531
+ return list(
2532
+ sorted(
2533
+ (
2534
+ field_name,
2535
+ model_field.name,
2536
+ _serialize_type(model_field.type_),
2537
+ (
2538
+ model_field.required
2539
+ if isinstance(model_field.required, bool)
2540
+ else None
2541
+ ),
2542
+ )
2543
+ for field_name, model_field in state.__fields__.items()
2544
+ )
2545
+ )
2546
+
2547
+
2548
+ def reset_disk_state_manager():
2549
+ """Reset the disk state manager."""
2550
+ states_directory = prerequisites.get_web_dir() / constants.Dirs.STATES
2551
+ if states_directory.exists():
2552
+ for path in states_directory.iterdir():
2553
+ path.unlink()
2554
+
2555
+
2556
+ class StateManagerDisk(StateManager):
2557
+ """A state manager that stores states in memory."""
2558
+
2559
+ # The mapping of client ids to states.
2560
+ states: Dict[str, BaseState] = {}
2561
+
2562
+ # The mutex ensures the dict of mutexes is updated exclusively
2563
+ _state_manager_lock = asyncio.Lock()
2564
+
2565
+ # The dict of mutexes for each client
2566
+ _states_locks: Dict[str, asyncio.Lock] = pydantic.PrivateAttr({})
2567
+
2568
+ # The token expiration time (s).
2569
+ token_expiration: int = pydantic.Field(default_factory=_default_token_expiration)
2570
+
2571
+ class Config:
2572
+ """The Pydantic config."""
2573
+
2574
+ fields = {
2575
+ "_states_locks": {"exclude": True},
2576
+ }
2577
+ keep_untouched = (functools.cached_property,)
2578
+
2579
+ def __init__(self, state: Type[BaseState]):
2580
+ """Create a new state manager.
2581
+
2582
+ Args:
2583
+ state: The state class to use.
2584
+ """
2585
+ super().__init__(state=state)
2586
+
2587
+ path_ops.mkdir(self.states_directory)
2588
+
2589
+ self._purge_expired_states()
2590
+
2591
+ @functools.cached_property
2592
+ def states_directory(self) -> Path:
2593
+ """Get the states directory.
2594
+
2595
+ Returns:
2596
+ The states directory.
2597
+ """
2598
+ return prerequisites.get_web_dir() / constants.Dirs.STATES
2599
+
2600
+ def _purge_expired_states(self):
2601
+ """Purge expired states from the disk."""
2602
+ import time
2603
+
2604
+ for path in path_ops.ls(self.states_directory):
2605
+ # check path is a pickle file
2606
+ if path.suffix != ".pkl":
2607
+ continue
2608
+
2609
+ # load last edited field from file
2610
+ last_edited = path.stat().st_mtime
2611
+
2612
+ # check if the file is older than the token expiration time
2613
+ if time.time() - last_edited > self.token_expiration:
2614
+ # remove the file
2615
+ path.unlink()
2616
+
2617
+ def token_path(self, token: str) -> Path:
2618
+ """Get the path for a token.
2619
+
2620
+ Args:
2621
+ token: The token to get the path for.
2622
+
2623
+ Returns:
2624
+ The path for the token.
2625
+ """
2626
+ return (self.states_directory / f"{token}.pkl").absolute()
2627
+
2628
+ async def load_state(self, token: str, root_state: BaseState) -> BaseState:
2629
+ """Load a state object based on the provided token.
2630
+
2631
+ Args:
2632
+ token: The token used to identify the state object.
2633
+ root_state: The root state object.
2634
+
2635
+ Returns:
2636
+ The loaded state object.
2637
+ """
2638
+ if token in self.states:
2639
+ return self.states[token]
2640
+
2641
+ client_token, substate_address = _split_substate_key(token)
2642
+
2643
+ token_path = self.token_path(token)
2644
+
2645
+ if token_path.exists():
2646
+ try:
2647
+ with token_path.open(mode="rb") as file:
2648
+ (substate_schema, substate) = dill.load(file)
2649
+ if substate_schema == state_to_schema(substate):
2650
+ await self.populate_substates(client_token, substate, root_state)
2651
+ return substate
2652
+ except Exception:
2653
+ pass
2654
+
2655
+ return root_state.get_substate(substate_address.split(".")[1:])
2656
+
2657
+ async def populate_substates(
2658
+ self, client_token: str, state: BaseState, root_state: BaseState
2659
+ ):
2660
+ """Populate the substates of a state object.
2661
+
2662
+ Args:
2663
+ client_token: The client token.
2664
+ state: The state object to populate.
2665
+ root_state: The root state object.
2666
+ """
2667
+ for substate in state.get_substates():
2668
+ substate_token = _substate_key(client_token, substate)
2669
+
2670
+ substate = await self.load_state(substate_token, root_state)
2671
+
2672
+ state.substates[substate.get_name()] = substate
2673
+ substate.parent_state = state
2674
+
2675
+ @override
2676
+ async def get_state(
2677
+ self,
2678
+ token: str,
2679
+ ) -> BaseState:
2680
+ """Get the state for a token.
2681
+
2682
+ Args:
2683
+ token: The token to get the state for.
2684
+
2685
+ Returns:
2686
+ The state for the token.
2687
+ """
2688
+ client_token, substate_address = _split_substate_key(token)
2689
+
2690
+ root_state_token = _substate_key(client_token, substate_address.split(".")[0])
2691
+
2692
+ return await self.load_state(
2693
+ root_state_token, self.state(_reflex_internal_init=True)
2694
+ )
2695
+
2696
+ async def set_state_for_substate(self, client_token: str, substate: BaseState):
2697
+ """Set the state for a substate.
2698
+
2699
+ Args:
2700
+ client_token: The client token.
2701
+ substate: The substate to set.
2702
+ """
2703
+ substate_token = _substate_key(client_token, substate)
2704
+
2705
+ self.states[substate_token] = substate
2706
+
2707
+ state_dilled = dill.dumps((state_to_schema(substate), substate))
2708
+ if not self.states_directory.exists():
2709
+ self.states_directory.mkdir(parents=True, exist_ok=True)
2710
+ self.token_path(substate_token).write_bytes(state_dilled)
2711
+
2712
+ for substate_substate in substate.substates.values():
2713
+ await self.set_state_for_substate(client_token, substate_substate)
2714
+
2715
+ @override
2716
+ async def set_state(self, token: str, state: BaseState):
2717
+ """Set the state for a token.
2718
+
2719
+ Args:
2720
+ token: The token to set the state for.
2721
+ state: The state to set.
2722
+ """
2723
+ client_token, substate = _split_substate_key(token)
2724
+ await self.set_state_for_substate(client_token, state)
2725
+
2726
+ @override
2727
+ @contextlib.asynccontextmanager
2728
+ async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
2729
+ """Modify the state for a token while holding exclusive lock.
2730
+
2731
+ Args:
2732
+ token: The token to modify the state for.
2733
+
2734
+ Yields:
2735
+ The state for the token.
2736
+ """
2737
+ # Memory state manager ignores the substate suffix and always returns the top-level state.
2738
+ client_token, substate = _split_substate_key(token)
2739
+ if client_token not in self._states_locks:
2740
+ async with self._state_manager_lock:
2741
+ if client_token not in self._states_locks:
2742
+ self._states_locks[client_token] = asyncio.Lock()
2743
+
2744
+ async with self._states_locks[client_token]:
2745
+ state = await self.get_state(token)
2746
+ yield state
2747
+ await self.set_state(token, state)
2748
+
2749
+
2428
2750
  # Workaround https://github.com/cloudpipe/cloudpickle/issues/408 for dynamic pydantic classes
2429
2751
  if not isinstance(State.validate.__func__, FunctionType):
2430
2752
  cython_function_or_method = type(State.validate.__func__)
@@ -2453,15 +2775,6 @@ def _default_lock_expiration() -> int:
2453
2775
  return get_config().redis_lock_expiration
2454
2776
 
2455
2777
 
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
2778
  class StateManagerRedis(StateManager):
2466
2779
  """A state manager that stores states in redis."""
2467
2780
 
@@ -3340,5 +3653,7 @@ def reload_state_module(
3340
3653
  if subclass.__module__ == module and module is not None:
3341
3654
  state.class_subclasses.remove(subclass)
3342
3655
  state._always_dirty_substates.discard(subclass.get_name())
3343
- state._init_var_dependency_dicts()
3656
+ state._computed_var_dependencies = defaultdict(set)
3657
+ state._substate_var_dependencies = defaultdict(set)
3658
+ state._init_var_dependency_dicts()
3344
3659
  state.get_class_substate.cache_clear()