reflex 0.6.8a2__py3-none-any.whl → 0.7.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of reflex might be problematic. Click here for more details.
- reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/_app.js.jinja2 +7 -7
- reflex/.templates/jinja/web/pages/utils.js.jinja2 +3 -3
- reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +1 -4
- reflex/.templates/web/utils/state.js +65 -36
- reflex/__init__.py +4 -17
- reflex/__init__.pyi +1 -2
- reflex/app.py +249 -116
- reflex/app_mixins/lifespan.py +9 -9
- reflex/app_mixins/middleware.py +6 -6
- reflex/app_module_for_backend.py +3 -7
- reflex/base.py +7 -7
- reflex/compiler/compiler.py +8 -0
- reflex/compiler/utils.py +35 -6
- reflex/components/base/app_wrap.pyi +16 -16
- reflex/components/base/bare.py +1 -1
- reflex/components/base/body.pyi +16 -16
- reflex/components/base/document.pyi +76 -76
- reflex/components/base/error_boundary.py +2 -1
- reflex/components/base/error_boundary.pyi +19 -22
- reflex/components/base/fragment.pyi +16 -16
- reflex/components/base/head.pyi +31 -31
- reflex/components/base/link.pyi +31 -31
- reflex/components/base/meta.py +2 -2
- reflex/components/base/meta.pyi +61 -61
- reflex/components/base/script.pyi +19 -19
- reflex/components/base/strict_mode.py +10 -0
- reflex/components/base/strict_mode.pyi +57 -0
- reflex/components/component.py +38 -77
- reflex/components/core/banner.py +159 -4
- reflex/components/core/banner.pyi +162 -76
- reflex/components/core/breakpoints.py +3 -1
- reflex/components/core/client_side_routing.py +1 -1
- reflex/components/core/client_side_routing.pyi +32 -32
- reflex/components/core/clipboard.pyi +17 -20
- reflex/components/core/cond.py +9 -10
- reflex/components/core/debounce.py +1 -1
- reflex/components/core/debounce.pyi +17 -17
- reflex/components/core/foreach.py +28 -3
- reflex/components/core/html.py +1 -1
- reflex/components/core/html.pyi +16 -16
- reflex/components/core/match.py +5 -5
- reflex/components/core/sticky.py +160 -0
- reflex/components/core/sticky.pyi +449 -0
- reflex/components/core/upload.py +2 -2
- reflex/components/core/upload.pyi +80 -88
- reflex/components/datadisplay/code.py +5 -14
- reflex/components/datadisplay/code.pyi +31 -31
- reflex/components/datadisplay/dataeditor.py +7 -4
- reflex/components/datadisplay/dataeditor.pyi +40 -54
- reflex/components/datadisplay/logo.py +13 -8
- reflex/components/datadisplay/shiki_code_block.py +14 -9
- reflex/components/datadisplay/shiki_code_block.pyi +46 -46
- reflex/components/dynamic.py +22 -3
- reflex/components/el/constants/reflex.py +1 -1
- reflex/components/el/element.py +1 -1
- reflex/components/el/element.pyi +16 -16
- reflex/components/el/elements/base.pyi +16 -16
- reflex/components/el/elements/forms.py +4 -4
- reflex/components/el/elements/forms.pyi +224 -258
- reflex/components/el/elements/inline.pyi +421 -421
- reflex/components/el/elements/media.pyi +376 -376
- reflex/components/el/elements/metadata.pyi +91 -91
- reflex/components/el/elements/other.pyi +106 -106
- reflex/components/el/elements/scripts.pyi +46 -46
- reflex/components/el/elements/sectioning.pyi +226 -226
- reflex/components/el/elements/tables.pyi +151 -151
- reflex/components/el/elements/typography.pyi +226 -226
- reflex/components/gridjs/datatable.pyi +31 -31
- reflex/components/lucide/icon.py +46 -8
- reflex/components/lucide/icon.pyi +85 -31
- reflex/components/markdown/markdown.py +10 -8
- reflex/components/markdown/markdown.pyi +16 -16
- reflex/components/moment/moment.py +2 -2
- reflex/components/moment/moment.pyi +17 -19
- reflex/components/next/base.pyi +16 -16
- reflex/components/next/image.py +16 -4
- reflex/components/next/image.pyi +22 -20
- reflex/components/next/link.py +1 -1
- reflex/components/next/link.pyi +16 -16
- reflex/components/next/video.pyi +16 -16
- reflex/components/plotly/plotly.py +5 -5
- reflex/components/plotly/plotly.pyi +34 -44
- reflex/components/props.py +3 -3
- reflex/components/radix/__init__.pyi +1 -1
- reflex/components/radix/primitives/accordion.py +9 -5
- reflex/components/radix/primitives/accordion.pyi +110 -108
- reflex/components/radix/primitives/base.pyi +31 -31
- reflex/components/radix/primitives/drawer.py +5 -2
- reflex/components/radix/primitives/drawer.pyi +179 -187
- reflex/components/radix/primitives/form.pyi +160 -172
- reflex/components/radix/primitives/progress.py +1 -1
- reflex/components/radix/primitives/progress.pyi +76 -76
- reflex/components/radix/primitives/slider.py +1 -1
- reflex/components/radix/primitives/slider.pyi +78 -82
- reflex/components/radix/themes/base.pyi +121 -121
- reflex/components/radix/themes/color_mode.py +11 -9
- reflex/components/radix/themes/color_mode.pyi +47 -49
- reflex/components/radix/themes/components/alert_dialog.py +3 -0
- reflex/components/radix/themes/components/alert_dialog.pyi +110 -112
- reflex/components/radix/themes/components/aspect_ratio.pyi +16 -16
- reflex/components/radix/themes/components/avatar.pyi +16 -16
- reflex/components/radix/themes/components/badge.pyi +16 -16
- reflex/components/radix/themes/components/button.pyi +16 -16
- reflex/components/radix/themes/components/callout.pyi +76 -76
- reflex/components/radix/themes/components/card.py +1 -1
- reflex/components/radix/themes/components/card.pyi +17 -17
- reflex/components/radix/themes/components/checkbox.pyi +49 -55
- reflex/components/radix/themes/components/checkbox_cards.pyi +31 -31
- reflex/components/radix/themes/components/checkbox_group.pyi +31 -31
- reflex/components/radix/themes/components/context_menu.py +5 -0
- reflex/components/radix/themes/components/context_menu.pyi +149 -155
- reflex/components/radix/themes/components/data_list.pyi +61 -61
- reflex/components/radix/themes/components/dialog.py +3 -0
- reflex/components/radix/themes/components/dialog.pyi +113 -117
- reflex/components/radix/themes/components/dropdown_menu.py +5 -0
- reflex/components/radix/themes/components/dropdown_menu.pyi +133 -137
- reflex/components/radix/themes/components/hover_card.py +3 -0
- reflex/components/radix/themes/components/hover_card.pyi +63 -67
- reflex/components/radix/themes/components/icon_button.py +2 -2
- reflex/components/radix/themes/components/icon_button.pyi +17 -16
- reflex/components/radix/themes/components/inset.pyi +16 -16
- reflex/components/radix/themes/components/popover.py +3 -0
- reflex/components/radix/themes/components/popover.pyi +68 -70
- reflex/components/radix/themes/components/progress.pyi +16 -16
- reflex/components/radix/themes/components/radio.pyi +16 -16
- reflex/components/radix/themes/components/radio_cards.py +2 -0
- reflex/components/radix/themes/components/radio_cards.pyi +32 -34
- reflex/components/radix/themes/components/radio_group.py +1 -1
- reflex/components/radix/themes/components/radio_group.pyi +62 -64
- reflex/components/radix/themes/components/scroll_area.pyi +16 -16
- reflex/components/radix/themes/components/segmented_control.pyi +32 -35
- reflex/components/radix/themes/components/select.py +4 -0
- reflex/components/radix/themes/components/select.pyi +145 -157
- reflex/components/radix/themes/components/separator.pyi +16 -16
- reflex/components/radix/themes/components/skeleton.py +3 -0
- reflex/components/radix/themes/components/skeleton.pyi +16 -16
- reflex/components/radix/themes/components/slider.pyi +22 -28
- reflex/components/radix/themes/components/spinner.pyi +16 -16
- reflex/components/radix/themes/components/switch.pyi +17 -19
- reflex/components/radix/themes/components/table.pyi +106 -106
- reflex/components/radix/themes/components/tabs.py +3 -0
- reflex/components/radix/themes/components/tabs.pyi +78 -82
- reflex/components/radix/themes/components/text_area.py +12 -0
- reflex/components/radix/themes/components/text_area.pyi +21 -33
- reflex/components/radix/themes/components/text_field.py +1 -1
- reflex/components/radix/themes/components/text_field.pyi +52 -80
- reflex/components/radix/themes/components/tooltip.py +6 -1
- reflex/components/radix/themes/components/tooltip.pyi +20 -21
- reflex/components/radix/themes/layout/__init__.pyi +1 -1
- reflex/components/radix/themes/layout/base.pyi +16 -16
- reflex/components/radix/themes/layout/box.pyi +16 -16
- reflex/components/radix/themes/layout/center.pyi +16 -16
- reflex/components/radix/themes/layout/container.pyi +16 -16
- reflex/components/radix/themes/layout/flex.pyi +16 -16
- reflex/components/radix/themes/layout/grid.pyi +16 -16
- reflex/components/radix/themes/layout/list.py +2 -2
- reflex/components/radix/themes/layout/list.pyi +76 -76
- reflex/components/radix/themes/layout/section.pyi +16 -16
- reflex/components/radix/themes/layout/spacer.pyi +16 -16
- reflex/components/radix/themes/layout/stack.py +2 -2
- reflex/components/radix/themes/layout/stack.pyi +46 -46
- reflex/components/radix/themes/typography/blockquote.pyi +16 -16
- reflex/components/radix/themes/typography/code.pyi +16 -16
- reflex/components/radix/themes/typography/heading.pyi +16 -16
- reflex/components/radix/themes/typography/link.py +1 -1
- reflex/components/radix/themes/typography/link.pyi +16 -16
- reflex/components/radix/themes/typography/text.py +2 -2
- reflex/components/radix/themes/typography/text.pyi +106 -106
- reflex/components/react_player/audio.pyi +33 -39
- reflex/components/react_player/react_player.py +1 -1
- reflex/components/react_player/react_player.pyi +32 -38
- reflex/components/react_player/video.pyi +33 -39
- reflex/components/recharts/__init__.py +2 -0
- reflex/components/recharts/__init__.pyi +2 -0
- reflex/components/recharts/cartesian.pyi +282 -282
- reflex/components/recharts/charts.py +15 -15
- reflex/components/recharts/charts.pyi +164 -164
- reflex/components/recharts/general.py +19 -4
- reflex/components/recharts/general.pyi +132 -81
- reflex/components/recharts/polar.py +2 -2
- reflex/components/recharts/polar.pyi +55 -55
- reflex/components/recharts/recharts.py +4 -4
- reflex/components/recharts/recharts.pyi +31 -31
- reflex/components/sonner/toast.py +15 -13
- reflex/components/sonner/toast.pyi +22 -22
- reflex/components/suneditor/editor.py +6 -4
- reflex/components/suneditor/editor.pyi +26 -40
- reflex/components/tags/iter_tag.py +3 -3
- reflex/components/tags/tag.py +25 -3
- reflex/config.py +48 -15
- reflex/constants/__init__.py +1 -0
- reflex/constants/base.py +4 -1
- reflex/constants/compiler.py +5 -2
- reflex/constants/config.py +8 -1
- reflex/constants/installer.py +9 -9
- reflex/constants/style.py +1 -1
- reflex/custom_components/custom_components.py +9 -7
- reflex/event.py +215 -208
- reflex/experimental/__init__.py +19 -11
- reflex/experimental/client_state.py +53 -28
- reflex/experimental/hooks.py +5 -5
- reflex/experimental/layout.py +8 -5
- reflex/experimental/layout.pyi +79 -83
- reflex/experimental/misc.py +3 -3
- reflex/istate/wrappers.py +1 -1
- reflex/middleware/hydrate_middleware.py +2 -2
- reflex/model.py +11 -6
- reflex/page.py +5 -5
- reflex/reflex.py +90 -19
- reflex/route.py +1 -1
- reflex/state.py +358 -401
- reflex/style.py +27 -3
- reflex/testing.py +29 -23
- reflex/utils/build.py +6 -2
- reflex/utils/codespaces.py +1 -4
- reflex/utils/compat.py +6 -5
- reflex/utils/console.py +52 -16
- reflex/utils/exceptions.py +89 -26
- reflex/utils/exec.py +69 -74
- reflex/utils/export.py +6 -1
- reflex/utils/format.py +8 -40
- reflex/utils/imports.py +2 -2
- reflex/utils/lazy_loader.py +7 -1
- reflex/utils/path_ops.py +28 -14
- reflex/utils/prerequisites.py +326 -67
- reflex/utils/processes.py +45 -32
- reflex/utils/pyi_generator.py +39 -33
- reflex/utils/registry.py +4 -4
- reflex/utils/serializers.py +1 -1
- reflex/utils/telemetry.py +5 -4
- reflex/utils/types.py +42 -18
- reflex/vars/base.py +656 -333
- reflex/vars/datetime.py +6 -7
- reflex/vars/dep_tracking.py +344 -0
- reflex/vars/function.py +11 -5
- reflex/vars/number.py +31 -43
- reflex/vars/object.py +63 -62
- reflex/vars/sequence.py +79 -67
- {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/METADATA +7 -8
- reflex-0.7.0a2.dist-info/RECORD +401 -0
- {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/WHEEL +1 -1
- reflex/experimental/assets.py +0 -37
- reflex-0.6.8a2.dist-info/RECORD +0 -397
- {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/LICENSE +0 -0
- {reflex-0.6.8a2.dist-info → reflex-0.7.0a2.dist-info}/entry_points.txt +0 -0
reflex/app.py
CHANGED
|
@@ -25,8 +25,8 @@ from typing import (
|
|
|
25
25
|
Callable,
|
|
26
26
|
Coroutine,
|
|
27
27
|
Dict,
|
|
28
|
-
Generic,
|
|
29
28
|
List,
|
|
29
|
+
MutableMapping,
|
|
30
30
|
Optional,
|
|
31
31
|
Set,
|
|
32
32
|
Type,
|
|
@@ -53,22 +53,28 @@ from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
|
|
|
53
53
|
from reflex.components.base.app_wrap import AppWrap
|
|
54
54
|
from reflex.components.base.error_boundary import ErrorBoundary
|
|
55
55
|
from reflex.components.base.fragment import Fragment
|
|
56
|
+
from reflex.components.base.strict_mode import StrictMode
|
|
56
57
|
from reflex.components.component import (
|
|
57
58
|
Component,
|
|
58
59
|
ComponentStyle,
|
|
59
60
|
evaluate_style_namespaces,
|
|
60
61
|
)
|
|
61
|
-
from reflex.components.core.banner import
|
|
62
|
+
from reflex.components.core.banner import (
|
|
63
|
+
backend_disabled,
|
|
64
|
+
connection_pulser,
|
|
65
|
+
connection_toaster,
|
|
66
|
+
)
|
|
62
67
|
from reflex.components.core.breakpoints import set_breakpoints
|
|
63
68
|
from reflex.components.core.client_side_routing import (
|
|
64
69
|
Default404Page,
|
|
65
70
|
wait_for_client_redirect,
|
|
66
71
|
)
|
|
72
|
+
from reflex.components.core.sticky import sticky
|
|
67
73
|
from reflex.components.core.upload import Upload, get_upload_dir
|
|
68
74
|
from reflex.components.radix import themes
|
|
69
75
|
from reflex.config import environment, get_config
|
|
70
76
|
from reflex.event import (
|
|
71
|
-
|
|
77
|
+
_EVENT_FIELDS,
|
|
72
78
|
Event,
|
|
73
79
|
EventHandler,
|
|
74
80
|
EventSpec,
|
|
@@ -144,7 +150,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
|
|
|
144
150
|
position="top-center",
|
|
145
151
|
id="backend_error",
|
|
146
152
|
style={"width": "500px"},
|
|
147
|
-
)
|
|
153
|
+
)
|
|
148
154
|
else:
|
|
149
155
|
error_message.insert(0, "An error occurred.")
|
|
150
156
|
return window_alert("\n".join(error_message))
|
|
@@ -156,9 +162,12 @@ def default_overlay_component() -> Component:
|
|
|
156
162
|
Returns:
|
|
157
163
|
The default overlay_component, which is a connection_modal.
|
|
158
164
|
"""
|
|
165
|
+
config = get_config()
|
|
166
|
+
|
|
159
167
|
return Fragment.create(
|
|
160
168
|
connection_pulser(),
|
|
161
169
|
connection_toaster(),
|
|
170
|
+
*([backend_disabled()] if config.is_reflex_cloud else []),
|
|
162
171
|
*codespaces.codespaces_auto_redirect(),
|
|
163
172
|
)
|
|
164
173
|
|
|
@@ -185,7 +194,7 @@ class OverlayFragment(Fragment):
|
|
|
185
194
|
@dataclasses.dataclass(
|
|
186
195
|
frozen=True,
|
|
187
196
|
)
|
|
188
|
-
class UnevaluatedPage
|
|
197
|
+
class UnevaluatedPage:
|
|
189
198
|
"""An uncompiled page."""
|
|
190
199
|
|
|
191
200
|
component: Union[Component, ComponentCallable]
|
|
@@ -193,7 +202,7 @@ class UnevaluatedPage(Generic[BASE_STATE]):
|
|
|
193
202
|
title: Union[Var, str, None]
|
|
194
203
|
description: Union[Var, str, None]
|
|
195
204
|
image: str
|
|
196
|
-
on_load: Union[EventType[
|
|
205
|
+
on_load: Union[EventType[()], None]
|
|
197
206
|
meta: List[Dict[str, str]]
|
|
198
207
|
|
|
199
208
|
|
|
@@ -250,36 +259,36 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
250
259
|
# Attributes to add to the html root tag of every page.
|
|
251
260
|
html_custom_attrs: Optional[Dict[str, str]] = None
|
|
252
261
|
|
|
253
|
-
# A map from a route to an unevaluated page.
|
|
254
|
-
|
|
262
|
+
# A map from a route to an unevaluated page.
|
|
263
|
+
_unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
|
|
255
264
|
default_factory=dict
|
|
256
265
|
)
|
|
257
266
|
|
|
258
|
-
# A map from a page route to the component to render. Users should use `add_page`.
|
|
259
|
-
|
|
267
|
+
# A map from a page route to the component to render. Users should use `add_page`.
|
|
268
|
+
_pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
|
|
260
269
|
|
|
261
|
-
# The backend API object.
|
|
262
|
-
|
|
270
|
+
# The backend API object.
|
|
271
|
+
_api: FastAPI | None = None
|
|
263
272
|
|
|
264
|
-
# The state class to use for the app.
|
|
265
|
-
|
|
273
|
+
# The state class to use for the app.
|
|
274
|
+
_state: Optional[Type[BaseState]] = None
|
|
266
275
|
|
|
267
276
|
# Class to manage many client states.
|
|
268
277
|
_state_manager: Optional[StateManager] = None
|
|
269
278
|
|
|
270
|
-
# Mapping from a route to event handlers to trigger when the page loads.
|
|
271
|
-
|
|
279
|
+
# Mapping from a route to event handlers to trigger when the page loads.
|
|
280
|
+
_load_events: Dict[str, List[IndividualEventType[()]]] = dataclasses.field(
|
|
272
281
|
default_factory=dict
|
|
273
282
|
)
|
|
274
283
|
|
|
275
|
-
# Admin dashboard to view and manage the database.
|
|
284
|
+
# Admin dashboard to view and manage the database.
|
|
276
285
|
admin_dash: Optional[AdminDash] = None
|
|
277
286
|
|
|
278
|
-
# The async server name space.
|
|
279
|
-
|
|
287
|
+
# The async server name space.
|
|
288
|
+
_event_namespace: Optional[EventNamespace] = None
|
|
280
289
|
|
|
281
|
-
# Background tasks that are currently running.
|
|
282
|
-
|
|
290
|
+
# Background tasks that are currently running.
|
|
291
|
+
_background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
|
|
283
292
|
|
|
284
293
|
# Frontend Error Handler Function
|
|
285
294
|
frontend_exception_handler: Callable[[Exception], None] = (
|
|
@@ -291,6 +300,24 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
291
300
|
[Exception], Union[EventSpec, List[EventSpec], None]
|
|
292
301
|
] = default_backend_exception_handler
|
|
293
302
|
|
|
303
|
+
@property
|
|
304
|
+
def api(self) -> FastAPI | None:
|
|
305
|
+
"""Get the backend api.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
The backend api.
|
|
309
|
+
"""
|
|
310
|
+
return self._api
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def event_namespace(self) -> EventNamespace | None:
|
|
314
|
+
"""Get the event namespace.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
The event namespace.
|
|
318
|
+
"""
|
|
319
|
+
return self._event_namespace
|
|
320
|
+
|
|
294
321
|
def __post_init__(self):
|
|
295
322
|
"""Initialize the app.
|
|
296
323
|
|
|
@@ -310,7 +337,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
310
337
|
set_breakpoints(self.style.pop("breakpoints"))
|
|
311
338
|
|
|
312
339
|
# Set up the API.
|
|
313
|
-
self.
|
|
340
|
+
self._api = FastAPI(lifespan=self._run_lifespan_tasks)
|
|
314
341
|
self._add_cors()
|
|
315
342
|
self._add_default_endpoints()
|
|
316
343
|
|
|
@@ -333,8 +360,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
333
360
|
|
|
334
361
|
def _enable_state(self) -> None:
|
|
335
362
|
"""Enable state for the app."""
|
|
336
|
-
if not self.
|
|
337
|
-
self.
|
|
363
|
+
if not self._state:
|
|
364
|
+
self._state = State
|
|
338
365
|
self._setup_state()
|
|
339
366
|
|
|
340
367
|
def _setup_state(self) -> None:
|
|
@@ -343,13 +370,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
343
370
|
Raises:
|
|
344
371
|
RuntimeError: If the socket server is invalid.
|
|
345
372
|
"""
|
|
346
|
-
if not self.
|
|
373
|
+
if not self._state:
|
|
347
374
|
return
|
|
348
375
|
|
|
349
376
|
config = get_config()
|
|
350
377
|
|
|
351
378
|
# Set up the state manager.
|
|
352
|
-
self._state_manager = StateManager.create(state=self.
|
|
379
|
+
self._state_manager = StateManager.create(state=self._state)
|
|
353
380
|
|
|
354
381
|
# Set up the Socket.IO AsyncServer.
|
|
355
382
|
if not self.sio:
|
|
@@ -380,12 +407,42 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
380
407
|
namespace = config.get_event_namespace()
|
|
381
408
|
|
|
382
409
|
# Create the event namespace and attach the main app. Not related to any paths.
|
|
383
|
-
self.
|
|
410
|
+
self._event_namespace = EventNamespace(namespace, self)
|
|
384
411
|
|
|
385
412
|
# Register the event namespace with the socket.
|
|
386
413
|
self.sio.register_namespace(self.event_namespace)
|
|
387
414
|
# Mount the socket app with the API.
|
|
388
|
-
self.api
|
|
415
|
+
if self.api:
|
|
416
|
+
|
|
417
|
+
class HeaderMiddleware:
|
|
418
|
+
def __init__(self, app: ASGIApp):
|
|
419
|
+
self.app = app
|
|
420
|
+
|
|
421
|
+
async def __call__(
|
|
422
|
+
self, scope: MutableMapping[str, Any], receive: Any, send: Callable
|
|
423
|
+
):
|
|
424
|
+
original_send = send
|
|
425
|
+
|
|
426
|
+
async def modified_send(message: dict):
|
|
427
|
+
if message["type"] == "websocket.accept":
|
|
428
|
+
if scope.get("subprotocols"):
|
|
429
|
+
# The following *does* say "subprotocol" instead of "subprotocols", intentionally.
|
|
430
|
+
message["subprotocol"] = scope["subprotocols"][0]
|
|
431
|
+
|
|
432
|
+
headers = dict(message.get("headers", []))
|
|
433
|
+
header_key = b"sec-websocket-protocol"
|
|
434
|
+
if subprotocol := headers.get(header_key):
|
|
435
|
+
message["headers"] = [
|
|
436
|
+
*message.get("headers", []),
|
|
437
|
+
(header_key, subprotocol),
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
return await original_send(message)
|
|
441
|
+
|
|
442
|
+
return await self.app(scope, receive, modified_send)
|
|
443
|
+
|
|
444
|
+
socket_app_with_headers = HeaderMiddleware(socket_app)
|
|
445
|
+
self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
|
|
389
446
|
|
|
390
447
|
# Check the exception handlers
|
|
391
448
|
self._validate_exception_handlers()
|
|
@@ -396,24 +453,35 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
396
453
|
Returns:
|
|
397
454
|
The string representation of the app.
|
|
398
455
|
"""
|
|
399
|
-
return f"<App state={self.
|
|
456
|
+
return f"<App state={self._state.__name__ if self._state else None}>"
|
|
400
457
|
|
|
401
458
|
def __call__(self) -> FastAPI:
|
|
402
459
|
"""Run the backend api instance.
|
|
403
460
|
|
|
461
|
+
Raises:
|
|
462
|
+
ValueError: If the app has not been initialized.
|
|
463
|
+
|
|
404
464
|
Returns:
|
|
405
465
|
The backend api.
|
|
406
466
|
"""
|
|
467
|
+
if not self.api:
|
|
468
|
+
raise ValueError("The app has not been initialized.")
|
|
407
469
|
return self.api
|
|
408
470
|
|
|
409
471
|
def _add_default_endpoints(self):
|
|
410
472
|
"""Add default api endpoints (ping)."""
|
|
411
473
|
# To test the server.
|
|
474
|
+
if not self.api:
|
|
475
|
+
return
|
|
476
|
+
|
|
412
477
|
self.api.get(str(constants.Endpoint.PING))(ping)
|
|
413
478
|
self.api.get(str(constants.Endpoint.HEALTH))(health)
|
|
414
479
|
|
|
415
480
|
def _add_optional_endpoints(self):
|
|
416
481
|
"""Add optional api endpoints (_upload)."""
|
|
482
|
+
if not self.api:
|
|
483
|
+
return
|
|
484
|
+
|
|
417
485
|
if Upload.is_used:
|
|
418
486
|
# To upload files.
|
|
419
487
|
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
|
@@ -431,6 +499,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
431
499
|
|
|
432
500
|
def _add_cors(self):
|
|
433
501
|
"""Add CORS middleware to the app."""
|
|
502
|
+
if not self.api:
|
|
503
|
+
return
|
|
434
504
|
self.api.add_middleware(
|
|
435
505
|
cors.CORSMiddleware,
|
|
436
506
|
allow_credentials=True,
|
|
@@ -462,14 +532,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
462
532
|
|
|
463
533
|
Returns:
|
|
464
534
|
The generated component.
|
|
465
|
-
|
|
466
|
-
Raises:
|
|
467
|
-
exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
|
|
468
535
|
"""
|
|
469
|
-
|
|
470
|
-
return component if isinstance(component, Component) else component()
|
|
471
|
-
except exceptions.MatchTypeError:
|
|
472
|
-
raise
|
|
536
|
+
return component if isinstance(component, Component) else component()
|
|
473
537
|
|
|
474
538
|
def add_page(
|
|
475
539
|
self,
|
|
@@ -478,7 +542,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
478
542
|
title: str | Var | None = None,
|
|
479
543
|
description: str | Var | None = None,
|
|
480
544
|
image: str = constants.DefaultPage.IMAGE,
|
|
481
|
-
on_load: EventType[
|
|
545
|
+
on_load: EventType[()] | None = None,
|
|
482
546
|
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
|
|
483
547
|
):
|
|
484
548
|
"""Add a page to the app.
|
|
@@ -526,13 +590,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
526
590
|
# Check if the route given is valid
|
|
527
591
|
verify_route_validity(route)
|
|
528
592
|
|
|
529
|
-
if route in self.
|
|
593
|
+
if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
|
|
530
594
|
# when the app is reloaded(typically for app harness tests), we should maintain
|
|
531
595
|
# the latest render function of a route.This applies typically to decorated pages
|
|
532
596
|
# since they are only added when app._compile is called.
|
|
533
|
-
self.
|
|
597
|
+
self._unevaluated_pages.pop(route)
|
|
534
598
|
|
|
535
|
-
if route in self.
|
|
599
|
+
if route in self._unevaluated_pages:
|
|
536
600
|
route_name = (
|
|
537
601
|
f"`{route}` or `/`"
|
|
538
602
|
if route == constants.PageNames.INDEX_ROUTE
|
|
@@ -545,15 +609,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
545
609
|
|
|
546
610
|
# Setup dynamic args for the route.
|
|
547
611
|
# this state assignment is only required for tests using the deprecated state kwarg for App
|
|
548
|
-
state = self.
|
|
612
|
+
state = self._state if self._state else State
|
|
549
613
|
state.setup_dynamic_args(get_route_args(route))
|
|
550
614
|
|
|
551
615
|
if on_load:
|
|
552
|
-
self.
|
|
616
|
+
self._load_events[route] = (
|
|
553
617
|
on_load if isinstance(on_load, list) else [on_load]
|
|
554
618
|
)
|
|
555
619
|
|
|
556
|
-
self.
|
|
620
|
+
self._unevaluated_pages[route] = UnevaluatedPage(
|
|
557
621
|
component=component,
|
|
558
622
|
route=route,
|
|
559
623
|
title=title,
|
|
@@ -563,14 +627,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
563
627
|
meta=meta,
|
|
564
628
|
)
|
|
565
629
|
|
|
566
|
-
def _compile_page(self, route: str):
|
|
630
|
+
def _compile_page(self, route: str, save_page: bool = True):
|
|
567
631
|
"""Compile a page.
|
|
568
632
|
|
|
569
633
|
Args:
|
|
570
634
|
route: The route of the page to compile.
|
|
635
|
+
save_page: If True, the compiled page is saved to self._pages.
|
|
571
636
|
"""
|
|
572
637
|
component, enable_state = compiler.compile_unevaluated_page(
|
|
573
|
-
route, self.
|
|
638
|
+
route, self._unevaluated_pages[route], self._state, self.style, self.theme
|
|
574
639
|
)
|
|
575
640
|
|
|
576
641
|
if enable_state:
|
|
@@ -578,9 +643,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
578
643
|
|
|
579
644
|
# Add the page.
|
|
580
645
|
self._check_routes_conflict(route)
|
|
581
|
-
|
|
646
|
+
if save_page:
|
|
647
|
+
self._pages[route] = component
|
|
582
648
|
|
|
583
|
-
def get_load_events(self, route: str) -> list[IndividualEventType[
|
|
649
|
+
def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
|
|
584
650
|
"""Get the load events for a route.
|
|
585
651
|
|
|
586
652
|
Args:
|
|
@@ -592,7 +658,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
592
658
|
route = route.lstrip("/")
|
|
593
659
|
if route == "":
|
|
594
660
|
route = constants.PageNames.INDEX_ROUTE
|
|
595
|
-
return self.
|
|
661
|
+
return self._load_events.get(route, [])
|
|
596
662
|
|
|
597
663
|
def _check_routes_conflict(self, new_route: str):
|
|
598
664
|
"""Verify if there is any conflict between the new route and any existing route.
|
|
@@ -616,10 +682,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
616
682
|
constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
|
|
617
683
|
constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
|
|
618
684
|
)
|
|
619
|
-
for route in self.
|
|
685
|
+
for route in self._pages:
|
|
620
686
|
replaced_route = replace_brackets_with_keywords(route)
|
|
621
687
|
for rw, r, nr in zip(
|
|
622
|
-
replaced_route.split("/"),
|
|
688
|
+
replaced_route.split("/"),
|
|
689
|
+
route.split("/"),
|
|
690
|
+
new_route.split("/"),
|
|
691
|
+
strict=False,
|
|
623
692
|
):
|
|
624
693
|
if rw in segments and r != nr:
|
|
625
694
|
# If the slugs in the segments of both routes are not the same, then the route is invalid
|
|
@@ -639,7 +708,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
639
708
|
title: str = constants.Page404.TITLE,
|
|
640
709
|
image: str = constants.Page404.IMAGE,
|
|
641
710
|
description: str = constants.Page404.DESCRIPTION,
|
|
642
|
-
on_load: EventType[
|
|
711
|
+
on_load: EventType[()] | None = None,
|
|
643
712
|
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
|
|
644
713
|
):
|
|
645
714
|
"""Define a custom 404 page for any url having no match.
|
|
@@ -650,8 +719,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
650
719
|
Args:
|
|
651
720
|
component: The component to display at the page.
|
|
652
721
|
title: The title of the page.
|
|
653
|
-
description: The description of the page.
|
|
654
722
|
image: The image to display on the page.
|
|
723
|
+
description: The description of the page.
|
|
655
724
|
on_load: The event handler(s) that will be called each time the page load.
|
|
656
725
|
meta: The metadata of the page.
|
|
657
726
|
"""
|
|
@@ -674,6 +743,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
674
743
|
def _setup_admin_dash(self):
|
|
675
744
|
"""Setup the admin dash."""
|
|
676
745
|
# Get the admin dash.
|
|
746
|
+
if not self.api:
|
|
747
|
+
return
|
|
748
|
+
|
|
677
749
|
admin_dash = self.admin_dash
|
|
678
750
|
|
|
679
751
|
if admin_dash and admin_dash.models:
|
|
@@ -715,7 +787,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
715
787
|
frontend_packages = get_config().frontend_packages
|
|
716
788
|
_frontend_packages = []
|
|
717
789
|
for package in frontend_packages:
|
|
718
|
-
if package in (get_config().tailwind or {}).get("plugins", []):
|
|
790
|
+
if package in (get_config().tailwind or {}).get("plugins", []):
|
|
719
791
|
console.warn(
|
|
720
792
|
f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
|
|
721
793
|
)
|
|
@@ -778,10 +850,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
778
850
|
|
|
779
851
|
def _setup_overlay_component(self):
|
|
780
852
|
"""If a State is not used and no overlay_component is specified, do not render the connection modal."""
|
|
781
|
-
if self.
|
|
853
|
+
if self._state is None and self.overlay_component is default_overlay_component:
|
|
782
854
|
self.overlay_component = None
|
|
783
|
-
for k, component in self.
|
|
784
|
-
self.
|
|
855
|
+
for k, component in self._pages.items():
|
|
856
|
+
self._pages[k] = self._add_overlay_to_component(component)
|
|
785
857
|
|
|
786
858
|
def _add_error_boundary_to_component(self, component: Component) -> Component:
|
|
787
859
|
if self.error_boundary is None:
|
|
@@ -793,14 +865,23 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
793
865
|
|
|
794
866
|
def _setup_error_boundary(self):
|
|
795
867
|
"""If a State is not used and no error_boundary is specified, do not render the error boundary."""
|
|
796
|
-
if self.
|
|
868
|
+
if self._state is None and self.error_boundary is default_error_boundary:
|
|
797
869
|
self.error_boundary = None
|
|
798
870
|
|
|
799
|
-
for k, component in self.
|
|
871
|
+
for k, component in self._pages.items():
|
|
800
872
|
# Skip the 404 page
|
|
801
873
|
if k == constants.Page404.SLUG:
|
|
802
874
|
continue
|
|
803
|
-
self.
|
|
875
|
+
self._pages[k] = self._add_error_boundary_to_component(component)
|
|
876
|
+
|
|
877
|
+
def _setup_sticky_badge(self):
|
|
878
|
+
"""Add the sticky badge to the app."""
|
|
879
|
+
for k, component in self._pages.items():
|
|
880
|
+
# Would be nice to share single sticky_badge across all pages, but
|
|
881
|
+
# it bungles the StatefulComponent compile step.
|
|
882
|
+
sticky_badge = sticky()
|
|
883
|
+
sticky_badge._add_style_recursive({})
|
|
884
|
+
self._pages[k] = Fragment.create(sticky_badge, component)
|
|
804
885
|
|
|
805
886
|
def _apply_decorated_pages(self):
|
|
806
887
|
"""Add @rx.page decorated pages to the app.
|
|
@@ -826,21 +907,27 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
826
907
|
Raises:
|
|
827
908
|
VarDependencyError: When a computed var has an invalid dependency.
|
|
828
909
|
"""
|
|
829
|
-
if not self.
|
|
910
|
+
if not self._state:
|
|
830
911
|
return
|
|
831
912
|
|
|
832
913
|
if not state:
|
|
833
|
-
state = self.
|
|
914
|
+
state = self._state
|
|
834
915
|
|
|
835
916
|
for var in state.computed_vars.values():
|
|
836
917
|
if not var._cache:
|
|
837
918
|
continue
|
|
838
919
|
deps = var._deps(objclass=state)
|
|
839
|
-
for
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
920
|
+
for state_name, dep_set in deps.items():
|
|
921
|
+
state_cls = (
|
|
922
|
+
state.get_root_state().get_class_substate(state_name)
|
|
923
|
+
if state_name != state.get_full_name()
|
|
924
|
+
else state
|
|
925
|
+
)
|
|
926
|
+
for dep in dep_set:
|
|
927
|
+
if dep not in state_cls.vars and dep not in state_cls.backend_vars:
|
|
928
|
+
raise exceptions.VarDependencyError(
|
|
929
|
+
f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {state_name}.{dep}"
|
|
930
|
+
)
|
|
844
931
|
|
|
845
932
|
for substate in state.class_subclasses:
|
|
846
933
|
self._validate_var_dependencies(substate)
|
|
@@ -856,13 +943,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
856
943
|
"""
|
|
857
944
|
from reflex.utils.exceptions import ReflexRuntimeError
|
|
858
945
|
|
|
859
|
-
self.
|
|
946
|
+
self._pages = {}
|
|
860
947
|
|
|
861
948
|
def get_compilation_time() -> str:
|
|
862
949
|
return str(datetime.now().time()).split(".")[0]
|
|
863
950
|
|
|
864
951
|
# Render a default 404 page if the user didn't supply one
|
|
865
|
-
if constants.Page404.SLUG not in self.
|
|
952
|
+
if constants.Page404.SLUG not in self._unevaluated_pages:
|
|
866
953
|
self.add_page(route=constants.Page404.SLUG)
|
|
867
954
|
|
|
868
955
|
# Fix up the style.
|
|
@@ -878,19 +965,23 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
878
965
|
# If a theme component was provided, wrap the app with it
|
|
879
966
|
app_wrappers[(20, "Theme")] = self.theme
|
|
880
967
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
self._compile_page(route)
|
|
968
|
+
# Get the env mode.
|
|
969
|
+
config = get_config()
|
|
884
970
|
|
|
885
|
-
|
|
886
|
-
|
|
971
|
+
if config.react_strict_mode:
|
|
972
|
+
app_wrappers[(200, "StrictMode")] = StrictMode.create()
|
|
887
973
|
|
|
888
|
-
|
|
889
|
-
return
|
|
974
|
+
should_compile = self._should_compile()
|
|
890
975
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
976
|
+
if not should_compile:
|
|
977
|
+
for route in self._unevaluated_pages:
|
|
978
|
+
console.debug(f"Evaluating page: {route}")
|
|
979
|
+
self._compile_page(route, save_page=should_compile)
|
|
980
|
+
|
|
981
|
+
# Add the optional endpoints (_upload)
|
|
982
|
+
self._add_optional_endpoints()
|
|
983
|
+
|
|
984
|
+
return
|
|
894
985
|
|
|
895
986
|
# Create a progress bar.
|
|
896
987
|
progress = Progress(
|
|
@@ -900,18 +991,32 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
900
991
|
)
|
|
901
992
|
|
|
902
993
|
# try to be somewhat accurate - but still not 100%
|
|
903
|
-
adhoc_steps_without_executor =
|
|
994
|
+
adhoc_steps_without_executor = 7
|
|
904
995
|
fixed_pages_within_executor = 5
|
|
905
996
|
progress.start()
|
|
906
997
|
task = progress.add_task(
|
|
907
998
|
f"[{get_compilation_time()}] Compiling:",
|
|
908
|
-
total=len(self.
|
|
999
|
+
total=len(self._pages)
|
|
1000
|
+
+ (len(self._unevaluated_pages) * 2)
|
|
909
1001
|
+ fixed_pages_within_executor
|
|
910
1002
|
+ adhoc_steps_without_executor,
|
|
911
1003
|
)
|
|
912
1004
|
|
|
913
|
-
|
|
914
|
-
|
|
1005
|
+
for route in self._unevaluated_pages:
|
|
1006
|
+
console.debug(f"Evaluating page: {route}")
|
|
1007
|
+
self._compile_page(route, save_page=should_compile)
|
|
1008
|
+
progress.advance(task)
|
|
1009
|
+
|
|
1010
|
+
# Add the optional endpoints (_upload)
|
|
1011
|
+
self._add_optional_endpoints()
|
|
1012
|
+
|
|
1013
|
+
self._validate_var_dependencies()
|
|
1014
|
+
self._setup_overlay_component()
|
|
1015
|
+
self._setup_error_boundary()
|
|
1016
|
+
if config.show_built_with_reflex:
|
|
1017
|
+
self._setup_sticky_badge()
|
|
1018
|
+
|
|
1019
|
+
progress.advance(task)
|
|
915
1020
|
|
|
916
1021
|
# Store the compile results.
|
|
917
1022
|
compile_results = []
|
|
@@ -924,7 +1029,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
924
1029
|
|
|
925
1030
|
# This has to happen before compiling stateful components as that
|
|
926
1031
|
# prevents recursive functions from reaching all components.
|
|
927
|
-
for component in self.
|
|
1032
|
+
for component in self._pages.values():
|
|
928
1033
|
# Add component._get_all_imports() to all_imports.
|
|
929
1034
|
all_imports.update(component._get_all_imports())
|
|
930
1035
|
|
|
@@ -939,12 +1044,12 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
939
1044
|
stateful_components_path,
|
|
940
1045
|
stateful_components_code,
|
|
941
1046
|
page_components,
|
|
942
|
-
) = compiler.compile_stateful_components(self.
|
|
1047
|
+
) = compiler.compile_stateful_components(self._pages.values())
|
|
943
1048
|
|
|
944
1049
|
progress.advance(task)
|
|
945
1050
|
|
|
946
1051
|
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
|
|
947
|
-
if code_uses_state_contexts(stateful_components_code) and self.
|
|
1052
|
+
if code_uses_state_contexts(stateful_components_code) and self._state is None:
|
|
948
1053
|
raise ReflexRuntimeError(
|
|
949
1054
|
"To access rx.State in frontend components, at least one "
|
|
950
1055
|
"subclass of rx.State must be defined in the app."
|
|
@@ -958,7 +1063,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
958
1063
|
compiler.compile_document_root(
|
|
959
1064
|
self.head_components,
|
|
960
1065
|
html_lang=self.html_lang,
|
|
961
|
-
html_custom_attrs=self.html_custom_attrs, #
|
|
1066
|
+
html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
|
|
962
1067
|
)
|
|
963
1068
|
)
|
|
964
1069
|
|
|
@@ -981,20 +1086,20 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
981
1086
|
max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
|
|
982
1087
|
)
|
|
983
1088
|
|
|
984
|
-
for route, component in zip(self.
|
|
1089
|
+
for route, component in zip(self._pages, page_components, strict=True):
|
|
985
1090
|
ExecutorSafeFunctions.COMPONENTS[route] = component
|
|
986
1091
|
|
|
987
|
-
ExecutorSafeFunctions.STATE = self.
|
|
1092
|
+
ExecutorSafeFunctions.STATE = self._state
|
|
988
1093
|
|
|
989
1094
|
with executor:
|
|
990
1095
|
result_futures = []
|
|
991
1096
|
|
|
992
|
-
def _submit_work(fn, *args, **kwargs):
|
|
1097
|
+
def _submit_work(fn: Callable, *args, **kwargs):
|
|
993
1098
|
f = executor.submit(fn, *args, **kwargs)
|
|
994
1099
|
result_futures.append(f)
|
|
995
1100
|
|
|
996
1101
|
# Compile the pre-compiled pages.
|
|
997
|
-
for route in self.
|
|
1102
|
+
for route in self._pages:
|
|
998
1103
|
_submit_work(
|
|
999
1104
|
ExecutorSafeFunctions.compile_page,
|
|
1000
1105
|
route,
|
|
@@ -1029,7 +1134,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1029
1134
|
|
|
1030
1135
|
# Compile the contexts.
|
|
1031
1136
|
compile_results.append(
|
|
1032
|
-
compiler.compile_contexts(self.
|
|
1137
|
+
compiler.compile_contexts(self._state, self.theme),
|
|
1033
1138
|
)
|
|
1034
1139
|
if self.theme is not None:
|
|
1035
1140
|
# Fix #2992 by removing the top-level appearance prop
|
|
@@ -1151,9 +1256,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1151
1256
|
)
|
|
1152
1257
|
|
|
1153
1258
|
task = asyncio.create_task(_coro())
|
|
1154
|
-
self.
|
|
1259
|
+
self._background_tasks.add(task)
|
|
1155
1260
|
# Clean up task from background_tasks set when complete.
|
|
1156
|
-
task.add_done_callback(self.
|
|
1261
|
+
task.add_done_callback(self._background_tasks.discard)
|
|
1157
1262
|
return task
|
|
1158
1263
|
|
|
1159
1264
|
def _validate_exception_handlers(self):
|
|
@@ -1163,11 +1268,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1163
1268
|
ValueError: If the custom exception handlers are invalid.
|
|
1164
1269
|
|
|
1165
1270
|
"""
|
|
1166
|
-
|
|
1271
|
+
frontend_arg_spec = {
|
|
1167
1272
|
"exception": Exception,
|
|
1168
1273
|
}
|
|
1169
1274
|
|
|
1170
|
-
|
|
1275
|
+
backend_arg_spec = {
|
|
1171
1276
|
"exception": Exception,
|
|
1172
1277
|
}
|
|
1173
1278
|
|
|
@@ -1175,9 +1280,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1175
1280
|
["frontend", "backend"],
|
|
1176
1281
|
[self.frontend_exception_handler, self.backend_exception_handler],
|
|
1177
1282
|
[
|
|
1178
|
-
|
|
1179
|
-
|
|
1283
|
+
frontend_arg_spec,
|
|
1284
|
+
backend_arg_spec,
|
|
1180
1285
|
],
|
|
1286
|
+
strict=True,
|
|
1181
1287
|
):
|
|
1182
1288
|
if hasattr(handler_fn, "__name__"):
|
|
1183
1289
|
_fn_name = handler_fn.__name__
|
|
@@ -1218,7 +1324,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1218
1324
|
):
|
|
1219
1325
|
raise ValueError(
|
|
1220
1326
|
f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order."
|
|
1221
|
-
f"Expected `{required_arg}` as the {required_arg_index+1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
|
|
1327
|
+
f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
|
|
1222
1328
|
)
|
|
1223
1329
|
|
|
1224
1330
|
if not issubclass(arg_annotations[required_arg], Exception):
|
|
@@ -1319,15 +1425,14 @@ async def process(
|
|
|
1319
1425
|
if app._process_background(state, event) is not None:
|
|
1320
1426
|
# `final=True` allows the frontend send more events immediately.
|
|
1321
1427
|
yield StateUpdate(final=True)
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
yield update
|
|
1428
|
+
else:
|
|
1429
|
+
# Process the event synchronously.
|
|
1430
|
+
async for update in state._process(event):
|
|
1431
|
+
# Postprocess the event.
|
|
1432
|
+
update = await app._postprocess(state, event, update)
|
|
1433
|
+
|
|
1434
|
+
# Yield the update.
|
|
1435
|
+
yield update
|
|
1331
1436
|
except Exception as ex:
|
|
1332
1437
|
telemetry.send_error(ex, context="backend")
|
|
1333
1438
|
|
|
@@ -1522,16 +1627,20 @@ class EventNamespace(AsyncNamespace):
|
|
|
1522
1627
|
self.sid_to_token = {}
|
|
1523
1628
|
self.app = app
|
|
1524
1629
|
|
|
1525
|
-
def on_connect(self, sid, environ):
|
|
1630
|
+
def on_connect(self, sid: str, environ: dict):
|
|
1526
1631
|
"""Event for when the websocket is connected.
|
|
1527
1632
|
|
|
1528
1633
|
Args:
|
|
1529
1634
|
sid: The Socket.IO session id.
|
|
1530
1635
|
environ: The request information, including HTTP headers.
|
|
1531
1636
|
"""
|
|
1532
|
-
|
|
1637
|
+
subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
|
|
1638
|
+
if subprotocol and subprotocol != constants.Reflex.VERSION:
|
|
1639
|
+
console.warn(
|
|
1640
|
+
f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}."
|
|
1641
|
+
)
|
|
1533
1642
|
|
|
1534
|
-
def on_disconnect(self, sid):
|
|
1643
|
+
def on_disconnect(self, sid: str):
|
|
1535
1644
|
"""Event for when the websocket disconnects.
|
|
1536
1645
|
|
|
1537
1646
|
Args:
|
|
@@ -1553,7 +1662,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
1553
1662
|
self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
|
|
1554
1663
|
)
|
|
1555
1664
|
|
|
1556
|
-
async def on_event(self, sid, data):
|
|
1665
|
+
async def on_event(self, sid: str, data: Any):
|
|
1557
1666
|
"""Event for receiving front-end websocket events.
|
|
1558
1667
|
|
|
1559
1668
|
Raises:
|
|
@@ -1562,12 +1671,36 @@ class EventNamespace(AsyncNamespace):
|
|
|
1562
1671
|
Args:
|
|
1563
1672
|
sid: The Socket.IO session id.
|
|
1564
1673
|
data: The event data.
|
|
1674
|
+
|
|
1675
|
+
Raises:
|
|
1676
|
+
EventDeserializationError: If the event data is not a dictionary.
|
|
1565
1677
|
"""
|
|
1566
1678
|
fields = data
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1679
|
+
|
|
1680
|
+
if isinstance(fields, str):
|
|
1681
|
+
console.warn(
|
|
1682
|
+
"Received event data as a string. This generally should not happen and may indicate a bug."
|
|
1683
|
+
f" Event data: {fields}"
|
|
1684
|
+
)
|
|
1685
|
+
try:
|
|
1686
|
+
fields = json.loads(fields)
|
|
1687
|
+
except json.JSONDecodeError as ex:
|
|
1688
|
+
raise exceptions.EventDeserializationError(
|
|
1689
|
+
f"Failed to deserialize event data: {fields}."
|
|
1690
|
+
) from ex
|
|
1691
|
+
|
|
1692
|
+
if not isinstance(fields, dict):
|
|
1693
|
+
raise exceptions.EventDeserializationError(
|
|
1694
|
+
f"Event data must be a dictionary, but received {fields} of type {type(fields)}."
|
|
1695
|
+
)
|
|
1696
|
+
|
|
1697
|
+
try:
|
|
1698
|
+
# Get the event.
|
|
1699
|
+
event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
|
|
1700
|
+
except (TypeError, ValueError) as ex:
|
|
1701
|
+
raise exceptions.EventDeserializationError(
|
|
1702
|
+
f"Failed to deserialize event data: {fields}."
|
|
1703
|
+
) from ex
|
|
1571
1704
|
|
|
1572
1705
|
self.token_to_sid[event.token] = sid
|
|
1573
1706
|
self.sid_to_token[sid] = event.token
|
|
@@ -1596,7 +1729,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
1596
1729
|
# Emit the update from processing the event.
|
|
1597
1730
|
await self.emit_update(update=update, sid=sid)
|
|
1598
1731
|
|
|
1599
|
-
async def on_ping(self, sid):
|
|
1732
|
+
async def on_ping(self, sid: str):
|
|
1600
1733
|
"""Event for testing the API endpoint.
|
|
1601
1734
|
|
|
1602
1735
|
Args:
|