reflex 0.8.14a2__py3-none-any.whl → 0.8.15__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.

Files changed (52) hide show
  1. reflex/.templates/web/utils/state.js +68 -8
  2. reflex/__init__.py +12 -7
  3. reflex/__init__.pyi +11 -3
  4. reflex/app.py +10 -7
  5. reflex/base.py +58 -33
  6. reflex/components/datadisplay/dataeditor.py +17 -2
  7. reflex/components/datadisplay/dataeditor.pyi +6 -2
  8. reflex/components/field.py +3 -1
  9. reflex/components/lucide/icon.py +2 -1
  10. reflex/components/lucide/icon.pyi +2 -1
  11. reflex/components/markdown/markdown.py +101 -27
  12. reflex/components/sonner/toast.py +3 -2
  13. reflex/components/sonner/toast.pyi +3 -2
  14. reflex/constants/base.py +5 -0
  15. reflex/constants/installer.py +3 -3
  16. reflex/environment.py +9 -1
  17. reflex/event.py +3 -0
  18. reflex/experimental/client_state.py +1 -1
  19. reflex/istate/manager/__init__.py +120 -0
  20. reflex/istate/manager/disk.py +210 -0
  21. reflex/istate/manager/memory.py +76 -0
  22. reflex/istate/{manager.py → manager/redis.py} +5 -372
  23. reflex/istate/proxy.py +35 -24
  24. reflex/model.py +534 -511
  25. reflex/plugins/tailwind_v4.py +2 -2
  26. reflex/reflex.py +16 -10
  27. reflex/state.py +35 -34
  28. reflex/testing.py +12 -14
  29. reflex/utils/build.py +11 -1
  30. reflex/utils/codespaces.py +30 -1
  31. reflex/utils/compat.py +51 -48
  32. reflex/utils/misc.py +2 -1
  33. reflex/utils/monitoring.py +1 -2
  34. reflex/utils/prerequisites.py +19 -4
  35. reflex/utils/processes.py +3 -1
  36. reflex/utils/redir.py +21 -37
  37. reflex/utils/serializers.py +21 -20
  38. reflex/utils/telemetry.py +0 -2
  39. reflex/utils/templates.py +4 -4
  40. reflex/utils/types.py +89 -90
  41. reflex/vars/base.py +108 -41
  42. reflex/vars/color.py +28 -8
  43. reflex/vars/datetime.py +6 -2
  44. reflex/vars/dep_tracking.py +2 -2
  45. reflex/vars/number.py +26 -0
  46. reflex/vars/object.py +51 -7
  47. reflex/vars/sequence.py +32 -1
  48. {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/METADATA +8 -3
  49. {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/RECORD +52 -49
  50. {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/WHEEL +0 -0
  51. {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/entry_points.txt +0 -0
  52. {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/licenses/LICENSE +0 -0
@@ -2,9 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import dataclasses
5
6
  from typing import Any, Literal
6
7
 
7
- from reflex.base import Base
8
8
  from reflex.components.component import Component, ComponentNamespace
9
9
  from reflex.components.lucide.icon import Icon
10
10
  from reflex.components.props import NoExtrasAllowedProps
@@ -35,7 +35,8 @@ toast_ref = Var(
35
35
  )
36
36
 
37
37
 
38
- class ToastAction(Base):
38
+ @dataclasses.dataclass
39
+ class ToastAction:
39
40
  """A toast action that render a button in the toast."""
40
41
 
41
42
  label: str
@@ -3,10 +3,10 @@
3
3
  # ------------------- DO NOT EDIT ----------------------
4
4
  # This file was generated by `reflex/utils/pyi_generator.py`!
5
5
  # ------------------------------------------------------
6
+ import dataclasses
6
7
  from collections.abc import Mapping, Sequence
7
8
  from typing import Any, Literal
8
9
 
9
- from reflex.base import Base
10
10
  from reflex.components.component import Component, ComponentNamespace
11
11
  from reflex.components.core.breakpoints import Breakpoints
12
12
  from reflex.components.lucide.icon import Icon
@@ -32,7 +32,8 @@ toast_ref = Var(
32
32
  _var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}),
33
33
  )
34
34
 
35
- class ToastAction(Base):
35
+ @dataclasses.dataclass
36
+ class ToastAction:
36
37
  label: str
37
38
  on_click: Any
38
39
 
reflex/constants/base.py CHANGED
@@ -134,6 +134,11 @@ class Templates(SimpleNamespace):
134
134
  # The reflex.build frontend host
135
135
  REFLEX_BUILD_FRONTEND = "https://build.reflex.dev"
136
136
 
137
+ # The reflex.build frontend with referrer
138
+ REFLEX_BUILD_FRONTEND_WITH_REFERRER = (
139
+ f"{REFLEX_BUILD_FRONTEND}/?utm_source=reflex_cli"
140
+ )
141
+
137
142
  class Dirs(SimpleNamespace):
138
143
  """Folders used by the template system of Reflex."""
139
144
 
@@ -87,7 +87,7 @@ def _determine_react_router_version() -> str:
87
87
 
88
88
 
89
89
  def _determine_react_version() -> str:
90
- default_version = "19.1.1"
90
+ default_version = "19.2.0"
91
91
  if (version := os.getenv("REACT_VERSION")) and version != default_version:
92
92
  from reflex.utils import console
93
93
 
@@ -143,11 +143,11 @@ class PackageJson(SimpleNamespace):
143
143
  "postcss-import": "16.1.1",
144
144
  "@react-router/dev": _react_router_version,
145
145
  "@react-router/fs-routes": _react_router_version,
146
- "vite": "npm:rolldown-vite@7.1.13",
146
+ "vite": "npm:rolldown-vite@7.1.16",
147
147
  }
148
148
  OVERRIDES = {
149
149
  # This should always match the `react` version in DEPENDENCIES for recharts compatibility.
150
150
  "react-is": _react_version,
151
151
  "cookie": "1.0.2",
152
- "vite": "npm:rolldown-vite@7.1.13",
152
+ "vite": "npm:rolldown-vite@7.1.16",
153
153
  }
reflex/environment.py CHANGED
@@ -24,6 +24,7 @@ from typing import (
24
24
  )
25
25
 
26
26
  from reflex import constants
27
+ from reflex.constants.base import LogLevel
27
28
  from reflex.plugins import Plugin
28
29
  from reflex.utils.exceptions import EnvironmentVarValueError
29
30
  from reflex.utils.types import GenericType, is_union, value_inside_optional
@@ -204,7 +205,8 @@ def interpret_env_var_value(
204
205
  The interpreted value.
205
206
 
206
207
  Raises:
207
- ValueError: If the environment variable type is invalid.
208
+ ValueError: If the value is invalid.
209
+ EnvironmentVarValueError: If the value is invalid for the specific type.
208
210
  """
209
211
  field_type = value_inside_optional(field_type)
210
212
 
@@ -218,6 +220,12 @@ def interpret_env_var_value(
218
220
  return interpret_boolean_env(value, field_name)
219
221
  if field_type is str:
220
222
  return value
223
+ if field_type is LogLevel:
224
+ loglevel = LogLevel.from_string(value)
225
+ if loglevel is None:
226
+ msg = f"Invalid log level value: {value} for {field_name}"
227
+ raise EnvironmentVarValueError(msg)
228
+ return loglevel
221
229
  if field_type is int:
222
230
  return interpret_int_env(value, field_name)
223
231
  if field_type is Path:
reflex/event.py CHANGED
@@ -1866,6 +1866,9 @@ def fix_events(
1866
1866
  # Fix the events created by the handler.
1867
1867
  out = []
1868
1868
  for e in events:
1869
+ if callable(e) and getattr(e, "__name__", "") == "<lambda>":
1870
+ # A lambda was returned, assume the user wants to call it with no args.
1871
+ e = e()
1869
1872
  if isinstance(e, Event):
1870
1873
  # If the event is already an event, append it to the list.
1871
1874
  out.append(e)
@@ -236,7 +236,7 @@ class ClientStateVar(Var):
236
236
 
237
237
  setter = ArgsFunctionOperationBuilder.create(
238
238
  # remove patterns of ["*"] from the value_str using regex
239
- args_names=(re.sub(r"\[\".*\"\]", "", value_str),)
239
+ args_names=(re.sub(r"(\?\.)?\[\".*\"\]", "", value_str),)
240
240
  if value_str.startswith("_")
241
241
  else (),
242
242
  return_expr=setter.call(value_var),
@@ -0,0 +1,120 @@
1
+ """State manager for managing client states."""
2
+
3
+ import contextlib
4
+ import dataclasses
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import AsyncIterator
7
+
8
+ from reflex import constants
9
+ from reflex.config import get_config
10
+ from reflex.state import BaseState
11
+ from reflex.utils import console, prerequisites
12
+ from reflex.utils.exceptions import InvalidStateManagerModeError
13
+
14
+
15
+ @dataclasses.dataclass
16
+ class StateManager(ABC):
17
+ """A class to manage many client states."""
18
+
19
+ # The state class to use.
20
+ state: type[BaseState]
21
+
22
+ @classmethod
23
+ def create(cls, state: type[BaseState]):
24
+ """Create a new state manager.
25
+
26
+ Args:
27
+ state: The state class to use.
28
+
29
+ Raises:
30
+ InvalidStateManagerModeError: If the state manager mode is invalid.
31
+
32
+ Returns:
33
+ The state manager (either disk, memory or redis).
34
+ """
35
+ config = get_config()
36
+ if prerequisites.parse_redis_url() is not None:
37
+ config.state_manager_mode = constants.StateManagerMode.REDIS
38
+ if config.state_manager_mode == constants.StateManagerMode.MEMORY:
39
+ from reflex.istate.manager.memory import StateManagerMemory
40
+
41
+ return StateManagerMemory(state=state)
42
+ if config.state_manager_mode == constants.StateManagerMode.DISK:
43
+ from reflex.istate.manager.disk import StateManagerDisk
44
+
45
+ return StateManagerDisk(state=state)
46
+ if config.state_manager_mode == constants.StateManagerMode.REDIS:
47
+ redis = prerequisites.get_redis()
48
+ if redis is not None:
49
+ from reflex.istate.manager.redis import StateManagerRedis
50
+
51
+ # make sure expiration values are obtained only from the config object on creation
52
+ return StateManagerRedis(
53
+ state=state,
54
+ redis=redis,
55
+ token_expiration=config.redis_token_expiration,
56
+ lock_expiration=config.redis_lock_expiration,
57
+ lock_warning_threshold=config.redis_lock_warning_threshold,
58
+ )
59
+ msg = f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
60
+ raise InvalidStateManagerModeError(msg)
61
+
62
+ @abstractmethod
63
+ async def get_state(self, token: str) -> BaseState:
64
+ """Get the state for a token.
65
+
66
+ Args:
67
+ token: The token to get the state for.
68
+
69
+ Returns:
70
+ The state for the token.
71
+ """
72
+
73
+ @abstractmethod
74
+ async def set_state(self, token: str, state: BaseState):
75
+ """Set the state for a token.
76
+
77
+ Args:
78
+ token: The token to set the state for.
79
+ state: The state to set.
80
+ """
81
+
82
+ @abstractmethod
83
+ @contextlib.asynccontextmanager
84
+ async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
85
+ """Modify the state for a token while holding exclusive lock.
86
+
87
+ Args:
88
+ token: The token to modify the state for.
89
+
90
+ Yields:
91
+ The state for the token.
92
+ """
93
+ yield self.state()
94
+
95
+
96
+ def _default_token_expiration() -> int:
97
+ """Get the default token expiration time.
98
+
99
+ Returns:
100
+ The default token expiration time.
101
+ """
102
+ return get_config().redis_token_expiration
103
+
104
+
105
+ def reset_disk_state_manager():
106
+ """Reset the disk state manager."""
107
+ console.debug("Resetting disk state manager.")
108
+ states_directory = prerequisites.get_states_dir()
109
+ if states_directory.exists():
110
+ for path in states_directory.iterdir():
111
+ path.unlink()
112
+
113
+
114
+ def get_state_manager() -> StateManager:
115
+ """Get the state manager for the app that is currently running.
116
+
117
+ Returns:
118
+ The state manager.
119
+ """
120
+ return prerequisites.get_and_validate_app().app.state_manager
@@ -0,0 +1,210 @@
1
+ """A state manager that stores states on disk."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import dataclasses
6
+ import functools
7
+ from collections.abc import AsyncIterator
8
+ from hashlib import md5
9
+ from pathlib import Path
10
+
11
+ from typing_extensions import override
12
+
13
+ from reflex.istate.manager import StateManager, _default_token_expiration
14
+ from reflex.state import BaseState, _split_substate_key, _substate_key
15
+ from reflex.utils import path_ops, prerequisites
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class StateManagerDisk(StateManager):
20
+ """A state manager that stores states on disk."""
21
+
22
+ # The mapping of client ids to states.
23
+ states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
24
+
25
+ # The mutex ensures the dict of mutexes is updated exclusively
26
+ _state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
27
+
28
+ # The dict of mutexes for each client
29
+ _states_locks: dict[str, asyncio.Lock] = dataclasses.field(
30
+ default_factory=dict,
31
+ init=False,
32
+ )
33
+
34
+ # The token expiration time (s).
35
+ token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
36
+
37
+ def __post_init__(self):
38
+ """Create a new state manager."""
39
+ path_ops.mkdir(self.states_directory)
40
+
41
+ self._purge_expired_states()
42
+
43
+ @functools.cached_property
44
+ def states_directory(self) -> Path:
45
+ """Get the states directory.
46
+
47
+ Returns:
48
+ The states directory.
49
+ """
50
+ return prerequisites.get_states_dir()
51
+
52
+ def _purge_expired_states(self):
53
+ """Purge expired states from the disk."""
54
+ import time
55
+
56
+ for path in path_ops.ls(self.states_directory):
57
+ # check path is a pickle file
58
+ if path.suffix != ".pkl":
59
+ continue
60
+
61
+ # load last edited field from file
62
+ last_edited = path.stat().st_mtime
63
+
64
+ # check if the file is older than the token expiration time
65
+ if time.time() - last_edited > self.token_expiration:
66
+ # remove the file
67
+ path.unlink()
68
+
69
+ def token_path(self, token: str) -> Path:
70
+ """Get the path for a token.
71
+
72
+ Args:
73
+ token: The token to get the path for.
74
+
75
+ Returns:
76
+ The path for the token.
77
+ """
78
+ return (
79
+ self.states_directory / f"{md5(token.encode()).hexdigest()}.pkl"
80
+ ).absolute()
81
+
82
+ async def load_state(self, token: str) -> BaseState | None:
83
+ """Load a state object based on the provided token.
84
+
85
+ Args:
86
+ token: The token used to identify the state object.
87
+
88
+ Returns:
89
+ The loaded state object or None.
90
+ """
91
+ token_path = self.token_path(token)
92
+
93
+ if token_path.exists():
94
+ try:
95
+ with token_path.open(mode="rb") as file:
96
+ return BaseState._deserialize(fp=file)
97
+ except Exception:
98
+ pass
99
+ return None
100
+
101
+ async def populate_substates(
102
+ self, client_token: str, state: BaseState, root_state: BaseState
103
+ ):
104
+ """Populate the substates of a state object.
105
+
106
+ Args:
107
+ client_token: The client token.
108
+ state: The state object to populate.
109
+ root_state: The root state object.
110
+ """
111
+ for substate in state.get_substates():
112
+ substate_token = _substate_key(client_token, substate)
113
+
114
+ fresh_instance = await root_state.get_state(substate)
115
+ instance = await self.load_state(substate_token)
116
+ if instance is not None:
117
+ # Ensure all substates exist, even if they weren't serialized previously.
118
+ instance.substates = fresh_instance.substates
119
+ else:
120
+ instance = fresh_instance
121
+ state.substates[substate.get_name()] = instance
122
+ instance.parent_state = state
123
+
124
+ await self.populate_substates(client_token, instance, root_state)
125
+
126
+ @override
127
+ async def get_state(
128
+ self,
129
+ token: str,
130
+ ) -> BaseState:
131
+ """Get the state for a token.
132
+
133
+ Args:
134
+ token: The token to get the state for.
135
+
136
+ Returns:
137
+ The state for the token.
138
+ """
139
+ client_token = _split_substate_key(token)[0]
140
+ root_state = self.states.get(client_token)
141
+ if root_state is not None:
142
+ # Retrieved state from memory.
143
+ return root_state
144
+
145
+ # Deserialize root state from disk.
146
+ root_state = await self.load_state(_substate_key(client_token, self.state))
147
+ # Create a new root state tree with all substates instantiated.
148
+ fresh_root_state = self.state(_reflex_internal_init=True)
149
+ if root_state is None:
150
+ root_state = fresh_root_state
151
+ else:
152
+ # Ensure all substates exist, even if they were not serialized previously.
153
+ root_state.substates = fresh_root_state.substates
154
+ self.states[client_token] = root_state
155
+ await self.populate_substates(client_token, root_state, root_state)
156
+ return root_state
157
+
158
+ async def set_state_for_substate(self, client_token: str, substate: BaseState):
159
+ """Set the state for a substate.
160
+
161
+ Args:
162
+ client_token: The client token.
163
+ substate: The substate to set.
164
+ """
165
+ substate_token = _substate_key(client_token, substate)
166
+
167
+ if substate._get_was_touched():
168
+ substate._was_touched = False # Reset the touched flag after serializing.
169
+ pickle_state = substate._serialize()
170
+ if pickle_state:
171
+ if not self.states_directory.exists():
172
+ self.states_directory.mkdir(parents=True, exist_ok=True)
173
+ self.token_path(substate_token).write_bytes(pickle_state)
174
+
175
+ for substate_substate in substate.substates.values():
176
+ await self.set_state_for_substate(client_token, substate_substate)
177
+
178
+ @override
179
+ async def set_state(self, token: str, state: BaseState):
180
+ """Set the state for a token.
181
+
182
+ Args:
183
+ token: The token to set the state for.
184
+ state: The state to set.
185
+ """
186
+ client_token, _ = _split_substate_key(token)
187
+ await self.set_state_for_substate(client_token, state)
188
+
189
+ @override
190
+ @contextlib.asynccontextmanager
191
+ async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
192
+ """Modify the state for a token while holding exclusive lock.
193
+
194
+ Args:
195
+ token: The token to modify the state for.
196
+
197
+ Yields:
198
+ The state for the token.
199
+ """
200
+ # Disk state manager ignores the substate suffix and always returns the top-level state.
201
+ client_token, _ = _split_substate_key(token)
202
+ if client_token not in self._states_locks:
203
+ async with self._state_manager_lock:
204
+ if client_token not in self._states_locks:
205
+ self._states_locks[client_token] = asyncio.Lock()
206
+
207
+ async with self._states_locks[client_token]:
208
+ state = await self.get_state(token)
209
+ yield state
210
+ await self.set_state(token, state)
@@ -0,0 +1,76 @@
1
+ """A state manager that stores states in memory."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import dataclasses
6
+ from collections.abc import AsyncIterator
7
+
8
+ from typing_extensions import override
9
+
10
+ from reflex.istate.manager import StateManager
11
+ from reflex.state import BaseState, _split_substate_key
12
+
13
+
14
+ @dataclasses.dataclass
15
+ class StateManagerMemory(StateManager):
16
+ """A state manager that stores states in memory."""
17
+
18
+ # The mapping of client ids to states.
19
+ states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
20
+
21
+ # The mutex ensures the dict of mutexes is updated exclusively
22
+ _state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
23
+
24
+ # The dict of mutexes for each client
25
+ _states_locks: dict[str, asyncio.Lock] = dataclasses.field(
26
+ default_factory=dict, init=False
27
+ )
28
+
29
+ @override
30
+ async def get_state(self, token: str) -> BaseState:
31
+ """Get the state for a token.
32
+
33
+ Args:
34
+ token: The token to get the state for.
35
+
36
+ Returns:
37
+ The state for the token.
38
+ """
39
+ # Memory state manager ignores the substate suffix and always returns the top-level state.
40
+ token = _split_substate_key(token)[0]
41
+ if token not in self.states:
42
+ self.states[token] = self.state(_reflex_internal_init=True)
43
+ return self.states[token]
44
+
45
+ @override
46
+ async def set_state(self, token: str, state: BaseState):
47
+ """Set the state for a token.
48
+
49
+ Args:
50
+ token: The token to set the state for.
51
+ state: The state to set.
52
+ """
53
+ token = _split_substate_key(token)[0]
54
+ self.states[token] = state
55
+
56
+ @override
57
+ @contextlib.asynccontextmanager
58
+ async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
59
+ """Modify the state for a token while holding exclusive lock.
60
+
61
+ Args:
62
+ token: The token to modify the state for.
63
+
64
+ Yields:
65
+ The state for the token.
66
+ """
67
+ # Memory state manager ignores the substate suffix and always returns the top-level state.
68
+ token = _split_substate_key(token)[0]
69
+ if token not in self._states_locks:
70
+ async with self._state_manager_lock:
71
+ if token not in self._states_locks:
72
+ self._states_locks[token] = asyncio.Lock()
73
+
74
+ async with self._states_locks[token]:
75
+ state = await self.get_state(token)
76
+ yield state