reflex 0.5.6a1__py3-none-any.whl → 0.5.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/utils/context.js.jinja2 +2 -0
- reflex/.templates/web/utils/state.js +31 -2
- reflex/__init__.py +7 -1
- reflex/__init__.pyi +3 -0
- reflex/app.py +190 -4
- reflex/compiler/templates.py +1 -0
- reflex/components/base/__init__.py +4 -0
- reflex/components/base/__init__.pyi +2 -0
- reflex/components/base/error_boundary.py +78 -0
- reflex/components/base/error_boundary.pyi +98 -0
- reflex/components/core/foreach.py +3 -2
- reflex/components/core/upload.py +1 -1
- reflex/components/radix/primitives/form.py +3 -2
- reflex/components/recharts/cartesian.py +31 -16
- reflex/components/recharts/cartesian.pyi +46 -22
- reflex/components/recharts/charts.py +6 -6
- reflex/components/recharts/charts.pyi +10 -10
- reflex/components/recharts/general.py +7 -0
- reflex/components/recharts/general.pyi +5 -0
- reflex/components/recharts/recharts.py +16 -1
- reflex/components/recharts/recharts.pyi +1 -0
- reflex/config.py +2 -1
- reflex/constants/compiler.py +20 -2
- reflex/constants/config.py +1 -1
- reflex/event.py +9 -0
- reflex/experimental/__init__.py +2 -0
- reflex/experimental/vars/__init__.py +3 -0
- reflex/experimental/vars/base.py +210 -0
- reflex/state.py +105 -21
- reflex/testing.py +28 -0
- reflex/utils/compat.py +7 -5
- reflex/utils/console.py +10 -1
- reflex/utils/prerequisites.py +11 -5
- reflex/utils/processes.py +7 -2
- reflex/utils/pyi_generator.py +4 -1
- reflex/utils/types.py +35 -30
- reflex/vars.py +58 -23
- reflex/vars.pyi +4 -1
- {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/METADATA +1 -1
- {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/RECORD +43 -39
- {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/LICENSE +0 -0
- {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/WHEEL +0 -0
- {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/entry_points.txt +0 -0
reflex/constants/compiler.py
CHANGED
|
@@ -62,9 +62,17 @@ class CompileVars(SimpleNamespace):
|
|
|
62
62
|
# The name of the function for converting a dict to an event.
|
|
63
63
|
TO_EVENT = "Event"
|
|
64
64
|
# The name of the internal on_load event.
|
|
65
|
-
ON_LOAD_INTERNAL = "
|
|
65
|
+
ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal"
|
|
66
66
|
# The name of the internal event to update generic state vars.
|
|
67
|
-
UPDATE_VARS_INTERNAL =
|
|
67
|
+
UPDATE_VARS_INTERNAL = (
|
|
68
|
+
"reflex___state____update_vars_internal_state.update_vars_internal"
|
|
69
|
+
)
|
|
70
|
+
# The name of the frontend event exception state
|
|
71
|
+
FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state"
|
|
72
|
+
# The full name of the frontend exception state
|
|
73
|
+
FRONTEND_EXCEPTION_STATE_FULL = (
|
|
74
|
+
f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}"
|
|
75
|
+
)
|
|
68
76
|
|
|
69
77
|
|
|
70
78
|
class PageNames(SimpleNamespace):
|
|
@@ -124,6 +132,16 @@ class Hooks(SimpleNamespace):
|
|
|
124
132
|
}
|
|
125
133
|
})"""
|
|
126
134
|
|
|
135
|
+
FRONTEND_ERRORS = f"""
|
|
136
|
+
const logFrontendError = (error, info) => {{
|
|
137
|
+
if (process.env.NODE_ENV === "production") {{
|
|
138
|
+
addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE_FULL}.handle_frontend_exception", {{
|
|
139
|
+
stack: error.stack,
|
|
140
|
+
}})])
|
|
141
|
+
}}
|
|
142
|
+
}}
|
|
143
|
+
"""
|
|
144
|
+
|
|
127
145
|
|
|
128
146
|
class MemoizationDisposition(enum.Enum):
|
|
129
147
|
"""The conditions under which a component should be memoized."""
|
reflex/constants/config.py
CHANGED
|
@@ -39,7 +39,7 @@ class GitIgnore(SimpleNamespace):
|
|
|
39
39
|
# The gitignore file.
|
|
40
40
|
FILE = ".gitignore"
|
|
41
41
|
# Files to gitignore.
|
|
42
|
-
DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]"}
|
|
42
|
+
DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]", "assets/external/"}
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class RequirementsTxt(SimpleNamespace):
|
reflex/event.py
CHANGED
|
@@ -509,6 +509,15 @@ def console_log(message: str | Var[str]) -> EventSpec:
|
|
|
509
509
|
return server_side("_console", get_fn_signature(console_log), message=message)
|
|
510
510
|
|
|
511
511
|
|
|
512
|
+
def back() -> EventSpec:
|
|
513
|
+
"""Do a history.back on the browser.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
An event to go back one page.
|
|
517
|
+
"""
|
|
518
|
+
return call_script("window.history.back()")
|
|
519
|
+
|
|
520
|
+
|
|
512
521
|
def window_alert(message: str | Var[str]) -> EventSpec:
|
|
513
522
|
"""Create a window alert on the browser.
|
|
514
523
|
|
reflex/experimental/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ from reflex.components.sonner.toast import toast as toast
|
|
|
8
8
|
|
|
9
9
|
from ..utils.console import warn
|
|
10
10
|
from . import hooks as hooks
|
|
11
|
+
from . import vars as vars
|
|
11
12
|
from .assets import asset as asset
|
|
12
13
|
from .client_state import ClientStateVar as ClientStateVar
|
|
13
14
|
from .layout import layout as layout
|
|
@@ -42,6 +43,7 @@ _x = ExperimentalNamespace(
|
|
|
42
43
|
asset=asset,
|
|
43
44
|
client_state=ClientStateVar.create,
|
|
44
45
|
hooks=hooks,
|
|
46
|
+
vars=vars,
|
|
45
47
|
layout=layout,
|
|
46
48
|
progress=progress,
|
|
47
49
|
PropsBase=PropsBase,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Collection of base classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Optional, Type
|
|
8
|
+
|
|
9
|
+
from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG
|
|
10
|
+
from reflex.utils import serializers, types
|
|
11
|
+
from reflex.utils.exceptions import VarTypeError
|
|
12
|
+
from reflex.vars import Var, VarData, _decode_var, _extract_var_data, _global_vars
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass(
|
|
16
|
+
eq=False,
|
|
17
|
+
frozen=True,
|
|
18
|
+
**{"slots": True} if sys.version_info >= (3, 10) else {},
|
|
19
|
+
)
|
|
20
|
+
class ImmutableVar(Var):
|
|
21
|
+
"""Base class for immutable vars."""
|
|
22
|
+
|
|
23
|
+
# The name of the var.
|
|
24
|
+
_var_name: str = dataclasses.field()
|
|
25
|
+
|
|
26
|
+
# The type of the var.
|
|
27
|
+
_var_type: Type = dataclasses.field(default=Any)
|
|
28
|
+
|
|
29
|
+
# Extra metadata associated with the Var
|
|
30
|
+
_var_data: Optional[VarData] = dataclasses.field(default=None)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def _var_is_local(self) -> bool:
|
|
34
|
+
"""Whether this is a local javascript variable.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
False
|
|
38
|
+
"""
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def _var_is_string(self) -> bool:
|
|
43
|
+
"""Whether the var is a string literal.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
False
|
|
47
|
+
"""
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def _var_full_name_needs_state_prefix(self) -> bool:
|
|
52
|
+
"""Whether the full name of the var needs a _var_state prefix.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
False
|
|
56
|
+
"""
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
"""Post-initialize the var."""
|
|
61
|
+
# Decode any inline Var markup and apply it to the instance
|
|
62
|
+
_var_data, _var_name = _decode_var(self._var_name)
|
|
63
|
+
if _var_data:
|
|
64
|
+
self.__init__(
|
|
65
|
+
_var_name, self._var_type, VarData.merge(self._var_data, _var_data)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _replace(self, merge_var_data=None, **kwargs: Any):
|
|
69
|
+
"""Make a copy of this Var with updated fields.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
merge_var_data: VarData to merge into the existing VarData.
|
|
73
|
+
**kwargs: Var fields to update.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A new ImmutableVar with the updated fields overwriting the corresponding fields in this Var.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
TypeError: If _var_is_local, _var_is_string, or _var_full_name_needs_state_prefix is not None.
|
|
80
|
+
"""
|
|
81
|
+
if kwargs.get("_var_is_local", False) is not False:
|
|
82
|
+
raise TypeError(
|
|
83
|
+
"The _var_is_local argument is not supported for ImmutableVar."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if kwargs.get("_var_is_string", False) is not False:
|
|
87
|
+
raise TypeError(
|
|
88
|
+
"The _var_is_string argument is not supported for ImmutableVar."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if kwargs.get("_var_full_name_needs_state_prefix", False) is not False:
|
|
92
|
+
raise TypeError(
|
|
93
|
+
"The _var_full_name_needs_state_prefix argument is not supported for ImmutableVar."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
field_values = dict(
|
|
97
|
+
_var_name=kwargs.pop("_var_name", self._var_name),
|
|
98
|
+
_var_type=kwargs.pop("_var_type", self._var_type),
|
|
99
|
+
_var_data=VarData.merge(
|
|
100
|
+
kwargs.get("_var_data", self._var_data), merge_var_data
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
return ImmutableVar(**field_values)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def create(
|
|
107
|
+
cls,
|
|
108
|
+
value: Any,
|
|
109
|
+
_var_is_local: bool | None = None,
|
|
110
|
+
_var_is_string: bool | None = None,
|
|
111
|
+
_var_data: VarData | None = None,
|
|
112
|
+
) -> Var | None:
|
|
113
|
+
"""Create a var from a value.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
value: The value to create the var from.
|
|
117
|
+
_var_is_local: Whether the var is local. Deprecated.
|
|
118
|
+
_var_is_string: Whether the var is a string literal. Deprecated.
|
|
119
|
+
_var_data: Additional hooks and imports associated with the Var.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The var.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
VarTypeError: If the value is JSON-unserializable.
|
|
126
|
+
TypeError: If _var_is_local or _var_is_string is not None.
|
|
127
|
+
"""
|
|
128
|
+
if _var_is_local is not None:
|
|
129
|
+
raise TypeError(
|
|
130
|
+
"The _var_is_local argument is not supported for ImmutableVar."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if _var_is_string is not None:
|
|
134
|
+
raise TypeError(
|
|
135
|
+
"The _var_is_string argument is not supported for ImmutableVar."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
from reflex.utils import format
|
|
139
|
+
|
|
140
|
+
# Check for none values.
|
|
141
|
+
if value is None:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# If the value is already a var, do nothing.
|
|
145
|
+
if isinstance(value, Var):
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
# Try to pull the imports and hooks from contained values.
|
|
149
|
+
if not isinstance(value, str):
|
|
150
|
+
_var_data = VarData.merge(*_extract_var_data(value), _var_data)
|
|
151
|
+
|
|
152
|
+
# Try to serialize the value.
|
|
153
|
+
type_ = type(value)
|
|
154
|
+
if type_ in types.JSONType:
|
|
155
|
+
name = value
|
|
156
|
+
else:
|
|
157
|
+
name, _serialized_type = serializers.serialize(value, get_type=True)
|
|
158
|
+
if name is None:
|
|
159
|
+
raise VarTypeError(
|
|
160
|
+
f"No JSON serializer found for var {value} of type {type_}."
|
|
161
|
+
)
|
|
162
|
+
name = name if isinstance(name, str) else format.json_dumps(name)
|
|
163
|
+
|
|
164
|
+
return ImmutableVar(
|
|
165
|
+
_var_name=name,
|
|
166
|
+
_var_type=type_,
|
|
167
|
+
_var_data=_var_data,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def create_safe(
|
|
172
|
+
cls,
|
|
173
|
+
value: Any,
|
|
174
|
+
_var_is_local: bool | None = None,
|
|
175
|
+
_var_is_string: bool | None = None,
|
|
176
|
+
_var_data: VarData | None = None,
|
|
177
|
+
) -> Var:
|
|
178
|
+
"""Create a var from a value, asserting that it is not None.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
value: The value to create the var from.
|
|
182
|
+
_var_is_local: Whether the var is local. Deprecated.
|
|
183
|
+
_var_is_string: Whether the var is a string literal. Deprecated.
|
|
184
|
+
_var_data: Additional hooks and imports associated with the Var.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The var.
|
|
188
|
+
"""
|
|
189
|
+
var = cls.create(
|
|
190
|
+
value,
|
|
191
|
+
_var_is_local=_var_is_local,
|
|
192
|
+
_var_is_string=_var_is_string,
|
|
193
|
+
_var_data=_var_data,
|
|
194
|
+
)
|
|
195
|
+
assert var is not None
|
|
196
|
+
return var
|
|
197
|
+
|
|
198
|
+
def __format__(self, format_spec: str) -> str:
|
|
199
|
+
"""Format the var into a Javascript equivalent to an f-string.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
format_spec: The format specifier (Ignored for now).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The formatted var.
|
|
206
|
+
"""
|
|
207
|
+
_global_vars[hash(self)] = self
|
|
208
|
+
|
|
209
|
+
# Encode the _var_data into the formatted output for tracking purposes.
|
|
210
|
+
return f"{REFLEX_VAR_OPENING_TAG}{hash(self)}{REFLEX_VAR_CLOSING_TAG}{self._var_name}"
|
reflex/state.py
CHANGED
|
@@ -8,7 +8,6 @@ import copy
|
|
|
8
8
|
import functools
|
|
9
9
|
import inspect
|
|
10
10
|
import os
|
|
11
|
-
import traceback
|
|
12
11
|
import uuid
|
|
13
12
|
from abc import ABC, abstractmethod
|
|
14
13
|
from collections import defaultdict
|
|
@@ -25,9 +24,12 @@ from typing import (
|
|
|
25
24
|
Sequence,
|
|
26
25
|
Set,
|
|
27
26
|
Type,
|
|
27
|
+
Union,
|
|
28
|
+
cast,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
import dill
|
|
32
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
31
33
|
|
|
32
34
|
try:
|
|
33
35
|
import pydantic.v1 as pydantic
|
|
@@ -47,7 +49,6 @@ from reflex.event import (
|
|
|
47
49
|
EventHandler,
|
|
48
50
|
EventSpec,
|
|
49
51
|
fix_events,
|
|
50
|
-
window_alert,
|
|
51
52
|
)
|
|
52
53
|
from reflex.utils import console, format, prerequisites, types
|
|
53
54
|
from reflex.utils.exceptions import ImmutableStateError, LockExpiredError
|
|
@@ -61,7 +62,6 @@ if TYPE_CHECKING:
|
|
|
61
62
|
|
|
62
63
|
Delta = Dict[str, Any]
|
|
63
64
|
var = computed_var
|
|
64
|
-
config = get_config()
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
# If the state is this large, it's considered a performance issue.
|
|
@@ -426,6 +426,21 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
426
426
|
if isinstance(v, ComputedVar)
|
|
427
427
|
]
|
|
428
428
|
|
|
429
|
+
@classmethod
|
|
430
|
+
def _validate_module_name(cls) -> None:
|
|
431
|
+
"""Check if the module name is valid.
|
|
432
|
+
|
|
433
|
+
Reflex uses ___ as state name module separator.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
NameError: If the module name is invalid.
|
|
437
|
+
"""
|
|
438
|
+
if "___" in cls.__module__:
|
|
439
|
+
raise NameError(
|
|
440
|
+
"The module name of a State class cannot contain '___'. "
|
|
441
|
+
"Please rename the module."
|
|
442
|
+
)
|
|
443
|
+
|
|
429
444
|
@classmethod
|
|
430
445
|
def __init_subclass__(cls, mixin: bool = False, **kwargs):
|
|
431
446
|
"""Do some magic for the subclass initialization.
|
|
@@ -445,8 +460,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
445
460
|
if mixin:
|
|
446
461
|
return
|
|
447
462
|
|
|
463
|
+
# Validate the module name.
|
|
464
|
+
cls._validate_module_name()
|
|
465
|
+
|
|
448
466
|
# Event handlers should not shadow builtin state methods.
|
|
449
467
|
cls._check_overridden_methods()
|
|
468
|
+
|
|
450
469
|
# Computed vars should not shadow builtin state props.
|
|
451
470
|
cls._check_overriden_basevars()
|
|
452
471
|
|
|
@@ -463,20 +482,22 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
463
482
|
cls.inherited_backend_vars = parent_state.backend_vars
|
|
464
483
|
|
|
465
484
|
# Check if another substate class with the same name has already been defined.
|
|
466
|
-
if cls.
|
|
485
|
+
if cls.get_name() in set(
|
|
486
|
+
c.get_name() for c in parent_state.class_subclasses
|
|
487
|
+
):
|
|
467
488
|
if is_testing_env():
|
|
468
489
|
# Clear existing subclass with same name when app is reloaded via
|
|
469
490
|
# utils.prerequisites.get_app(reload=True)
|
|
470
491
|
parent_state.class_subclasses = set(
|
|
471
492
|
c
|
|
472
493
|
for c in parent_state.class_subclasses
|
|
473
|
-
if c.
|
|
494
|
+
if c.get_name() != cls.get_name()
|
|
474
495
|
)
|
|
475
496
|
else:
|
|
476
497
|
# During normal operation, subclasses cannot have the same name, even if they are
|
|
477
498
|
# defined in different modules.
|
|
478
499
|
raise StateValueError(
|
|
479
|
-
f"The substate class '{cls.
|
|
500
|
+
f"The substate class '{cls.get_name()}' has been defined multiple times. "
|
|
480
501
|
"Shadowing substate classes is not allowed."
|
|
481
502
|
)
|
|
482
503
|
# Track this new subclass in the parent state's subclasses set.
|
|
@@ -759,7 +780,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
759
780
|
Returns:
|
|
760
781
|
The name of the state.
|
|
761
782
|
"""
|
|
762
|
-
|
|
783
|
+
module = cls.__module__.replace(".", "___")
|
|
784
|
+
return format.to_snake_case(f"{module}___{cls.__name__}")
|
|
763
785
|
|
|
764
786
|
@classmethod
|
|
765
787
|
@functools.lru_cache()
|
|
@@ -1431,15 +1453,39 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1431
1453
|
# Convert valid EventHandler and EventSpec into Event
|
|
1432
1454
|
fixed_events = fix_events(self._check_valid(handler, events), token)
|
|
1433
1455
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1456
|
+
try:
|
|
1457
|
+
# Get the delta after processing the event.
|
|
1458
|
+
delta = state.get_delta()
|
|
1459
|
+
state._clean()
|
|
1460
|
+
|
|
1461
|
+
return StateUpdate(
|
|
1462
|
+
delta=delta,
|
|
1463
|
+
events=fixed_events,
|
|
1464
|
+
final=final if not handler.is_background else True,
|
|
1465
|
+
)
|
|
1466
|
+
except Exception as ex:
|
|
1467
|
+
state._clean()
|
|
1437
1468
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1469
|
+
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
|
1470
|
+
|
|
1471
|
+
event_specs = app_instance.backend_exception_handler(ex)
|
|
1472
|
+
|
|
1473
|
+
if event_specs is None:
|
|
1474
|
+
return StateUpdate()
|
|
1475
|
+
|
|
1476
|
+
event_specs_correct_type = cast(
|
|
1477
|
+
Union[List[Union[EventSpec, EventHandler]], None],
|
|
1478
|
+
[event_specs] if isinstance(event_specs, EventSpec) else event_specs,
|
|
1479
|
+
)
|
|
1480
|
+
fixed_events = fix_events(
|
|
1481
|
+
event_specs_correct_type,
|
|
1482
|
+
token,
|
|
1483
|
+
router_data=state.router_data,
|
|
1484
|
+
)
|
|
1485
|
+
return StateUpdate(
|
|
1486
|
+
events=fixed_events,
|
|
1487
|
+
final=True,
|
|
1488
|
+
)
|
|
1443
1489
|
|
|
1444
1490
|
async def _process_event(
|
|
1445
1491
|
self, handler: EventHandler, state: BaseState | StateProxy, payload: Dict
|
|
@@ -1492,12 +1538,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1492
1538
|
|
|
1493
1539
|
# If an error occurs, throw a window alert.
|
|
1494
1540
|
except Exception as ex:
|
|
1495
|
-
error = traceback.format_exc()
|
|
1496
|
-
print(error)
|
|
1497
1541
|
telemetry.send_error(ex, context="backend")
|
|
1542
|
+
|
|
1543
|
+
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
|
1544
|
+
|
|
1545
|
+
event_specs = app_instance.backend_exception_handler(ex)
|
|
1546
|
+
|
|
1498
1547
|
yield state._as_state_update(
|
|
1499
1548
|
handler,
|
|
1500
|
-
|
|
1549
|
+
event_specs,
|
|
1501
1550
|
final=True,
|
|
1502
1551
|
)
|
|
1503
1552
|
|
|
@@ -1799,6 +1848,23 @@ class State(BaseState):
|
|
|
1799
1848
|
is_hydrated: bool = False
|
|
1800
1849
|
|
|
1801
1850
|
|
|
1851
|
+
class FrontendEventExceptionState(State):
|
|
1852
|
+
"""Substate for handling frontend exceptions."""
|
|
1853
|
+
|
|
1854
|
+
def handle_frontend_exception(self, stack: str) -> None:
|
|
1855
|
+
"""Handle frontend exceptions.
|
|
1856
|
+
|
|
1857
|
+
If a frontend exception handler is provided, it will be called.
|
|
1858
|
+
Otherwise, the default frontend exception handler will be called.
|
|
1859
|
+
|
|
1860
|
+
Args:
|
|
1861
|
+
stack: The stack trace of the exception.
|
|
1862
|
+
|
|
1863
|
+
"""
|
|
1864
|
+
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
|
1865
|
+
app_instance.frontend_exception_handler(Exception(stack))
|
|
1866
|
+
|
|
1867
|
+
|
|
1802
1868
|
class UpdateVarsInternalState(State):
|
|
1803
1869
|
"""Substate for handling internal state var updates."""
|
|
1804
1870
|
|
|
@@ -2319,6 +2385,24 @@ def _dill_reduce_state(pickler, obj):
|
|
|
2319
2385
|
dill.Pickler.dispatch[type](pickler, obj)
|
|
2320
2386
|
|
|
2321
2387
|
|
|
2388
|
+
def _default_lock_expiration() -> int:
|
|
2389
|
+
"""Get the default lock expiration time.
|
|
2390
|
+
|
|
2391
|
+
Returns:
|
|
2392
|
+
The default lock expiration time.
|
|
2393
|
+
"""
|
|
2394
|
+
return get_config().redis_lock_expiration
|
|
2395
|
+
|
|
2396
|
+
|
|
2397
|
+
def _default_token_expiration() -> int:
|
|
2398
|
+
"""Get the default token expiration time.
|
|
2399
|
+
|
|
2400
|
+
Returns:
|
|
2401
|
+
The default token expiration time.
|
|
2402
|
+
"""
|
|
2403
|
+
return get_config().redis_token_expiration
|
|
2404
|
+
|
|
2405
|
+
|
|
2322
2406
|
class StateManagerRedis(StateManager):
|
|
2323
2407
|
"""A state manager that stores states in redis."""
|
|
2324
2408
|
|
|
@@ -2326,10 +2410,10 @@ class StateManagerRedis(StateManager):
|
|
|
2326
2410
|
redis: Redis
|
|
2327
2411
|
|
|
2328
2412
|
# The token expiration time (s).
|
|
2329
|
-
token_expiration: int =
|
|
2413
|
+
token_expiration: int = pydantic.Field(default_factory=_default_token_expiration)
|
|
2330
2414
|
|
|
2331
2415
|
# The maximum time to hold a lock (ms).
|
|
2332
|
-
lock_expiration: int =
|
|
2416
|
+
lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
|
|
2333
2417
|
|
|
2334
2418
|
# The keyspace subscription string when redis is waiting for lock to be released
|
|
2335
2419
|
_redis_notify_keyspace_events: str = (
|
|
@@ -2902,7 +2986,7 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
2902
2986
|
pydantic.BaseModel.__dict__
|
|
2903
2987
|
)
|
|
2904
2988
|
|
|
2905
|
-
__mutable_types__ = (list, dict, set, Base)
|
|
2989
|
+
__mutable_types__ = (list, dict, set, Base, DeclarativeBase)
|
|
2906
2990
|
|
|
2907
2991
|
def __init__(self, wrapped: Any, state: BaseState, field_name: str):
|
|
2908
2992
|
"""Create a proxy for a mutable object that tracks changes.
|
reflex/testing.py
CHANGED
|
@@ -40,6 +40,7 @@ import reflex
|
|
|
40
40
|
import reflex.reflex
|
|
41
41
|
import reflex.utils.build
|
|
42
42
|
import reflex.utils.exec
|
|
43
|
+
import reflex.utils.format
|
|
43
44
|
import reflex.utils.prerequisites
|
|
44
45
|
import reflex.utils.processes
|
|
45
46
|
from reflex.state import (
|
|
@@ -177,6 +178,33 @@ class AppHarness:
|
|
|
177
178
|
app_module_path=root / app_name / f"{app_name}.py",
|
|
178
179
|
)
|
|
179
180
|
|
|
181
|
+
def get_state_name(self, state_cls_name: str) -> str:
|
|
182
|
+
"""Get the state name for the given state class name.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
state_cls_name: The state class name
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The state name
|
|
189
|
+
"""
|
|
190
|
+
return reflex.utils.format.to_snake_case(
|
|
191
|
+
f"{self.app_name}___{self.app_name}___" + state_cls_name
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def get_full_state_name(self, path: List[str]) -> str:
|
|
195
|
+
"""Get the full state name for the given state class name.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
path: A list of state class names
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The full state name
|
|
202
|
+
"""
|
|
203
|
+
# NOTE: using State.get_name() somehow causes trouble here
|
|
204
|
+
# path = [State.get_name()] + [self.get_state_name(p) for p in path]
|
|
205
|
+
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
|
|
206
|
+
return ".".join(path)
|
|
207
|
+
|
|
180
208
|
def _get_globals_from_signature(self, func: Any) -> dict[str, Any]:
|
|
181
209
|
"""Get the globals from a function or module object.
|
|
182
210
|
|
reflex/utils/compat.py
CHANGED
|
@@ -32,6 +32,13 @@ def pydantic_v1_patch():
|
|
|
32
32
|
Yields:
|
|
33
33
|
None when the Pydantic module is patched.
|
|
34
34
|
"""
|
|
35
|
+
import pydantic
|
|
36
|
+
|
|
37
|
+
if pydantic.__version__.startswith("1."):
|
|
38
|
+
# pydantic v1 is already installed
|
|
39
|
+
yield
|
|
40
|
+
return
|
|
41
|
+
|
|
35
42
|
patched_modules = [
|
|
36
43
|
"pydantic",
|
|
37
44
|
"pydantic.fields",
|
|
@@ -42,11 +49,6 @@ def pydantic_v1_patch():
|
|
|
42
49
|
try:
|
|
43
50
|
import pydantic.v1 # type: ignore
|
|
44
51
|
|
|
45
|
-
if pydantic.__version__.startswith("1."):
|
|
46
|
-
# pydantic v1 is already installed
|
|
47
|
-
yield
|
|
48
|
-
return
|
|
49
|
-
|
|
50
52
|
sys.modules["pydantic.fields"] = pydantic.v1.fields # type: ignore
|
|
51
53
|
sys.modules["pydantic.main"] = pydantic.v1.main # type: ignore
|
|
52
54
|
sys.modules["pydantic.errors"] = pydantic.v1.errors # type: ignore
|
reflex/utils/console.py
CHANGED
|
@@ -17,6 +17,9 @@ _LOG_LEVEL = LogLevel.INFO
|
|
|
17
17
|
# Deprecated features who's warning has been printed.
|
|
18
18
|
_EMITTED_DEPRECATION_WARNINGS = set()
|
|
19
19
|
|
|
20
|
+
# Info messages which have been printed.
|
|
21
|
+
_EMITTED_INFO = set()
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
def set_log_level(log_level: LogLevel):
|
|
22
25
|
"""Set the log level.
|
|
@@ -62,14 +65,20 @@ def debug(msg: str, **kwargs):
|
|
|
62
65
|
print(msg_, **kwargs)
|
|
63
66
|
|
|
64
67
|
|
|
65
|
-
def info(msg: str, **kwargs):
|
|
68
|
+
def info(msg: str, dedupe: bool = False, **kwargs):
|
|
66
69
|
"""Print an info message.
|
|
67
70
|
|
|
68
71
|
Args:
|
|
69
72
|
msg: The info message.
|
|
73
|
+
dedupe: If True, suppress multiple console logs of info message.
|
|
70
74
|
kwargs: Keyword arguments to pass to the print function.
|
|
71
75
|
"""
|
|
72
76
|
if _LOG_LEVEL <= LogLevel.INFO:
|
|
77
|
+
if dedupe:
|
|
78
|
+
if msg in _EMITTED_INFO:
|
|
79
|
+
return
|
|
80
|
+
else:
|
|
81
|
+
_EMITTED_INFO.add(msg)
|
|
73
82
|
print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
|
|
74
83
|
|
|
75
84
|
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import functools
|
|
6
6
|
import glob
|
|
7
7
|
import importlib
|
|
8
|
+
import importlib.metadata
|
|
8
9
|
import inspect
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
@@ -23,7 +24,6 @@ from types import ModuleType
|
|
|
23
24
|
from typing import Callable, List, Optional
|
|
24
25
|
|
|
25
26
|
import httpx
|
|
26
|
-
import pkg_resources
|
|
27
27
|
import typer
|
|
28
28
|
from alembic.util.exc import CommandError
|
|
29
29
|
from packaging import version
|
|
@@ -61,7 +61,7 @@ class CpuInfo(Base):
|
|
|
61
61
|
def get_web_dir() -> Path:
|
|
62
62
|
"""Get the working directory for the next.js commands.
|
|
63
63
|
|
|
64
|
-
Can be
|
|
64
|
+
Can be overridden with REFLEX_WEB_WORKDIR.
|
|
65
65
|
|
|
66
66
|
Returns:
|
|
67
67
|
The working directory.
|
|
@@ -78,7 +78,7 @@ def check_latest_package_version(package_name: str):
|
|
|
78
78
|
"""
|
|
79
79
|
try:
|
|
80
80
|
# Get the latest version from PyPI
|
|
81
|
-
current_version =
|
|
81
|
+
current_version = importlib.metadata.version(package_name)
|
|
82
82
|
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
83
83
|
response = httpx.get(url)
|
|
84
84
|
latest_version = response.json()["info"]["version"]
|
|
@@ -404,9 +404,15 @@ def initialize_gitignore(
|
|
|
404
404
|
files_to_ignore: The files to add to the .gitignore file.
|
|
405
405
|
"""
|
|
406
406
|
# Combine with the current ignored files.
|
|
407
|
+
current_ignore: set[str] = set()
|
|
407
408
|
if os.path.exists(gitignore_file):
|
|
408
409
|
with open(gitignore_file, "r") as f:
|
|
409
|
-
|
|
410
|
+
current_ignore |= set([line.strip() for line in f.readlines()])
|
|
411
|
+
|
|
412
|
+
if files_to_ignore == current_ignore:
|
|
413
|
+
console.debug(f"{gitignore_file} already up to date.")
|
|
414
|
+
return
|
|
415
|
+
files_to_ignore |= current_ignore
|
|
410
416
|
|
|
411
417
|
# Write files to the .gitignore file.
|
|
412
418
|
with open(gitignore_file, "w", newline="\n") as f:
|
|
@@ -970,7 +976,7 @@ def is_latest_template() -> bool:
|
|
|
970
976
|
json_file = get_web_dir() / constants.Reflex.JSON
|
|
971
977
|
if not json_file.exists():
|
|
972
978
|
return False
|
|
973
|
-
app_version = json.
|
|
979
|
+
app_version = json.loads(json_file.read_text()).get("version")
|
|
974
980
|
return app_version == constants.Reflex.VERSION
|
|
975
981
|
|
|
976
982
|
|