reflex 0.8.5a2__py3-none-any.whl → 0.8.6a1__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/{web/vite.config.js → jinja/web/vite.config.js.jinja2} +11 -0
- reflex/.templates/web/utils/state.js +5 -0
- reflex/__init__.py +1 -0
- reflex/__init__.pyi +2 -0
- reflex/app.py +77 -13
- reflex/compiler/templates.py +3 -0
- reflex/components/base/error_boundary.py +2 -0
- reflex/components/datadisplay/dataeditor.py +3 -0
- reflex/components/datadisplay/dataeditor.pyi +2 -0
- reflex/components/el/__init__.pyi +4 -0
- reflex/components/el/elements/__init__.py +1 -0
- reflex/components/el/elements/__init__.pyi +5 -0
- reflex/components/el/elements/media.py +32 -0
- reflex/components/el/elements/media.pyi +261 -0
- reflex/components/lucide/icon.py +4 -1
- reflex/components/lucide/icon.pyi +4 -1
- reflex/components/sonner/toast.py +1 -1
- reflex/config.py +15 -0
- reflex/constants/base.py +3 -0
- reflex/istate/manager.py +2 -1
- reflex/plugins/__init__.py +2 -0
- reflex/plugins/_screenshot.py +144 -0
- reflex/plugins/base.py +14 -1
- reflex/state.py +28 -6
- reflex/testing.py +11 -0
- reflex/utils/decorator.py +1 -0
- reflex/utils/monitoring.py +180 -0
- reflex/utils/prerequisites.py +17 -0
- reflex/utils/token_manager.py +215 -0
- {reflex-0.8.5a2.dist-info → reflex-0.8.6a1.dist-info}/METADATA +5 -2
- {reflex-0.8.5a2.dist-info → reflex-0.8.6a1.dist-info}/RECORD +34 -31
- {reflex-0.8.5a2.dist-info → reflex-0.8.6a1.dist-info}/WHEEL +0 -0
- {reflex-0.8.5a2.dist-info → reflex-0.8.6a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.5a2.dist-info → reflex-0.8.6a1.dist-info}/licenses/LICENSE +0 -0
reflex/components/lucide/icon.py
CHANGED
|
@@ -6,7 +6,7 @@ from reflex.utils.imports import ImportVar
|
|
|
6
6
|
from reflex.vars.base import LiteralVar, Var
|
|
7
7
|
from reflex.vars.sequence import LiteralStringVar, StringVar
|
|
8
8
|
|
|
9
|
-
LUCIDE_LIBRARY = "lucide-react@0.
|
|
9
|
+
LUCIDE_LIBRARY = "lucide-react@0.536.0"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class LucideIconComponent(Component):
|
|
@@ -93,6 +93,7 @@ class DynamicIcon(LucideIconComponent):
|
|
|
93
93
|
tag = "DynamicIcon"
|
|
94
94
|
|
|
95
95
|
name: Var[str]
|
|
96
|
+
size: Var[int]
|
|
96
97
|
|
|
97
98
|
def _get_imports(self):
|
|
98
99
|
_imports = super()._get_imports()
|
|
@@ -874,6 +875,7 @@ LUCIDE_ICON_LIST = [
|
|
|
874
875
|
"hamburger",
|
|
875
876
|
"hammer",
|
|
876
877
|
"hand_coins",
|
|
878
|
+
"hand_fist",
|
|
877
879
|
"hand_heart",
|
|
878
880
|
"hand_helping",
|
|
879
881
|
"hand_metal",
|
|
@@ -1644,6 +1646,7 @@ LUCIDE_ICON_LIST = [
|
|
|
1644
1646
|
"truck_electric",
|
|
1645
1647
|
"truck",
|
|
1646
1648
|
"turkish_lira",
|
|
1649
|
+
"turntable",
|
|
1647
1650
|
"turtle",
|
|
1648
1651
|
"tv_2",
|
|
1649
1652
|
"tv_minimal_play",
|
|
@@ -11,7 +11,7 @@ from reflex.components.core.breakpoints import Breakpoints
|
|
|
11
11
|
from reflex.event import EventType, PointerEventInfo
|
|
12
12
|
from reflex.vars.base import Var
|
|
13
13
|
|
|
14
|
-
LUCIDE_LIBRARY = "lucide-react@0.
|
|
14
|
+
LUCIDE_LIBRARY = "lucide-react@0.536.0"
|
|
15
15
|
|
|
16
16
|
class LucideIconComponent(Component):
|
|
17
17
|
@classmethod
|
|
@@ -125,6 +125,7 @@ class DynamicIcon(LucideIconComponent):
|
|
|
125
125
|
cls,
|
|
126
126
|
*children,
|
|
127
127
|
name: Var[str] | str | None = None,
|
|
128
|
+
size: Var[int] | int | None = None,
|
|
128
129
|
style: Sequence[Mapping[str, Any]]
|
|
129
130
|
| Mapping[str, Any]
|
|
130
131
|
| Var[Mapping[str, Any]]
|
|
@@ -939,6 +940,7 @@ LUCIDE_ICON_LIST = [
|
|
|
939
940
|
"hamburger",
|
|
940
941
|
"hammer",
|
|
941
942
|
"hand_coins",
|
|
943
|
+
"hand_fist",
|
|
942
944
|
"hand_heart",
|
|
943
945
|
"hand_helping",
|
|
944
946
|
"hand_metal",
|
|
@@ -1709,6 +1711,7 @@ LUCIDE_ICON_LIST = [
|
|
|
1709
1711
|
"truck_electric",
|
|
1710
1712
|
"truck",
|
|
1711
1713
|
"turkish_lira",
|
|
1714
|
+
"turntable",
|
|
1712
1715
|
"turtle",
|
|
1713
1716
|
"tv_2",
|
|
1714
1717
|
"tv_minimal_play",
|
reflex/config.py
CHANGED
|
@@ -29,6 +29,9 @@ from reflex.plugins.sitemap import SitemapPlugin
|
|
|
29
29
|
from reflex.utils import console
|
|
30
30
|
from reflex.utils.exceptions import ConfigError
|
|
31
31
|
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from pyleak.base import LeakAction
|
|
34
|
+
|
|
32
35
|
|
|
33
36
|
@dataclasses.dataclass(kw_only=True)
|
|
34
37
|
class DBConfig:
|
|
@@ -186,6 +189,18 @@ class BaseConfig:
|
|
|
186
189
|
# Telemetry opt-in.
|
|
187
190
|
telemetry_enabled: bool = True
|
|
188
191
|
|
|
192
|
+
# PyLeak monitoring configuration for detecting event loop blocking and resource leaks.
|
|
193
|
+
enable_pyleak_monitoring: bool = False
|
|
194
|
+
|
|
195
|
+
# Threshold in seconds for detecting event loop blocking operations.
|
|
196
|
+
pyleak_blocking_threshold: float = 0.1
|
|
197
|
+
|
|
198
|
+
# Grace period in seconds for thread leak detection cleanup.
|
|
199
|
+
pyleak_thread_grace_period: float = 0.2
|
|
200
|
+
|
|
201
|
+
# Action to take when PyLeak detects issues
|
|
202
|
+
pyleak_action: "LeakAction | None" = None
|
|
203
|
+
|
|
189
204
|
# The bun path
|
|
190
205
|
bun_path: ExistingPath = constants.Bun.DEFAULT_PATH
|
|
191
206
|
|
reflex/constants/base.py
CHANGED
|
@@ -160,6 +160,9 @@ class ReactRouter(Javascript):
|
|
|
160
160
|
# The react router config file
|
|
161
161
|
CONFIG_FILE = "react-router.config.js"
|
|
162
162
|
|
|
163
|
+
# The associated Vite config file
|
|
164
|
+
VITE_CONFIG_FILE = "vite.config.js"
|
|
165
|
+
|
|
163
166
|
# Regex to check for message displayed when frontend comes up
|
|
164
167
|
DEV_FRONTEND_LISTENING_REGEX = r"Local:[\s]+"
|
|
165
168
|
|
reflex/istate/manager.py
CHANGED
|
@@ -143,6 +143,8 @@ class StateManagerMemory(StateManager):
|
|
|
143
143
|
token: The token to set the state for.
|
|
144
144
|
state: The state to set.
|
|
145
145
|
"""
|
|
146
|
+
token = _split_substate_key(token)[0]
|
|
147
|
+
self.states[token] = state
|
|
146
148
|
|
|
147
149
|
@override
|
|
148
150
|
@contextlib.asynccontextmanager
|
|
@@ -165,7 +167,6 @@ class StateManagerMemory(StateManager):
|
|
|
165
167
|
async with self._states_locks[token]:
|
|
166
168
|
state = await self.get_state(token)
|
|
167
169
|
yield state
|
|
168
|
-
await self.set_state(token, state)
|
|
169
170
|
|
|
170
171
|
|
|
171
172
|
def _default_token_expiration() -> int:
|
reflex/plugins/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Reflex Plugin System."""
|
|
2
2
|
|
|
3
|
+
from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin
|
|
3
4
|
from .base import CommonContext, Plugin, PreCompileContext
|
|
4
5
|
from .sitemap import SitemapPlugin
|
|
5
6
|
from .tailwind_v3 import TailwindV3Plugin
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"SitemapPlugin",
|
|
13
14
|
"TailwindV3Plugin",
|
|
14
15
|
"TailwindV4Plugin",
|
|
16
|
+
"_ScreenshotPlugin",
|
|
15
17
|
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Plugin to enable screenshot functionality."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from reflex.plugins.base import Plugin as BasePlugin
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
from starlette.responses import Response
|
|
10
|
+
from typing_extensions import Unpack
|
|
11
|
+
|
|
12
|
+
from reflex.app import App
|
|
13
|
+
from reflex.plugins.base import PostCompileContext
|
|
14
|
+
from reflex.state import BaseState
|
|
15
|
+
|
|
16
|
+
ACTIVE_CONNECTIONS = "/_active_connections"
|
|
17
|
+
CLONE_STATE = "/_clone_state"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _deep_copy(state: "BaseState") -> "BaseState":
|
|
21
|
+
"""Create a deep copy of the state.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
state: The state to copy.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A deep copy of the state.
|
|
28
|
+
"""
|
|
29
|
+
import copy
|
|
30
|
+
|
|
31
|
+
copy_of_state = copy.deepcopy(state)
|
|
32
|
+
|
|
33
|
+
def copy_substate(substate: "BaseState") -> "BaseState":
|
|
34
|
+
substate_copy = _deep_copy(substate)
|
|
35
|
+
|
|
36
|
+
substate_copy.parent_state = copy_of_state
|
|
37
|
+
|
|
38
|
+
return substate_copy
|
|
39
|
+
|
|
40
|
+
copy_of_state.substates = {
|
|
41
|
+
substate_name: copy_substate(substate)
|
|
42
|
+
for substate_name, substate in state.substates.items()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return copy_of_state
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ScreenshotPlugin(BasePlugin):
|
|
49
|
+
"""Plugin to handle screenshot functionality."""
|
|
50
|
+
|
|
51
|
+
def post_compile(self, **context: "Unpack[PostCompileContext]") -> None:
|
|
52
|
+
"""Called after the compilation of the plugin.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
context: The context for the plugin.
|
|
56
|
+
"""
|
|
57
|
+
app = context["app"]
|
|
58
|
+
self._add_active_connections_endpoint(app)
|
|
59
|
+
self._add_clone_state_endpoint(app)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _add_active_connections_endpoint(app: "App") -> None:
|
|
63
|
+
"""Add an endpoint to the app that returns the active connections.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
app: The application instance to which the endpoint will be added.
|
|
67
|
+
"""
|
|
68
|
+
if not app._api:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
async def active_connections(_request: "Request") -> "Response":
|
|
72
|
+
from starlette.responses import JSONResponse
|
|
73
|
+
|
|
74
|
+
if not app.event_namespace:
|
|
75
|
+
return JSONResponse({})
|
|
76
|
+
|
|
77
|
+
return JSONResponse(app.event_namespace.token_to_sid)
|
|
78
|
+
|
|
79
|
+
app._api.add_route(
|
|
80
|
+
ACTIVE_CONNECTIONS,
|
|
81
|
+
active_connections,
|
|
82
|
+
methods=["GET"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _add_clone_state_endpoint(app: "App") -> None:
|
|
87
|
+
"""Add an endpoint to the app that clones the current state.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
app: The application instance to which the endpoint will be added.
|
|
91
|
+
"""
|
|
92
|
+
if not app._api:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
async def clone_state(request: "Request") -> "Response":
|
|
96
|
+
import uuid
|
|
97
|
+
|
|
98
|
+
from starlette.responses import JSONResponse
|
|
99
|
+
|
|
100
|
+
from reflex.state import _substate_key
|
|
101
|
+
|
|
102
|
+
if not app.event_namespace:
|
|
103
|
+
return JSONResponse({})
|
|
104
|
+
|
|
105
|
+
token_to_clone = await request.json()
|
|
106
|
+
|
|
107
|
+
if not isinstance(token_to_clone, str):
|
|
108
|
+
return JSONResponse(
|
|
109
|
+
{"error": "Token to clone must be a string."}, status_code=400
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
old_state = await app.state_manager.get_state(token_to_clone)
|
|
113
|
+
|
|
114
|
+
new_state = _deep_copy(old_state)
|
|
115
|
+
|
|
116
|
+
new_token = uuid.uuid4().hex
|
|
117
|
+
|
|
118
|
+
all_states = [new_state]
|
|
119
|
+
|
|
120
|
+
found_new = True
|
|
121
|
+
|
|
122
|
+
while found_new:
|
|
123
|
+
found_new = False
|
|
124
|
+
|
|
125
|
+
for state in all_states:
|
|
126
|
+
for substate in state.substates.values():
|
|
127
|
+
substate._was_touched = True
|
|
128
|
+
|
|
129
|
+
if substate not in all_states:
|
|
130
|
+
all_states.append(substate)
|
|
131
|
+
|
|
132
|
+
found_new = True
|
|
133
|
+
|
|
134
|
+
await app.state_manager.set_state(
|
|
135
|
+
_substate_key(new_token, new_state), new_state
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return JSONResponse(new_token)
|
|
139
|
+
|
|
140
|
+
app._api.add_route(
|
|
141
|
+
CLONE_STATE,
|
|
142
|
+
clone_state,
|
|
143
|
+
methods=["POST"],
|
|
144
|
+
)
|
reflex/plugins/base.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict
|
|
|
7
7
|
from typing_extensions import Unpack
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from reflex.app import UnevaluatedPage
|
|
10
|
+
from reflex.app import App, UnevaluatedPage
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CommonContext(TypedDict):
|
|
@@ -44,6 +44,12 @@ class PreCompileContext(CommonContext):
|
|
|
44
44
|
unevaluated_pages: Sequence["UnevaluatedPage"]
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class PostCompileContext(CommonContext):
|
|
48
|
+
"""Context for post-compile hooks."""
|
|
49
|
+
|
|
50
|
+
app: "App"
|
|
51
|
+
|
|
52
|
+
|
|
47
53
|
class Plugin:
|
|
48
54
|
"""Base class for all plugins."""
|
|
49
55
|
|
|
@@ -104,6 +110,13 @@ class Plugin:
|
|
|
104
110
|
context: The context for the plugin.
|
|
105
111
|
"""
|
|
106
112
|
|
|
113
|
+
def post_compile(self, **context: Unpack[PostCompileContext]) -> None:
|
|
114
|
+
"""Called after the compilation of the plugin.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
context: The context for the plugin.
|
|
118
|
+
"""
|
|
119
|
+
|
|
107
120
|
def __repr__(self):
|
|
108
121
|
"""Return a string representation of the plugin.
|
|
109
122
|
|
reflex/state.py
CHANGED
|
@@ -60,6 +60,7 @@ from reflex.utils.exceptions import (
|
|
|
60
60
|
)
|
|
61
61
|
from reflex.utils.exceptions import ImmutableStateError as ImmutableStateError
|
|
62
62
|
from reflex.utils.exec import is_testing_env
|
|
63
|
+
from reflex.utils.monitoring import is_pyleak_enabled, monitor_loopblocks
|
|
63
64
|
from reflex.utils.types import _isinstance, is_union, value_inside_optional
|
|
64
65
|
from reflex.vars import Field, VarData, field
|
|
65
66
|
from reflex.vars.base import (
|
|
@@ -508,7 +509,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
508
509
|
|
|
509
510
|
new_backend_vars = {
|
|
510
511
|
name: value
|
|
511
|
-
for name, value in cls.__dict__.items()
|
|
512
|
+
for name, value in list(cls.__dict__.items())
|
|
512
513
|
if types.is_backend_base_variable(name, cls)
|
|
513
514
|
}
|
|
514
515
|
# Add annotated backend vars that may not have a default value.
|
|
@@ -1784,7 +1785,11 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1784
1785
|
from reflex.utils import telemetry
|
|
1785
1786
|
|
|
1786
1787
|
# Get the function to process the event.
|
|
1787
|
-
|
|
1788
|
+
if is_pyleak_enabled():
|
|
1789
|
+
console.debug(f"Monitoring leaks for handler: {handler.fn.__qualname__}")
|
|
1790
|
+
fn = functools.partial(monitor_loopblocks(handler.fn), state)
|
|
1791
|
+
else:
|
|
1792
|
+
fn = functools.partial(handler.fn, state)
|
|
1788
1793
|
|
|
1789
1794
|
try:
|
|
1790
1795
|
type_hints = typing.get_type_hints(handler.fn)
|
|
@@ -2463,16 +2468,30 @@ class OnLoadInternalState(State):
|
|
|
2463
2468
|
This is a separate substate to avoid deserializing the entire state tree for every page navigation.
|
|
2464
2469
|
"""
|
|
2465
2470
|
|
|
2471
|
+
# Cannot properly annotate this as `App` due to circular import issues.
|
|
2472
|
+
_app_ref: ClassVar[Any] = None
|
|
2473
|
+
|
|
2466
2474
|
def on_load_internal(self) -> list[Event | EventSpec | event.EventCallback] | None:
|
|
2467
2475
|
"""Queue on_load handlers for the current page.
|
|
2468
2476
|
|
|
2469
2477
|
Returns:
|
|
2470
2478
|
The list of events to queue for on load handling.
|
|
2479
|
+
|
|
2480
|
+
Raises:
|
|
2481
|
+
TypeError: If the app reference is not of type App.
|
|
2471
2482
|
"""
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
)
|
|
2483
|
+
from reflex.app import App
|
|
2484
|
+
|
|
2485
|
+
app = type(self)._app_ref or prerequisites.get_and_validate_app().app
|
|
2486
|
+
if not isinstance(app, App):
|
|
2487
|
+
msg = (
|
|
2488
|
+
f"Expected app to be of type {App.__name__}, got {type(app).__name__}."
|
|
2489
|
+
)
|
|
2490
|
+
raise TypeError(msg)
|
|
2491
|
+
# Cache the app reference for subsequent calls.
|
|
2492
|
+
if type(self)._app_ref is None:
|
|
2493
|
+
type(self)._app_ref = app
|
|
2494
|
+
load_events = app.get_load_events(self.router._page.path)
|
|
2476
2495
|
if not load_events:
|
|
2477
2496
|
self.is_hydrated = True
|
|
2478
2497
|
return None # Fast path for navigation with no on_load events defined.
|
|
@@ -2646,6 +2665,9 @@ def reload_state_module(
|
|
|
2646
2665
|
state: Recursive argument for the state class to reload.
|
|
2647
2666
|
|
|
2648
2667
|
"""
|
|
2668
|
+
# Reset the _app_ref of OnLoadInternalState to avoid stale references.
|
|
2669
|
+
if state is OnLoadInternalState:
|
|
2670
|
+
state._app_ref = None
|
|
2649
2671
|
# Clean out all potentially dirty states of reloaded modules.
|
|
2650
2672
|
for pd_state in tuple(state._potentially_dirty_states):
|
|
2651
2673
|
with contextlib.suppress(ValueError):
|
reflex/testing.py
CHANGED
|
@@ -376,6 +376,17 @@ class AppHarness:
|
|
|
376
376
|
msg = "Failed to reset state manager."
|
|
377
377
|
raise RuntimeError(msg)
|
|
378
378
|
|
|
379
|
+
# Also reset the TokenManager to avoid loop affinity issues
|
|
380
|
+
if (
|
|
381
|
+
hasattr(self.app_instance, "event_namespace")
|
|
382
|
+
and self.app_instance.event_namespace is not None
|
|
383
|
+
and hasattr(self.app_instance.event_namespace, "_token_manager")
|
|
384
|
+
):
|
|
385
|
+
# Import here to avoid circular imports
|
|
386
|
+
from reflex.utils.token_manager import TokenManager
|
|
387
|
+
|
|
388
|
+
self.app_instance.event_namespace._token_manager = TokenManager.create()
|
|
389
|
+
|
|
379
390
|
def _start_frontend(self):
|
|
380
391
|
# Set up the frontend.
|
|
381
392
|
with chdir(self.app_path):
|
reflex/utils/decorator.py
CHANGED
|
@@ -76,6 +76,7 @@ def debug(f: Callable[P, T]) -> Callable[P, T]:
|
|
|
76
76
|
def _write_cached_procedure_file(payload: str, cache_file: Path, value: object):
|
|
77
77
|
import pickle
|
|
78
78
|
|
|
79
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
79
80
|
cache_file.write_bytes(pickle.dumps((payload, value)))
|
|
80
81
|
|
|
81
82
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""PyLeak integration for monitoring event loop blocking and resource leaks in Reflex applications."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import threading
|
|
8
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
|
9
|
+
from typing import TypeVar, overload
|
|
10
|
+
|
|
11
|
+
from reflex.config import get_config
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from pyleak import no_event_loop_blocking, no_task_leaks, no_thread_leaks
|
|
15
|
+
from pyleak.base import LeakAction
|
|
16
|
+
|
|
17
|
+
PYLEAK_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
PYLEAK_AVAILABLE = False
|
|
20
|
+
no_event_loop_blocking = no_task_leaks = no_thread_leaks = None # pyright: ignore[reportAssignmentType]
|
|
21
|
+
LeakAction = None # pyright: ignore[reportAssignmentType]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Thread-local storage to track if monitoring is already active
|
|
25
|
+
_thread_local = threading.local()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_pyleak_enabled() -> bool:
|
|
29
|
+
"""Check if PyLeak monitoring is enabled and available.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if PyLeak monitoring is enabled in config and PyLeak is available.
|
|
33
|
+
"""
|
|
34
|
+
if not PYLEAK_AVAILABLE:
|
|
35
|
+
return False
|
|
36
|
+
config = get_config()
|
|
37
|
+
return config.enable_pyleak_monitoring
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextlib.contextmanager
|
|
41
|
+
def monitor_sync():
|
|
42
|
+
"""Sync context manager for PyLeak monitoring.
|
|
43
|
+
|
|
44
|
+
Yields:
|
|
45
|
+
None: Context for monitoring sync operations.
|
|
46
|
+
"""
|
|
47
|
+
if not is_pyleak_enabled():
|
|
48
|
+
yield
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Check if monitoring is already active in this thread
|
|
52
|
+
if getattr(_thread_local, "monitoring_active", False):
|
|
53
|
+
yield
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
config = get_config()
|
|
57
|
+
action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess]
|
|
58
|
+
|
|
59
|
+
# Mark monitoring as active
|
|
60
|
+
_thread_local.monitoring_active = True
|
|
61
|
+
try:
|
|
62
|
+
with contextlib.ExitStack() as stack:
|
|
63
|
+
# Thread leak detection has issues with background tasks (no_thread_leaks)
|
|
64
|
+
stack.enter_context(
|
|
65
|
+
no_event_loop_blocking( # pyright: ignore[reportOptionalCall]
|
|
66
|
+
action=action,
|
|
67
|
+
threshold=config.pyleak_blocking_threshold,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
yield
|
|
71
|
+
finally:
|
|
72
|
+
_thread_local.monitoring_active = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@contextlib.asynccontextmanager
|
|
76
|
+
async def monitor_async():
|
|
77
|
+
"""Async context manager for PyLeak monitoring.
|
|
78
|
+
|
|
79
|
+
Yields:
|
|
80
|
+
None: Context for monitoring async operations.
|
|
81
|
+
"""
|
|
82
|
+
if not is_pyleak_enabled():
|
|
83
|
+
yield
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Check if monitoring is already active in this thread
|
|
87
|
+
if getattr(_thread_local, "monitoring_active", False):
|
|
88
|
+
yield
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
config = get_config()
|
|
92
|
+
action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess]
|
|
93
|
+
|
|
94
|
+
# Mark monitoring as active
|
|
95
|
+
_thread_local.monitoring_active = True
|
|
96
|
+
try:
|
|
97
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
98
|
+
# Thread leak detection has issues with background tasks (no_thread_leaks)
|
|
99
|
+
# Re-add thread leak later.
|
|
100
|
+
|
|
101
|
+
# Block detection for event loops
|
|
102
|
+
stack.enter_context(
|
|
103
|
+
no_event_loop_blocking( # pyright: ignore[reportOptionalCall]
|
|
104
|
+
action=action,
|
|
105
|
+
threshold=config.pyleak_blocking_threshold,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
# Task leak detection has issues with background tasks (no_task_leaks)
|
|
109
|
+
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
_thread_local.monitoring_active = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
YieldType = TypeVar("YieldType")
|
|
116
|
+
SendType = TypeVar("SendType")
|
|
117
|
+
ReturnType = TypeVar("ReturnType")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
def monitor_loopblocks(
|
|
122
|
+
func: Callable[..., AsyncGenerator[YieldType, ReturnType]],
|
|
123
|
+
) -> Callable[..., AsyncGenerator[YieldType, ReturnType]]: ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@overload
|
|
127
|
+
def monitor_loopblocks(
|
|
128
|
+
func: Callable[..., Generator[YieldType, SendType, ReturnType]],
|
|
129
|
+
) -> Callable[..., Generator[YieldType, SendType, ReturnType]]: ...
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@overload
|
|
133
|
+
def monitor_loopblocks(
|
|
134
|
+
func: Callable[..., Awaitable[ReturnType]],
|
|
135
|
+
) -> Callable[..., Awaitable[ReturnType]]: ...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def monitor_loopblocks(func: Callable) -> Callable:
|
|
139
|
+
"""Framework decorator using the monitoring module's context manager.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
func: The function to be monitored for leaks.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Decorator function that applies PyLeak monitoring to sync/async functions.
|
|
146
|
+
"""
|
|
147
|
+
if inspect.isasyncgenfunction(func):
|
|
148
|
+
|
|
149
|
+
@functools.wraps(func)
|
|
150
|
+
async def async_gen_wrapper(*args, **kwargs):
|
|
151
|
+
async with monitor_async():
|
|
152
|
+
async for item in func(*args, **kwargs):
|
|
153
|
+
yield item
|
|
154
|
+
|
|
155
|
+
return async_gen_wrapper
|
|
156
|
+
|
|
157
|
+
if asyncio.iscoroutinefunction(func):
|
|
158
|
+
|
|
159
|
+
@functools.wraps(func)
|
|
160
|
+
async def async_wrapper(*args, **kwargs):
|
|
161
|
+
async with monitor_async():
|
|
162
|
+
return await func(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
return async_wrapper
|
|
165
|
+
|
|
166
|
+
if inspect.isgeneratorfunction(func):
|
|
167
|
+
|
|
168
|
+
@functools.wraps(func)
|
|
169
|
+
def gen_wrapper(*args, **kwargs):
|
|
170
|
+
with monitor_sync():
|
|
171
|
+
yield from func(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
return gen_wrapper
|
|
174
|
+
|
|
175
|
+
@functools.wraps(func)
|
|
176
|
+
def sync_wrapper(*args, **kwargs):
|
|
177
|
+
with monitor_sync():
|
|
178
|
+
return func(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
return sync_wrapper # pyright: ignore[reportReturnType]
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -972,6 +972,9 @@ def initialize_web_directory():
|
|
|
972
972
|
console.debug("Initializing the react-router.config.js file.")
|
|
973
973
|
update_react_router_config()
|
|
974
974
|
|
|
975
|
+
console.debug("Initializing the vite.config.js file.")
|
|
976
|
+
initialize_vite_config()
|
|
977
|
+
|
|
975
978
|
console.debug("Initializing the reflex.json file.")
|
|
976
979
|
# Initialize the reflex json file.
|
|
977
980
|
init_reflex_json(project_hash=project_hash)
|
|
@@ -996,6 +999,20 @@ def initialize_package_json():
|
|
|
996
999
|
output_path.write_text(_compile_package_json())
|
|
997
1000
|
|
|
998
1001
|
|
|
1002
|
+
def _compile_vite_config(config: Config):
|
|
1003
|
+
# base must have exactly one trailing slash
|
|
1004
|
+
base = "/"
|
|
1005
|
+
if frontend_path := config.frontend_path.strip("/"):
|
|
1006
|
+
base += frontend_path + "/"
|
|
1007
|
+
return templates.VITE_CONFIG.render(base=base)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def initialize_vite_config():
|
|
1011
|
+
"""Render and write in .web the vite.config.js file using Reflex config."""
|
|
1012
|
+
vite_config_file_path = get_web_dir() / constants.ReactRouter.VITE_CONFIG_FILE
|
|
1013
|
+
vite_config_file_path.write_text(_compile_vite_config(get_config()))
|
|
1014
|
+
|
|
1015
|
+
|
|
999
1016
|
def initialize_bun_config():
|
|
1000
1017
|
"""Initialize the bun config file."""
|
|
1001
1018
|
bun_config_path = get_web_dir() / constants.Bun.CONFIG_PATH
|