reflex 0.4.5a1__py3-none-any.whl → 0.4.6__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 +5 -15
- reflex/.templates/jinja/web/pages/index.js.jinja2 +4 -0
- reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
- reflex/.templates/web/utils/state.js +3 -0
- reflex/__init__.py +12 -2
- reflex/__init__.pyi +4 -0
- reflex/app.py +23 -3
- reflex/base.py +16 -4
- reflex/compiler/compiler.py +1 -0
- reflex/compiler/utils.py +11 -2
- reflex/components/base/app_wrap.pyi +1 -1
- reflex/components/base/bare.py +3 -4
- reflex/components/base/body.pyi +1 -1
- reflex/components/base/document.pyi +1 -1
- reflex/components/base/fragment.pyi +1 -1
- reflex/components/base/head.pyi +1 -1
- reflex/components/base/link.pyi +1 -1
- reflex/components/base/meta.pyi +1 -1
- reflex/components/base/script.pyi +1 -1
- reflex/components/chakra/base.pyi +1 -1
- reflex/components/chakra/datadisplay/badge.pyi +1 -1
- reflex/components/chakra/datadisplay/code.pyi +1 -1
- reflex/components/chakra/datadisplay/divider.pyi +1 -1
- reflex/components/chakra/datadisplay/keyboard_key.pyi +1 -1
- reflex/components/chakra/datadisplay/list.pyi +1 -1
- reflex/components/chakra/datadisplay/stat.pyi +1 -1
- reflex/components/chakra/datadisplay/table.pyi +1 -1
- reflex/components/chakra/datadisplay/tag.pyi +1 -1
- reflex/components/chakra/disclosure/accordion.pyi +1 -1
- reflex/components/chakra/disclosure/tabs.pyi +1 -1
- reflex/components/chakra/disclosure/transition.pyi +1 -1
- reflex/components/chakra/disclosure/visuallyhidden.pyi +1 -1
- reflex/components/chakra/feedback/alert.pyi +1 -1
- reflex/components/chakra/feedback/circularprogress.pyi +1 -1
- reflex/components/chakra/feedback/progress.pyi +1 -1
- reflex/components/chakra/feedback/skeleton.pyi +1 -1
- reflex/components/chakra/feedback/spinner.pyi +1 -1
- reflex/components/chakra/forms/button.pyi +1 -1
- reflex/components/chakra/forms/checkbox.pyi +1 -1
- reflex/components/chakra/forms/colormodeswitch.pyi +1 -1
- reflex/components/chakra/forms/date_picker.pyi +1 -1
- reflex/components/chakra/forms/date_time_picker.pyi +1 -1
- reflex/components/chakra/forms/editable.pyi +1 -1
- reflex/components/chakra/forms/email.pyi +1 -1
- reflex/components/chakra/forms/form.pyi +1 -1
- reflex/components/chakra/forms/iconbutton.pyi +1 -1
- reflex/components/chakra/forms/input.pyi +1 -1
- reflex/components/chakra/forms/numberinput.pyi +1 -1
- reflex/components/chakra/forms/password.pyi +1 -1
- reflex/components/chakra/forms/pininput.pyi +1 -1
- reflex/components/chakra/forms/radio.pyi +1 -1
- reflex/components/chakra/forms/rangeslider.pyi +1 -1
- reflex/components/chakra/forms/select.pyi +1 -1
- reflex/components/chakra/forms/slider.pyi +1 -1
- reflex/components/chakra/forms/switch.pyi +1 -1
- reflex/components/chakra/forms/textarea.pyi +1 -1
- reflex/components/chakra/forms/time_picker.pyi +1 -1
- reflex/components/chakra/layout/aspect_ratio.pyi +1 -1
- reflex/components/chakra/layout/box.pyi +1 -1
- reflex/components/chakra/layout/card.pyi +1 -1
- reflex/components/chakra/layout/center.pyi +1 -1
- reflex/components/chakra/layout/container.pyi +1 -1
- reflex/components/chakra/layout/flex.pyi +1 -1
- reflex/components/chakra/layout/grid.pyi +1 -1
- reflex/components/chakra/layout/spacer.pyi +1 -1
- reflex/components/chakra/layout/stack.pyi +1 -1
- reflex/components/chakra/layout/wrap.pyi +1 -1
- reflex/components/chakra/media/avatar.pyi +1 -1
- reflex/components/chakra/media/icon.pyi +1 -1
- reflex/components/chakra/media/image.pyi +1 -1
- reflex/components/chakra/navigation/breadcrumb.pyi +1 -1
- reflex/components/chakra/navigation/link.pyi +1 -1
- reflex/components/chakra/navigation/linkoverlay.pyi +1 -1
- reflex/components/chakra/navigation/stepper.pyi +1 -1
- reflex/components/chakra/overlay/alertdialog.pyi +1 -1
- reflex/components/chakra/overlay/drawer.pyi +1 -1
- reflex/components/chakra/overlay/menu.pyi +1 -1
- reflex/components/chakra/overlay/modal.pyi +1 -1
- reflex/components/chakra/overlay/popover.pyi +1 -1
- reflex/components/chakra/overlay/tooltip.pyi +1 -1
- reflex/components/chakra/typography/heading.pyi +1 -1
- reflex/components/chakra/typography/highlight.pyi +1 -1
- reflex/components/chakra/typography/span.pyi +1 -1
- reflex/components/chakra/typography/text.pyi +1 -1
- reflex/components/component.py +82 -30
- reflex/components/core/banner.py +1 -2
- reflex/components/core/banner.pyi +1 -2
- reflex/components/core/client_side_routing.pyi +1 -1
- reflex/components/core/cond.py +1 -1
- reflex/components/core/debounce.pyi +1 -1
- reflex/components/core/html.pyi +1 -1
- reflex/components/core/responsive.py +1 -1
- reflex/components/core/upload.pyi +1 -1
- reflex/components/datadisplay/code.py +17 -9
- reflex/components/datadisplay/code.pyi +3 -1
- reflex/components/datadisplay/dataeditor.pyi +1 -1
- reflex/components/el/element.pyi +1 -1
- reflex/components/el/elements/base.pyi +1 -1
- reflex/components/el/elements/forms.py +78 -1
- reflex/components/el/elements/forms.pyi +10 -2
- reflex/components/el/elements/inline.pyi +1 -1
- reflex/components/el/elements/media.pyi +1 -1
- reflex/components/el/elements/metadata.pyi +1 -1
- reflex/components/el/elements/other.pyi +1 -1
- reflex/components/el/elements/scripts.pyi +1 -1
- reflex/components/el/elements/sectioning.pyi +1 -1
- reflex/components/el/elements/tables.pyi +1 -1
- reflex/components/el/elements/typography.pyi +1 -1
- reflex/components/gridjs/datatable.pyi +1 -1
- reflex/components/lucide/icon.py +275 -115
- reflex/components/lucide/icon.pyi +259 -114
- reflex/components/markdown/markdown.pyi +1 -1
- reflex/components/moment/moment.pyi +1 -1
- reflex/components/next/base.pyi +1 -1
- reflex/components/next/image.pyi +1 -1
- reflex/components/next/link.pyi +1 -1
- reflex/components/next/video.pyi +1 -1
- reflex/components/plotly/plotly.pyi +1 -1
- reflex/components/radix/primitives/accordion.pyi +1 -1
- reflex/components/radix/primitives/base.pyi +1 -1
- reflex/components/radix/primitives/drawer.pyi +1 -1
- reflex/components/radix/primitives/form.pyi +1 -1
- reflex/components/radix/primitives/progress.pyi +1 -1
- reflex/components/radix/primitives/slider.pyi +1 -1
- reflex/components/radix/themes/base.pyi +1 -1
- reflex/components/radix/themes/color_mode.pyi +1 -1
- reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
- reflex/components/radix/themes/components/aspect_ratio.pyi +1 -1
- reflex/components/radix/themes/components/avatar.pyi +1 -1
- reflex/components/radix/themes/components/badge.pyi +1 -1
- reflex/components/radix/themes/components/button.pyi +1 -1
- reflex/components/radix/themes/components/callout.pyi +1 -1
- reflex/components/radix/themes/components/card.pyi +1 -1
- reflex/components/radix/themes/components/checkbox.pyi +1 -1
- reflex/components/radix/themes/components/context_menu.pyi +1 -1
- reflex/components/radix/themes/components/dialog.pyi +1 -1
- reflex/components/radix/themes/components/dropdown_menu.pyi +1 -1
- reflex/components/radix/themes/components/hover_card.pyi +1 -1
- reflex/components/radix/themes/components/icon_button.pyi +1 -1
- reflex/components/radix/themes/components/inset.pyi +1 -1
- reflex/components/radix/themes/components/popover.pyi +1 -1
- reflex/components/radix/themes/components/radio_group.pyi +1 -1
- reflex/components/radix/themes/components/scroll_area.pyi +1 -1
- reflex/components/radix/themes/components/select.py +4 -1
- reflex/components/radix/themes/components/select.pyi +5 -1
- reflex/components/radix/themes/components/separator.pyi +1 -1
- reflex/components/radix/themes/components/slider.pyi +1 -1
- reflex/components/radix/themes/components/switch.pyi +1 -1
- reflex/components/radix/themes/components/table.pyi +1 -1
- reflex/components/radix/themes/components/tabs.pyi +1 -1
- reflex/components/radix/themes/components/text_area.pyi +5 -1
- reflex/components/radix/themes/components/text_field.pyi +1 -1
- reflex/components/radix/themes/components/tooltip.pyi +1 -1
- reflex/components/radix/themes/layout/__init__.py +5 -4
- reflex/components/radix/themes/layout/base.pyi +1 -1
- reflex/components/radix/themes/layout/box.pyi +1 -1
- reflex/components/radix/themes/layout/center.pyi +1 -1
- reflex/components/radix/themes/layout/container.pyi +1 -1
- reflex/components/radix/themes/layout/flex.pyi +1 -1
- reflex/components/radix/themes/layout/grid.pyi +1 -1
- reflex/components/radix/themes/layout/list.py +21 -13
- reflex/components/radix/themes/layout/list.pyi +139 -481
- reflex/components/radix/themes/layout/section.pyi +1 -1
- reflex/components/radix/themes/layout/spacer.pyi +1 -1
- reflex/components/radix/themes/layout/stack.pyi +1 -1
- reflex/components/radix/themes/typography/blockquote.pyi +1 -1
- reflex/components/radix/themes/typography/code.pyi +1 -1
- reflex/components/radix/themes/typography/heading.pyi +1 -1
- reflex/components/radix/themes/typography/link.pyi +1 -1
- reflex/components/radix/themes/typography/text.pyi +1 -1
- reflex/components/react_player/audio.pyi +1 -1
- reflex/components/react_player/react_player.pyi +1 -1
- reflex/components/react_player/video.pyi +1 -1
- reflex/components/recharts/cartesian.pyi +1 -1
- reflex/components/recharts/charts.pyi +1 -1
- reflex/components/recharts/general.pyi +1 -1
- reflex/components/recharts/polar.pyi +1 -1
- reflex/components/recharts/recharts.pyi +1 -1
- reflex/components/suneditor/editor.pyi +1 -1
- reflex/config.py +12 -2
- reflex/custom_components/custom_components.py +305 -21
- reflex/event.py +36 -46
- reflex/reflex.py +19 -13
- reflex/state.py +184 -39
- reflex/testing.py +15 -11
- reflex/utils/console.py +15 -7
- reflex/utils/exec.py +9 -0
- reflex/utils/prerequisites.py +12 -1
- reflex/utils/processes.py +8 -25
- reflex/utils/pyi_generator.py +842 -0
- reflex/utils/telemetry.py +18 -1
- reflex/utils/types.py +14 -2
- reflex/vars.py +33 -3
- {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/METADATA +31 -29
- {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/RECORD +198 -198
- reflex/page.pyi +0 -17
- {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/LICENSE +0 -0
- {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/WHEEL +0 -0
- {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/entry_points.txt +0 -0
reflex/event.py
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"""Define event classes to connect the frontend and backend."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import inspect
|
|
5
6
|
from base64 import b64encode
|
|
6
7
|
from types import FunctionType
|
|
7
8
|
from typing import (
|
|
8
|
-
TYPE_CHECKING,
|
|
9
9
|
Any,
|
|
10
10
|
Callable,
|
|
11
11
|
Dict,
|
|
12
12
|
List,
|
|
13
13
|
Optional,
|
|
14
14
|
Tuple,
|
|
15
|
-
Type,
|
|
16
15
|
Union,
|
|
16
|
+
_GenericAlias, # type: ignore
|
|
17
|
+
get_type_hints,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
from reflex import constants
|
|
@@ -22,9 +23,6 @@ from reflex.utils import console, format
|
|
|
22
23
|
from reflex.utils.types import ArgsSpec
|
|
23
24
|
from reflex.vars import BaseVar, Var
|
|
24
25
|
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from reflex.state import BaseState
|
|
27
|
-
|
|
28
26
|
|
|
29
27
|
class Event(Base):
|
|
30
28
|
"""An event that describes any state change in the app."""
|
|
@@ -74,44 +72,6 @@ def background(fn):
|
|
|
74
72
|
return fn
|
|
75
73
|
|
|
76
74
|
|
|
77
|
-
def _no_chain_background_task(
|
|
78
|
-
state_cls: Type["BaseState"], name: str, fn: Callable
|
|
79
|
-
) -> Callable:
|
|
80
|
-
"""Protect against directly chaining a background task from another event handler.
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
state_cls: The state class that the event handler is in.
|
|
84
|
-
name: The name of the background task.
|
|
85
|
-
fn: The background task coroutine function / generator.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
A compatible coroutine function / generator that raises a runtime error.
|
|
89
|
-
|
|
90
|
-
Raises:
|
|
91
|
-
TypeError: If the background task is not async.
|
|
92
|
-
"""
|
|
93
|
-
call = f"{state_cls.__name__}.{name}"
|
|
94
|
-
message = (
|
|
95
|
-
f"Cannot directly call background task {name!r}, use "
|
|
96
|
-
f"`yield {call}` or `return {call}` instead."
|
|
97
|
-
)
|
|
98
|
-
if inspect.iscoroutinefunction(fn):
|
|
99
|
-
|
|
100
|
-
async def _no_chain_background_task_co(*args, **kwargs):
|
|
101
|
-
raise RuntimeError(message)
|
|
102
|
-
|
|
103
|
-
return _no_chain_background_task_co
|
|
104
|
-
if inspect.isasyncgenfunction(fn):
|
|
105
|
-
|
|
106
|
-
async def _no_chain_background_task_gen(*args, **kwargs):
|
|
107
|
-
yield
|
|
108
|
-
raise RuntimeError(message)
|
|
109
|
-
|
|
110
|
-
return _no_chain_background_task_gen
|
|
111
|
-
|
|
112
|
-
raise TypeError(f"{fn} is marked as a background task, but is not async.")
|
|
113
|
-
|
|
114
|
-
|
|
115
75
|
class EventActionsMixin(Base):
|
|
116
76
|
"""Mixin for DOM event actions."""
|
|
117
77
|
|
|
@@ -148,7 +108,7 @@ class EventHandler(EventActionsMixin):
|
|
|
148
108
|
fn: Any
|
|
149
109
|
|
|
150
110
|
# The full name of the state class this event handler is attached to.
|
|
151
|
-
#
|
|
111
|
+
# Empty string means this event handler is a server side event.
|
|
152
112
|
state_full_name: str = ""
|
|
153
113
|
|
|
154
114
|
class Config:
|
|
@@ -157,6 +117,21 @@ class EventHandler(EventActionsMixin):
|
|
|
157
117
|
# Needed to allow serialization of Callable.
|
|
158
118
|
frozen = True
|
|
159
119
|
|
|
120
|
+
@classmethod
|
|
121
|
+
def __class_getitem__(cls, args_spec: str) -> _GenericAlias:
|
|
122
|
+
"""Get a typed EventHandler.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
args_spec: The args_spec of the EventHandler.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The EventHandler class item.
|
|
129
|
+
"""
|
|
130
|
+
gen = _GenericAlias(cls, Any)
|
|
131
|
+
# Cannot subclass special typing classes, so we need to set the args_spec dynamically as an attribute.
|
|
132
|
+
gen.args_spec = args_spec
|
|
133
|
+
return gen
|
|
134
|
+
|
|
160
135
|
@property
|
|
161
136
|
def is_background(self) -> bool:
|
|
162
137
|
"""Whether the event handler is a background task.
|
|
@@ -324,7 +299,7 @@ class FileUpload(Base):
|
|
|
324
299
|
on_upload_progress: Optional[Union[EventHandler, Callable]] = None
|
|
325
300
|
|
|
326
301
|
@staticmethod
|
|
327
|
-
def on_upload_progress_args_spec(_prog:
|
|
302
|
+
def on_upload_progress_args_spec(_prog: Dict[str, Union[int, float, bool]]):
|
|
328
303
|
"""Args spec for on_upload_progress event handler.
|
|
329
304
|
|
|
330
305
|
Returns:
|
|
@@ -485,6 +460,20 @@ def set_focus(ref: str) -> EventSpec:
|
|
|
485
460
|
)
|
|
486
461
|
|
|
487
462
|
|
|
463
|
+
def scroll_to(elem_id: str) -> EventSpec:
|
|
464
|
+
"""Select the id of a html element for scrolling into view.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
elem_id: the id of the element
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
An EventSpec to scroll the page to the selected element.
|
|
471
|
+
"""
|
|
472
|
+
js_code = f"document.getElementById('{elem_id}').scrollIntoView();"
|
|
473
|
+
|
|
474
|
+
return call_script(js_code)
|
|
475
|
+
|
|
476
|
+
|
|
488
477
|
def set_value(ref: str, value: Any) -> EventSpec:
|
|
489
478
|
"""Set the value of a ref.
|
|
490
479
|
|
|
@@ -771,11 +760,12 @@ def parse_args_spec(arg_spec: ArgsSpec):
|
|
|
771
760
|
The parsed args.
|
|
772
761
|
"""
|
|
773
762
|
spec = inspect.getfullargspec(arg_spec)
|
|
763
|
+
annotations = get_type_hints(arg_spec)
|
|
774
764
|
return arg_spec(
|
|
775
765
|
*[
|
|
776
766
|
BaseVar(
|
|
777
767
|
_var_name=f"_{l_arg}",
|
|
778
|
-
_var_type=
|
|
768
|
+
_var_type=annotations.get(l_arg, FrontendEvent),
|
|
779
769
|
_var_is_local=True,
|
|
780
770
|
)
|
|
781
771
|
for l_arg in spec.args
|
reflex/reflex.py
CHANGED
|
@@ -169,10 +169,10 @@ def _run(
|
|
|
169
169
|
|
|
170
170
|
# If something is running on the ports, ask the user if they want to kill or change it.
|
|
171
171
|
if frontend and processes.is_process_on_port(frontend_port):
|
|
172
|
-
frontend_port = processes.
|
|
172
|
+
frontend_port = processes.change_port(frontend_port, "frontend")
|
|
173
173
|
|
|
174
174
|
if backend and processes.is_process_on_port(backend_port):
|
|
175
|
-
backend_port = processes.
|
|
175
|
+
backend_port = processes.change_port(backend_port, "backend")
|
|
176
176
|
|
|
177
177
|
# Apply the new ports to the config.
|
|
178
178
|
if frontend_port != str(config.frontend_port):
|
|
@@ -300,22 +300,14 @@ def export(
|
|
|
300
300
|
)
|
|
301
301
|
|
|
302
302
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
loglevel: constants.LogLevel = typer.Option(
|
|
306
|
-
config.loglevel, help="The log level to use."
|
|
307
|
-
),
|
|
308
|
-
):
|
|
309
|
-
"""Authenticate with Reflex hosting service."""
|
|
303
|
+
def _login() -> str:
|
|
304
|
+
"""Helper function to authenticate with Reflex hosting service."""
|
|
310
305
|
from reflex_cli.utils import hosting
|
|
311
306
|
|
|
312
|
-
# Set the log level.
|
|
313
|
-
console.set_log_level(loglevel)
|
|
314
|
-
|
|
315
307
|
access_token, invitation_code = hosting.authenticated_token()
|
|
316
308
|
if access_token:
|
|
317
309
|
console.print("You already logged in.")
|
|
318
|
-
return
|
|
310
|
+
return access_token
|
|
319
311
|
|
|
320
312
|
# If not already logged in, open a browser window/tab to the login page.
|
|
321
313
|
access_token = hosting.authenticate_on_browser(invitation_code)
|
|
@@ -325,6 +317,20 @@ def login(
|
|
|
325
317
|
raise typer.Exit(1)
|
|
326
318
|
|
|
327
319
|
console.print("Successfully logged in.")
|
|
320
|
+
return access_token
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@cli.command()
|
|
324
|
+
def login(
|
|
325
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
326
|
+
config.loglevel, help="The log level to use."
|
|
327
|
+
),
|
|
328
|
+
):
|
|
329
|
+
"""Authenticate with Reflex hosting service."""
|
|
330
|
+
# Set the log level.
|
|
331
|
+
console.set_log_level(loglevel)
|
|
332
|
+
|
|
333
|
+
_login()
|
|
328
334
|
|
|
329
335
|
|
|
330
336
|
@cli.command()
|
reflex/state.py
CHANGED
|
@@ -15,6 +15,7 @@ from abc import ABC, abstractmethod
|
|
|
15
15
|
from collections import defaultdict
|
|
16
16
|
from types import FunctionType, MethodType
|
|
17
17
|
from typing import (
|
|
18
|
+
TYPE_CHECKING,
|
|
18
19
|
Any,
|
|
19
20
|
AsyncIterator,
|
|
20
21
|
Callable,
|
|
@@ -27,8 +28,19 @@ from typing import (
|
|
|
27
28
|
Type,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
|
-
import
|
|
31
|
-
|
|
31
|
+
import dill
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# TODO The type checking guard can be removed once
|
|
35
|
+
# reflex-hosting-cli tools are compatible with pydantic v2
|
|
36
|
+
|
|
37
|
+
if not TYPE_CHECKING:
|
|
38
|
+
import pydantic.v1 as pydantic
|
|
39
|
+
else:
|
|
40
|
+
raise ModuleNotFoundError
|
|
41
|
+
except ModuleNotFoundError:
|
|
42
|
+
import pydantic
|
|
43
|
+
|
|
32
44
|
import wrapt
|
|
33
45
|
from redis.asyncio import Redis
|
|
34
46
|
|
|
@@ -38,7 +50,6 @@ from reflex.event import (
|
|
|
38
50
|
Event,
|
|
39
51
|
EventHandler,
|
|
40
52
|
EventSpec,
|
|
41
|
-
_no_chain_background_task,
|
|
42
53
|
fix_events,
|
|
43
54
|
window_alert,
|
|
44
55
|
)
|
|
@@ -48,10 +59,18 @@ from reflex.utils.exec import is_testing_env
|
|
|
48
59
|
from reflex.utils.serializers import SerializedType, serialize, serializer
|
|
49
60
|
from reflex.vars import BaseVar, ComputedVar, Var, computed_var
|
|
50
61
|
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from reflex.components.component import Component
|
|
64
|
+
|
|
65
|
+
|
|
51
66
|
Delta = Dict[str, Any]
|
|
52
67
|
var = computed_var
|
|
53
68
|
|
|
54
69
|
|
|
70
|
+
# If the state is this large, it's considered a performance issue.
|
|
71
|
+
TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb
|
|
72
|
+
|
|
73
|
+
|
|
55
74
|
class HeaderData(Base):
|
|
56
75
|
"""An object containing headers data."""
|
|
57
76
|
|
|
@@ -145,6 +164,44 @@ class RouterData(Base):
|
|
|
145
164
|
self.page = PageData(router_data)
|
|
146
165
|
|
|
147
166
|
|
|
167
|
+
def _no_chain_background_task(
|
|
168
|
+
state_cls: Type["BaseState"], name: str, fn: Callable
|
|
169
|
+
) -> Callable:
|
|
170
|
+
"""Protect against directly chaining a background task from another event handler.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
state_cls: The state class that the event handler is in.
|
|
174
|
+
name: The name of the background task.
|
|
175
|
+
fn: The background task coroutine function / generator.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A compatible coroutine function / generator that raises a runtime error.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
TypeError: If the background task is not async.
|
|
182
|
+
"""
|
|
183
|
+
call = f"{state_cls.__name__}.{name}"
|
|
184
|
+
message = (
|
|
185
|
+
f"Cannot directly call background task {name!r}, use "
|
|
186
|
+
f"`yield {call}` or `return {call}` instead."
|
|
187
|
+
)
|
|
188
|
+
if inspect.iscoroutinefunction(fn):
|
|
189
|
+
|
|
190
|
+
async def _no_chain_background_task_co(*args, **kwargs):
|
|
191
|
+
raise RuntimeError(message)
|
|
192
|
+
|
|
193
|
+
return _no_chain_background_task_co
|
|
194
|
+
if inspect.isasyncgenfunction(fn):
|
|
195
|
+
|
|
196
|
+
async def _no_chain_background_task_gen(*args, **kwargs):
|
|
197
|
+
yield
|
|
198
|
+
raise RuntimeError(message)
|
|
199
|
+
|
|
200
|
+
return _no_chain_background_task_gen
|
|
201
|
+
|
|
202
|
+
raise TypeError(f"{fn} is marked as a background task, but is not async.")
|
|
203
|
+
|
|
204
|
+
|
|
148
205
|
RESERVED_BACKEND_VAR_NAMES = {
|
|
149
206
|
"_backend_vars",
|
|
150
207
|
"_computed_var_dependencies",
|
|
@@ -290,8 +347,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
290
347
|
parent_state=self,
|
|
291
348
|
_reflex_internal_init=True,
|
|
292
349
|
)
|
|
293
|
-
# Convert the event handlers to functions.
|
|
294
|
-
self._init_event_handlers()
|
|
295
350
|
|
|
296
351
|
# Create a fresh copy of the backend variables for this instance
|
|
297
352
|
self._backend_vars = copy.deepcopy(
|
|
@@ -302,32 +357,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
302
357
|
}
|
|
303
358
|
)
|
|
304
359
|
|
|
305
|
-
def _init_event_handlers(self, state: BaseState | None = None):
|
|
306
|
-
"""Initialize event handlers.
|
|
307
|
-
|
|
308
|
-
Allow event handlers to be called directly on the instance. This is
|
|
309
|
-
called recursively for all parent states.
|
|
310
|
-
|
|
311
|
-
Args:
|
|
312
|
-
state: The state to initialize the event handlers on.
|
|
313
|
-
"""
|
|
314
|
-
if state is None:
|
|
315
|
-
state = self
|
|
316
|
-
|
|
317
|
-
# Convert the event handlers to functions.
|
|
318
|
-
for name, event_handler in state.event_handlers.items():
|
|
319
|
-
if event_handler.is_background:
|
|
320
|
-
fn = _no_chain_background_task(type(state), name, event_handler.fn)
|
|
321
|
-
else:
|
|
322
|
-
fn = functools.partial(event_handler.fn, self)
|
|
323
|
-
fn.__module__ = event_handler.fn.__module__ # type: ignore
|
|
324
|
-
fn.__qualname__ = event_handler.fn.__qualname__ # type: ignore
|
|
325
|
-
setattr(self, name, fn)
|
|
326
|
-
|
|
327
|
-
# Also allow direct calling of parent state event handlers
|
|
328
|
-
if state.parent_state is not None:
|
|
329
|
-
self._init_event_handlers(state.parent_state)
|
|
330
|
-
|
|
331
360
|
def __repr__(self) -> str:
|
|
332
361
|
"""Get the string representation of the state.
|
|
333
362
|
|
|
@@ -1033,12 +1062,31 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1033
1062
|
if parent_state is not None:
|
|
1034
1063
|
return getattr(parent_state, name)
|
|
1035
1064
|
|
|
1065
|
+
# Allow event handlers to be called on the instance directly.
|
|
1066
|
+
event_handlers = super().__getattribute__("event_handlers")
|
|
1067
|
+
if name in event_handlers:
|
|
1068
|
+
handler = event_handlers[name]
|
|
1069
|
+
if handler.is_background:
|
|
1070
|
+
fn = _no_chain_background_task(type(self), name, handler.fn)
|
|
1071
|
+
else:
|
|
1072
|
+
fn = functools.partial(handler.fn, self)
|
|
1073
|
+
fn.__module__ = handler.fn.__module__ # type: ignore
|
|
1074
|
+
fn.__qualname__ = handler.fn.__qualname__ # type: ignore
|
|
1075
|
+
return fn
|
|
1076
|
+
|
|
1036
1077
|
backend_vars = super().__getattribute__("_backend_vars")
|
|
1037
1078
|
if name in backend_vars:
|
|
1038
1079
|
value = backend_vars[name]
|
|
1039
1080
|
else:
|
|
1040
1081
|
value = super().__getattribute__(name)
|
|
1041
1082
|
|
|
1083
|
+
if isinstance(value, EventHandler):
|
|
1084
|
+
# The event handler is inherited from a parent, so let the parent convert
|
|
1085
|
+
# it to a callable function.
|
|
1086
|
+
parent_state = super().__getattribute__("parent_state")
|
|
1087
|
+
if parent_state is not None:
|
|
1088
|
+
return getattr(parent_state, name)
|
|
1089
|
+
|
|
1042
1090
|
if isinstance(value, MutableProxy.__mutable_types__) and (
|
|
1043
1091
|
name in super().__getattribute__("base_vars") or name in backend_vars
|
|
1044
1092
|
):
|
|
@@ -1184,9 +1232,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1184
1232
|
|
|
1185
1233
|
# Determine which parent states to fetch from the common ancestor down to the target_state_cls.
|
|
1186
1234
|
fetch_parent_states = [common_ancestor_name]
|
|
1187
|
-
for
|
|
1235
|
+
for relative_parent_state_name in relative_target_state_parts:
|
|
1188
1236
|
fetch_parent_states.append(
|
|
1189
|
-
".".join(
|
|
1237
|
+
".".join((fetch_parent_states[-1], relative_parent_state_name))
|
|
1190
1238
|
)
|
|
1191
1239
|
|
|
1192
1240
|
return common_ancestor_name, fetch_parent_states[1:-1]
|
|
@@ -1230,9 +1278,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1230
1278
|
) = self._determine_missing_parent_states(target_state_cls)
|
|
1231
1279
|
|
|
1232
1280
|
# Fetch all missing parent states and link them up to the common ancestor.
|
|
1233
|
-
|
|
1281
|
+
parent_states_tuple = self._get_parent_states()
|
|
1282
|
+
root_state = parent_states_tuple[-1][1]
|
|
1283
|
+
parent_states_by_name = dict(parent_states_tuple)
|
|
1234
1284
|
parent_state = parent_states_by_name[common_ancestor_name]
|
|
1235
1285
|
for parent_state_name in missing_parent_states:
|
|
1286
|
+
try:
|
|
1287
|
+
parent_state = root_state.get_substate(parent_state_name.split("."))
|
|
1288
|
+
# The requested state is already cached, do NOT fetch it again.
|
|
1289
|
+
continue
|
|
1290
|
+
except ValueError:
|
|
1291
|
+
# The requested state is missing, fetch from redis.
|
|
1292
|
+
pass
|
|
1236
1293
|
parent_state = await state_manager.get_state(
|
|
1237
1294
|
token=_substate_key(
|
|
1238
1295
|
self.router.session.client_token, parent_state_name
|
|
@@ -1787,11 +1844,9 @@ class OnLoadInternalState(State):
|
|
|
1787
1844
|
# Do not app.compile_()! It should be already compiled by now.
|
|
1788
1845
|
app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
|
|
1789
1846
|
load_events = app.get_load_events(self.router.page.path)
|
|
1790
|
-
if not load_events and self.is_hydrated:
|
|
1791
|
-
return # Fast path for page-to-page navigation
|
|
1792
1847
|
if not load_events:
|
|
1793
1848
|
self.is_hydrated = True
|
|
1794
|
-
return # Fast path for
|
|
1849
|
+
return # Fast path for navigation with no on_load events defined.
|
|
1795
1850
|
self.is_hydrated = False
|
|
1796
1851
|
return [
|
|
1797
1852
|
*fix_events(
|
|
@@ -1803,6 +1858,45 @@ class OnLoadInternalState(State):
|
|
|
1803
1858
|
]
|
|
1804
1859
|
|
|
1805
1860
|
|
|
1861
|
+
class ComponentState(Base):
|
|
1862
|
+
"""The base class for a State that is copied for each Component associated with it."""
|
|
1863
|
+
|
|
1864
|
+
_per_component_state_instance_count: ClassVar[int] = 0
|
|
1865
|
+
|
|
1866
|
+
@classmethod
|
|
1867
|
+
def get_component(cls, *children, **props) -> "Component":
|
|
1868
|
+
"""Get the component instance.
|
|
1869
|
+
|
|
1870
|
+
Args:
|
|
1871
|
+
children: The children of the component.
|
|
1872
|
+
props: The props of the component.
|
|
1873
|
+
|
|
1874
|
+
Raises:
|
|
1875
|
+
NotImplementedError: if the subclass does not override this method.
|
|
1876
|
+
"""
|
|
1877
|
+
raise NotImplementedError(
|
|
1878
|
+
f"{cls.__name__} must implement get_component to return the component instance."
|
|
1879
|
+
)
|
|
1880
|
+
|
|
1881
|
+
@classmethod
|
|
1882
|
+
def create(cls, *children, **props) -> "Component":
|
|
1883
|
+
"""Create a new instance of the Component.
|
|
1884
|
+
|
|
1885
|
+
Args:
|
|
1886
|
+
children: The children of the component.
|
|
1887
|
+
props: The props of the component.
|
|
1888
|
+
|
|
1889
|
+
Returns:
|
|
1890
|
+
A new instance of the Component with an independent copy of the State.
|
|
1891
|
+
"""
|
|
1892
|
+
cls._per_component_state_instance_count += 1
|
|
1893
|
+
state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}"
|
|
1894
|
+
component_state = type(state_cls_name, (cls, State), {})
|
|
1895
|
+
component = component_state.get_component(*children, **props)
|
|
1896
|
+
component.State = component_state
|
|
1897
|
+
return component
|
|
1898
|
+
|
|
1899
|
+
|
|
1806
1900
|
class StateProxy(wrapt.ObjectProxy):
|
|
1807
1901
|
"""Proxy of a state instance to control mutability of vars for a background task.
|
|
1808
1902
|
|
|
@@ -2155,6 +2249,25 @@ class StateManagerMemory(StateManager):
|
|
|
2155
2249
|
await self.set_state(token, state)
|
|
2156
2250
|
|
|
2157
2251
|
|
|
2252
|
+
# Workaround https://github.com/cloudpipe/cloudpickle/issues/408 for dynamic pydantic classes
|
|
2253
|
+
if not isinstance(State.validate.__func__, FunctionType):
|
|
2254
|
+
cython_function_or_method = type(State.validate.__func__)
|
|
2255
|
+
|
|
2256
|
+
@dill.register(cython_function_or_method)
|
|
2257
|
+
def _dill_reduce_cython_function_or_method(pickler, obj):
|
|
2258
|
+
# Ignore cython function when pickling.
|
|
2259
|
+
pass
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
@dill.register(type(State))
|
|
2263
|
+
def _dill_reduce_state(pickler, obj):
|
|
2264
|
+
if obj is not State and issubclass(obj, State):
|
|
2265
|
+
# Avoid serializing subclasses of State, instead get them by reference from the State class.
|
|
2266
|
+
pickler.save_reduce(State.get_class_substate, (obj.get_full_name(),), obj=obj)
|
|
2267
|
+
else:
|
|
2268
|
+
dill.Pickler.dispatch[type](pickler, obj)
|
|
2269
|
+
|
|
2270
|
+
|
|
2158
2271
|
class StateManagerRedis(StateManager):
|
|
2159
2272
|
"""A state manager that stores states in redis."""
|
|
2160
2273
|
|
|
@@ -2183,6 +2296,9 @@ class StateManagerRedis(StateManager):
|
|
|
2183
2296
|
b"evicted",
|
|
2184
2297
|
}
|
|
2185
2298
|
|
|
2299
|
+
# Only warn about each state class size once.
|
|
2300
|
+
_warned_about_state_size: ClassVar[Set[str]] = set()
|
|
2301
|
+
|
|
2186
2302
|
def _get_root_state(self, state: BaseState) -> BaseState:
|
|
2187
2303
|
"""Chase parent_state pointers to find an instance of the top-level state.
|
|
2188
2304
|
|
|
@@ -2294,7 +2410,7 @@ class StateManagerRedis(StateManager):
|
|
|
2294
2410
|
|
|
2295
2411
|
if redis_state is not None:
|
|
2296
2412
|
# Deserialize the substate.
|
|
2297
|
-
state =
|
|
2413
|
+
state = dill.loads(redis_state)
|
|
2298
2414
|
|
|
2299
2415
|
# Populate parent state if missing and requested.
|
|
2300
2416
|
if parent_state is None:
|
|
@@ -2334,6 +2450,29 @@ class StateManagerRedis(StateManager):
|
|
|
2334
2450
|
return self._get_root_state(state)
|
|
2335
2451
|
return state
|
|
2336
2452
|
|
|
2453
|
+
def _warn_if_too_large(
|
|
2454
|
+
self,
|
|
2455
|
+
state: BaseState,
|
|
2456
|
+
pickle_state_size: int,
|
|
2457
|
+
):
|
|
2458
|
+
"""Print a warning when the state is too large.
|
|
2459
|
+
|
|
2460
|
+
Args:
|
|
2461
|
+
state: The state to check.
|
|
2462
|
+
pickle_state_size: The size of the pickled state.
|
|
2463
|
+
"""
|
|
2464
|
+
state_full_name = state.get_full_name()
|
|
2465
|
+
if (
|
|
2466
|
+
state_full_name not in self._warned_about_state_size
|
|
2467
|
+
and pickle_state_size > TOO_LARGE_SERIALIZED_STATE
|
|
2468
|
+
and state.substates
|
|
2469
|
+
):
|
|
2470
|
+
console.warn(
|
|
2471
|
+
f"State {state_full_name} serializes to {pickle_state_size} bytes "
|
|
2472
|
+
"which may present performance issues. Consider reducing the size of this state."
|
|
2473
|
+
)
|
|
2474
|
+
self._warned_about_state_size.add(state_full_name)
|
|
2475
|
+
|
|
2337
2476
|
async def set_state(
|
|
2338
2477
|
self,
|
|
2339
2478
|
token: str,
|
|
@@ -2382,9 +2521,11 @@ class StateManagerRedis(StateManager):
|
|
|
2382
2521
|
)
|
|
2383
2522
|
# Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
|
|
2384
2523
|
if state._get_was_touched():
|
|
2524
|
+
pickle_state = dill.dumps(state, byref=True)
|
|
2525
|
+
self._warn_if_too_large(state, len(pickle_state))
|
|
2385
2526
|
await self.redis.set(
|
|
2386
2527
|
_substate_key(client_token, state),
|
|
2387
|
-
|
|
2528
|
+
pickle_state,
|
|
2388
2529
|
ex=self.token_expiration,
|
|
2389
2530
|
)
|
|
2390
2531
|
|
|
@@ -2944,8 +3085,12 @@ def reload_state_module(
|
|
|
2944
3085
|
Args:
|
|
2945
3086
|
module: The module to reload.
|
|
2946
3087
|
state: Recursive argument for the state class to reload.
|
|
3088
|
+
|
|
2947
3089
|
"""
|
|
2948
3090
|
for subclass in tuple(state.class_subclasses):
|
|
2949
3091
|
reload_state_module(module=module, state=subclass)
|
|
2950
3092
|
if subclass.__module__ == module and module is not None:
|
|
2951
3093
|
state.class_subclasses.remove(subclass)
|
|
3094
|
+
state._always_dirty_substates.discard(subclass.get_name())
|
|
3095
|
+
state._init_var_dependency_dicts()
|
|
3096
|
+
state.get_class_substate.cache_clear()
|
reflex/testing.py
CHANGED
|
@@ -40,7 +40,13 @@ import reflex.utils.build
|
|
|
40
40
|
import reflex.utils.exec
|
|
41
41
|
import reflex.utils.prerequisites
|
|
42
42
|
import reflex.utils.processes
|
|
43
|
-
from reflex.state import
|
|
43
|
+
from reflex.state import (
|
|
44
|
+
BaseState,
|
|
45
|
+
State,
|
|
46
|
+
StateManagerMemory,
|
|
47
|
+
StateManagerRedis,
|
|
48
|
+
reload_state_module,
|
|
49
|
+
)
|
|
44
50
|
|
|
45
51
|
try:
|
|
46
52
|
from selenium import webdriver # pyright: ignore [reportMissingImports]
|
|
@@ -67,16 +73,13 @@ FRONTEND_POPEN_ARGS = {}
|
|
|
67
73
|
T = TypeVar("T")
|
|
68
74
|
TimeoutType = Optional[Union[int, float]]
|
|
69
75
|
|
|
70
|
-
if platform.system == "Windows":
|
|
76
|
+
if platform.system() == "Windows":
|
|
71
77
|
FRONTEND_POPEN_ARGS["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
78
|
+
FRONTEND_POPEN_ARGS["shell"] = True
|
|
72
79
|
else:
|
|
73
80
|
FRONTEND_POPEN_ARGS["start_new_session"] = True
|
|
74
81
|
|
|
75
82
|
|
|
76
|
-
# Save a copy of internal substates to reset after each test.
|
|
77
|
-
INTERNAL_STATES = State.class_subclasses.copy()
|
|
78
|
-
|
|
79
|
-
|
|
80
83
|
# borrowed from py3.11
|
|
81
84
|
class chdir(contextlib.AbstractContextManager):
|
|
82
85
|
"""Non thread-safe context manager to change the current working directory."""
|
|
@@ -229,11 +232,6 @@ class AppHarness:
|
|
|
229
232
|
reflex.config.get_config(reload=True)
|
|
230
233
|
# Clean out any `rx.page` decorators from other tests.
|
|
231
234
|
reflex.app.DECORATED_PAGES.clear()
|
|
232
|
-
# reset rx.State subclasses
|
|
233
|
-
State.class_subclasses.clear()
|
|
234
|
-
State.class_subclasses.update(INTERNAL_STATES)
|
|
235
|
-
State._always_dirty_substates = set()
|
|
236
|
-
State.get_class_substate.cache_clear()
|
|
237
235
|
# Ensure the AppHarness test does not skip State assignment due to running via pytest
|
|
238
236
|
os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
|
|
239
237
|
self.app_module = reflex.utils.prerequisites.get_compiled_app(reload=True)
|
|
@@ -244,6 +242,10 @@ class AppHarness:
|
|
|
244
242
|
else:
|
|
245
243
|
self.state_manager = self.app_instance._state_manager
|
|
246
244
|
|
|
245
|
+
def _reload_state_module(self):
|
|
246
|
+
"""Reload the rx.State module to avoid conflict when reloading."""
|
|
247
|
+
reload_state_module(module=f"{self.app_name}.{self.app_name}")
|
|
248
|
+
|
|
247
249
|
def _get_backend_shutdown_handler(self):
|
|
248
250
|
if self.backend is None:
|
|
249
251
|
raise RuntimeError("Backend was not initialized.")
|
|
@@ -361,6 +363,8 @@ class AppHarness:
|
|
|
361
363
|
|
|
362
364
|
def stop(self) -> None:
|
|
363
365
|
"""Stop the frontend and backend servers."""
|
|
366
|
+
self._reload_state_module()
|
|
367
|
+
|
|
364
368
|
if self.backend is not None:
|
|
365
369
|
self.backend.should_exit = True
|
|
366
370
|
if self.frontend_process is not None:
|
reflex/utils/console.py
CHANGED
|
@@ -16,6 +16,9 @@ _console = Console()
|
|
|
16
16
|
# The current log level.
|
|
17
17
|
_LOG_LEVEL = LogLevel.INFO
|
|
18
18
|
|
|
19
|
+
# Deprecated features who's warning has been printed.
|
|
20
|
+
_EMITTED_DEPRECATION_WARNINGS = set()
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def set_log_level(log_level: LogLevel):
|
|
21
24
|
"""Set the log level.
|
|
@@ -111,6 +114,7 @@ def deprecate(
|
|
|
111
114
|
reason: str,
|
|
112
115
|
deprecation_version: str,
|
|
113
116
|
removal_version: str,
|
|
117
|
+
dedupe: bool = True,
|
|
114
118
|
**kwargs,
|
|
115
119
|
):
|
|
116
120
|
"""Print a deprecation warning.
|
|
@@ -119,15 +123,19 @@ def deprecate(
|
|
|
119
123
|
feature_name: The feature to deprecate.
|
|
120
124
|
reason: The reason for deprecation.
|
|
121
125
|
deprecation_version: The version the feature was deprecated
|
|
122
|
-
removal_version: The version the deprecated feature will be removed
|
|
126
|
+
removal_version: The version the deprecated feature will be removed
|
|
127
|
+
dedupe: If True, suppress multiple console logs of deprecation message.
|
|
123
128
|
kwargs: Keyword arguments to pass to the print function.
|
|
124
129
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
if feature_name not in _EMITTED_DEPRECATION_WARNINGS:
|
|
131
|
+
msg = (
|
|
132
|
+
f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely "
|
|
133
|
+
f"removed in {removal_version}"
|
|
134
|
+
)
|
|
135
|
+
if _LOG_LEVEL <= LogLevel.WARNING:
|
|
136
|
+
print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
|
|
137
|
+
if dedupe:
|
|
138
|
+
_EMITTED_DEPRECATION_WARNINGS.add(feature_name)
|
|
131
139
|
|
|
132
140
|
|
|
133
141
|
def error(msg: str, **kwargs):
|
reflex/utils/exec.py
CHANGED
|
@@ -307,3 +307,12 @@ def is_prod_mode() -> bool:
|
|
|
307
307
|
constants.Env.DEV.value,
|
|
308
308
|
)
|
|
309
309
|
return current_mode == constants.Env.PROD.value
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def should_skip_compile() -> bool:
|
|
313
|
+
"""Whether the app should skip compile.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
True if the app should skip compile.
|
|
317
|
+
"""
|
|
318
|
+
return os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes"
|