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.
- reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +2 -2
- reflex/.templates/jinja/web/pages/_app.js.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/utils.js.jinja2 +4 -4
- reflex/.templates/jinja/web/utils/context.js.jinja2 +1 -1
- reflex/.templates/jinja/web/utils/theme.js.jinja2 +1 -1
- reflex/.templates/web/utils/state.js +3 -1
- reflex/__init__.py +10 -3
- reflex/__init__.pyi +3 -2
- reflex/app.py +47 -11
- reflex/app_module_for_backend.py +1 -1
- reflex/base.py +3 -2
- reflex/compiler/compiler.py +5 -5
- reflex/compiler/utils.py +5 -3
- reflex/components/base/app_wrap.py +2 -4
- reflex/components/base/app_wrap.pyi +16 -26
- reflex/components/base/bare.py +6 -4
- reflex/components/base/body.pyi +16 -26
- reflex/components/base/document.pyi +76 -126
- reflex/components/base/error_boundary.py +9 -8
- reflex/components/base/error_boundary.pyi +18 -30
- reflex/components/base/fragment.pyi +16 -26
- reflex/components/base/head.pyi +31 -51
- reflex/components/base/link.py +1 -1
- reflex/components/base/link.pyi +31 -51
- reflex/components/base/meta.pyi +61 -101
- reflex/components/base/script.py +2 -2
- reflex/components/base/script.pyi +20 -36
- reflex/components/component.py +107 -130
- reflex/components/core/banner.py +61 -66
- reflex/components/core/banner.pyi +129 -230
- reflex/components/core/client_side_routing.py +2 -2
- reflex/components/core/client_side_routing.pyi +31 -51
- reflex/components/core/clipboard.py +4 -3
- reflex/components/core/clipboard.pyi +19 -31
- reflex/components/core/cond.py +21 -44
- reflex/components/core/debounce.py +7 -9
- reflex/components/core/debounce.pyi +19 -31
- reflex/components/core/foreach.py +4 -14
- reflex/components/core/html.py +1 -1
- reflex/components/core/html.pyi +34 -44
- reflex/components/core/match.py +36 -43
- reflex/components/core/upload.py +27 -26
- reflex/components/core/upload.pyi +81 -116
- reflex/components/datadisplay/code.py +55 -29
- reflex/components/datadisplay/code.pyi +303 -410
- reflex/components/datadisplay/dataeditor.py +13 -9
- reflex/components/datadisplay/dataeditor.pyi +39 -51
- reflex/components/el/__init__.py +0 -1
- reflex/components/el/__init__.pyi +0 -11
- reflex/components/el/element.pyi +16 -26
- reflex/components/el/elements/__init__.py +1 -7
- reflex/components/el/elements/__init__.pyi +1 -15
- reflex/components/el/elements/base.py +1 -1
- reflex/components/el/elements/base.pyi +33 -43
- reflex/components/el/elements/forms.py +26 -33
- reflex/components/el/elements/forms.pyi +542 -694
- reflex/components/el/elements/inline.py +1 -1
- reflex/components/el/elements/inline.pyi +909 -1189
- reflex/components/el/elements/media.py +1 -22
- reflex/components/el/elements/media.pyi +765 -975
- reflex/components/el/elements/metadata.py +3 -5
- reflex/components/el/elements/metadata.pyi +175 -235
- reflex/components/el/elements/other.py +1 -1
- reflex/components/el/elements/other.pyi +228 -298
- reflex/components/el/elements/scripts.py +1 -1
- reflex/components/el/elements/scripts.pyi +106 -136
- reflex/components/el/elements/sectioning.py +0 -2
- reflex/components/el/elements/sectioning.pyi +481 -631
- reflex/components/el/elements/tables.py +1 -1
- reflex/components/el/elements/tables.pyi +341 -441
- reflex/components/el/elements/typography.py +1 -1
- reflex/components/el/elements/typography.pyi +491 -641
- reflex/components/gridjs/datatable.py +9 -13
- reflex/components/gridjs/datatable.pyi +33 -53
- reflex/components/lucide/icon.py +3 -127
- reflex/components/lucide/icon.pyi +31 -160
- reflex/components/markdown/markdown.py +32 -42
- reflex/components/markdown/markdown.pyi +28 -41
- reflex/components/moment/moment.py +13 -12
- reflex/components/moment/moment.pyi +22 -33
- reflex/components/next/base.pyi +16 -26
- reflex/components/next/image.py +1 -5
- reflex/components/next/image.pyi +21 -35
- reflex/components/next/link.py +1 -1
- reflex/components/next/link.pyi +16 -26
- reflex/components/next/video.py +1 -1
- reflex/components/next/video.pyi +16 -26
- reflex/components/plotly/plotly.py +17 -30
- reflex/components/plotly/plotly.pyi +38 -52
- reflex/components/props.py +21 -10
- reflex/components/radix/__init__.pyi +2 -1
- reflex/components/radix/primitives/accordion.py +6 -7
- reflex/components/radix/primitives/accordion.pyi +415 -485
- reflex/components/radix/primitives/base.py +1 -1
- reflex/components/radix/primitives/base.pyi +31 -51
- reflex/components/radix/primitives/drawer.py +1 -1
- reflex/components/radix/primitives/drawer.pyi +162 -262
- reflex/components/radix/primitives/form.py +1 -1
- reflex/components/radix/primitives/form.pyi +247 -353
- reflex/components/radix/primitives/progress.py +1 -1
- reflex/components/radix/primitives/progress.pyi +226 -276
- reflex/components/radix/primitives/slider.py +1 -1
- reflex/components/radix/primitives/slider.pyi +82 -132
- reflex/components/radix/themes/base.py +8 -25
- reflex/components/radix/themes/base.pyi +171 -242
- reflex/components/radix/themes/color_mode.py +11 -20
- reflex/components/radix/themes/color_mode.pyi +198 -231
- reflex/components/radix/themes/components/__init__.pyi +1 -0
- reflex/components/radix/themes/components/alert_dialog.py +1 -1
- reflex/components/radix/themes/components/alert_dialog.pyi +129 -199
- reflex/components/radix/themes/components/aspect_ratio.py +1 -1
- reflex/components/radix/themes/components/aspect_ratio.pyi +16 -26
- reflex/components/radix/themes/components/avatar.py +1 -1
- reflex/components/radix/themes/components/avatar.pyi +69 -79
- reflex/components/radix/themes/components/badge.py +1 -1
- reflex/components/radix/themes/components/badge.pyi +87 -97
- reflex/components/radix/themes/components/button.py +1 -1
- reflex/components/radix/themes/components/button.pyi +97 -107
- reflex/components/radix/themes/components/callout.py +1 -1
- reflex/components/radix/themes/components/callout.pyi +317 -367
- reflex/components/radix/themes/components/card.py +1 -1
- reflex/components/radix/themes/components/card.pyi +37 -47
- reflex/components/radix/themes/components/checkbox.py +2 -4
- reflex/components/radix/themes/components/checkbox.pyi +205 -241
- reflex/components/radix/themes/components/checkbox_cards.py +1 -1
- reflex/components/radix/themes/components/checkbox_cards.pyi +92 -112
- reflex/components/radix/themes/components/checkbox_group.py +1 -1
- reflex/components/radix/themes/components/checkbox_group.pyi +84 -104
- reflex/components/radix/themes/components/context_menu.py +1 -1
- reflex/components/radix/themes/components/context_menu.pyi +230 -310
- reflex/components/radix/themes/components/data_list.py +6 -1
- reflex/components/radix/themes/components/data_list.pyi +131 -166
- reflex/components/radix/themes/components/dialog.py +1 -1
- reflex/components/radix/themes/components/dialog.pyi +132 -202
- reflex/components/radix/themes/components/dropdown_menu.py +1 -1
- reflex/components/radix/themes/components/dropdown_menu.pyi +241 -323
- reflex/components/radix/themes/components/hover_card.py +1 -1
- reflex/components/radix/themes/components/hover_card.pyi +86 -126
- reflex/components/radix/themes/components/icon_button.py +1 -1
- reflex/components/radix/themes/components/icon_button.pyi +97 -107
- reflex/components/radix/themes/components/inset.py +1 -1
- reflex/components/radix/themes/components/inset.pyi +46 -56
- reflex/components/radix/themes/components/popover.py +1 -1
- reflex/components/radix/themes/components/popover.pyi +91 -131
- reflex/components/radix/themes/components/progress.py +1 -1
- reflex/components/radix/themes/components/progress.pyi +70 -80
- reflex/components/radix/themes/components/radio.py +1 -1
- reflex/components/radix/themes/components/radio.pyi +68 -78
- reflex/components/radix/themes/components/radio_cards.py +1 -1
- reflex/components/radix/themes/components/radio_cards.pyi +96 -116
- reflex/components/radix/themes/components/radio_group.py +32 -31
- reflex/components/radix/themes/components/radio_group.pyi +230 -266
- reflex/components/radix/themes/components/scroll_area.py +1 -1
- reflex/components/radix/themes/components/scroll_area.pyi +20 -30
- reflex/components/radix/themes/components/segmented_control.py +1 -1
- reflex/components/radix/themes/components/segmented_control.pyi +88 -110
- reflex/components/radix/themes/components/select.py +1 -1
- reflex/components/radix/themes/components/select.pyi +365 -461
- reflex/components/radix/themes/components/separator.py +2 -4
- reflex/components/radix/themes/components/separator.pyi +68 -78
- reflex/components/radix/themes/components/skeleton.py +1 -1
- reflex/components/radix/themes/components/skeleton.pyi +22 -32
- reflex/components/radix/themes/components/slider.py +1 -1
- reflex/components/radix/themes/components/slider.pyi +74 -86
- reflex/components/radix/themes/components/spinner.py +1 -1
- reflex/components/radix/themes/components/spinner.pyi +18 -28
- reflex/components/radix/themes/components/switch.py +1 -1
- reflex/components/radix/themes/components/switch.pyi +70 -82
- reflex/components/radix/themes/components/table.py +1 -1
- reflex/components/radix/themes/components/table.pyi +254 -324
- reflex/components/radix/themes/components/tabs.py +1 -1
- reflex/components/radix/themes/components/tabs.pyi +134 -188
- reflex/components/radix/themes/components/text_area.py +1 -1
- reflex/components/radix/themes/components/text_area.pyi +95 -109
- reflex/components/radix/themes/components/text_field.py +1 -80
- reflex/components/radix/themes/components/text_field.pyi +245 -290
- reflex/components/radix/themes/components/tooltip.py +1 -1
- reflex/components/radix/themes/components/tooltip.pyi +25 -35
- reflex/components/radix/themes/layout/__init__.pyi +1 -0
- reflex/components/radix/themes/layout/base.py +1 -1
- reflex/components/radix/themes/layout/base.pyi +55 -65
- reflex/components/radix/themes/layout/box.pyi +33 -43
- reflex/components/radix/themes/layout/center.pyi +55 -65
- reflex/components/radix/themes/layout/container.py +2 -4
- reflex/components/radix/themes/layout/container.pyi +35 -45
- reflex/components/radix/themes/layout/flex.py +1 -1
- reflex/components/radix/themes/layout/flex.pyi +55 -65
- reflex/components/radix/themes/layout/grid.py +1 -1
- reflex/components/radix/themes/layout/grid.pyi +63 -73
- reflex/components/radix/themes/layout/list.py +1 -1
- reflex/components/radix/themes/layout/list.pyi +188 -238
- reflex/components/radix/themes/layout/section.py +2 -4
- reflex/components/radix/themes/layout/section.pyi +35 -45
- reflex/components/radix/themes/layout/spacer.pyi +55 -65
- reflex/components/radix/themes/layout/stack.py +1 -1
- reflex/components/radix/themes/layout/stack.pyi +125 -155
- reflex/components/radix/themes/typography/blockquote.py +1 -1
- reflex/components/radix/themes/typography/blockquote.pyi +88 -98
- reflex/components/radix/themes/typography/code.py +1 -1
- reflex/components/radix/themes/typography/code.pyi +89 -99
- reflex/components/radix/themes/typography/heading.py +1 -1
- reflex/components/radix/themes/typography/heading.pyi +95 -105
- reflex/components/radix/themes/typography/link.py +1 -1
- reflex/components/radix/themes/typography/link.pyi +101 -111
- reflex/components/radix/themes/typography/text.py +1 -1
- reflex/components/radix/themes/typography/text.pyi +494 -564
- reflex/components/react_player/audio.pyi +32 -58
- reflex/components/react_player/react_player.py +1 -1
- reflex/components/react_player/react_player.pyi +32 -58
- reflex/components/react_player/video.pyi +32 -58
- reflex/components/recharts/cartesian.py +22 -19
- reflex/components/recharts/cartesian.pyi +658 -840
- reflex/components/recharts/charts.py +3 -3
- reflex/components/recharts/charts.pyi +238 -342
- reflex/components/recharts/general.py +8 -8
- reflex/components/recharts/general.pyi +175 -225
- reflex/components/recharts/polar.py +11 -11
- reflex/components/recharts/polar.pyi +135 -171
- reflex/components/recharts/recharts.pyi +31 -51
- reflex/components/sonner/toast.py +27 -31
- reflex/components/sonner/toast.pyi +36 -45
- reflex/components/suneditor/editor.py +1 -1
- reflex/components/suneditor/editor.pyi +54 -76
- reflex/components/tags/cond_tag.py +6 -4
- reflex/components/tags/iter_tag.py +37 -25
- reflex/components/tags/match_tag.py +6 -4
- reflex/components/tags/tag.py +43 -28
- reflex/constants/base.py +3 -1
- reflex/constants/event.py +1 -0
- reflex/custom_components/custom_components.py +3 -1
- reflex/event.py +166 -108
- reflex/experimental/__init__.py +25 -6
- reflex/experimental/client_state.py +34 -57
- reflex/experimental/hooks.py +12 -17
- reflex/experimental/layout.py +4 -4
- reflex/experimental/layout.pyi +130 -180
- reflex/middleware/hydrate_middleware.py +2 -0
- reflex/middleware/middleware.py +3 -3
- reflex/model.py +22 -0
- reflex/reflex.py +4 -0
- reflex/state.py +491 -110
- reflex/style.py +56 -39
- reflex/testing.py +8 -3
- reflex/utils/exceptions.py +32 -0
- reflex/utils/exec.py +0 -14
- reflex/utils/format.py +80 -209
- reflex/utils/imports.py +16 -73
- reflex/utils/net.py +43 -0
- reflex/utils/path_ops.py +13 -1
- reflex/utils/prerequisites.py +81 -41
- reflex/utils/pyi_generator.py +12 -6
- reflex/utils/serializers.py +13 -41
- reflex/utils/telemetry.py +3 -2
- reflex/utils/types.py +47 -7
- reflex/{experimental/vars → vars}/__init__.py +6 -3
- reflex/vars/base.py +2563 -0
- reflex/vars/function.py +196 -0
- reflex/vars/number.py +1158 -0
- reflex/vars/object.py +562 -0
- reflex/vars/sequence.py +1604 -0
- {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/METADATA +6 -9
- reflex-0.6.0.dist-info/RECORD +382 -0
- reflex/.templates/apps/demo/.gitignore +0 -4
- reflex/.templates/apps/demo/assets/favicon.ico +0 -0
- reflex/.templates/apps/demo/assets/github.svg +0 -10
- reflex/.templates/apps/demo/assets/icon.svg +0 -37
- reflex/.templates/apps/demo/assets/logo.svg +0 -68
- reflex/.templates/apps/demo/assets/paneleft.svg +0 -13
- reflex/.templates/apps/demo/code/__init__.py +0 -1
- reflex/.templates/apps/demo/code/demo.py +0 -127
- reflex/.templates/apps/demo/code/pages/__init__.py +0 -7
- reflex/.templates/apps/demo/code/pages/chatapp.py +0 -31
- reflex/.templates/apps/demo/code/pages/datatable.py +0 -360
- reflex/.templates/apps/demo/code/pages/forms.py +0 -257
- reflex/.templates/apps/demo/code/pages/graphing.py +0 -253
- reflex/.templates/apps/demo/code/pages/home.py +0 -56
- reflex/.templates/apps/demo/code/sidebar.py +0 -178
- reflex/.templates/apps/demo/code/state.py +0 -22
- reflex/.templates/apps/demo/code/states/form_state.py +0 -40
- reflex/.templates/apps/demo/code/states/pie_state.py +0 -47
- reflex/.templates/apps/demo/code/styles.py +0 -68
- reflex/.templates/apps/demo/code/webui/__init__.py +0 -0
- reflex/.templates/apps/demo/code/webui/components/__init__.py +0 -4
- reflex/.templates/apps/demo/code/webui/components/chat.py +0 -118
- reflex/.templates/apps/demo/code/webui/components/loading_icon.py +0 -19
- reflex/.templates/apps/demo/code/webui/components/modal.py +0 -56
- reflex/.templates/apps/demo/code/webui/components/navbar.py +0 -70
- reflex/.templates/apps/demo/code/webui/components/sidebar.py +0 -66
- reflex/.templates/apps/demo/code/webui/state.py +0 -146
- reflex/.templates/apps/demo/code/webui/styles.py +0 -88
- reflex/.templates/web/components/reflex/chakra_color_mode_provider.js +0 -36
- reflex/experimental/vars/base.py +0 -583
- reflex/experimental/vars/function.py +0 -290
- reflex/experimental/vars/number.py +0 -1458
- reflex/experimental/vars/object.py +0 -804
- reflex/experimental/vars/sequence.py +0 -1764
- reflex/utils/watch.py +0 -96
- reflex/vars.py +0 -2604
- reflex/vars.pyi +0 -218
- reflex-0.5.10a3.dist-info/RECORD +0 -413
- {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/LICENSE +0 -0
- {reflex-0.5.10a3.dist-info → reflex-0.6.0.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
self
|
|
162
|
-
self
|
|
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,
|
|
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__.
|
|
428
|
-
if
|
|
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.
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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:
|
|
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 = {
|
|
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
|
|
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.
|
|
559
|
-
cls.vars[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
|
-
|
|
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
|
|
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
|
|
758
|
+
def _check_overridden_basevars(cls):
|
|
720
759
|
"""Check for shadow base vars and raise error if any.
|
|
721
760
|
|
|
722
761
|
Raises:
|
|
723
|
-
|
|
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_.
|
|
727
|
-
raise
|
|
728
|
-
f"The computed var name `{computed_var_.
|
|
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:
|
|
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.
|
|
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 =
|
|
894
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
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
|
|
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 =
|
|
2396
|
+
self._self_mutable = original_mutable
|
|
2280
2397
|
|
|
2281
2398
|
|
|
2282
|
-
|
|
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
|
|
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
|
-
|
|
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()
|