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.

Files changed (199) hide show
  1. reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +5 -15
  2. reflex/.templates/jinja/web/pages/index.js.jinja2 +4 -0
  3. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
  4. reflex/.templates/web/utils/state.js +3 -0
  5. reflex/__init__.py +12 -2
  6. reflex/__init__.pyi +4 -0
  7. reflex/app.py +23 -3
  8. reflex/base.py +16 -4
  9. reflex/compiler/compiler.py +1 -0
  10. reflex/compiler/utils.py +11 -2
  11. reflex/components/base/app_wrap.pyi +1 -1
  12. reflex/components/base/bare.py +3 -4
  13. reflex/components/base/body.pyi +1 -1
  14. reflex/components/base/document.pyi +1 -1
  15. reflex/components/base/fragment.pyi +1 -1
  16. reflex/components/base/head.pyi +1 -1
  17. reflex/components/base/link.pyi +1 -1
  18. reflex/components/base/meta.pyi +1 -1
  19. reflex/components/base/script.pyi +1 -1
  20. reflex/components/chakra/base.pyi +1 -1
  21. reflex/components/chakra/datadisplay/badge.pyi +1 -1
  22. reflex/components/chakra/datadisplay/code.pyi +1 -1
  23. reflex/components/chakra/datadisplay/divider.pyi +1 -1
  24. reflex/components/chakra/datadisplay/keyboard_key.pyi +1 -1
  25. reflex/components/chakra/datadisplay/list.pyi +1 -1
  26. reflex/components/chakra/datadisplay/stat.pyi +1 -1
  27. reflex/components/chakra/datadisplay/table.pyi +1 -1
  28. reflex/components/chakra/datadisplay/tag.pyi +1 -1
  29. reflex/components/chakra/disclosure/accordion.pyi +1 -1
  30. reflex/components/chakra/disclosure/tabs.pyi +1 -1
  31. reflex/components/chakra/disclosure/transition.pyi +1 -1
  32. reflex/components/chakra/disclosure/visuallyhidden.pyi +1 -1
  33. reflex/components/chakra/feedback/alert.pyi +1 -1
  34. reflex/components/chakra/feedback/circularprogress.pyi +1 -1
  35. reflex/components/chakra/feedback/progress.pyi +1 -1
  36. reflex/components/chakra/feedback/skeleton.pyi +1 -1
  37. reflex/components/chakra/feedback/spinner.pyi +1 -1
  38. reflex/components/chakra/forms/button.pyi +1 -1
  39. reflex/components/chakra/forms/checkbox.pyi +1 -1
  40. reflex/components/chakra/forms/colormodeswitch.pyi +1 -1
  41. reflex/components/chakra/forms/date_picker.pyi +1 -1
  42. reflex/components/chakra/forms/date_time_picker.pyi +1 -1
  43. reflex/components/chakra/forms/editable.pyi +1 -1
  44. reflex/components/chakra/forms/email.pyi +1 -1
  45. reflex/components/chakra/forms/form.pyi +1 -1
  46. reflex/components/chakra/forms/iconbutton.pyi +1 -1
  47. reflex/components/chakra/forms/input.pyi +1 -1
  48. reflex/components/chakra/forms/numberinput.pyi +1 -1
  49. reflex/components/chakra/forms/password.pyi +1 -1
  50. reflex/components/chakra/forms/pininput.pyi +1 -1
  51. reflex/components/chakra/forms/radio.pyi +1 -1
  52. reflex/components/chakra/forms/rangeslider.pyi +1 -1
  53. reflex/components/chakra/forms/select.pyi +1 -1
  54. reflex/components/chakra/forms/slider.pyi +1 -1
  55. reflex/components/chakra/forms/switch.pyi +1 -1
  56. reflex/components/chakra/forms/textarea.pyi +1 -1
  57. reflex/components/chakra/forms/time_picker.pyi +1 -1
  58. reflex/components/chakra/layout/aspect_ratio.pyi +1 -1
  59. reflex/components/chakra/layout/box.pyi +1 -1
  60. reflex/components/chakra/layout/card.pyi +1 -1
  61. reflex/components/chakra/layout/center.pyi +1 -1
  62. reflex/components/chakra/layout/container.pyi +1 -1
  63. reflex/components/chakra/layout/flex.pyi +1 -1
  64. reflex/components/chakra/layout/grid.pyi +1 -1
  65. reflex/components/chakra/layout/spacer.pyi +1 -1
  66. reflex/components/chakra/layout/stack.pyi +1 -1
  67. reflex/components/chakra/layout/wrap.pyi +1 -1
  68. reflex/components/chakra/media/avatar.pyi +1 -1
  69. reflex/components/chakra/media/icon.pyi +1 -1
  70. reflex/components/chakra/media/image.pyi +1 -1
  71. reflex/components/chakra/navigation/breadcrumb.pyi +1 -1
  72. reflex/components/chakra/navigation/link.pyi +1 -1
  73. reflex/components/chakra/navigation/linkoverlay.pyi +1 -1
  74. reflex/components/chakra/navigation/stepper.pyi +1 -1
  75. reflex/components/chakra/overlay/alertdialog.pyi +1 -1
  76. reflex/components/chakra/overlay/drawer.pyi +1 -1
  77. reflex/components/chakra/overlay/menu.pyi +1 -1
  78. reflex/components/chakra/overlay/modal.pyi +1 -1
  79. reflex/components/chakra/overlay/popover.pyi +1 -1
  80. reflex/components/chakra/overlay/tooltip.pyi +1 -1
  81. reflex/components/chakra/typography/heading.pyi +1 -1
  82. reflex/components/chakra/typography/highlight.pyi +1 -1
  83. reflex/components/chakra/typography/span.pyi +1 -1
  84. reflex/components/chakra/typography/text.pyi +1 -1
  85. reflex/components/component.py +82 -30
  86. reflex/components/core/banner.py +1 -2
  87. reflex/components/core/banner.pyi +1 -2
  88. reflex/components/core/client_side_routing.pyi +1 -1
  89. reflex/components/core/cond.py +1 -1
  90. reflex/components/core/debounce.pyi +1 -1
  91. reflex/components/core/html.pyi +1 -1
  92. reflex/components/core/responsive.py +1 -1
  93. reflex/components/core/upload.pyi +1 -1
  94. reflex/components/datadisplay/code.py +17 -9
  95. reflex/components/datadisplay/code.pyi +3 -1
  96. reflex/components/datadisplay/dataeditor.pyi +1 -1
  97. reflex/components/el/element.pyi +1 -1
  98. reflex/components/el/elements/base.pyi +1 -1
  99. reflex/components/el/elements/forms.py +78 -1
  100. reflex/components/el/elements/forms.pyi +10 -2
  101. reflex/components/el/elements/inline.pyi +1 -1
  102. reflex/components/el/elements/media.pyi +1 -1
  103. reflex/components/el/elements/metadata.pyi +1 -1
  104. reflex/components/el/elements/other.pyi +1 -1
  105. reflex/components/el/elements/scripts.pyi +1 -1
  106. reflex/components/el/elements/sectioning.pyi +1 -1
  107. reflex/components/el/elements/tables.pyi +1 -1
  108. reflex/components/el/elements/typography.pyi +1 -1
  109. reflex/components/gridjs/datatable.pyi +1 -1
  110. reflex/components/lucide/icon.py +275 -115
  111. reflex/components/lucide/icon.pyi +259 -114
  112. reflex/components/markdown/markdown.pyi +1 -1
  113. reflex/components/moment/moment.pyi +1 -1
  114. reflex/components/next/base.pyi +1 -1
  115. reflex/components/next/image.pyi +1 -1
  116. reflex/components/next/link.pyi +1 -1
  117. reflex/components/next/video.pyi +1 -1
  118. reflex/components/plotly/plotly.pyi +1 -1
  119. reflex/components/radix/primitives/accordion.pyi +1 -1
  120. reflex/components/radix/primitives/base.pyi +1 -1
  121. reflex/components/radix/primitives/drawer.pyi +1 -1
  122. reflex/components/radix/primitives/form.pyi +1 -1
  123. reflex/components/radix/primitives/progress.pyi +1 -1
  124. reflex/components/radix/primitives/slider.pyi +1 -1
  125. reflex/components/radix/themes/base.pyi +1 -1
  126. reflex/components/radix/themes/color_mode.pyi +1 -1
  127. reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
  128. reflex/components/radix/themes/components/aspect_ratio.pyi +1 -1
  129. reflex/components/radix/themes/components/avatar.pyi +1 -1
  130. reflex/components/radix/themes/components/badge.pyi +1 -1
  131. reflex/components/radix/themes/components/button.pyi +1 -1
  132. reflex/components/radix/themes/components/callout.pyi +1 -1
  133. reflex/components/radix/themes/components/card.pyi +1 -1
  134. reflex/components/radix/themes/components/checkbox.pyi +1 -1
  135. reflex/components/radix/themes/components/context_menu.pyi +1 -1
  136. reflex/components/radix/themes/components/dialog.pyi +1 -1
  137. reflex/components/radix/themes/components/dropdown_menu.pyi +1 -1
  138. reflex/components/radix/themes/components/hover_card.pyi +1 -1
  139. reflex/components/radix/themes/components/icon_button.pyi +1 -1
  140. reflex/components/radix/themes/components/inset.pyi +1 -1
  141. reflex/components/radix/themes/components/popover.pyi +1 -1
  142. reflex/components/radix/themes/components/radio_group.pyi +1 -1
  143. reflex/components/radix/themes/components/scroll_area.pyi +1 -1
  144. reflex/components/radix/themes/components/select.py +4 -1
  145. reflex/components/radix/themes/components/select.pyi +5 -1
  146. reflex/components/radix/themes/components/separator.pyi +1 -1
  147. reflex/components/radix/themes/components/slider.pyi +1 -1
  148. reflex/components/radix/themes/components/switch.pyi +1 -1
  149. reflex/components/radix/themes/components/table.pyi +1 -1
  150. reflex/components/radix/themes/components/tabs.pyi +1 -1
  151. reflex/components/radix/themes/components/text_area.pyi +5 -1
  152. reflex/components/radix/themes/components/text_field.pyi +1 -1
  153. reflex/components/radix/themes/components/tooltip.pyi +1 -1
  154. reflex/components/radix/themes/layout/__init__.py +5 -4
  155. reflex/components/radix/themes/layout/base.pyi +1 -1
  156. reflex/components/radix/themes/layout/box.pyi +1 -1
  157. reflex/components/radix/themes/layout/center.pyi +1 -1
  158. reflex/components/radix/themes/layout/container.pyi +1 -1
  159. reflex/components/radix/themes/layout/flex.pyi +1 -1
  160. reflex/components/radix/themes/layout/grid.pyi +1 -1
  161. reflex/components/radix/themes/layout/list.py +21 -13
  162. reflex/components/radix/themes/layout/list.pyi +139 -481
  163. reflex/components/radix/themes/layout/section.pyi +1 -1
  164. reflex/components/radix/themes/layout/spacer.pyi +1 -1
  165. reflex/components/radix/themes/layout/stack.pyi +1 -1
  166. reflex/components/radix/themes/typography/blockquote.pyi +1 -1
  167. reflex/components/radix/themes/typography/code.pyi +1 -1
  168. reflex/components/radix/themes/typography/heading.pyi +1 -1
  169. reflex/components/radix/themes/typography/link.pyi +1 -1
  170. reflex/components/radix/themes/typography/text.pyi +1 -1
  171. reflex/components/react_player/audio.pyi +1 -1
  172. reflex/components/react_player/react_player.pyi +1 -1
  173. reflex/components/react_player/video.pyi +1 -1
  174. reflex/components/recharts/cartesian.pyi +1 -1
  175. reflex/components/recharts/charts.pyi +1 -1
  176. reflex/components/recharts/general.pyi +1 -1
  177. reflex/components/recharts/polar.pyi +1 -1
  178. reflex/components/recharts/recharts.pyi +1 -1
  179. reflex/components/suneditor/editor.pyi +1 -1
  180. reflex/config.py +12 -2
  181. reflex/custom_components/custom_components.py +305 -21
  182. reflex/event.py +36 -46
  183. reflex/reflex.py +19 -13
  184. reflex/state.py +184 -39
  185. reflex/testing.py +15 -11
  186. reflex/utils/console.py +15 -7
  187. reflex/utils/exec.py +9 -0
  188. reflex/utils/prerequisites.py +12 -1
  189. reflex/utils/processes.py +8 -25
  190. reflex/utils/pyi_generator.py +842 -0
  191. reflex/utils/telemetry.py +18 -1
  192. reflex/utils/types.py +14 -2
  193. reflex/vars.py +33 -3
  194. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/METADATA +31 -29
  195. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/RECORD +198 -198
  196. reflex/page.pyi +0 -17
  197. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/LICENSE +0 -0
  198. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/WHEEL +0 -0
  199. {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
- # Emtpy string means this event handler is a server side event.
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: dict[str, int | float | bool]):
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=spec.annotations.get(l_arg, FrontendEvent),
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.change_or_terminate_port(frontend_port, "frontend")
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.change_or_terminate_port(backend_port, "backend")
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
- @cli.command()
304
- def login(
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 cloudpickle
31
- import pydantic
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 ix, relative_parent_state_name in enumerate(relative_target_state_parts):
1235
+ for relative_parent_state_name in relative_target_state_parts:
1188
1236
  fetch_parent_states.append(
1189
- ".".join([*fetch_parent_states[: ix + 1], relative_parent_state_name])
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
- parent_states_by_name = dict(self._get_parent_states())
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 initial hydrate with no on_load events defined.
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 = cloudpickle.loads(redis_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
- cloudpickle.dumps(state),
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 BaseState, State, StateManagerMemory, StateManagerRedis
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
- msg = (
126
- f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely "
127
- f"removed in {removal_version}"
128
- )
129
- if _LOG_LEVEL <= LogLevel.WARNING:
130
- print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
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"