reflex 0.8.15a2__py3-none-any.whl → 0.8.16a1__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/app.py +40 -46
- reflex/app_mixins/lifespan.py +12 -5
- reflex/base.py +1 -0
- reflex/compiler/compiler.py +4 -6
- reflex/compiler/templates.py +25 -31
- reflex/compiler/utils.py +6 -5
- reflex/components/base/body.pyi +1 -195
- reflex/components/base/link.pyi +1 -407
- reflex/components/base/meta.pyi +1 -405
- reflex/components/base/script.pyi +1 -237
- reflex/components/component.py +41 -46
- reflex/components/core/auto_scroll.pyi +1 -195
- reflex/components/core/banner.pyi +1 -391
- reflex/components/core/breakpoints.py +14 -18
- reflex/components/core/html.pyi +1 -197
- reflex/components/core/match.py +2 -2
- reflex/components/core/sticky.py +11 -15
- reflex/components/core/sticky.pyi +0 -788
- reflex/components/core/upload.py +1 -3
- reflex/components/datadisplay/code.pyi +1 -0
- reflex/components/datadisplay/dataeditor.py +4 -6
- reflex/components/datadisplay/shiki_code_block.py +106 -110
- reflex/components/dynamic.py +4 -6
- reflex/components/el/elements/__init__.py +5 -7
- reflex/components/el/elements/__init__.pyi +5 -7
- reflex/components/el/elements/base.py +1 -1
- reflex/components/el/elements/base.pyi +1 -195
- reflex/components/el/elements/forms.py +7 -9
- reflex/components/el/elements/forms.pyi +12 -3112
- reflex/components/el/elements/inline.pyi +0 -5481
- reflex/components/el/elements/media.pyi +0 -10280
- reflex/components/el/elements/metadata.pyi +1 -835
- reflex/components/el/elements/other.pyi +1 -1365
- reflex/components/el/elements/scripts.pyi +1 -625
- reflex/components/el/elements/sectioning.pyi +1 -2911
- reflex/components/el/elements/tables.pyi +1 -1973
- reflex/components/el/elements/typography.pyi +1 -3125
- reflex/components/lucide/icon.py +4 -4
- reflex/components/lucide/icon.pyi +0 -4
- reflex/components/markdown/markdown.py +15 -19
- reflex/components/markdown/markdown.pyi +1 -0
- reflex/components/moment/moment.pyi +0 -49
- reflex/components/props.py +3 -3
- reflex/components/radix/primitives/accordion.py +4 -6
- reflex/components/radix/primitives/accordion.pyi +0 -14
- reflex/components/radix/primitives/base.pyi +0 -5
- reflex/components/radix/primitives/dialog.py +2 -0
- reflex/components/radix/primitives/dialog.pyi +1 -233
- reflex/components/radix/primitives/drawer.pyi +0 -18
- reflex/components/radix/primitives/form.pyi +30 -632
- reflex/components/radix/primitives/progress.pyi +0 -10
- reflex/components/radix/primitives/slider.pyi +0 -10
- reflex/components/radix/themes/color_mode.pyi +1 -284
- reflex/components/radix/themes/components/alert_dialog.pyi +0 -207
- reflex/components/radix/themes/components/aspect_ratio.pyi +0 -2
- reflex/components/radix/themes/components/avatar.pyi +0 -80
- reflex/components/radix/themes/components/badge.pyi +1 -270
- reflex/components/radix/themes/components/button.pyi +1 -274
- reflex/components/radix/themes/components/callout.pyi +0 -1197
- reflex/components/radix/themes/components/card.pyi +1 -209
- reflex/components/radix/themes/components/checkbox.pyi +0 -261
- reflex/components/radix/themes/components/checkbox_cards.pyi +1 -96
- reflex/components/radix/themes/components/checkbox_group.pyi +1 -80
- reflex/components/radix/themes/components/context_menu.pyi +13 -321
- reflex/components/radix/themes/components/data_list.pyi +1 -107
- reflex/components/radix/themes/components/dialog.pyi +1 -210
- reflex/components/radix/themes/components/dropdown_menu.pyi +0 -209
- reflex/components/radix/themes/components/hover_card.pyi +1 -246
- reflex/components/radix/themes/components/icon_button.pyi +1 -195
- reflex/components/radix/themes/components/inset.pyi +0 -252
- reflex/components/radix/themes/components/popover.pyi +1 -234
- reflex/components/radix/themes/components/progress.pyi +1 -84
- reflex/components/radix/themes/components/radio.pyi +1 -72
- reflex/components/radix/themes/components/radio_cards.pyi +1 -123
- reflex/components/radix/themes/components/scroll_area.pyi +1 -11
- reflex/components/radix/themes/components/select.pyi +1 -376
- reflex/components/radix/themes/components/separator.pyi +0 -77
- reflex/components/radix/themes/components/skeleton.pyi +0 -30
- reflex/components/radix/themes/components/slider.py +3 -5
- reflex/components/radix/themes/components/spinner.pyi +0 -5
- reflex/components/radix/themes/components/switch.pyi +0 -89
- reflex/components/radix/themes/components/table.pyi +0 -1453
- reflex/components/radix/themes/components/text_area.pyi +7 -282
- reflex/components/radix/themes/components/text_field.pyi +6 -392
- reflex/components/radix/themes/components/tooltip.pyi +0 -42
- reflex/components/radix/themes/layout/box.pyi +1 -195
- reflex/components/radix/themes/layout/center.pyi +0 -194
- reflex/components/radix/themes/layout/container.pyi +0 -178
- reflex/components/radix/themes/layout/flex.pyi +0 -194
- reflex/components/radix/themes/layout/grid.pyi +0 -194
- reflex/components/radix/themes/layout/list.pyi +0 -978
- reflex/components/radix/themes/layout/section.pyi +0 -194
- reflex/components/radix/themes/layout/spacer.pyi +0 -194
- reflex/components/radix/themes/layout/stack.pyi +0 -582
- reflex/components/radix/themes/typography/blockquote.pyi +0 -196
- reflex/components/radix/themes/typography/code.pyi +0 -194
- reflex/components/radix/themes/typography/heading.pyi +0 -194
- reflex/components/radix/themes/typography/link.pyi +0 -237
- reflex/components/radix/themes/typography/text.pyi +0 -1360
- reflex/components/react_router/dom.pyi +0 -237
- reflex/components/recharts/cartesian.py +12 -18
- reflex/components/recharts/general.py +12 -18
- reflex/constants/installer.py +5 -5
- reflex/custom_components/custom_components.py +6 -5
- reflex/environment.py +30 -7
- reflex/event.py +14 -12
- reflex/experimental/client_state.py +11 -12
- reflex/istate/data.py +8 -10
- reflex/istate/manager/__init__.py +3 -0
- reflex/istate/manager/disk.py +151 -5
- reflex/model.py +1 -1
- reflex/plugins/_screenshot.py +2 -2
- reflex/plugins/shared_tailwind.py +9 -14
- reflex/reflex.py +7 -9
- reflex/state.py +30 -37
- reflex/style.py +6 -6
- reflex/testing.py +54 -30
- reflex/utils/codespaces.py +1 -1
- reflex/utils/compat.py +1 -0
- reflex/utils/decorator.py +3 -3
- reflex/utils/format.py +18 -22
- reflex/utils/prerequisites.py +1 -1
- reflex/utils/pyi_generator.py +51 -57
- reflex/utils/serializers.py +1 -1
- reflex/utils/telemetry.py +1 -1
- reflex/utils/templates.py +4 -4
- reflex/utils/types.py +3 -3
- reflex/vars/base.py +26 -29
- reflex/vars/color.py +6 -8
- reflex/vars/dep_tracking.py +5 -3
- reflex/vars/function.py +3 -3
- reflex/vars/object.py +9 -13
- reflex/vars/sequence.py +18 -24
- {reflex-0.8.15a2.dist-info → reflex-0.8.16a1.dist-info}/METADATA +1 -1
- {reflex-0.8.15a2.dist-info → reflex-0.8.16a1.dist-info}/RECORD +138 -138
- {reflex-0.8.15a2.dist-info → reflex-0.8.16a1.dist-info}/WHEEL +0 -0
- {reflex-0.8.15a2.dist-info → reflex-0.8.16a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.15a2.dist-info → reflex-0.8.16a1.dist-info}/licenses/LICENSE +0 -0
reflex/istate/manager/disk.py
CHANGED
|
@@ -4,15 +4,27 @@ import asyncio
|
|
|
4
4
|
import contextlib
|
|
5
5
|
import dataclasses
|
|
6
6
|
import functools
|
|
7
|
+
import time
|
|
7
8
|
from collections.abc import AsyncIterator
|
|
8
9
|
from hashlib import md5
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
from typing_extensions import override
|
|
12
13
|
|
|
14
|
+
from reflex.environment import environment
|
|
13
15
|
from reflex.istate.manager import StateManager, _default_token_expiration
|
|
14
16
|
from reflex.state import BaseState, _split_substate_key, _substate_key
|
|
15
|
-
from reflex.utils import path_ops, prerequisites
|
|
17
|
+
from reflex.utils import console, path_ops, prerequisites
|
|
18
|
+
from reflex.utils.misc import run_in_thread
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass(frozen=True)
|
|
22
|
+
class QueueItem:
|
|
23
|
+
"""An item in the write queue."""
|
|
24
|
+
|
|
25
|
+
token: str
|
|
26
|
+
state: BaseState
|
|
27
|
+
timestamp: float
|
|
16
28
|
|
|
17
29
|
|
|
18
30
|
@dataclasses.dataclass
|
|
@@ -34,6 +46,22 @@ class StateManagerDisk(StateManager):
|
|
|
34
46
|
# The token expiration time (s).
|
|
35
47
|
token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
|
|
36
48
|
|
|
49
|
+
# Last time a token was touched.
|
|
50
|
+
_token_last_touched: dict[str, float] = dataclasses.field(
|
|
51
|
+
default_factory=dict,
|
|
52
|
+
init=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Pending writes
|
|
56
|
+
_write_queue: dict[str, QueueItem] = dataclasses.field(
|
|
57
|
+
default_factory=dict,
|
|
58
|
+
init=False,
|
|
59
|
+
)
|
|
60
|
+
_write_queue_task: asyncio.Task | None = None
|
|
61
|
+
_write_debounce_seconds: float = dataclasses.field(
|
|
62
|
+
default=environment.REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS.get()
|
|
63
|
+
)
|
|
64
|
+
|
|
37
65
|
def __post_init__(self):
|
|
38
66
|
"""Create a new state manager."""
|
|
39
67
|
path_ops.mkdir(self.states_directory)
|
|
@@ -51,8 +79,6 @@ class StateManagerDisk(StateManager):
|
|
|
51
79
|
|
|
52
80
|
def _purge_expired_states(self):
|
|
53
81
|
"""Purge expired states from the disk."""
|
|
54
|
-
import time
|
|
55
|
-
|
|
56
82
|
for path in path_ops.ls(self.states_directory):
|
|
57
83
|
# check path is a pickle file
|
|
58
84
|
if path.suffix != ".pkl":
|
|
@@ -137,6 +163,7 @@ class StateManagerDisk(StateManager):
|
|
|
137
163
|
The state for the token.
|
|
138
164
|
"""
|
|
139
165
|
client_token = _split_substate_key(token)[0]
|
|
166
|
+
self._token_last_touched[client_token] = time.time()
|
|
140
167
|
root_state = self.states.get(client_token)
|
|
141
168
|
if root_state is not None:
|
|
142
169
|
# Retrieved state from memory.
|
|
@@ -170,11 +197,109 @@ class StateManagerDisk(StateManager):
|
|
|
170
197
|
if pickle_state:
|
|
171
198
|
if not self.states_directory.exists():
|
|
172
199
|
self.states_directory.mkdir(parents=True, exist_ok=True)
|
|
173
|
-
|
|
200
|
+
await run_in_thread(
|
|
201
|
+
lambda: self.token_path(substate_token).write_bytes(pickle_state),
|
|
202
|
+
)
|
|
174
203
|
|
|
175
204
|
for substate_substate in substate.substates.values():
|
|
176
205
|
await self.set_state_for_substate(client_token, substate_substate)
|
|
177
206
|
|
|
207
|
+
async def _process_write_queue_delay(self):
|
|
208
|
+
"""Wait for the debounce period before processing the write queue again."""
|
|
209
|
+
now = time.time()
|
|
210
|
+
if self._write_queue:
|
|
211
|
+
# There are still items in the queue, schedule another run.
|
|
212
|
+
next_write_in = max(
|
|
213
|
+
0,
|
|
214
|
+
min(
|
|
215
|
+
self._write_debounce_seconds - (now - item.timestamp)
|
|
216
|
+
for item in self._write_queue.values()
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
await asyncio.sleep(next_write_in)
|
|
220
|
+
elif self._write_debounce_seconds > 0:
|
|
221
|
+
# No items left, wait a bit before checking again.
|
|
222
|
+
await asyncio.sleep(self._write_debounce_seconds)
|
|
223
|
+
else:
|
|
224
|
+
# Debounce is disabled, so sleep until the next token expiration.
|
|
225
|
+
oldest_token_last_touch = min(
|
|
226
|
+
self._token_last_touched.values(), default=now
|
|
227
|
+
)
|
|
228
|
+
next_expiration_in = self.token_expiration - (now - oldest_token_last_touch)
|
|
229
|
+
await asyncio.sleep(next_expiration_in)
|
|
230
|
+
|
|
231
|
+
async def _process_write_queue(self):
|
|
232
|
+
"""Long running task that checks for states to write to disk.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
asyncio.CancelledError: When the task is cancelled.
|
|
236
|
+
"""
|
|
237
|
+
while True:
|
|
238
|
+
try:
|
|
239
|
+
now = time.time()
|
|
240
|
+
# sort the _write_queue by oldest timestamp and exclude items younger than debounce time
|
|
241
|
+
items_to_write = sorted(
|
|
242
|
+
(
|
|
243
|
+
item
|
|
244
|
+
for item in self._write_queue.values()
|
|
245
|
+
if now - item.timestamp >= self._write_debounce_seconds
|
|
246
|
+
),
|
|
247
|
+
key=lambda item: item.timestamp,
|
|
248
|
+
)
|
|
249
|
+
for item in items_to_write:
|
|
250
|
+
token = item.token
|
|
251
|
+
client_token, _ = _split_substate_key(token)
|
|
252
|
+
await self.set_state_for_substate(
|
|
253
|
+
client_token, self._write_queue.pop(token).state
|
|
254
|
+
)
|
|
255
|
+
# Check for expired states to purge.
|
|
256
|
+
for token, last_touched in list(self._token_last_touched.items()):
|
|
257
|
+
if now - last_touched > self.token_expiration:
|
|
258
|
+
self._token_last_touched.pop(token)
|
|
259
|
+
self.states.pop(token, None)
|
|
260
|
+
await run_in_thread(self._purge_expired_states)
|
|
261
|
+
await self._process_write_queue_delay()
|
|
262
|
+
except asyncio.CancelledError: # noqa: PERF203
|
|
263
|
+
await self._flush_write_queue()
|
|
264
|
+
raise
|
|
265
|
+
except Exception as e:
|
|
266
|
+
console.error(f"Error processing write queue: {e!r}")
|
|
267
|
+
if e.args == ("cannot schedule new futures after shutdown",):
|
|
268
|
+
# Event loop is shutdown, nothing else we can really do...
|
|
269
|
+
return
|
|
270
|
+
await self._process_write_queue_delay()
|
|
271
|
+
|
|
272
|
+
async def _flush_write_queue(self):
|
|
273
|
+
"""Flush any remaining items in the write queue to disk."""
|
|
274
|
+
outstanding_items = list(self._write_queue.values())
|
|
275
|
+
n_outstanding_items = len(outstanding_items)
|
|
276
|
+
self._write_queue.clear()
|
|
277
|
+
# When the task is cancelled, write all remaining items to disk.
|
|
278
|
+
console.debug(
|
|
279
|
+
f"StateManagerDisk._flush_write_queue: writing {n_outstanding_items} remaining items to disk"
|
|
280
|
+
)
|
|
281
|
+
for item in outstanding_items:
|
|
282
|
+
token = item.token
|
|
283
|
+
client_token, _ = _split_substate_key(token)
|
|
284
|
+
await self.set_state_for_substate(
|
|
285
|
+
client_token,
|
|
286
|
+
item.state,
|
|
287
|
+
)
|
|
288
|
+
console.debug(
|
|
289
|
+
f"StateManagerDisk._flush_write_queue: Finished writing {n_outstanding_items} items"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def _schedule_process_write_queue(self):
|
|
293
|
+
"""Schedule the write queue processing task if not already running."""
|
|
294
|
+
if self._write_queue_task is None or self._write_queue_task.done():
|
|
295
|
+
async with self._state_manager_lock:
|
|
296
|
+
if self._write_queue_task is None or self._write_queue_task.done():
|
|
297
|
+
self._write_queue_task = asyncio.create_task(
|
|
298
|
+
self._process_write_queue(),
|
|
299
|
+
name="StateManagerDisk|WriteQueueProcessor",
|
|
300
|
+
)
|
|
301
|
+
await asyncio.sleep(0) # Yield to allow the task to start.
|
|
302
|
+
|
|
178
303
|
@override
|
|
179
304
|
async def set_state(self, token: str, state: BaseState):
|
|
180
305
|
"""Set the state for a token.
|
|
@@ -184,7 +309,19 @@ class StateManagerDisk(StateManager):
|
|
|
184
309
|
state: The state to set.
|
|
185
310
|
"""
|
|
186
311
|
client_token, _ = _split_substate_key(token)
|
|
187
|
-
|
|
312
|
+
if self._write_debounce_seconds > 0:
|
|
313
|
+
# Deferred write to reduce disk IO overhead.
|
|
314
|
+
if client_token not in self._write_queue:
|
|
315
|
+
self._write_queue[client_token] = QueueItem(
|
|
316
|
+
token=client_token,
|
|
317
|
+
state=state,
|
|
318
|
+
timestamp=time.time(),
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
# Immediate write to disk.
|
|
322
|
+
await self.set_state_for_substate(client_token, state)
|
|
323
|
+
# Ensure the processing task is scheduled to handle expirations and any deferred writes.
|
|
324
|
+
await self._schedule_process_write_queue()
|
|
188
325
|
|
|
189
326
|
@override
|
|
190
327
|
@contextlib.asynccontextmanager
|
|
@@ -208,3 +345,12 @@ class StateManagerDisk(StateManager):
|
|
|
208
345
|
state = await self.get_state(token)
|
|
209
346
|
yield state
|
|
210
347
|
await self.set_state(token, state)
|
|
348
|
+
|
|
349
|
+
async def close(self):
|
|
350
|
+
"""Close the state manager, flushing any pending writes to disk."""
|
|
351
|
+
async with self._state_manager_lock:
|
|
352
|
+
if self._write_queue_task:
|
|
353
|
+
self._write_queue_task.cancel()
|
|
354
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
355
|
+
await self._write_queue_task
|
|
356
|
+
self._write_queue_task = None
|
reflex/model.py
CHANGED
|
@@ -299,7 +299,7 @@ if find_spec("sqlmodel") and find_spec("sqlalchemy") and find_spec("pydantic"):
|
|
|
299
299
|
# Format output with message
|
|
300
300
|
return f" [{status_icon}] {current}{head_marker}, {message}"
|
|
301
301
|
|
|
302
|
-
|
|
302
|
+
def get_db_status() -> dict[str, bool]:
|
|
303
303
|
"""Checks the status of the database connection.
|
|
304
304
|
|
|
305
305
|
Attempts to connect to the database and execute a simple query to verify connectivity.
|
reflex/plugins/_screenshot.py
CHANGED
|
@@ -68,7 +68,7 @@ class ScreenshotPlugin(BasePlugin):
|
|
|
68
68
|
if not app._api:
|
|
69
69
|
return
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
def active_connections(_request: "Request") -> "Response":
|
|
72
72
|
from starlette.responses import JSONResponse
|
|
73
73
|
|
|
74
74
|
if not app.event_namespace:
|
|
@@ -122,7 +122,7 @@ class ScreenshotPlugin(BasePlugin):
|
|
|
122
122
|
while found_new:
|
|
123
123
|
found_new = False
|
|
124
124
|
|
|
125
|
-
for state in all_states:
|
|
125
|
+
for state in list(all_states):
|
|
126
126
|
for substate in state.substates.values():
|
|
127
127
|
substate._was_touched = True
|
|
128
128
|
|
|
@@ -112,12 +112,9 @@ def tailwind_config_js_template(
|
|
|
112
112
|
]
|
|
113
113
|
|
|
114
114
|
# Generate import statements for destructured imports
|
|
115
|
-
import_lines = "\n".join(
|
|
116
|
-
[
|
|
117
|
-
|
|
118
|
-
for imp in imports
|
|
119
|
-
]
|
|
120
|
-
)
|
|
115
|
+
import_lines = "\n".join([
|
|
116
|
+
f"import {{ {imp['name']} }} from {json.dumps(imp['from'])};" for imp in imports
|
|
117
|
+
])
|
|
121
118
|
|
|
122
119
|
# Generate plugin imports
|
|
123
120
|
plugin_imports = []
|
|
@@ -131,12 +128,10 @@ def tailwind_config_js_template(
|
|
|
131
128
|
|
|
132
129
|
plugin_imports_lines = "\n".join(plugin_imports)
|
|
133
130
|
|
|
134
|
-
presets_imports_lines = "\n".join(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
]
|
|
139
|
-
)
|
|
131
|
+
presets_imports_lines = "\n".join([
|
|
132
|
+
f"import preset{i} from {json.dumps(preset)};"
|
|
133
|
+
for i, preset in enumerate(presets, 1)
|
|
134
|
+
])
|
|
140
135
|
|
|
141
136
|
# Generate plugin array
|
|
142
137
|
plugin_list = []
|
|
@@ -159,8 +154,8 @@ def tailwind_config_js_template(
|
|
|
159
154
|
{presets_imports_lines}
|
|
160
155
|
|
|
161
156
|
export default {{
|
|
162
|
-
content: {json.dumps(content
|
|
163
|
-
theme: {json.dumps(theme
|
|
157
|
+
content: {json.dumps(content or default_content)},
|
|
158
|
+
theme: {json.dumps(theme or {})},
|
|
164
159
|
{f"darkMode: {json.dumps(dark_mode)}," if dark_mode is not None else ""}
|
|
165
160
|
{f"corePlugins: {json.dumps(core_plugins)}," if core_plugins is not None else ""}
|
|
166
161
|
{f"importants: {json.dumps(important)}," if important is not None else ""}
|
reflex/reflex.py
CHANGED
|
@@ -269,15 +269,13 @@ def _run(
|
|
|
269
269
|
|
|
270
270
|
# In prod mode, run the backend on a separate thread.
|
|
271
271
|
if backend and env == constants.Env.PROD:
|
|
272
|
-
commands.append(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
)
|
|
272
|
+
commands.append((
|
|
273
|
+
backend_cmd,
|
|
274
|
+
backend_host,
|
|
275
|
+
backend_port,
|
|
276
|
+
config.loglevel.subprocess_level(),
|
|
277
|
+
frontend,
|
|
278
|
+
))
|
|
281
279
|
|
|
282
280
|
if single_port:
|
|
283
281
|
setup_frontend(Path.cwd())
|
reflex/state.py
CHANGED
|
@@ -112,13 +112,13 @@ def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Call
|
|
|
112
112
|
)
|
|
113
113
|
if inspect.iscoroutinefunction(fn):
|
|
114
114
|
|
|
115
|
-
async def _no_chain_background_task_co(*args, **kwargs):
|
|
115
|
+
async def _no_chain_background_task_co(*args, **kwargs): # noqa: RUF029
|
|
116
116
|
raise RuntimeError(message)
|
|
117
117
|
|
|
118
118
|
return _no_chain_background_task_co
|
|
119
119
|
if inspect.isasyncgenfunction(fn):
|
|
120
120
|
|
|
121
|
-
async def _no_chain_background_task_gen(*args, **kwargs):
|
|
121
|
+
async def _no_chain_background_task_gen(*args, **kwargs): # noqa: RUF029
|
|
122
122
|
yield
|
|
123
123
|
raise RuntimeError(message)
|
|
124
124
|
|
|
@@ -187,14 +187,12 @@ class EventHandlerSetVar(EventHandler):
|
|
|
187
187
|
Returns:
|
|
188
188
|
The hash of the event handler.
|
|
189
189
|
"""
|
|
190
|
-
return hash(
|
|
191
|
-
(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
)
|
|
190
|
+
return hash((
|
|
191
|
+
tuple(self.event_actions.items()),
|
|
192
|
+
self.fn,
|
|
193
|
+
self.state_full_name,
|
|
194
|
+
self.state_cls,
|
|
195
|
+
))
|
|
198
196
|
|
|
199
197
|
def setvar(self, var_name: str, value: Any):
|
|
200
198
|
"""Set the state variable to the value of the event.
|
|
@@ -529,14 +527,12 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
529
527
|
if types.is_backend_base_variable(name, cls)
|
|
530
528
|
}
|
|
531
529
|
# Add annotated backend vars that may not have a default value.
|
|
532
|
-
new_backend_vars.update(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
)
|
|
530
|
+
new_backend_vars.update({
|
|
531
|
+
name: cls._get_var_default(name, annotation_value)
|
|
532
|
+
for name, annotation_value in cls._get_type_hints().items()
|
|
533
|
+
if name not in new_backend_vars
|
|
534
|
+
and types.is_backend_base_variable(name, cls)
|
|
535
|
+
})
|
|
540
536
|
|
|
541
537
|
cls.backend_vars = {
|
|
542
538
|
**cls.inherited_backend_vars,
|
|
@@ -794,9 +790,10 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
794
790
|
parent_state = defining_state_cls.get_parent_state()
|
|
795
791
|
if parent_state is not None:
|
|
796
792
|
defining_state_cls = parent_state
|
|
797
|
-
defining_state_cls._var_dependencies.setdefault(dvar, set()).add(
|
|
798
|
-
|
|
799
|
-
|
|
793
|
+
defining_state_cls._var_dependencies.setdefault(dvar, set()).add((
|
|
794
|
+
cls.get_full_name(),
|
|
795
|
+
cvar_name,
|
|
796
|
+
))
|
|
800
797
|
defining_state_cls._potentially_dirty_states.add(
|
|
801
798
|
cls.get_full_name()
|
|
802
799
|
)
|
|
@@ -1143,7 +1140,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1143
1140
|
from reflex.config import get_config
|
|
1144
1141
|
|
|
1145
1142
|
config = get_config()
|
|
1146
|
-
|
|
1143
|
+
create_event_handler_kwargs = {}
|
|
1147
1144
|
|
|
1148
1145
|
if config.state_auto_setters is None:
|
|
1149
1146
|
|
|
@@ -1159,14 +1156,14 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1159
1156
|
)
|
|
1160
1157
|
return super().__call__(*args, **kwargs)
|
|
1161
1158
|
|
|
1162
|
-
|
|
1159
|
+
create_event_handler_kwargs["event_handler_cls"] = (
|
|
1163
1160
|
EventHandlerDeprecatedSetter
|
|
1164
1161
|
)
|
|
1165
1162
|
|
|
1166
1163
|
setter_name = Var._get_setter_name_for_name(name)
|
|
1167
1164
|
if setter_name not in cls.__dict__:
|
|
1168
1165
|
event_handler = cls._create_event_handler(
|
|
1169
|
-
prop._get_setter(name), **
|
|
1166
|
+
prop._get_setter(name), **create_event_handler_kwargs
|
|
1170
1167
|
)
|
|
1171
1168
|
cls.event_handlers[setter_name] = event_handler
|
|
1172
1169
|
setattr(cls, setter_name, event_handler)
|
|
@@ -1524,12 +1521,10 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1524
1521
|
return {
|
|
1525
1522
|
cls.get_class_substate(substate_name)
|
|
1526
1523
|
for substate_name in cls._always_dirty_substates
|
|
1527
|
-
}.union(
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
)
|
|
1524
|
+
}.union({
|
|
1525
|
+
cls.get_root_state().get_class_substate(substate_name)
|
|
1526
|
+
for substate_name in cls._potentially_dirty_states
|
|
1527
|
+
})
|
|
1533
1528
|
|
|
1534
1529
|
def _get_root_state(self) -> BaseState:
|
|
1535
1530
|
"""Get the root state of the state tree.
|
|
@@ -1858,13 +1853,11 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1858
1853
|
):
|
|
1859
1854
|
if issubclass(hinted_args, Model):
|
|
1860
1855
|
# Remove non-fields from the payload
|
|
1861
|
-
payload[arg] = hinted_args(
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
}
|
|
1867
|
-
)
|
|
1856
|
+
payload[arg] = hinted_args(**{
|
|
1857
|
+
key: value
|
|
1858
|
+
for key, value in value.items()
|
|
1859
|
+
if key in hinted_args.__fields__
|
|
1860
|
+
})
|
|
1868
1861
|
elif dataclasses.is_dataclass(hinted_args):
|
|
1869
1862
|
payload[arg] = hinted_args(**value)
|
|
1870
1863
|
elif find_spec("pydantic"):
|
reflex/style.py
CHANGED
|
@@ -289,11 +289,11 @@ class Style(dict[str, Any]):
|
|
|
289
289
|
value: The value to set.
|
|
290
290
|
"""
|
|
291
291
|
# Create a Var to collapse VarData encoded in f-string.
|
|
292
|
-
|
|
293
|
-
if
|
|
292
|
+
var = LiteralVar.create(value)
|
|
293
|
+
if var is not None:
|
|
294
294
|
# Carry the imports/hooks when setting a Var as a value.
|
|
295
295
|
self._var_data = VarData.merge(
|
|
296
|
-
getattr(self, "_var_data", None),
|
|
296
|
+
getattr(self, "_var_data", None), var._get_all_var_data()
|
|
297
297
|
)
|
|
298
298
|
super().__setitem__(key, value)
|
|
299
299
|
|
|
@@ -348,7 +348,7 @@ def format_as_emotion(style_dict: dict[str, Any]) -> Style | None:
|
|
|
348
348
|
Returns:
|
|
349
349
|
The emotion style dict.
|
|
350
350
|
"""
|
|
351
|
-
|
|
351
|
+
var_data = style_dict._var_data if isinstance(style_dict, Style) else None
|
|
352
352
|
|
|
353
353
|
emotion_style = Style()
|
|
354
354
|
|
|
@@ -381,8 +381,8 @@ def format_as_emotion(style_dict: dict[str, Any]) -> Style | None:
|
|
|
381
381
|
else:
|
|
382
382
|
emotion_style[key] = value
|
|
383
383
|
if emotion_style:
|
|
384
|
-
if
|
|
385
|
-
emotion_style._var_data = VarData.merge(emotion_style._var_data,
|
|
384
|
+
if var_data is not None:
|
|
385
|
+
emotion_style._var_data = VarData.merge(emotion_style._var_data, var_data)
|
|
386
386
|
return emotion_style
|
|
387
387
|
return None
|
|
388
388
|
|
reflex/testing.py
CHANGED
|
@@ -28,10 +28,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
|
28
28
|
import uvicorn
|
|
29
29
|
|
|
30
30
|
import reflex
|
|
31
|
-
import reflex.environment
|
|
32
31
|
import reflex.reflex
|
|
33
32
|
import reflex.utils.build
|
|
34
|
-
import reflex.utils.exec
|
|
35
33
|
import reflex.utils.format
|
|
36
34
|
import reflex.utils.prerequisites
|
|
37
35
|
import reflex.utils.processes
|
|
@@ -41,7 +39,12 @@ from reflex.environment import environment
|
|
|
41
39
|
from reflex.istate.manager.disk import StateManagerDisk
|
|
42
40
|
from reflex.istate.manager.memory import StateManagerMemory
|
|
43
41
|
from reflex.istate.manager.redis import StateManagerRedis
|
|
44
|
-
from reflex.state import
|
|
42
|
+
from reflex.state import (
|
|
43
|
+
BaseState,
|
|
44
|
+
StateManager,
|
|
45
|
+
_split_substate_key,
|
|
46
|
+
reload_state_module,
|
|
47
|
+
)
|
|
45
48
|
from reflex.utils import console, js_runtimes
|
|
46
49
|
from reflex.utils.export import export
|
|
47
50
|
from reflex.utils.token_manager import TokenManager
|
|
@@ -246,14 +249,12 @@ class AppHarness:
|
|
|
246
249
|
if isinstance(self.app_source, functools.partial):
|
|
247
250
|
self.app_source = self.app_source.func
|
|
248
251
|
# get the source from a function or module object
|
|
249
|
-
source_code = "\n".join(
|
|
250
|
-
[
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
]
|
|
256
|
-
)
|
|
252
|
+
source_code = "\n".join([
|
|
253
|
+
"\n".join([
|
|
254
|
+
self.get_app_global_source(k, v) for k, v in app_globals.items()
|
|
255
|
+
]),
|
|
256
|
+
self._get_source_from_app_source(self.app_source),
|
|
257
|
+
])
|
|
257
258
|
get_config().loglevel = reflex.constants.LogLevel.INFO
|
|
258
259
|
with chdir(self.app_path):
|
|
259
260
|
reflex.reflex._init(
|
|
@@ -279,15 +280,16 @@ class AppHarness:
|
|
|
279
280
|
)
|
|
280
281
|
)
|
|
281
282
|
self.app_asgi = self.app_instance()
|
|
282
|
-
if self.app_instance and
|
|
283
|
-
self.app_instance._state_manager, StateManagerRedis
|
|
284
|
-
):
|
|
283
|
+
if self.app_instance and self.app_instance._state_manager is not None:
|
|
285
284
|
if self.app_instance._state is None:
|
|
286
285
|
msg = "State is not set."
|
|
287
286
|
raise RuntimeError(msg)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
287
|
+
if isinstance(self.app_instance._state_manager, StateManagerRedis):
|
|
288
|
+
# Create our own redis connection for testing.
|
|
289
|
+
self.state_manager = StateManagerRedis.create(self.app_instance._state)
|
|
290
|
+
elif isinstance(self.app_instance._state_manager, StateManagerDisk):
|
|
291
|
+
self.state_manager = StateManagerDisk.create(self.app_instance._state)
|
|
292
|
+
if self.state_manager is None:
|
|
291
293
|
self.state_manager = (
|
|
292
294
|
self.app_instance._state_manager if self.app_instance else None
|
|
293
295
|
)
|
|
@@ -305,8 +307,9 @@ class AppHarness:
|
|
|
305
307
|
|
|
306
308
|
async def _shutdown(*args, **kwargs) -> None:
|
|
307
309
|
# ensure redis is closed before event loop
|
|
308
|
-
if
|
|
309
|
-
self.app_instance
|
|
310
|
+
if (
|
|
311
|
+
self.app_instance is not None
|
|
312
|
+
and self.app_instance._state_manager is not None
|
|
310
313
|
):
|
|
311
314
|
with contextlib.suppress(ValueError):
|
|
312
315
|
await self.app_instance._state_manager.close()
|
|
@@ -358,6 +361,12 @@ class AppHarness:
|
|
|
358
361
|
Raises:
|
|
359
362
|
RuntimeError: when the state manager cannot be reset
|
|
360
363
|
"""
|
|
364
|
+
if (
|
|
365
|
+
self.app_instance is not None
|
|
366
|
+
and self.app_instance._state_manager is not None
|
|
367
|
+
):
|
|
368
|
+
with contextlib.suppress(RuntimeError):
|
|
369
|
+
await self.app_instance._state_manager.close()
|
|
361
370
|
if (
|
|
362
371
|
self.app_instance is not None
|
|
363
372
|
and isinstance(
|
|
@@ -366,8 +375,6 @@ class AppHarness:
|
|
|
366
375
|
)
|
|
367
376
|
and self.app_instance._state is not None
|
|
368
377
|
):
|
|
369
|
-
with contextlib.suppress(RuntimeError):
|
|
370
|
-
await self.app_instance._state_manager.close()
|
|
371
378
|
self.app_instance._state_manager = StateManagerRedis.create(
|
|
372
379
|
state=self.app_instance._state,
|
|
373
380
|
)
|
|
@@ -713,11 +720,20 @@ class AppHarness:
|
|
|
713
720
|
if self.state_manager is None:
|
|
714
721
|
msg = "state_manager is not set."
|
|
715
722
|
raise RuntimeError(msg)
|
|
723
|
+
if self.app_instance is not None and isinstance(
|
|
724
|
+
self.app_instance.state_manager, StateManagerDisk
|
|
725
|
+
):
|
|
726
|
+
# Song and dance to convince the instance's state manager to flush
|
|
727
|
+
# (we can't directly await the _other_ loop's Future)
|
|
728
|
+
await self.app_instance.state_manager._flush_write_queue()
|
|
729
|
+
if isinstance(self.state_manager, StateManagerDisk):
|
|
730
|
+
# Force reload the latest state from disk.
|
|
731
|
+
client_token, _ = _split_substate_key(token)
|
|
732
|
+
self.state_manager.states.pop(client_token, None)
|
|
716
733
|
try:
|
|
717
734
|
return await self.state_manager.get_state(token)
|
|
718
735
|
finally:
|
|
719
|
-
|
|
720
|
-
await self.state_manager.close()
|
|
736
|
+
await self.state_manager.close()
|
|
721
737
|
|
|
722
738
|
async def set_state(self, token: str, **kwargs) -> None:
|
|
723
739
|
"""Set the state associated with the given token.
|
|
@@ -738,8 +754,13 @@ class AppHarness:
|
|
|
738
754
|
try:
|
|
739
755
|
await self.state_manager.set_state(token, state)
|
|
740
756
|
finally:
|
|
741
|
-
if
|
|
742
|
-
|
|
757
|
+
if self.app_instance is not None and isinstance(
|
|
758
|
+
self.app_instance.state_manager, StateManagerDisk
|
|
759
|
+
):
|
|
760
|
+
# Clear the token from the backend's cache so it will be reloaded.
|
|
761
|
+
client_token, _ = _split_substate_key(token)
|
|
762
|
+
self.app_instance.state_manager.states.pop(client_token, None)
|
|
763
|
+
await self.state_manager.close()
|
|
743
764
|
|
|
744
765
|
@contextlib.asynccontextmanager
|
|
745
766
|
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
@@ -761,17 +782,20 @@ class AppHarness:
|
|
|
761
782
|
msg = "App is not running."
|
|
762
783
|
raise RuntimeError(msg)
|
|
763
784
|
app_state_manager = self.app_instance.state_manager
|
|
764
|
-
if isinstance(self.state_manager, StateManagerRedis):
|
|
785
|
+
if isinstance(self.state_manager, (StateManagerRedis, StateManagerDisk)):
|
|
765
786
|
# Temporarily replace the app's state manager with our own, since
|
|
766
|
-
# the redis connection is on the backend_thread event loop
|
|
787
|
+
# the redis/disk connection is on the backend_thread event loop
|
|
767
788
|
self.app_instance._state_manager = self.state_manager
|
|
768
789
|
try:
|
|
769
790
|
async with self.app_instance.modify_state(token) as state:
|
|
770
791
|
yield state
|
|
771
792
|
finally:
|
|
772
|
-
if isinstance(
|
|
773
|
-
|
|
774
|
-
|
|
793
|
+
if isinstance(app_state_manager, StateManagerDisk):
|
|
794
|
+
# Clear the token from the cache so it will be reloaded.
|
|
795
|
+
client_token, _ = _split_substate_key(token)
|
|
796
|
+
app_state_manager.states.pop(client_token, None)
|
|
797
|
+
await self.state_manager.close()
|
|
798
|
+
self.app_instance._state_manager = app_state_manager
|
|
775
799
|
|
|
776
800
|
def token_manager(self) -> TokenManager:
|
|
777
801
|
"""Get the token manager for the app instance.
|
reflex/utils/codespaces.py
CHANGED
|
@@ -104,7 +104,7 @@ def codespaces_auto_redirect() -> list[Component]:
|
|
|
104
104
|
return []
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
def auth_codespace(_request: Request) -> HTMLResponse:
|
|
108
108
|
"""Page automatically redirecting back to the app after authenticating a codespace port forward.
|
|
109
109
|
|
|
110
110
|
Args:
|
reflex/utils/compat.py
CHANGED
|
@@ -74,6 +74,7 @@ if find_spec("pydantic") and find_spec("pydantic.v1"):
|
|
|
74
74
|
"""
|
|
75
75
|
namespace["__annotations__"] = annotations_from_namespace(namespace)
|
|
76
76
|
return super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
77
|
+
|
|
77
78
|
else:
|
|
78
79
|
ModelMetaclassLazyAnnotations = type # type: ignore[assignment]
|
|
79
80
|
|