reflex 0.6.6.post3__py3-none-any.whl → 0.6.7__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/web/pages/stateful_component.js.jinja2 +5 -1
- reflex/.templates/web/utils/state.js +36 -28
- reflex/__init__.py +1 -1
- reflex/__init__.pyi +1 -0
- reflex/app.py +41 -16
- reflex/assets.py +2 -2
- reflex/base.py +8 -7
- reflex/compiler/templates.py +1 -0
- reflex/compiler/utils.py +2 -3
- reflex/components/base/bare.py +2 -2
- reflex/components/component.py +54 -29
- reflex/components/core/banner.py +2 -2
- reflex/components/core/banner.pyi +1 -1
- reflex/components/core/client_side_routing.py +2 -2
- reflex/components/core/client_side_routing.pyi +1 -1
- reflex/components/core/clipboard.py +11 -9
- reflex/components/core/clipboard.pyi +1 -1
- reflex/components/core/cond.py +3 -3
- reflex/components/core/foreach.py +1 -1
- reflex/components/core/html.pyi +1 -1
- reflex/components/core/upload.py +8 -8
- reflex/components/datadisplay/code.py +5 -5
- reflex/components/datadisplay/dataeditor.py +8 -28
- reflex/components/datadisplay/dataeditor.pyi +1 -1
- reflex/components/datadisplay/shiki_code_block.py +7 -7
- reflex/components/dynamic.py +2 -2
- reflex/components/el/elements/__init__.py +1 -1
- reflex/components/el/elements/__init__.pyi +1 -1
- reflex/components/el/elements/base.py +2 -2
- reflex/components/el/elements/base.pyi +1 -1
- reflex/components/el/elements/forms.py +40 -10
- reflex/components/el/elements/forms.pyi +17 -15
- reflex/components/el/elements/inline.py +1 -1
- reflex/components/el/elements/inline.pyi +28 -28
- reflex/components/el/elements/media.py +1 -4
- reflex/components/el/elements/media.pyi +25 -26
- reflex/components/el/elements/metadata.py +6 -6
- reflex/components/el/elements/metadata.pyi +4 -4
- reflex/components/el/elements/other.py +17 -9
- reflex/components/el/elements/other.pyi +7 -7
- reflex/components/el/elements/scripts.py +1 -2
- reflex/components/el/elements/scripts.pyi +3 -3
- reflex/components/el/elements/sectioning.py +16 -16
- reflex/components/el/elements/sectioning.pyi +15 -15
- reflex/components/el/elements/tables.py +1 -1
- reflex/components/el/elements/tables.pyi +10 -10
- reflex/components/el/elements/typography.py +1 -1
- reflex/components/el/elements/typography.pyi +15 -15
- reflex/components/markdown/markdown.py +3 -3
- reflex/components/next/image.py +1 -1
- reflex/components/next/image.pyi +1 -1
- reflex/components/plotly/plotly.py +2 -2
- reflex/components/radix/primitives/accordion.py +2 -1
- reflex/components/radix/primitives/form.pyi +3 -3
- reflex/components/radix/primitives/slider.py +1 -1
- reflex/components/radix/themes/base.py +4 -10
- reflex/components/radix/themes/color_mode.pyi +2 -2
- reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
- reflex/components/radix/themes/components/badge.pyi +1 -1
- reflex/components/radix/themes/components/button.pyi +1 -1
- reflex/components/radix/themes/components/callout.pyi +5 -5
- reflex/components/radix/themes/components/card.pyi +1 -1
- reflex/components/radix/themes/components/checkbox.pyi +3 -3
- reflex/components/radix/themes/components/context_menu.py +11 -0
- reflex/components/radix/themes/components/context_menu.pyi +155 -0
- reflex/components/radix/themes/components/dialog.pyi +1 -1
- reflex/components/radix/themes/components/hover_card.pyi +1 -1
- reflex/components/radix/themes/components/icon_button.py +1 -1
- reflex/components/radix/themes/components/icon_button.pyi +1 -1
- reflex/components/radix/themes/components/inset.pyi +1 -1
- reflex/components/radix/themes/components/popover.pyi +1 -1
- reflex/components/radix/themes/components/radio_group.py +2 -4
- reflex/components/radix/themes/components/radio_group.pyi +1 -1
- reflex/components/radix/themes/components/select.pyi +3 -3
- reflex/components/radix/themes/components/slider.pyi +1 -1
- reflex/components/radix/themes/components/switch.pyi +1 -1
- reflex/components/radix/themes/components/table.pyi +7 -7
- reflex/components/radix/themes/components/tabs.pyi +2 -2
- reflex/components/radix/themes/components/text_area.py +3 -0
- reflex/components/radix/themes/components/text_area.pyi +3 -1
- reflex/components/radix/themes/components/text_field.py +16 -1
- reflex/components/radix/themes/components/text_field.pyi +105 -17
- reflex/components/radix/themes/layout/box.pyi +1 -1
- reflex/components/radix/themes/layout/center.pyi +1 -1
- reflex/components/radix/themes/layout/flex.pyi +1 -1
- reflex/components/radix/themes/layout/grid.pyi +1 -1
- reflex/components/radix/themes/layout/list.py +0 -4
- reflex/components/radix/themes/layout/list.pyi +3 -8
- reflex/components/radix/themes/layout/section.pyi +1 -1
- reflex/components/radix/themes/layout/spacer.pyi +1 -1
- reflex/components/radix/themes/layout/stack.pyi +3 -3
- reflex/components/radix/themes/typography/blockquote.pyi +1 -1
- reflex/components/radix/themes/typography/code.pyi +1 -1
- reflex/components/radix/themes/typography/heading.pyi +1 -1
- reflex/components/radix/themes/typography/link.py +5 -1
- reflex/components/radix/themes/typography/link.pyi +1 -1
- reflex/components/radix/themes/typography/text.pyi +7 -7
- reflex/components/recharts/cartesian.py +1 -1
- reflex/components/recharts/charts.py +4 -4
- reflex/components/recharts/polar.py +1 -1
- reflex/components/recharts/polar.pyi +1 -1
- reflex/components/sonner/toast.py +4 -7
- reflex/components/suneditor/editor.py +6 -6
- reflex/components/suneditor/editor.pyi +6 -6
- reflex/config.py +25 -10
- reflex/constants/compiler.py +6 -0
- reflex/constants/config.py +2 -0
- reflex/constants/custom_components.py +1 -1
- reflex/constants/route.py +1 -1
- reflex/custom_components/custom_components.py +21 -21
- reflex/event.py +57 -22
- reflex/experimental/client_state.py +2 -1
- reflex/experimental/layout.py +0 -6
- reflex/model.py +125 -9
- reflex/reflex.py +5 -6
- reflex/state.py +200 -88
- reflex/style.py +1 -4
- reflex/testing.py +10 -11
- reflex/utils/build.py +1 -1
- reflex/utils/console.py +75 -6
- reflex/utils/exceptions.py +12 -0
- reflex/utils/exec.py +10 -10
- reflex/utils/export.py +1 -2
- reflex/utils/format.py +11 -8
- reflex/utils/path_ops.py +2 -2
- reflex/utils/prerequisites.py +31 -28
- reflex/utils/processes.py +4 -4
- reflex/utils/pyi_generator.py +12 -11
- reflex/utils/types.py +6 -3
- reflex/vars/__init__.py +1 -0
- reflex/vars/base.py +75 -38
- reflex/vars/datetime.py +222 -0
- reflex/vars/function.py +3 -3
- reflex/vars/number.py +3 -3
- reflex/vars/object.py +5 -5
- reflex/vars/sequence.py +7 -7
- {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/METADATA +2 -2
- {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/RECORD +141 -140
- {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/LICENSE +0 -0
- {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/WHEEL +0 -0
- {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/entry_points.txt +0 -0
reflex/state.py
CHANGED
|
@@ -11,6 +11,7 @@ import inspect
|
|
|
11
11
|
import json
|
|
12
12
|
import pickle
|
|
13
13
|
import sys
|
|
14
|
+
import time
|
|
14
15
|
import typing
|
|
15
16
|
import uuid
|
|
16
17
|
from abc import ABC, abstractmethod
|
|
@@ -39,6 +40,7 @@ from typing import (
|
|
|
39
40
|
get_type_hints,
|
|
40
41
|
)
|
|
41
42
|
|
|
43
|
+
from redis.asyncio.client import PubSub
|
|
42
44
|
from sqlalchemy.orm import DeclarativeBase
|
|
43
45
|
from typing_extensions import Self
|
|
44
46
|
|
|
@@ -69,6 +71,11 @@ try:
|
|
|
69
71
|
except ModuleNotFoundError:
|
|
70
72
|
BaseModelV1 = BaseModelV2
|
|
71
73
|
|
|
74
|
+
try:
|
|
75
|
+
from pydantic.v1 import validator
|
|
76
|
+
except ModuleNotFoundError:
|
|
77
|
+
from pydantic import validator
|
|
78
|
+
|
|
72
79
|
import wrapt
|
|
73
80
|
from redis.asyncio import Redis
|
|
74
81
|
from redis.exceptions import ResponseError
|
|
@@ -92,11 +99,13 @@ from reflex.utils.exceptions import (
|
|
|
92
99
|
DynamicRouteArgShadowsStateVar,
|
|
93
100
|
EventHandlerShadowsBuiltInStateMethod,
|
|
94
101
|
ImmutableStateError,
|
|
102
|
+
InvalidLockWarningThresholdError,
|
|
95
103
|
InvalidStateManagerMode,
|
|
96
104
|
LockExpiredError,
|
|
97
105
|
ReflexRuntimeError,
|
|
98
106
|
SetUndefinedStateVarError,
|
|
99
107
|
StateSchemaMismatchError,
|
|
108
|
+
StateSerializationError,
|
|
100
109
|
StateTooLargeError,
|
|
101
110
|
)
|
|
102
111
|
from reflex.utils.exec import is_testing_env
|
|
@@ -104,6 +113,7 @@ from reflex.utils.serializers import serializer
|
|
|
104
113
|
from reflex.utils.types import (
|
|
105
114
|
_isinstance,
|
|
106
115
|
get_origin,
|
|
116
|
+
is_optional,
|
|
107
117
|
is_union,
|
|
108
118
|
override,
|
|
109
119
|
value_inside_optional,
|
|
@@ -278,6 +288,22 @@ if TYPE_CHECKING:
|
|
|
278
288
|
from pydantic.v1.fields import ModelField
|
|
279
289
|
|
|
280
290
|
|
|
291
|
+
def _unwrap_field_type(type_: Type) -> Type:
|
|
292
|
+
"""Unwrap rx.Field type annotations.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
type_: The type to unwrap.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
The unwrapped type.
|
|
299
|
+
"""
|
|
300
|
+
from reflex.vars import Field
|
|
301
|
+
|
|
302
|
+
if get_origin(type_) is Field:
|
|
303
|
+
return get_args(type_)[0]
|
|
304
|
+
return type_
|
|
305
|
+
|
|
306
|
+
|
|
281
307
|
def get_var_for_field(cls: Type[BaseState], f: ModelField):
|
|
282
308
|
"""Get a Var instance for a Pydantic field.
|
|
283
309
|
|
|
@@ -288,16 +314,12 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField):
|
|
|
288
314
|
Returns:
|
|
289
315
|
The Var instance.
|
|
290
316
|
"""
|
|
291
|
-
from reflex.vars import Field
|
|
292
|
-
|
|
293
317
|
field_name = format.format_state_name(cls.get_full_name()) + "." + f.name
|
|
294
318
|
|
|
295
319
|
return dispatch(
|
|
296
320
|
field_name=field_name,
|
|
297
321
|
var_data=VarData.from_state(cls, f.name),
|
|
298
|
-
result_var_type=f.outer_type_
|
|
299
|
-
if get_origin(f.outer_type_) is not Field
|
|
300
|
-
else get_args(f.outer_type_)[0],
|
|
322
|
+
result_var_type=_unwrap_field_type(f.outer_type_),
|
|
301
323
|
)
|
|
302
324
|
|
|
303
325
|
|
|
@@ -415,9 +437,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
415
437
|
)
|
|
416
438
|
|
|
417
439
|
# Create a fresh copy of the backend variables for this instance
|
|
418
|
-
self._backend_vars = copy.deepcopy(
|
|
419
|
-
{name: item for name, item in self.backend_vars.items()}
|
|
420
|
-
)
|
|
440
|
+
self._backend_vars = copy.deepcopy(self.backend_vars)
|
|
421
441
|
|
|
422
442
|
def __repr__(self) -> str:
|
|
423
443
|
"""Get the string representation of the state.
|
|
@@ -425,7 +445,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
425
445
|
Returns:
|
|
426
446
|
The string representation of the state.
|
|
427
447
|
"""
|
|
428
|
-
return f"{self.
|
|
448
|
+
return f"{type(self).__name__}({self.dict()})"
|
|
429
449
|
|
|
430
450
|
@classmethod
|
|
431
451
|
def _get_computed_vars(cls) -> list[ComputedVar]:
|
|
@@ -436,7 +456,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
436
456
|
"""
|
|
437
457
|
return [
|
|
438
458
|
v
|
|
439
|
-
for mixin in cls._mixins()
|
|
459
|
+
for mixin in [*cls._mixins(), cls]
|
|
440
460
|
for name, v in mixin.__dict__.items()
|
|
441
461
|
if is_computed_var(v) and name not in cls.inherited_vars
|
|
442
462
|
]
|
|
@@ -501,9 +521,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
501
521
|
cls.inherited_backend_vars = parent_state.backend_vars
|
|
502
522
|
|
|
503
523
|
# Check if another substate class with the same name has already been defined.
|
|
504
|
-
if cls.get_name() in
|
|
505
|
-
c.get_name() for c in parent_state.class_subclasses
|
|
506
|
-
):
|
|
524
|
+
if cls.get_name() in {c.get_name() for c in parent_state.class_subclasses}:
|
|
507
525
|
# This should not happen, since we have added module prefix to state names in #3214
|
|
508
526
|
raise StateValueError(
|
|
509
527
|
f"The substate class '{cls.get_name()}' has been defined multiple times. "
|
|
@@ -766,11 +784,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
766
784
|
)
|
|
767
785
|
|
|
768
786
|
# ComputedVar with cache=False always need to be recomputed
|
|
769
|
-
cls._always_dirty_computed_vars =
|
|
787
|
+
cls._always_dirty_computed_vars = {
|
|
770
788
|
cvar_name
|
|
771
789
|
for cvar_name, cvar in cls.computed_vars.items()
|
|
772
790
|
if not cvar._cache
|
|
773
|
-
|
|
791
|
+
}
|
|
774
792
|
|
|
775
793
|
# Any substate containing a ComputedVar with cache=False always needs to be recomputed
|
|
776
794
|
if cls._always_dirty_computed_vars:
|
|
@@ -1081,6 +1099,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1081
1099
|
if (
|
|
1082
1100
|
not field.required
|
|
1083
1101
|
and field.default is None
|
|
1102
|
+
and field.default_factory is None
|
|
1084
1103
|
and not types.is_optional(prop._var_type)
|
|
1085
1104
|
):
|
|
1086
1105
|
# Ensure frontend uses null coalescing when accessing.
|
|
@@ -1310,8 +1329,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1310
1329
|
|
|
1311
1330
|
if name in fields:
|
|
1312
1331
|
field = fields[name]
|
|
1313
|
-
field_type = field.outer_type_
|
|
1314
|
-
if field.allow_none:
|
|
1332
|
+
field_type = _unwrap_field_type(field.outer_type_)
|
|
1333
|
+
if field.allow_none and not is_optional(field_type):
|
|
1315
1334
|
field_type = Union[field_type, None]
|
|
1316
1335
|
if not _isinstance(value, field_type):
|
|
1317
1336
|
console.deprecate(
|
|
@@ -1836,11 +1855,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1836
1855
|
Returns:
|
|
1837
1856
|
Set of computed vars to include in the delta.
|
|
1838
1857
|
"""
|
|
1839
|
-
return
|
|
1858
|
+
return {
|
|
1840
1859
|
cvar
|
|
1841
1860
|
for cvar in self.computed_vars
|
|
1842
1861
|
if self.computed_vars[cvar].needs_update(instance=self)
|
|
1843
|
-
|
|
1862
|
+
}
|
|
1844
1863
|
|
|
1845
1864
|
def _dirty_computed_vars(
|
|
1846
1865
|
self, from_vars: set[str] | None = None, include_backend: bool = True
|
|
@@ -1854,12 +1873,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1854
1873
|
Returns:
|
|
1855
1874
|
Set of computed vars to include in the delta.
|
|
1856
1875
|
"""
|
|
1857
|
-
return
|
|
1876
|
+
return {
|
|
1858
1877
|
cvar
|
|
1859
1878
|
for dirty_var in from_vars or self.dirty_vars
|
|
1860
1879
|
for cvar in self._computed_var_dependencies[dirty_var]
|
|
1861
1880
|
if include_backend or not self.computed_vars[cvar]._backend
|
|
1862
|
-
|
|
1881
|
+
}
|
|
1863
1882
|
|
|
1864
1883
|
@classmethod
|
|
1865
1884
|
def _potentially_dirty_substates(cls) -> set[Type[BaseState]]:
|
|
@@ -1869,16 +1888,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1869
1888
|
Set of State classes that may need to be fetched to recalc computed vars.
|
|
1870
1889
|
"""
|
|
1871
1890
|
# _always_dirty_substates need to be fetched to recalc computed vars.
|
|
1872
|
-
fetch_substates =
|
|
1891
|
+
fetch_substates = {
|
|
1873
1892
|
cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
|
|
1874
1893
|
for substate_name in cls._always_dirty_substates
|
|
1875
|
-
|
|
1894
|
+
}
|
|
1876
1895
|
for dependent_substates in cls._substate_var_dependencies.values():
|
|
1877
1896
|
fetch_substates.update(
|
|
1878
|
-
|
|
1897
|
+
{
|
|
1879
1898
|
cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
|
|
1880
1899
|
for substate_name in dependent_substates
|
|
1881
|
-
|
|
1900
|
+
}
|
|
1882
1901
|
)
|
|
1883
1902
|
return fetch_substates
|
|
1884
1903
|
|
|
@@ -2105,14 +2124,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
2105
2124
|
state["__dict__"].pop("router", None)
|
|
2106
2125
|
state["__dict__"].pop("router_data", None)
|
|
2107
2126
|
# Never serialize parent_state or substates.
|
|
2108
|
-
state["__dict__"]
|
|
2109
|
-
state["__dict__"]
|
|
2127
|
+
state["__dict__"].pop("parent_state", None)
|
|
2128
|
+
state["__dict__"].pop("substates", None)
|
|
2110
2129
|
state["__dict__"].pop("_was_touched", None)
|
|
2111
2130
|
# Remove all inherited vars.
|
|
2112
2131
|
for inherited_var_name in self.inherited_vars:
|
|
2113
2132
|
state["__dict__"].pop(inherited_var_name, None)
|
|
2114
2133
|
return state
|
|
2115
2134
|
|
|
2135
|
+
def __setstate__(self, state: dict[str, Any]):
|
|
2136
|
+
"""Set the state from redis deserialization.
|
|
2137
|
+
|
|
2138
|
+
This method is called by pickle to deserialize the object.
|
|
2139
|
+
|
|
2140
|
+
Args:
|
|
2141
|
+
state: The state dict for deserialization.
|
|
2142
|
+
"""
|
|
2143
|
+
state["__dict__"]["parent_state"] = None
|
|
2144
|
+
state["__dict__"]["substates"] = {}
|
|
2145
|
+
super().__setstate__(state)
|
|
2146
|
+
|
|
2116
2147
|
def _check_state_size(
|
|
2117
2148
|
self,
|
|
2118
2149
|
pickle_state_size: int,
|
|
@@ -2168,7 +2199,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
2168
2199
|
|
|
2169
2200
|
return md5(
|
|
2170
2201
|
pickle.dumps(
|
|
2171
|
-
|
|
2202
|
+
sorted(_field_tuple(field_name) for field_name in cls.base_vars)
|
|
2172
2203
|
)
|
|
2173
2204
|
).hexdigest()
|
|
2174
2205
|
|
|
@@ -2177,12 +2208,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
2177
2208
|
|
|
2178
2209
|
Returns:
|
|
2179
2210
|
The serialized state.
|
|
2211
|
+
|
|
2212
|
+
Raises:
|
|
2213
|
+
StateSerializationError: If the state cannot be serialized.
|
|
2180
2214
|
"""
|
|
2215
|
+
payload = b""
|
|
2216
|
+
error = ""
|
|
2181
2217
|
try:
|
|
2182
|
-
|
|
2183
|
-
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
|
|
2184
|
-
self._check_state_size(len(pickle_state))
|
|
2185
|
-
return pickle_state
|
|
2218
|
+
payload = pickle.dumps((self._to_schema(), self))
|
|
2186
2219
|
except HANDLED_PICKLE_ERRORS as og_pickle_error:
|
|
2187
2220
|
error = (
|
|
2188
2221
|
f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
|
|
@@ -2191,7 +2224,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
2191
2224
|
try:
|
|
2192
2225
|
import dill
|
|
2193
2226
|
|
|
2194
|
-
|
|
2227
|
+
payload = dill.dumps((self._to_schema(), self))
|
|
2195
2228
|
except ImportError:
|
|
2196
2229
|
error += (
|
|
2197
2230
|
f"Pickle error: {og_pickle_error}. "
|
|
@@ -2199,8 +2232,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
2199
2232
|
)
|
|
2200
2233
|
except HANDLED_PICKLE_ERRORS as ex:
|
|
2201
2234
|
error += f"Dill was also unable to pickle the state: {ex}"
|
|
2202
|
-
|
|
2203
|
-
|
|
2235
|
+
console.warn(error)
|
|
2236
|
+
|
|
2237
|
+
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
|
|
2238
|
+
self._check_state_size(len(payload))
|
|
2239
|
+
|
|
2240
|
+
if not payload:
|
|
2241
|
+
raise StateSerializationError(error)
|
|
2242
|
+
|
|
2243
|
+
return payload
|
|
2204
2244
|
|
|
2205
2245
|
@classmethod
|
|
2206
2246
|
def _deserialize(
|
|
@@ -2793,6 +2833,7 @@ class StateManager(Base, ABC):
|
|
|
2793
2833
|
redis=redis,
|
|
2794
2834
|
token_expiration=config.redis_token_expiration,
|
|
2795
2835
|
lock_expiration=config.redis_lock_expiration,
|
|
2836
|
+
lock_warning_threshold=config.redis_lock_warning_threshold,
|
|
2796
2837
|
)
|
|
2797
2838
|
raise InvalidStateManagerMode(
|
|
2798
2839
|
f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
|
|
@@ -3162,6 +3203,15 @@ def _default_lock_expiration() -> int:
|
|
|
3162
3203
|
return get_config().redis_lock_expiration
|
|
3163
3204
|
|
|
3164
3205
|
|
|
3206
|
+
def _default_lock_warning_threshold() -> int:
|
|
3207
|
+
"""Get the default lock warning threshold.
|
|
3208
|
+
|
|
3209
|
+
Returns:
|
|
3210
|
+
The default lock warning threshold.
|
|
3211
|
+
"""
|
|
3212
|
+
return get_config().redis_lock_warning_threshold
|
|
3213
|
+
|
|
3214
|
+
|
|
3165
3215
|
class StateManagerRedis(StateManager):
|
|
3166
3216
|
"""A state manager that stores states in redis."""
|
|
3167
3217
|
|
|
@@ -3174,6 +3224,11 @@ class StateManagerRedis(StateManager):
|
|
|
3174
3224
|
# The maximum time to hold a lock (ms).
|
|
3175
3225
|
lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
|
|
3176
3226
|
|
|
3227
|
+
# The maximum time to hold a lock (ms) before warning.
|
|
3228
|
+
lock_warning_threshold: int = pydantic.Field(
|
|
3229
|
+
default_factory=_default_lock_warning_threshold
|
|
3230
|
+
)
|
|
3231
|
+
|
|
3177
3232
|
# The keyspace subscription string when redis is waiting for lock to be released
|
|
3178
3233
|
_redis_notify_keyspace_events: str = (
|
|
3179
3234
|
"K" # Enable keyspace notifications (target a particular key)
|
|
@@ -3292,7 +3347,7 @@ class StateManagerRedis(StateManager):
|
|
|
3292
3347
|
state_cls = self.state.get_class_substate(state_path)
|
|
3293
3348
|
else:
|
|
3294
3349
|
raise RuntimeError(
|
|
3295
|
-
"StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
|
|
3350
|
+
f"StateManagerRedis requires token to be specified in the form of {{token}}_{{state_full_name}}, but got {token}"
|
|
3296
3351
|
)
|
|
3297
3352
|
|
|
3298
3353
|
# The deserialized or newly created (sub)state instance.
|
|
@@ -3361,6 +3416,17 @@ class StateManagerRedis(StateManager):
|
|
|
3361
3416
|
f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
|
|
3362
3417
|
"or use `@rx.event(background=True)` decorator for long-running tasks."
|
|
3363
3418
|
)
|
|
3419
|
+
elif lock_id is not None:
|
|
3420
|
+
time_taken = self.lock_expiration / 1000 - (
|
|
3421
|
+
await self.redis.ttl(self._lock_key(token))
|
|
3422
|
+
)
|
|
3423
|
+
if time_taken > self.lock_warning_threshold / 1000:
|
|
3424
|
+
console.warn(
|
|
3425
|
+
f"Lock for token {token} was held too long {time_taken=}s, "
|
|
3426
|
+
f"use `@rx.event(background=True)` decorator for long-running tasks.",
|
|
3427
|
+
dedupe=True,
|
|
3428
|
+
)
|
|
3429
|
+
|
|
3364
3430
|
client_token, substate_name = _split_substate_key(token)
|
|
3365
3431
|
# If the substate name on the token doesn't match the instance name, it cannot have a parent.
|
|
3366
3432
|
if state.parent_state is not None and state.get_full_name() != substate_name:
|
|
@@ -3369,17 +3435,16 @@ class StateManagerRedis(StateManager):
|
|
|
3369
3435
|
)
|
|
3370
3436
|
|
|
3371
3437
|
# Recursively set_state on all known substates.
|
|
3372
|
-
tasks = [
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
state=substate,
|
|
3379
|
-
lock_id=lock_id,
|
|
3380
|
-
)
|
|
3438
|
+
tasks = [
|
|
3439
|
+
asyncio.create_task(
|
|
3440
|
+
self.set_state(
|
|
3441
|
+
_substate_key(client_token, substate),
|
|
3442
|
+
substate,
|
|
3443
|
+
lock_id,
|
|
3381
3444
|
)
|
|
3382
3445
|
)
|
|
3446
|
+
for substate in state.substates.values()
|
|
3447
|
+
]
|
|
3383
3448
|
# Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
|
|
3384
3449
|
if state._get_was_touched():
|
|
3385
3450
|
pickle_state = state._serialize()
|
|
@@ -3410,6 +3475,27 @@ class StateManagerRedis(StateManager):
|
|
|
3410
3475
|
yield state
|
|
3411
3476
|
await self.set_state(token, state, lock_id)
|
|
3412
3477
|
|
|
3478
|
+
@validator("lock_warning_threshold")
|
|
3479
|
+
@classmethod
|
|
3480
|
+
def validate_lock_warning_threshold(cls, lock_warning_threshold: int, values):
|
|
3481
|
+
"""Validate the lock warning threshold.
|
|
3482
|
+
|
|
3483
|
+
Args:
|
|
3484
|
+
lock_warning_threshold: The lock warning threshold.
|
|
3485
|
+
values: The validated attributes.
|
|
3486
|
+
|
|
3487
|
+
Returns:
|
|
3488
|
+
The lock warning threshold.
|
|
3489
|
+
|
|
3490
|
+
Raises:
|
|
3491
|
+
InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
|
|
3492
|
+
"""
|
|
3493
|
+
if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]):
|
|
3494
|
+
raise InvalidLockWarningThresholdError(
|
|
3495
|
+
f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
|
|
3496
|
+
)
|
|
3497
|
+
return lock_warning_threshold
|
|
3498
|
+
|
|
3413
3499
|
@staticmethod
|
|
3414
3500
|
def _lock_key(token: str) -> bytes:
|
|
3415
3501
|
"""Get the redis key for a token's lock.
|
|
@@ -3441,6 +3527,35 @@ class StateManagerRedis(StateManager):
|
|
|
3441
3527
|
nx=True, # only set if it doesn't exist
|
|
3442
3528
|
)
|
|
3443
3529
|
|
|
3530
|
+
async def _get_pubsub_message(
|
|
3531
|
+
self, pubsub: PubSub, timeout: float | None = None
|
|
3532
|
+
) -> None:
|
|
3533
|
+
"""Get lock release events from the pubsub.
|
|
3534
|
+
|
|
3535
|
+
Args:
|
|
3536
|
+
pubsub: The pubsub to get a message from.
|
|
3537
|
+
timeout: Remaining time to wait for a message.
|
|
3538
|
+
|
|
3539
|
+
Returns:
|
|
3540
|
+
The message.
|
|
3541
|
+
"""
|
|
3542
|
+
if timeout is None:
|
|
3543
|
+
timeout = self.lock_expiration / 1000.0
|
|
3544
|
+
|
|
3545
|
+
started = time.time()
|
|
3546
|
+
message = await pubsub.get_message(
|
|
3547
|
+
ignore_subscribe_messages=True,
|
|
3548
|
+
timeout=timeout,
|
|
3549
|
+
)
|
|
3550
|
+
if (
|
|
3551
|
+
message is None
|
|
3552
|
+
or message["data"] not in self._redis_keyspace_lock_release_events
|
|
3553
|
+
):
|
|
3554
|
+
remaining = timeout - (time.time() - started)
|
|
3555
|
+
if remaining <= 0:
|
|
3556
|
+
return
|
|
3557
|
+
await self._get_pubsub_message(pubsub, timeout=remaining)
|
|
3558
|
+
|
|
3444
3559
|
async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
|
|
3445
3560
|
"""Wait for a redis lock to be released via pubsub.
|
|
3446
3561
|
|
|
@@ -3453,7 +3568,6 @@ class StateManagerRedis(StateManager):
|
|
|
3453
3568
|
Raises:
|
|
3454
3569
|
ResponseError: when the keyspace config cannot be set.
|
|
3455
3570
|
"""
|
|
3456
|
-
state_is_locked = False
|
|
3457
3571
|
lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
|
|
3458
3572
|
# Enable keyspace notifications for the lock key, so we know when it is available.
|
|
3459
3573
|
try:
|
|
@@ -3467,20 +3581,13 @@ class StateManagerRedis(StateManager):
|
|
|
3467
3581
|
raise
|
|
3468
3582
|
async with self.redis.pubsub() as pubsub:
|
|
3469
3583
|
await pubsub.psubscribe(lock_key_channel)
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
timeout=self.lock_expiration / 1000.0,
|
|
3478
|
-
)
|
|
3479
|
-
if message is None:
|
|
3480
|
-
continue
|
|
3481
|
-
if message["data"] in self._redis_keyspace_lock_release_events:
|
|
3482
|
-
break
|
|
3483
|
-
state_is_locked = await self._try_get_lock(lock_key, lock_id)
|
|
3584
|
+
# wait for the lock to be released
|
|
3585
|
+
while True:
|
|
3586
|
+
# fast path
|
|
3587
|
+
if await self._try_get_lock(lock_key, lock_id):
|
|
3588
|
+
return
|
|
3589
|
+
# wait for lock events
|
|
3590
|
+
await self._get_pubsub_message(pubsub)
|
|
3484
3591
|
|
|
3485
3592
|
@contextlib.asynccontextmanager
|
|
3486
3593
|
async def _lock(self, token: str):
|
|
@@ -3539,33 +3646,30 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
3539
3646
|
"""A proxy for a mutable object that tracks changes."""
|
|
3540
3647
|
|
|
3541
3648
|
# Methods on wrapped objects which should mark the state as dirty.
|
|
3542
|
-
__mark_dirty_attrs__ =
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
)
|
|
3649
|
+
__mark_dirty_attrs__ = {
|
|
3650
|
+
"add",
|
|
3651
|
+
"append",
|
|
3652
|
+
"clear",
|
|
3653
|
+
"difference_update",
|
|
3654
|
+
"discard",
|
|
3655
|
+
"extend",
|
|
3656
|
+
"insert",
|
|
3657
|
+
"intersection_update",
|
|
3658
|
+
"pop",
|
|
3659
|
+
"popitem",
|
|
3660
|
+
"remove",
|
|
3661
|
+
"reverse",
|
|
3662
|
+
"setdefault",
|
|
3663
|
+
"sort",
|
|
3664
|
+
"symmetric_difference_update",
|
|
3665
|
+
"update",
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3562
3668
|
# Methods on wrapped objects might return mutable objects that should be tracked.
|
|
3563
|
-
__wrap_mutable_attrs__ =
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
]
|
|
3568
|
-
)
|
|
3669
|
+
__wrap_mutable_attrs__ = {
|
|
3670
|
+
"get",
|
|
3671
|
+
"setdefault",
|
|
3672
|
+
}
|
|
3569
3673
|
|
|
3570
3674
|
# These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
|
|
3571
3675
|
__never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
|
|
@@ -3596,11 +3700,19 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
3596
3700
|
self._self_state = state
|
|
3597
3701
|
self._self_field_name = field_name
|
|
3598
3702
|
|
|
3703
|
+
def __repr__(self) -> str:
|
|
3704
|
+
"""Get the representation of the wrapped object.
|
|
3705
|
+
|
|
3706
|
+
Returns:
|
|
3707
|
+
The representation of the wrapped object.
|
|
3708
|
+
"""
|
|
3709
|
+
return f"{type(self).__name__}({self.__wrapped__})"
|
|
3710
|
+
|
|
3599
3711
|
def _mark_dirty(
|
|
3600
3712
|
self,
|
|
3601
3713
|
wrapped=None,
|
|
3602
3714
|
instance=None,
|
|
3603
|
-
args=
|
|
3715
|
+
args=(),
|
|
3604
3716
|
kwargs=None,
|
|
3605
3717
|
) -> Any:
|
|
3606
3718
|
"""Mark the state as dirty, then call a wrapped function.
|
|
@@ -3856,7 +3968,7 @@ class ImmutableMutableProxy(MutableProxy):
|
|
|
3856
3968
|
self,
|
|
3857
3969
|
wrapped=None,
|
|
3858
3970
|
instance=None,
|
|
3859
|
-
args=
|
|
3971
|
+
args=(),
|
|
3860
3972
|
kwargs=None,
|
|
3861
3973
|
) -> Any:
|
|
3862
3974
|
"""Raise an exception when an attempt is made to modify the object.
|
reflex/style.py
CHANGED
|
@@ -74,7 +74,7 @@ def set_color_mode(
|
|
|
74
74
|
new_color_mode = LiteralVar.create(new_color_mode)
|
|
75
75
|
|
|
76
76
|
return Var(
|
|
77
|
-
f"() => {
|
|
77
|
+
f"() => {base_setter!s}({new_color_mode!s})",
|
|
78
78
|
_var_data=VarData.merge(
|
|
79
79
|
base_setter._get_all_var_data(), new_color_mode._get_all_var_data()
|
|
80
80
|
),
|
|
@@ -138,9 +138,6 @@ def convert_item(
|
|
|
138
138
|
if isinstance(style_item, Var):
|
|
139
139
|
return style_item, style_item._get_all_var_data()
|
|
140
140
|
|
|
141
|
-
# if isinstance(style_item, str) and REFLEX_VAR_OPENING_TAG not in style_item:
|
|
142
|
-
# return style_item, None
|
|
143
|
-
|
|
144
141
|
# Otherwise, convert to Var to collapse VarData encoded in f-string.
|
|
145
142
|
new_var = LiteralVar.create(style_item)
|
|
146
143
|
var_data = new_var._get_all_var_data() if new_var is not None else None
|
reflex/testing.py
CHANGED
|
@@ -8,7 +8,6 @@ import dataclasses
|
|
|
8
8
|
import functools
|
|
9
9
|
import inspect
|
|
10
10
|
import os
|
|
11
|
-
import pathlib
|
|
12
11
|
import platform
|
|
13
12
|
import re
|
|
14
13
|
import signal
|
|
@@ -20,6 +19,7 @@ import threading
|
|
|
20
19
|
import time
|
|
21
20
|
import types
|
|
22
21
|
from http.server import SimpleHTTPRequestHandler
|
|
22
|
+
from pathlib import Path
|
|
23
23
|
from typing import (
|
|
24
24
|
TYPE_CHECKING,
|
|
25
25
|
Any,
|
|
@@ -100,7 +100,7 @@ class chdir(contextlib.AbstractContextManager):
|
|
|
100
100
|
|
|
101
101
|
def __enter__(self):
|
|
102
102
|
"""Save current directory and perform chdir."""
|
|
103
|
-
self._old_cwd.append(
|
|
103
|
+
self._old_cwd.append(Path.cwd())
|
|
104
104
|
os.chdir(self.path)
|
|
105
105
|
|
|
106
106
|
def __exit__(self, *excinfo):
|
|
@@ -120,8 +120,8 @@ class AppHarness:
|
|
|
120
120
|
app_source: Optional[
|
|
121
121
|
Callable[[], None] | types.ModuleType | str | functools.partial[Any]
|
|
122
122
|
]
|
|
123
|
-
app_path:
|
|
124
|
-
app_module_path:
|
|
123
|
+
app_path: Path
|
|
124
|
+
app_module_path: Path
|
|
125
125
|
app_module: Optional[types.ModuleType] = None
|
|
126
126
|
app_instance: Optional[reflex.App] = None
|
|
127
127
|
frontend_process: Optional[subprocess.Popen] = None
|
|
@@ -136,7 +136,7 @@ class AppHarness:
|
|
|
136
136
|
@classmethod
|
|
137
137
|
def create(
|
|
138
138
|
cls,
|
|
139
|
-
root:
|
|
139
|
+
root: Path,
|
|
140
140
|
app_source: Optional[
|
|
141
141
|
Callable[[], None] | types.ModuleType | str | functools.partial[Any]
|
|
142
142
|
] = None,
|
|
@@ -206,7 +206,7 @@ class AppHarness:
|
|
|
206
206
|
The full state name
|
|
207
207
|
"""
|
|
208
208
|
# NOTE: using State.get_name() somehow causes trouble here
|
|
209
|
-
# path = [State.get_name()] + [self.get_state_name(p) for p in path]
|
|
209
|
+
# path = [State.get_name()] + [self.get_state_name(p) for p in path] # noqa: ERA001
|
|
210
210
|
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
|
|
211
211
|
return ".".join(path)
|
|
212
212
|
|
|
@@ -436,7 +436,6 @@ class AppHarness:
|
|
|
436
436
|
|
|
437
437
|
Returns:
|
|
438
438
|
The rendered app global code.
|
|
439
|
-
|
|
440
439
|
"""
|
|
441
440
|
if not inspect.isclass(value) and not inspect.isfunction(value):
|
|
442
441
|
return f"{key} = {value!r}"
|
|
@@ -815,7 +814,7 @@ class AppHarness:
|
|
|
815
814
|
class SimpleHTTPRequestHandlerCustomErrors(SimpleHTTPRequestHandler):
|
|
816
815
|
"""SimpleHTTPRequestHandler with custom error page handling."""
|
|
817
816
|
|
|
818
|
-
def __init__(self, *args, error_page_map: dict[int,
|
|
817
|
+
def __init__(self, *args, error_page_map: dict[int, Path], **kwargs):
|
|
819
818
|
"""Initialize the handler.
|
|
820
819
|
|
|
821
820
|
Args:
|
|
@@ -858,8 +857,8 @@ class Subdir404TCPServer(socketserver.TCPServer):
|
|
|
858
857
|
def __init__(
|
|
859
858
|
self,
|
|
860
859
|
*args,
|
|
861
|
-
root:
|
|
862
|
-
error_page_map: dict[int,
|
|
860
|
+
root: Path,
|
|
861
|
+
error_page_map: dict[int, Path] | None,
|
|
863
862
|
**kwargs,
|
|
864
863
|
):
|
|
865
864
|
"""Initialize the server.
|
|
@@ -879,7 +878,7 @@ class Subdir404TCPServer(socketserver.TCPServer):
|
|
|
879
878
|
|
|
880
879
|
Args:
|
|
881
880
|
request: the requesting socket
|
|
882
|
-
client_address: (host, port) referring to the client
|
|
881
|
+
client_address: (host, port) referring to the client's address.
|
|
883
882
|
"""
|
|
884
883
|
self.RequestHandlerClass(
|
|
885
884
|
request,
|
reflex/utils/build.py
CHANGED
|
@@ -150,7 +150,7 @@ def zip_app(
|
|
|
150
150
|
_zip(
|
|
151
151
|
component_name=constants.ComponentName.BACKEND,
|
|
152
152
|
target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
|
|
153
|
-
root_dir=Path(
|
|
153
|
+
root_dir=Path.cwd(),
|
|
154
154
|
dirs_to_exclude={"__pycache__"},
|
|
155
155
|
files_to_exclude=files_to_exclude,
|
|
156
156
|
top_level_dirs_to_exclude={"assets"},
|