reflex 0.6.0a3__py3-none-any.whl → 0.6.1__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 +14 -0
- reflex/.templates/web/utils/state.js +70 -41
- reflex/app.py +11 -11
- reflex/app_mixins/lifespan.py +24 -6
- reflex/app_module_for_backend.py +1 -1
- reflex/base.py +7 -13
- reflex/compiler/utils.py +17 -8
- reflex/components/base/bare.py +3 -1
- reflex/components/base/meta.py +5 -3
- reflex/components/component.py +28 -20
- reflex/components/core/breakpoints.py +1 -3
- reflex/components/core/cond.py +4 -4
- reflex/components/datadisplay/__init__.py +0 -1
- reflex/components/datadisplay/__init__.pyi +0 -1
- reflex/components/datadisplay/code.py +93 -106
- reflex/components/datadisplay/code.pyi +710 -53
- reflex/components/datadisplay/logo.py +22 -20
- reflex/components/dynamic.py +157 -0
- reflex/components/el/elements/forms.py +4 -1
- reflex/components/gridjs/datatable.py +2 -1
- reflex/components/markdown/markdown.py +10 -6
- reflex/components/markdown/markdown.pyi +3 -0
- reflex/components/radix/themes/components/progress.py +22 -0
- reflex/components/radix/themes/components/progress.pyi +2 -0
- reflex/components/radix/themes/components/segmented_control.py +3 -0
- reflex/components/radix/themes/components/segmented_control.pyi +2 -0
- reflex/components/radix/themes/layout/stack.py +1 -1
- reflex/components/recharts/cartesian.py +1 -1
- reflex/components/sonner/toast.py +3 -3
- reflex/components/tags/iter_tag.py +5 -1
- reflex/config.py +2 -2
- reflex/constants/base.py +4 -1
- reflex/constants/installer.py +8 -1
- reflex/event.py +63 -22
- reflex/experimental/assets.py +3 -1
- reflex/experimental/client_state.py +12 -7
- reflex/experimental/misc.py +5 -3
- reflex/middleware/hydrate_middleware.py +1 -2
- reflex/page.py +10 -3
- reflex/reflex.py +20 -3
- reflex/state.py +105 -44
- reflex/style.py +12 -2
- reflex/testing.py +8 -4
- reflex/utils/console.py +1 -1
- reflex/utils/exceptions.py +4 -0
- reflex/utils/exec.py +170 -18
- reflex/utils/format.py +6 -44
- reflex/utils/path_ops.py +36 -1
- reflex/utils/prerequisites.py +62 -21
- reflex/utils/serializers.py +7 -46
- reflex/utils/telemetry.py +1 -1
- reflex/utils/types.py +18 -3
- reflex/vars/base.py +303 -43
- reflex/vars/number.py +3 -0
- reflex/vars/sequence.py +43 -8
- {reflex-0.6.0a3.dist-info → reflex-0.6.1.dist-info}/METADATA +5 -5
- {reflex-0.6.0a3.dist-info → reflex-0.6.1.dist-info}/RECORD +61 -60
- {reflex-0.6.0a3.dist-info → reflex-0.6.1.dist-info}/LICENSE +0 -0
- {reflex-0.6.0a3.dist-info → reflex-0.6.1.dist-info}/WHEEL +0 -0
- {reflex-0.6.0a3.dist-info → reflex-0.6.1.dist-info}/entry_points.txt +0 -0
|
@@ -8,7 +8,7 @@ version = "0.0.1"
|
|
|
8
8
|
description = "Reflex custom component {{ module_name }}"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
12
|
authors = [{ name = "", email = "YOUREMAIL@domain.com" }]
|
|
13
13
|
keywords = ["reflex","reflex-custom-components"]
|
|
14
14
|
|
|
@@ -7,6 +7,10 @@ import '/styles/styles.css'
|
|
|
7
7
|
{% block declaration %}
|
|
8
8
|
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
|
|
9
9
|
import { ThemeProvider } from 'next-themes'
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import * as utils_context from "/utils/context.js";
|
|
12
|
+
import * as utils_state from "/utils/state.js";
|
|
13
|
+
import * as radix from "@radix-ui/themes";
|
|
10
14
|
|
|
11
15
|
{% for custom_code in custom_codes %}
|
|
12
16
|
{{custom_code}}
|
|
@@ -26,6 +30,16 @@ function AppWrap({children}) {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export default function MyApp({ Component, pageProps }) {
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
// Make contexts and state objects available globally for dynamic eval'd components
|
|
35
|
+
let windowImports = {
|
|
36
|
+
"react": React,
|
|
37
|
+
"@radix-ui/themes": radix,
|
|
38
|
+
"/utils/context": utils_context,
|
|
39
|
+
"/utils/state": utils_state,
|
|
40
|
+
};
|
|
41
|
+
window["__reflex"] = windowImports;
|
|
42
|
+
}, []);
|
|
29
43
|
return (
|
|
30
44
|
<ThemeProvider defaultTheme={ defaultColorMode } attribute="class">
|
|
31
45
|
<AppWrap>
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "utils/context.js";
|
|
16
16
|
import debounce from "/utils/helpers/debounce";
|
|
17
17
|
import throttle from "/utils/helpers/throttle";
|
|
18
|
+
import * as Babel from "@babel/standalone";
|
|
18
19
|
|
|
19
20
|
// Endpoint URLs.
|
|
20
21
|
const EVENTURL = env.EVENT;
|
|
@@ -117,8 +118,8 @@ export const isStateful = () => {
|
|
|
117
118
|
if (event_queue.length === 0) {
|
|
118
119
|
return false;
|
|
119
120
|
}
|
|
120
|
-
return event_queue.some(event => event.name.startsWith("reflex___state"));
|
|
121
|
-
}
|
|
121
|
+
return event_queue.some((event) => event.name.startsWith("reflex___state"));
|
|
122
|
+
};
|
|
122
123
|
|
|
123
124
|
/**
|
|
124
125
|
* Apply a delta to the state.
|
|
@@ -129,6 +130,22 @@ export const applyDelta = (state, delta) => {
|
|
|
129
130
|
return { ...state, ...delta };
|
|
130
131
|
};
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Evaluate a dynamic component.
|
|
135
|
+
* @param component The component to evaluate.
|
|
136
|
+
* @returns The evaluated component.
|
|
137
|
+
*/
|
|
138
|
+
export const evalReactComponent = async (component) => {
|
|
139
|
+
if (!window.React && window.__reflex) {
|
|
140
|
+
window.React = window.__reflex.react;
|
|
141
|
+
}
|
|
142
|
+
const output = Babel.transform(component, { presets: ["react"] }).code;
|
|
143
|
+
const encodedJs = encodeURIComponent(output);
|
|
144
|
+
const dataUri = "data:text/javascript;charset=utf-8," + encodedJs;
|
|
145
|
+
const module = await eval(`import(dataUri)`);
|
|
146
|
+
return module.default;
|
|
147
|
+
};
|
|
148
|
+
|
|
132
149
|
/**
|
|
133
150
|
* Only Queue and process events when websocket connection exists.
|
|
134
151
|
* @param event The event to queue.
|
|
@@ -141,7 +158,7 @@ export const queueEventIfSocketExists = async (events, socket) => {
|
|
|
141
158
|
return;
|
|
142
159
|
}
|
|
143
160
|
await queueEvents(events, socket);
|
|
144
|
-
}
|
|
161
|
+
};
|
|
145
162
|
|
|
146
163
|
/**
|
|
147
164
|
* Handle frontend event or send the event to the backend via Websocket.
|
|
@@ -208,7 +225,10 @@ export const applyEvent = async (event, socket) => {
|
|
|
208
225
|
const a = document.createElement("a");
|
|
209
226
|
a.hidden = true;
|
|
210
227
|
// Special case when linking to uploaded files
|
|
211
|
-
a.href = event.payload.url.replace(
|
|
228
|
+
a.href = event.payload.url.replace(
|
|
229
|
+
"${getBackendURL(env.UPLOAD)}",
|
|
230
|
+
getBackendURL(env.UPLOAD)
|
|
231
|
+
);
|
|
212
232
|
a.download = event.payload.filename;
|
|
213
233
|
a.click();
|
|
214
234
|
a.remove();
|
|
@@ -249,7 +269,7 @@ export const applyEvent = async (event, socket) => {
|
|
|
249
269
|
} catch (e) {
|
|
250
270
|
console.log("_call_script", e);
|
|
251
271
|
if (window && window?.onerror) {
|
|
252
|
-
window.onerror(e.message, null, null, null, e)
|
|
272
|
+
window.onerror(e.message, null, null, null, e);
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
return false;
|
|
@@ -290,10 +310,9 @@ export const applyEvent = async (event, socket) => {
|
|
|
290
310
|
export const applyRestEvent = async (event, socket) => {
|
|
291
311
|
let eventSent = false;
|
|
292
312
|
if (event.handler === "uploadFiles") {
|
|
293
|
-
|
|
294
313
|
if (event.payload.files === undefined || event.payload.files.length === 0) {
|
|
295
314
|
// Submit the event over the websocket to trigger the event handler.
|
|
296
|
-
return await applyEvent(Event(event.name), socket)
|
|
315
|
+
return await applyEvent(Event(event.name), socket);
|
|
297
316
|
}
|
|
298
317
|
|
|
299
318
|
// Start upload, but do not wait for it, which would block other events.
|
|
@@ -397,7 +416,7 @@ export const connect = async (
|
|
|
397
416
|
console.log("Disconnect backend before bfcache on navigation");
|
|
398
417
|
socket.current.disconnect();
|
|
399
418
|
}
|
|
400
|
-
}
|
|
419
|
+
};
|
|
401
420
|
|
|
402
421
|
// Once the socket is open, hydrate the page.
|
|
403
422
|
socket.current.on("connect", () => {
|
|
@@ -416,7 +435,7 @@ export const connect = async (
|
|
|
416
435
|
});
|
|
417
436
|
|
|
418
437
|
// On each received message, queue the updates and events.
|
|
419
|
-
socket.current.on("event", (message) => {
|
|
438
|
+
socket.current.on("event", async (message) => {
|
|
420
439
|
const update = JSON5.parse(message);
|
|
421
440
|
for (const substate in update.delta) {
|
|
422
441
|
dispatch[substate](update.delta[substate]);
|
|
@@ -574,7 +593,11 @@ export const hydrateClientStorage = (client_storage) => {
|
|
|
574
593
|
}
|
|
575
594
|
}
|
|
576
595
|
}
|
|
577
|
-
if (
|
|
596
|
+
if (
|
|
597
|
+
client_storage.cookies ||
|
|
598
|
+
client_storage.local_storage ||
|
|
599
|
+
client_storage.session_storage
|
|
600
|
+
) {
|
|
578
601
|
return client_storage_values;
|
|
579
602
|
}
|
|
580
603
|
return {};
|
|
@@ -614,15 +637,17 @@ const applyClientStorageDelta = (client_storage, delta) => {
|
|
|
614
637
|
) {
|
|
615
638
|
const options = client_storage.local_storage[state_key];
|
|
616
639
|
localStorage.setItem(options.name || state_key, delta[substate][key]);
|
|
617
|
-
} else if(
|
|
640
|
+
} else if (
|
|
618
641
|
client_storage.session_storage &&
|
|
619
642
|
state_key in client_storage.session_storage &&
|
|
620
643
|
typeof window !== "undefined"
|
|
621
644
|
) {
|
|
622
645
|
const session_options = client_storage.session_storage[state_key];
|
|
623
|
-
sessionStorage.setItem(
|
|
646
|
+
sessionStorage.setItem(
|
|
647
|
+
session_options.name || state_key,
|
|
648
|
+
delta[substate][key]
|
|
649
|
+
);
|
|
624
650
|
}
|
|
625
|
-
|
|
626
651
|
}
|
|
627
652
|
}
|
|
628
653
|
};
|
|
@@ -651,7 +676,7 @@ export const useEventLoop = (
|
|
|
651
676
|
if (!(args instanceof Array)) {
|
|
652
677
|
args = [args];
|
|
653
678
|
}
|
|
654
|
-
const _e = args.filter((o) => o?.preventDefault !== undefined)[0]
|
|
679
|
+
const _e = args.filter((o) => o?.preventDefault !== undefined)[0];
|
|
655
680
|
|
|
656
681
|
if (event_actions?.preventDefault && _e?.preventDefault) {
|
|
657
682
|
_e.preventDefault();
|
|
@@ -671,7 +696,7 @@ export const useEventLoop = (
|
|
|
671
696
|
debounce(
|
|
672
697
|
combined_name,
|
|
673
698
|
() => queueEvents(events, socket),
|
|
674
|
-
event_actions.debounce
|
|
699
|
+
event_actions.debounce
|
|
675
700
|
);
|
|
676
701
|
} else {
|
|
677
702
|
queueEvents(events, socket);
|
|
@@ -696,30 +721,32 @@ export const useEventLoop = (
|
|
|
696
721
|
}
|
|
697
722
|
}, [router.isReady]);
|
|
698
723
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
724
|
+
// Handle frontend errors and send them to the backend via websocket.
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
if (typeof window === "undefined") {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
|
731
|
+
addEvents([
|
|
732
|
+
Event(`${exception_state_name}.handle_frontend_exception`, {
|
|
708
733
|
stack: error.stack,
|
|
709
|
-
})
|
|
710
|
-
|
|
711
|
-
|
|
734
|
+
}),
|
|
735
|
+
]);
|
|
736
|
+
return false;
|
|
737
|
+
};
|
|
712
738
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
739
|
+
//NOTE: Only works in Chrome v49+
|
|
740
|
+
//https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events
|
|
741
|
+
window.onunhandledrejection = function (event) {
|
|
742
|
+
addEvents([
|
|
743
|
+
Event(`${exception_state_name}.handle_frontend_exception`, {
|
|
744
|
+
stack: event.reason.stack,
|
|
745
|
+
}),
|
|
746
|
+
]);
|
|
747
|
+
return false;
|
|
748
|
+
};
|
|
749
|
+
}, []);
|
|
723
750
|
|
|
724
751
|
// Main event loop.
|
|
725
752
|
useEffect(() => {
|
|
@@ -782,11 +809,11 @@ export const useEventLoop = (
|
|
|
782
809
|
// Route after the initial page hydration.
|
|
783
810
|
useEffect(() => {
|
|
784
811
|
const change_start = () => {
|
|
785
|
-
const main_state_dispatch = dispatch["reflex___state____state"]
|
|
812
|
+
const main_state_dispatch = dispatch["reflex___state____state"];
|
|
786
813
|
if (main_state_dispatch !== undefined) {
|
|
787
|
-
main_state_dispatch({ is_hydrated: false })
|
|
814
|
+
main_state_dispatch({ is_hydrated: false });
|
|
788
815
|
}
|
|
789
|
-
}
|
|
816
|
+
};
|
|
790
817
|
const change_complete = () => addEvents(onLoadInternalEvent());
|
|
791
818
|
router.events.on("routeChangeStart", change_start);
|
|
792
819
|
router.events.on("routeChangeComplete", change_complete);
|
|
@@ -805,7 +832,9 @@ export const useEventLoop = (
|
|
|
805
832
|
* @returns True if the value is truthy, false otherwise.
|
|
806
833
|
*/
|
|
807
834
|
export const isTrue = (val) => {
|
|
808
|
-
|
|
835
|
+
if (Array.isArray(val)) return val.length > 0;
|
|
836
|
+
if (val === Object(val)) return Object.keys(val).length > 0;
|
|
837
|
+
return Boolean(val);
|
|
809
838
|
};
|
|
810
839
|
|
|
811
840
|
/**
|
reflex/app.py
CHANGED
|
@@ -482,9 +482,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|
|
482
482
|
"""
|
|
483
483
|
# If the route is not set, get it from the callable.
|
|
484
484
|
if route is None:
|
|
485
|
-
|
|
486
|
-
component
|
|
487
|
-
), "Route must be set if component is not a callable."
|
|
485
|
+
if not isinstance(component, Callable):
|
|
486
|
+
raise ValueError("Route must be set if component is not a callable.")
|
|
488
487
|
# Format the route.
|
|
489
488
|
route = format.format_route(component.__name__)
|
|
490
489
|
else:
|
|
@@ -606,10 +605,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|
|
606
605
|
for route in self.pages:
|
|
607
606
|
replaced_route = replace_brackets_with_keywords(route)
|
|
608
607
|
for rw, r, nr in zip(
|
|
609
|
-
replaced_route.split("/"),
|
|
610
|
-
route.split("/"),
|
|
611
|
-
new_route.split("/"),
|
|
612
|
-
strict=False,
|
|
608
|
+
replaced_route.split("/"), route.split("/"), new_route.split("/")
|
|
613
609
|
):
|
|
614
610
|
if rw in segments and r != nr:
|
|
615
611
|
# If the slugs in the segments of both routes are not the same, then the route is invalid
|
|
@@ -958,7 +954,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|
|
958
954
|
|
|
959
955
|
# Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
|
|
960
956
|
# This is required for multiprocessing to work, in presence of non-picklable inputs.
|
|
961
|
-
for route, component in zip(self.pages, page_components
|
|
957
|
+
for route, component in zip(self.pages, page_components):
|
|
962
958
|
ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
|
|
963
959
|
route,
|
|
964
960
|
component,
|
|
@@ -1172,7 +1168,6 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|
|
1172
1168
|
FRONTEND_ARG_SPEC,
|
|
1173
1169
|
BACKEND_ARG_SPEC,
|
|
1174
1170
|
],
|
|
1175
|
-
strict=False,
|
|
1176
1171
|
):
|
|
1177
1172
|
if hasattr(handler_fn, "__name__"):
|
|
1178
1173
|
_fn_name = handler_fn.__name__
|
|
@@ -1532,6 +1527,9 @@ class EventNamespace(AsyncNamespace):
|
|
|
1532
1527
|
async def on_event(self, sid, data):
|
|
1533
1528
|
"""Event for receiving front-end websocket events.
|
|
1534
1529
|
|
|
1530
|
+
Raises:
|
|
1531
|
+
RuntimeError: If the Socket.IO is badly initialized.
|
|
1532
|
+
|
|
1535
1533
|
Args:
|
|
1536
1534
|
sid: The Socket.IO session id.
|
|
1537
1535
|
data: The event data.
|
|
@@ -1544,9 +1542,11 @@ class EventNamespace(AsyncNamespace):
|
|
|
1544
1542
|
self.sid_to_token[sid] = event.token
|
|
1545
1543
|
|
|
1546
1544
|
# Get the event environment.
|
|
1547
|
-
|
|
1545
|
+
if self.app.sio is None:
|
|
1546
|
+
raise RuntimeError("Socket.IO is not initialized.")
|
|
1548
1547
|
environ = self.app.sio.get_environ(sid, self.namespace)
|
|
1549
|
-
|
|
1548
|
+
if environ is None:
|
|
1549
|
+
raise RuntimeError("Socket.IO environ is not initialized.")
|
|
1550
1550
|
|
|
1551
1551
|
# Get the client headers.
|
|
1552
1552
|
headers = {
|
reflex/app_mixins/lifespan.py
CHANGED
|
@@ -6,11 +6,13 @@ import asyncio
|
|
|
6
6
|
import contextlib
|
|
7
7
|
import functools
|
|
8
8
|
import inspect
|
|
9
|
-
import sys
|
|
10
9
|
from typing import Callable, Coroutine, Set, Union
|
|
11
10
|
|
|
12
11
|
from fastapi import FastAPI
|
|
13
12
|
|
|
13
|
+
from reflex.utils import console
|
|
14
|
+
from reflex.utils.exceptions import InvalidLifespanTaskType
|
|
15
|
+
|
|
14
16
|
from .mixin import AppMixin
|
|
15
17
|
|
|
16
18
|
|
|
@@ -26,6 +28,7 @@ class LifespanMixin(AppMixin):
|
|
|
26
28
|
try:
|
|
27
29
|
async with contextlib.AsyncExitStack() as stack:
|
|
28
30
|
for task in self.lifespan_tasks:
|
|
31
|
+
run_msg = f"Started lifespan task: {task.__name__} as {{type}}" # type: ignore
|
|
29
32
|
if isinstance(task, asyncio.Task):
|
|
30
33
|
running_tasks.append(task)
|
|
31
34
|
else:
|
|
@@ -35,15 +38,19 @@ class LifespanMixin(AppMixin):
|
|
|
35
38
|
_t = task()
|
|
36
39
|
if isinstance(_t, contextlib._AsyncGeneratorContextManager):
|
|
37
40
|
await stack.enter_async_context(_t)
|
|
41
|
+
console.debug(run_msg.format(type="asynccontextmanager"))
|
|
38
42
|
elif isinstance(_t, Coroutine):
|
|
39
|
-
|
|
43
|
+
task_ = asyncio.create_task(_t)
|
|
44
|
+
task_.add_done_callback(lambda t: t.result())
|
|
45
|
+
running_tasks.append(task_)
|
|
46
|
+
console.debug(run_msg.format(type="coroutine"))
|
|
47
|
+
else:
|
|
48
|
+
console.debug(run_msg.format(type="function"))
|
|
40
49
|
yield
|
|
41
50
|
finally:
|
|
42
|
-
cancel_kwargs = (
|
|
43
|
-
{"msg": "lifespan_cleanup"} if sys.version_info >= (3, 9) else {}
|
|
44
|
-
)
|
|
45
51
|
for task in running_tasks:
|
|
46
|
-
|
|
52
|
+
console.debug(f"Canceling lifespan task: {task}")
|
|
53
|
+
task.cancel(msg="lifespan_cleanup")
|
|
47
54
|
|
|
48
55
|
def register_lifespan_task(self, task: Callable | asyncio.Task, **task_kwargs):
|
|
49
56
|
"""Register a task to run during the lifespan of the app.
|
|
@@ -51,7 +58,18 @@ class LifespanMixin(AppMixin):
|
|
|
51
58
|
Args:
|
|
52
59
|
task: The task to register.
|
|
53
60
|
task_kwargs: The kwargs of the task.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
InvalidLifespanTaskType: If the task is a generator function.
|
|
54
64
|
"""
|
|
65
|
+
if inspect.isgeneratorfunction(task) or inspect.isasyncgenfunction(task):
|
|
66
|
+
raise InvalidLifespanTaskType(
|
|
67
|
+
f"Task {task.__name__} of type generator must be decorated with contextlib.asynccontextmanager."
|
|
68
|
+
)
|
|
69
|
+
|
|
55
70
|
if task_kwargs:
|
|
71
|
+
original_task = task
|
|
56
72
|
task = functools.partial(task, **task_kwargs) # type: ignore
|
|
73
|
+
functools.update_wrapper(task, original_task) # type: ignore
|
|
57
74
|
self.lifespan_tasks.add(task) # type: ignore
|
|
75
|
+
console.debug(f"Registered lifespan task: {task.__name__}") # type: ignore
|
reflex/app_module_for_backend.py
CHANGED
|
@@ -15,7 +15,7 @@ if constants.CompileVars.APP != "app":
|
|
|
15
15
|
telemetry.send("compile")
|
|
16
16
|
app_module = get_app(reload=False)
|
|
17
17
|
app = getattr(app_module, constants.CompileVars.APP)
|
|
18
|
-
# For py3.
|
|
18
|
+
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
|
19
19
|
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
|
20
20
|
app._apply_decorated_pages()
|
|
21
21
|
compile_future = ThreadPoolExecutor(max_workers=1).submit(app._compile)
|
reflex/base.py
CHANGED
|
@@ -47,6 +47,9 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None
|
|
|
47
47
|
# shadowed state vars when reloading app via utils.prerequisites.get_app(reload=True)
|
|
48
48
|
pydantic_main.validate_field_name = validate_field_name # type: ignore
|
|
49
49
|
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from reflex.vars import Var
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
|
52
55
|
"""The base class subclassed by all Reflex classes.
|
|
@@ -92,7 +95,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
|
|
92
95
|
return self
|
|
93
96
|
|
|
94
97
|
@classmethod
|
|
95
|
-
def get_fields(cls) -> dict[str,
|
|
98
|
+
def get_fields(cls) -> dict[str, ModelField]:
|
|
96
99
|
"""Get the fields of the object.
|
|
97
100
|
|
|
98
101
|
Returns:
|
|
@@ -101,7 +104,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
|
|
101
104
|
return cls.__fields__
|
|
102
105
|
|
|
103
106
|
@classmethod
|
|
104
|
-
def add_field(cls, var:
|
|
107
|
+
def add_field(cls, var: Var, default_value: Any):
|
|
105
108
|
"""Add a pydantic field after class definition.
|
|
106
109
|
|
|
107
110
|
Used by State.add_var() to correctly handle the new variable.
|
|
@@ -110,7 +113,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
|
|
110
113
|
var: The variable to add a pydantic field for.
|
|
111
114
|
default_value: The default value of the field
|
|
112
115
|
"""
|
|
113
|
-
var_name = var.
|
|
116
|
+
var_name = var._var_field_name
|
|
114
117
|
new_field = ModelField.infer(
|
|
115
118
|
name=var_name,
|
|
116
119
|
value=default_value,
|
|
@@ -133,13 +136,4 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
|
|
133
136
|
# Seems like this function signature was wrong all along?
|
|
134
137
|
# If the user wants a field that we know of, get it and pass it off to _get_value
|
|
135
138
|
key = getattr(self, key)
|
|
136
|
-
return
|
|
137
|
-
key,
|
|
138
|
-
to_dict=True,
|
|
139
|
-
by_alias=False,
|
|
140
|
-
include=None,
|
|
141
|
-
exclude=None,
|
|
142
|
-
exclude_unset=False,
|
|
143
|
-
exclude_defaults=False,
|
|
144
|
-
exclude_none=False,
|
|
145
|
-
)
|
|
139
|
+
return key
|
reflex/compiler/utils.py
CHANGED
|
@@ -44,6 +44,9 @@ def compile_import_statement(fields: list[ImportVar]) -> tuple[str, list[str]]:
|
|
|
44
44
|
Args:
|
|
45
45
|
fields: The set of fields to import from the library.
|
|
46
46
|
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If there is more than one default import.
|
|
49
|
+
|
|
47
50
|
Returns:
|
|
48
51
|
The libraries for default and rest.
|
|
49
52
|
default: default library. When install "import def from library".
|
|
@@ -54,7 +57,8 @@ def compile_import_statement(fields: list[ImportVar]) -> tuple[str, list[str]]:
|
|
|
54
57
|
|
|
55
58
|
# Check for default imports.
|
|
56
59
|
defaults = {field for field in fields_set if field.is_default}
|
|
57
|
-
|
|
60
|
+
if len(defaults) >= 2:
|
|
61
|
+
raise ValueError("Only one default import is allowed.")
|
|
58
62
|
|
|
59
63
|
# Get the default import, and the specific imports.
|
|
60
64
|
default = next(iter({field.name for field in defaults}), "")
|
|
@@ -92,6 +96,9 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]:
|
|
|
92
96
|
Args:
|
|
93
97
|
import_dict: The import dict to compile.
|
|
94
98
|
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If an import in the dict is invalid.
|
|
101
|
+
|
|
95
102
|
Returns:
|
|
96
103
|
The list of import dict.
|
|
97
104
|
"""
|
|
@@ -106,8 +113,10 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]:
|
|
|
106
113
|
continue
|
|
107
114
|
|
|
108
115
|
if not lib:
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
if default:
|
|
117
|
+
raise ValueError("No default field allowed for empty library.")
|
|
118
|
+
if rest is None or len(rest) == 0:
|
|
119
|
+
raise ValueError("No fields to import.")
|
|
111
120
|
for module in sorted(rest):
|
|
112
121
|
import_dicts.append(get_import_dict(module))
|
|
113
122
|
continue
|
|
@@ -155,7 +164,7 @@ def compile_state(state: Type[BaseState]) -> dict:
|
|
|
155
164
|
initial_state = state(_reflex_internal_init=True).dict(
|
|
156
165
|
initial=True, include_computed=False
|
|
157
166
|
)
|
|
158
|
-
return
|
|
167
|
+
return initial_state
|
|
159
168
|
|
|
160
169
|
|
|
161
170
|
def _compile_client_storage_field(
|
|
@@ -429,11 +438,11 @@ def add_meta(
|
|
|
429
438
|
Returns:
|
|
430
439
|
The component with the metadata added.
|
|
431
440
|
"""
|
|
432
|
-
meta_tags = [
|
|
433
|
-
|
|
434
|
-
children: list[Any] = [
|
|
435
|
-
Title.create(title),
|
|
441
|
+
meta_tags = [
|
|
442
|
+
item if isinstance(item, Component) else Meta.create(**item) for item in meta
|
|
436
443
|
]
|
|
444
|
+
|
|
445
|
+
children: list[Any] = [Title.create(title)]
|
|
437
446
|
if description:
|
|
438
447
|
children.append(Description.create(content=description))
|
|
439
448
|
children.append(Image.create(content=image))
|
reflex/components/base/bare.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Any, Iterator
|
|
|
7
7
|
from reflex.components.component import Component
|
|
8
8
|
from reflex.components.tags import Tag
|
|
9
9
|
from reflex.components.tags.tagless import Tagless
|
|
10
|
-
from reflex.vars
|
|
10
|
+
from reflex.vars import ArrayVar, BooleanVar, ObjectVar, Var
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Bare(Component):
|
|
@@ -33,6 +33,8 @@ class Bare(Component):
|
|
|
33
33
|
|
|
34
34
|
def _render(self) -> Tag:
|
|
35
35
|
if isinstance(self.contents, Var):
|
|
36
|
+
if isinstance(self.contents, (BooleanVar, ObjectVar, ArrayVar)):
|
|
37
|
+
return Tagless(contents=f"{{{str(self.contents.to_string())}}}")
|
|
36
38
|
return Tagless(contents=f"{{{str(self.contents)}}}")
|
|
37
39
|
return Tagless(contents=str(self.contents))
|
|
38
40
|
|
reflex/components/base/meta.py
CHANGED
|
@@ -16,13 +16,15 @@ class Title(Component):
|
|
|
16
16
|
def render(self) -> dict:
|
|
17
17
|
"""Render the title component.
|
|
18
18
|
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If the title is not a single string.
|
|
21
|
+
|
|
19
22
|
Returns:
|
|
20
23
|
The rendered title component.
|
|
21
24
|
"""
|
|
22
25
|
# Make sure the title is a single string.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
), "Title must be a single string."
|
|
26
|
+
if len(self.children) != 1 or not isinstance(self.children[0], Bare):
|
|
27
|
+
raise ValueError("Title must be a single string.")
|
|
26
28
|
return super().render()
|
|
27
29
|
|
|
28
30
|
|
reflex/components/component.py
CHANGED
|
@@ -25,6 +25,7 @@ import reflex.state
|
|
|
25
25
|
from reflex.base import Base
|
|
26
26
|
from reflex.compiler.templates import STATEFUL_COMPONENT
|
|
27
27
|
from reflex.components.core.breakpoints import Breakpoints
|
|
28
|
+
from reflex.components.dynamic import load_dynamic_serializer
|
|
28
29
|
from reflex.components.tags import Tag
|
|
29
30
|
from reflex.constants import (
|
|
30
31
|
Dirs,
|
|
@@ -52,9 +53,9 @@ from reflex.utils.imports import (
|
|
|
52
53
|
ParsedImportDict,
|
|
53
54
|
parse_imports,
|
|
54
55
|
)
|
|
55
|
-
from reflex.utils.serializers import serializer
|
|
56
56
|
from reflex.vars import VarData
|
|
57
57
|
from reflex.vars.base import LiteralVar, Var
|
|
58
|
+
from reflex.vars.sequence import LiteralArrayVar
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
class BaseComponent(Base, ABC):
|
|
@@ -448,8 +449,16 @@ class Component(BaseComponent, ABC):
|
|
|
448
449
|
and not types._issubclass(passed_type, expected_type, value)
|
|
449
450
|
):
|
|
450
451
|
value_name = value._js_expr if isinstance(value, Var) else value
|
|
452
|
+
|
|
453
|
+
additional_info = (
|
|
454
|
+
" You can call `.bool()` on the value to convert it to a boolean."
|
|
455
|
+
if expected_type is bool and isinstance(value, Var)
|
|
456
|
+
else ""
|
|
457
|
+
)
|
|
458
|
+
|
|
451
459
|
raise TypeError(
|
|
452
|
-
f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {
|
|
460
|
+
f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {passed_type}."
|
|
461
|
+
+ additional_info
|
|
453
462
|
)
|
|
454
463
|
# Check if the key is an event trigger.
|
|
455
464
|
if key in component_specific_triggers:
|
|
@@ -469,7 +478,7 @@ class Component(BaseComponent, ABC):
|
|
|
469
478
|
# Merge styles, the later ones overriding keys in the earlier ones.
|
|
470
479
|
style = {k: v for style_dict in style for k, v in style_dict.items()}
|
|
471
480
|
|
|
472
|
-
if isinstance(style, Breakpoints):
|
|
481
|
+
if isinstance(style, (Breakpoints, Var)):
|
|
473
482
|
style = {
|
|
474
483
|
# Assign the Breakpoints to the self-referential selector to avoid squashing down to a regular dict.
|
|
475
484
|
"&": style,
|
|
@@ -488,7 +497,12 @@ class Component(BaseComponent, ABC):
|
|
|
488
497
|
# Convert class_name to str if it's list
|
|
489
498
|
class_name = kwargs.get("class_name", "")
|
|
490
499
|
if isinstance(class_name, (List, tuple)):
|
|
491
|
-
|
|
500
|
+
if any(isinstance(c, Var) for c in class_name):
|
|
501
|
+
kwargs["class_name"] = LiteralArrayVar.create(
|
|
502
|
+
class_name, _var_type=List[str]
|
|
503
|
+
).join(" ")
|
|
504
|
+
else:
|
|
505
|
+
kwargs["class_name"] = " ".join(class_name)
|
|
492
506
|
|
|
493
507
|
# Construct the component.
|
|
494
508
|
super().__init__(*args, **kwargs)
|
|
@@ -615,8 +629,8 @@ class Component(BaseComponent, ABC):
|
|
|
615
629
|
if types._issubclass(field.type_, EventHandler):
|
|
616
630
|
args_spec = None
|
|
617
631
|
annotation = field.annotation
|
|
618
|
-
if
|
|
619
|
-
args_spec =
|
|
632
|
+
if (metadata := getattr(annotation, "__metadata__", None)) is not None:
|
|
633
|
+
args_spec = metadata[0]
|
|
620
634
|
default_triggers[field.name] = args_spec or (lambda: [])
|
|
621
635
|
return default_triggers
|
|
622
636
|
|
|
@@ -1730,10 +1744,14 @@ class CustomComponent(Component):
|
|
|
1730
1744
|
Args:
|
|
1731
1745
|
seen: The tags of the components that have already been seen.
|
|
1732
1746
|
|
|
1747
|
+
Raises:
|
|
1748
|
+
ValueError: If the tag is not set.
|
|
1749
|
+
|
|
1733
1750
|
Returns:
|
|
1734
1751
|
The set of custom components.
|
|
1735
1752
|
"""
|
|
1736
|
-
|
|
1753
|
+
if self.tag is None:
|
|
1754
|
+
raise ValueError("The tag must be set.")
|
|
1737
1755
|
|
|
1738
1756
|
# Store the seen components in a set to avoid infinite recursion.
|
|
1739
1757
|
if seen is None:
|
|
@@ -1882,19 +1900,6 @@ class NoSSRComponent(Component):
|
|
|
1882
1900
|
return "".join((library_import, mod_import, opts_fragment))
|
|
1883
1901
|
|
|
1884
1902
|
|
|
1885
|
-
@serializer
|
|
1886
|
-
def serialize_component(comp: Component):
|
|
1887
|
-
"""Serialize a component.
|
|
1888
|
-
|
|
1889
|
-
Args:
|
|
1890
|
-
comp: The component to serialize.
|
|
1891
|
-
|
|
1892
|
-
Returns:
|
|
1893
|
-
The serialized component.
|
|
1894
|
-
"""
|
|
1895
|
-
return str(comp)
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
1903
|
class StatefulComponent(BaseComponent):
|
|
1899
1904
|
"""A component that depends on state and is rendered outside of the page component.
|
|
1900
1905
|
|
|
@@ -2307,3 +2312,6 @@ class MemoizationLeaf(Component):
|
|
|
2307
2312
|
update={"disposition": MemoizationDisposition.ALWAYS}
|
|
2308
2313
|
)
|
|
2309
2314
|
return comp
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
load_dynamic_serializer()
|