reflex 0.6.6.post3__py3-none-any.whl → 0.6.7a1__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 (141) hide show
  1. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +5 -1
  2. reflex/.templates/web/utils/state.js +36 -28
  3. reflex/__init__.py +1 -1
  4. reflex/__init__.pyi +1 -0
  5. reflex/app.py +41 -16
  6. reflex/assets.py +2 -2
  7. reflex/base.py +8 -7
  8. reflex/compiler/templates.py +1 -0
  9. reflex/compiler/utils.py +2 -3
  10. reflex/components/base/bare.py +2 -2
  11. reflex/components/component.py +54 -29
  12. reflex/components/core/banner.py +2 -2
  13. reflex/components/core/banner.pyi +1 -1
  14. reflex/components/core/client_side_routing.py +2 -2
  15. reflex/components/core/client_side_routing.pyi +1 -1
  16. reflex/components/core/clipboard.py +11 -9
  17. reflex/components/core/clipboard.pyi +1 -1
  18. reflex/components/core/cond.py +3 -3
  19. reflex/components/core/foreach.py +1 -1
  20. reflex/components/core/html.pyi +1 -1
  21. reflex/components/core/upload.py +8 -8
  22. reflex/components/datadisplay/code.py +5 -5
  23. reflex/components/datadisplay/dataeditor.py +8 -28
  24. reflex/components/datadisplay/dataeditor.pyi +1 -1
  25. reflex/components/datadisplay/shiki_code_block.py +7 -7
  26. reflex/components/dynamic.py +2 -2
  27. reflex/components/el/elements/__init__.py +1 -1
  28. reflex/components/el/elements/__init__.pyi +1 -1
  29. reflex/components/el/elements/base.py +2 -2
  30. reflex/components/el/elements/base.pyi +1 -1
  31. reflex/components/el/elements/forms.py +40 -10
  32. reflex/components/el/elements/forms.pyi +17 -15
  33. reflex/components/el/elements/inline.py +1 -1
  34. reflex/components/el/elements/inline.pyi +28 -28
  35. reflex/components/el/elements/media.py +1 -4
  36. reflex/components/el/elements/media.pyi +25 -26
  37. reflex/components/el/elements/metadata.py +6 -6
  38. reflex/components/el/elements/metadata.pyi +4 -4
  39. reflex/components/el/elements/other.py +17 -9
  40. reflex/components/el/elements/other.pyi +7 -7
  41. reflex/components/el/elements/scripts.py +1 -2
  42. reflex/components/el/elements/scripts.pyi +3 -3
  43. reflex/components/el/elements/sectioning.py +16 -16
  44. reflex/components/el/elements/sectioning.pyi +15 -15
  45. reflex/components/el/elements/tables.py +1 -1
  46. reflex/components/el/elements/tables.pyi +10 -10
  47. reflex/components/el/elements/typography.py +1 -1
  48. reflex/components/el/elements/typography.pyi +15 -15
  49. reflex/components/markdown/markdown.py +3 -3
  50. reflex/components/next/image.py +1 -1
  51. reflex/components/next/image.pyi +1 -1
  52. reflex/components/plotly/plotly.py +2 -2
  53. reflex/components/radix/primitives/accordion.py +2 -1
  54. reflex/components/radix/primitives/form.pyi +3 -3
  55. reflex/components/radix/primitives/slider.py +1 -1
  56. reflex/components/radix/themes/base.py +4 -10
  57. reflex/components/radix/themes/color_mode.pyi +2 -2
  58. reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
  59. reflex/components/radix/themes/components/badge.pyi +1 -1
  60. reflex/components/radix/themes/components/button.pyi +1 -1
  61. reflex/components/radix/themes/components/callout.pyi +5 -5
  62. reflex/components/radix/themes/components/card.pyi +1 -1
  63. reflex/components/radix/themes/components/checkbox.pyi +3 -3
  64. reflex/components/radix/themes/components/context_menu.py +11 -0
  65. reflex/components/radix/themes/components/context_menu.pyi +155 -0
  66. reflex/components/radix/themes/components/dialog.pyi +1 -1
  67. reflex/components/radix/themes/components/hover_card.pyi +1 -1
  68. reflex/components/radix/themes/components/icon_button.py +1 -1
  69. reflex/components/radix/themes/components/icon_button.pyi +1 -1
  70. reflex/components/radix/themes/components/inset.pyi +1 -1
  71. reflex/components/radix/themes/components/popover.pyi +1 -1
  72. reflex/components/radix/themes/components/radio_group.py +2 -4
  73. reflex/components/radix/themes/components/radio_group.pyi +1 -1
  74. reflex/components/radix/themes/components/select.pyi +3 -3
  75. reflex/components/radix/themes/components/slider.pyi +1 -1
  76. reflex/components/radix/themes/components/switch.pyi +1 -1
  77. reflex/components/radix/themes/components/table.pyi +7 -7
  78. reflex/components/radix/themes/components/tabs.pyi +2 -2
  79. reflex/components/radix/themes/components/text_area.py +3 -0
  80. reflex/components/radix/themes/components/text_area.pyi +3 -1
  81. reflex/components/radix/themes/components/text_field.py +16 -1
  82. reflex/components/radix/themes/components/text_field.pyi +105 -17
  83. reflex/components/radix/themes/layout/box.pyi +1 -1
  84. reflex/components/radix/themes/layout/center.pyi +1 -1
  85. reflex/components/radix/themes/layout/flex.pyi +1 -1
  86. reflex/components/radix/themes/layout/grid.pyi +1 -1
  87. reflex/components/radix/themes/layout/list.py +0 -4
  88. reflex/components/radix/themes/layout/list.pyi +3 -8
  89. reflex/components/radix/themes/layout/section.pyi +1 -1
  90. reflex/components/radix/themes/layout/spacer.pyi +1 -1
  91. reflex/components/radix/themes/layout/stack.pyi +3 -3
  92. reflex/components/radix/themes/typography/blockquote.pyi +1 -1
  93. reflex/components/radix/themes/typography/code.pyi +1 -1
  94. reflex/components/radix/themes/typography/heading.pyi +1 -1
  95. reflex/components/radix/themes/typography/link.py +5 -1
  96. reflex/components/radix/themes/typography/link.pyi +1 -1
  97. reflex/components/radix/themes/typography/text.pyi +7 -7
  98. reflex/components/recharts/cartesian.py +1 -1
  99. reflex/components/recharts/charts.py +4 -4
  100. reflex/components/recharts/polar.py +1 -1
  101. reflex/components/recharts/polar.pyi +1 -1
  102. reflex/components/sonner/toast.py +4 -7
  103. reflex/components/suneditor/editor.py +6 -6
  104. reflex/components/suneditor/editor.pyi +6 -6
  105. reflex/config.py +25 -10
  106. reflex/constants/compiler.py +6 -0
  107. reflex/constants/config.py +2 -0
  108. reflex/constants/custom_components.py +1 -1
  109. reflex/constants/route.py +1 -1
  110. reflex/custom_components/custom_components.py +21 -21
  111. reflex/event.py +57 -22
  112. reflex/experimental/client_state.py +2 -1
  113. reflex/experimental/layout.py +0 -6
  114. reflex/model.py +125 -9
  115. reflex/reflex.py +5 -6
  116. reflex/state.py +203 -88
  117. reflex/style.py +1 -4
  118. reflex/testing.py +10 -11
  119. reflex/utils/build.py +1 -1
  120. reflex/utils/console.py +75 -6
  121. reflex/utils/exceptions.py +12 -0
  122. reflex/utils/exec.py +10 -10
  123. reflex/utils/export.py +1 -2
  124. reflex/utils/format.py +11 -8
  125. reflex/utils/path_ops.py +2 -2
  126. reflex/utils/prerequisites.py +31 -28
  127. reflex/utils/processes.py +4 -4
  128. reflex/utils/pyi_generator.py +12 -11
  129. reflex/utils/types.py +6 -3
  130. reflex/vars/__init__.py +1 -0
  131. reflex/vars/base.py +75 -38
  132. reflex/vars/datetime.py +222 -0
  133. reflex/vars/function.py +3 -3
  134. reflex/vars/number.py +3 -3
  135. reflex/vars/object.py +5 -5
  136. reflex/vars/sequence.py +7 -7
  137. {reflex-0.6.6.post3.dist-info → reflex-0.6.7a1.dist-info}/METADATA +2 -2
  138. {reflex-0.6.6.post3.dist-info → reflex-0.6.7a1.dist-info}/RECORD +141 -140
  139. {reflex-0.6.6.post3.dist-info → reflex-0.6.7a1.dist-info}/LICENSE +0 -0
  140. {reflex-0.6.6.post3.dist-info → reflex-0.6.7a1.dist-info}/WHEEL +0 -0
  141. {reflex-0.6.6.post3.dist-info → reflex-0.6.7a1.dist-info}/entry_points.txt +0 -0
reflex/state.py CHANGED
@@ -11,6 +11,7 @@ import inspect
11
11
  import json
12
12
  import pickle
13
13
  import sys
14
+ import time
14
15
  import typing
15
16
  import uuid
16
17
  from abc import ABC, abstractmethod
@@ -39,6 +40,7 @@ from typing import (
39
40
  get_type_hints,
40
41
  )
41
42
 
43
+ from redis.asyncio.client import PubSub
42
44
  from sqlalchemy.orm import DeclarativeBase
43
45
  from typing_extensions import Self
44
46
 
@@ -69,6 +71,11 @@ try:
69
71
  except ModuleNotFoundError:
70
72
  BaseModelV1 = BaseModelV2
71
73
 
74
+ try:
75
+ from pydantic.v1 import validator
76
+ except ModuleNotFoundError:
77
+ from pydantic import validator
78
+
72
79
  import wrapt
73
80
  from redis.asyncio import Redis
74
81
  from redis.exceptions import ResponseError
@@ -92,11 +99,13 @@ from reflex.utils.exceptions import (
92
99
  DynamicRouteArgShadowsStateVar,
93
100
  EventHandlerShadowsBuiltInStateMethod,
94
101
  ImmutableStateError,
102
+ InvalidLockWarningThresholdError,
95
103
  InvalidStateManagerMode,
96
104
  LockExpiredError,
97
105
  ReflexRuntimeError,
98
106
  SetUndefinedStateVarError,
99
107
  StateSchemaMismatchError,
108
+ StateSerializationError,
100
109
  StateTooLargeError,
101
110
  )
102
111
  from reflex.utils.exec import is_testing_env
@@ -104,6 +113,7 @@ from reflex.utils.serializers import serializer
104
113
  from reflex.utils.types import (
105
114
  _isinstance,
106
115
  get_origin,
116
+ is_optional,
107
117
  is_union,
108
118
  override,
109
119
  value_inside_optional,
@@ -278,6 +288,22 @@ if TYPE_CHECKING:
278
288
  from pydantic.v1.fields import ModelField
279
289
 
280
290
 
291
+ def _unwrap_field_type(type_: Type) -> Type:
292
+ """Unwrap rx.Field type annotations.
293
+
294
+ Args:
295
+ type_: The type to unwrap.
296
+
297
+ Returns:
298
+ The unwrapped type.
299
+ """
300
+ from reflex.vars import Field
301
+
302
+ if get_origin(type_) is Field:
303
+ return get_args(type_)[0]
304
+ return type_
305
+
306
+
281
307
  def get_var_for_field(cls: Type[BaseState], f: ModelField):
282
308
  """Get a Var instance for a Pydantic field.
283
309
 
@@ -288,16 +314,12 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField):
288
314
  Returns:
289
315
  The Var instance.
290
316
  """
291
- from reflex.vars import Field
292
-
293
317
  field_name = format.format_state_name(cls.get_full_name()) + "." + f.name
294
318
 
295
319
  return dispatch(
296
320
  field_name=field_name,
297
321
  var_data=VarData.from_state(cls, f.name),
298
- result_var_type=f.outer_type_
299
- if get_origin(f.outer_type_) is not Field
300
- else get_args(f.outer_type_)[0],
322
+ result_var_type=_unwrap_field_type(f.outer_type_),
301
323
  )
302
324
 
303
325
 
@@ -415,9 +437,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
415
437
  )
416
438
 
417
439
  # Create a fresh copy of the backend variables for this instance
418
- self._backend_vars = copy.deepcopy(
419
- {name: item for name, item in self.backend_vars.items()}
420
- )
440
+ self._backend_vars = copy.deepcopy(self.backend_vars)
421
441
 
422
442
  def __repr__(self) -> str:
423
443
  """Get the string representation of the state.
@@ -425,7 +445,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
425
445
  Returns:
426
446
  The string representation of the state.
427
447
  """
428
- return f"{self.__class__.__name__}({self.dict()})"
448
+ return f"{type(self).__name__}({self.dict()})"
429
449
 
430
450
  @classmethod
431
451
  def _get_computed_vars(cls) -> list[ComputedVar]:
@@ -436,7 +456,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
436
456
  """
437
457
  return [
438
458
  v
439
- for mixin in cls._mixins() + [cls]
459
+ for mixin in [*cls._mixins(), cls]
440
460
  for name, v in mixin.__dict__.items()
441
461
  if is_computed_var(v) and name not in cls.inherited_vars
442
462
  ]
@@ -501,9 +521,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
501
521
  cls.inherited_backend_vars = parent_state.backend_vars
502
522
 
503
523
  # Check if another substate class with the same name has already been defined.
504
- if cls.get_name() in set(
505
- c.get_name() for c in parent_state.class_subclasses
506
- ):
524
+ if cls.get_name() in {c.get_name() for c in parent_state.class_subclasses}:
507
525
  # This should not happen, since we have added module prefix to state names in #3214
508
526
  raise StateValueError(
509
527
  f"The substate class '{cls.get_name()}' has been defined multiple times. "
@@ -766,11 +784,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
766
784
  )
767
785
 
768
786
  # ComputedVar with cache=False always need to be recomputed
769
- cls._always_dirty_computed_vars = set(
787
+ cls._always_dirty_computed_vars = {
770
788
  cvar_name
771
789
  for cvar_name, cvar in cls.computed_vars.items()
772
790
  if not cvar._cache
773
- )
791
+ }
774
792
 
775
793
  # Any substate containing a ComputedVar with cache=False always needs to be recomputed
776
794
  if cls._always_dirty_computed_vars:
@@ -1081,6 +1099,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1081
1099
  if (
1082
1100
  not field.required
1083
1101
  and field.default is None
1102
+ and field.default_factory is None
1084
1103
  and not types.is_optional(prop._var_type)
1085
1104
  ):
1086
1105
  # Ensure frontend uses null coalescing when accessing.
@@ -1288,6 +1307,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1288
1307
  return
1289
1308
 
1290
1309
  if name in self.backend_vars:
1310
+ # abort if unchanged
1311
+ if self._backend_vars.get(name) == value:
1312
+ return
1291
1313
  self._backend_vars.__setitem__(name, value)
1292
1314
  self.dirty_vars.add(name)
1293
1315
  self._mark_dirty()
@@ -1310,8 +1332,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1310
1332
 
1311
1333
  if name in fields:
1312
1334
  field = fields[name]
1313
- field_type = field.outer_type_
1314
- if field.allow_none:
1335
+ field_type = _unwrap_field_type(field.outer_type_)
1336
+ if field.allow_none and not is_optional(field_type):
1315
1337
  field_type = Union[field_type, None]
1316
1338
  if not _isinstance(value, field_type):
1317
1339
  console.deprecate(
@@ -1836,11 +1858,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1836
1858
  Returns:
1837
1859
  Set of computed vars to include in the delta.
1838
1860
  """
1839
- return set(
1861
+ return {
1840
1862
  cvar
1841
1863
  for cvar in self.computed_vars
1842
1864
  if self.computed_vars[cvar].needs_update(instance=self)
1843
- )
1865
+ }
1844
1866
 
1845
1867
  def _dirty_computed_vars(
1846
1868
  self, from_vars: set[str] | None = None, include_backend: bool = True
@@ -1854,12 +1876,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1854
1876
  Returns:
1855
1877
  Set of computed vars to include in the delta.
1856
1878
  """
1857
- return set(
1879
+ return {
1858
1880
  cvar
1859
1881
  for dirty_var in from_vars or self.dirty_vars
1860
1882
  for cvar in self._computed_var_dependencies[dirty_var]
1861
1883
  if include_backend or not self.computed_vars[cvar]._backend
1862
- )
1884
+ }
1863
1885
 
1864
1886
  @classmethod
1865
1887
  def _potentially_dirty_substates(cls) -> set[Type[BaseState]]:
@@ -1869,16 +1891,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1869
1891
  Set of State classes that may need to be fetched to recalc computed vars.
1870
1892
  """
1871
1893
  # _always_dirty_substates need to be fetched to recalc computed vars.
1872
- fetch_substates = set(
1894
+ fetch_substates = {
1873
1895
  cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
1874
1896
  for substate_name in cls._always_dirty_substates
1875
- )
1897
+ }
1876
1898
  for dependent_substates in cls._substate_var_dependencies.values():
1877
1899
  fetch_substates.update(
1878
- set(
1900
+ {
1879
1901
  cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
1880
1902
  for substate_name in dependent_substates
1881
- )
1903
+ }
1882
1904
  )
1883
1905
  return fetch_substates
1884
1906
 
@@ -2105,14 +2127,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2105
2127
  state["__dict__"].pop("router", None)
2106
2128
  state["__dict__"].pop("router_data", None)
2107
2129
  # Never serialize parent_state or substates.
2108
- state["__dict__"]["parent_state"] = None
2109
- state["__dict__"]["substates"] = {}
2130
+ state["__dict__"].pop("parent_state", None)
2131
+ state["__dict__"].pop("substates", None)
2110
2132
  state["__dict__"].pop("_was_touched", None)
2111
2133
  # Remove all inherited vars.
2112
2134
  for inherited_var_name in self.inherited_vars:
2113
2135
  state["__dict__"].pop(inherited_var_name, None)
2114
2136
  return state
2115
2137
 
2138
+ def __setstate__(self, state: dict[str, Any]):
2139
+ """Set the state from redis deserialization.
2140
+
2141
+ This method is called by pickle to deserialize the object.
2142
+
2143
+ Args:
2144
+ state: The state dict for deserialization.
2145
+ """
2146
+ state["__dict__"]["parent_state"] = None
2147
+ state["__dict__"]["substates"] = {}
2148
+ super().__setstate__(state)
2149
+
2116
2150
  def _check_state_size(
2117
2151
  self,
2118
2152
  pickle_state_size: int,
@@ -2168,7 +2202,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2168
2202
 
2169
2203
  return md5(
2170
2204
  pickle.dumps(
2171
- list(sorted(_field_tuple(field_name) for field_name in cls.base_vars))
2205
+ sorted(_field_tuple(field_name) for field_name in cls.base_vars)
2172
2206
  )
2173
2207
  ).hexdigest()
2174
2208
 
@@ -2177,12 +2211,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2177
2211
 
2178
2212
  Returns:
2179
2213
  The serialized state.
2214
+
2215
+ Raises:
2216
+ StateSerializationError: If the state cannot be serialized.
2180
2217
  """
2218
+ payload = b""
2219
+ error = ""
2181
2220
  try:
2182
- pickle_state = pickle.dumps((self._to_schema(), self))
2183
- if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
2184
- self._check_state_size(len(pickle_state))
2185
- return pickle_state
2221
+ payload = pickle.dumps((self._to_schema(), self))
2186
2222
  except HANDLED_PICKLE_ERRORS as og_pickle_error:
2187
2223
  error = (
2188
2224
  f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
@@ -2191,7 +2227,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2191
2227
  try:
2192
2228
  import dill
2193
2229
 
2194
- return dill.dumps((self._to_schema(), self))
2230
+ payload = dill.dumps((self._to_schema(), self))
2195
2231
  except ImportError:
2196
2232
  error += (
2197
2233
  f"Pickle error: {og_pickle_error}. "
@@ -2199,8 +2235,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2199
2235
  )
2200
2236
  except HANDLED_PICKLE_ERRORS as ex:
2201
2237
  error += f"Dill was also unable to pickle the state: {ex}"
2202
- console.warn(error)
2203
- return b""
2238
+ console.warn(error)
2239
+
2240
+ if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
2241
+ self._check_state_size(len(payload))
2242
+
2243
+ if not payload:
2244
+ raise StateSerializationError(error)
2245
+
2246
+ return payload
2204
2247
 
2205
2248
  @classmethod
2206
2249
  def _deserialize(
@@ -2793,6 +2836,7 @@ class StateManager(Base, ABC):
2793
2836
  redis=redis,
2794
2837
  token_expiration=config.redis_token_expiration,
2795
2838
  lock_expiration=config.redis_lock_expiration,
2839
+ lock_warning_threshold=config.redis_lock_warning_threshold,
2796
2840
  )
2797
2841
  raise InvalidStateManagerMode(
2798
2842
  f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
@@ -3162,6 +3206,15 @@ def _default_lock_expiration() -> int:
3162
3206
  return get_config().redis_lock_expiration
3163
3207
 
3164
3208
 
3209
+ def _default_lock_warning_threshold() -> int:
3210
+ """Get the default lock warning threshold.
3211
+
3212
+ Returns:
3213
+ The default lock warning threshold.
3214
+ """
3215
+ return get_config().redis_lock_warning_threshold
3216
+
3217
+
3165
3218
  class StateManagerRedis(StateManager):
3166
3219
  """A state manager that stores states in redis."""
3167
3220
 
@@ -3174,6 +3227,11 @@ class StateManagerRedis(StateManager):
3174
3227
  # The maximum time to hold a lock (ms).
3175
3228
  lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
3176
3229
 
3230
+ # The maximum time to hold a lock (ms) before warning.
3231
+ lock_warning_threshold: int = pydantic.Field(
3232
+ default_factory=_default_lock_warning_threshold
3233
+ )
3234
+
3177
3235
  # The keyspace subscription string when redis is waiting for lock to be released
3178
3236
  _redis_notify_keyspace_events: str = (
3179
3237
  "K" # Enable keyspace notifications (target a particular key)
@@ -3292,7 +3350,7 @@ class StateManagerRedis(StateManager):
3292
3350
  state_cls = self.state.get_class_substate(state_path)
3293
3351
  else:
3294
3352
  raise RuntimeError(
3295
- "StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
3353
+ f"StateManagerRedis requires token to be specified in the form of {{token}}_{{state_full_name}}, but got {token}"
3296
3354
  )
3297
3355
 
3298
3356
  # The deserialized or newly created (sub)state instance.
@@ -3361,6 +3419,17 @@ class StateManagerRedis(StateManager):
3361
3419
  f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
3362
3420
  "or use `@rx.event(background=True)` decorator for long-running tasks."
3363
3421
  )
3422
+ elif lock_id is not None:
3423
+ time_taken = self.lock_expiration / 1000 - (
3424
+ await self.redis.ttl(self._lock_key(token))
3425
+ )
3426
+ if time_taken > self.lock_warning_threshold / 1000:
3427
+ console.warn(
3428
+ f"Lock for token {token} was held too long {time_taken=}s, "
3429
+ f"use `@rx.event(background=True)` decorator for long-running tasks.",
3430
+ dedupe=True,
3431
+ )
3432
+
3364
3433
  client_token, substate_name = _split_substate_key(token)
3365
3434
  # If the substate name on the token doesn't match the instance name, it cannot have a parent.
3366
3435
  if state.parent_state is not None and state.get_full_name() != substate_name:
@@ -3369,17 +3438,16 @@ class StateManagerRedis(StateManager):
3369
3438
  )
3370
3439
 
3371
3440
  # Recursively set_state on all known substates.
3372
- tasks = []
3373
- for substate in state.substates.values():
3374
- tasks.append(
3375
- asyncio.create_task(
3376
- self.set_state(
3377
- token=_substate_key(client_token, substate),
3378
- state=substate,
3379
- lock_id=lock_id,
3380
- )
3441
+ tasks = [
3442
+ asyncio.create_task(
3443
+ self.set_state(
3444
+ _substate_key(client_token, substate),
3445
+ substate,
3446
+ lock_id,
3381
3447
  )
3382
3448
  )
3449
+ for substate in state.substates.values()
3450
+ ]
3383
3451
  # Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
3384
3452
  if state._get_was_touched():
3385
3453
  pickle_state = state._serialize()
@@ -3410,6 +3478,27 @@ class StateManagerRedis(StateManager):
3410
3478
  yield state
3411
3479
  await self.set_state(token, state, lock_id)
3412
3480
 
3481
+ @validator("lock_warning_threshold")
3482
+ @classmethod
3483
+ def validate_lock_warning_threshold(cls, lock_warning_threshold: int, values):
3484
+ """Validate the lock warning threshold.
3485
+
3486
+ Args:
3487
+ lock_warning_threshold: The lock warning threshold.
3488
+ values: The validated attributes.
3489
+
3490
+ Returns:
3491
+ The lock warning threshold.
3492
+
3493
+ Raises:
3494
+ InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
3495
+ """
3496
+ if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]):
3497
+ raise InvalidLockWarningThresholdError(
3498
+ f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
3499
+ )
3500
+ return lock_warning_threshold
3501
+
3413
3502
  @staticmethod
3414
3503
  def _lock_key(token: str) -> bytes:
3415
3504
  """Get the redis key for a token's lock.
@@ -3441,6 +3530,35 @@ class StateManagerRedis(StateManager):
3441
3530
  nx=True, # only set if it doesn't exist
3442
3531
  )
3443
3532
 
3533
+ async def _get_pubsub_message(
3534
+ self, pubsub: PubSub, timeout: float | None = None
3535
+ ) -> None:
3536
+ """Get lock release events from the pubsub.
3537
+
3538
+ Args:
3539
+ pubsub: The pubsub to get a message from.
3540
+ timeout: Remaining time to wait for a message.
3541
+
3542
+ Returns:
3543
+ The message.
3544
+ """
3545
+ if timeout is None:
3546
+ timeout = self.lock_expiration / 1000.0
3547
+
3548
+ started = time.time()
3549
+ message = await pubsub.get_message(
3550
+ ignore_subscribe_messages=True,
3551
+ timeout=timeout,
3552
+ )
3553
+ if (
3554
+ message is None
3555
+ or message["data"] not in self._redis_keyspace_lock_release_events
3556
+ ):
3557
+ remaining = timeout - (time.time() - started)
3558
+ if remaining <= 0:
3559
+ return
3560
+ await self._get_pubsub_message(pubsub, timeout=remaining)
3561
+
3444
3562
  async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
3445
3563
  """Wait for a redis lock to be released via pubsub.
3446
3564
 
@@ -3453,7 +3571,6 @@ class StateManagerRedis(StateManager):
3453
3571
  Raises:
3454
3572
  ResponseError: when the keyspace config cannot be set.
3455
3573
  """
3456
- state_is_locked = False
3457
3574
  lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
3458
3575
  # Enable keyspace notifications for the lock key, so we know when it is available.
3459
3576
  try:
@@ -3467,20 +3584,13 @@ class StateManagerRedis(StateManager):
3467
3584
  raise
3468
3585
  async with self.redis.pubsub() as pubsub:
3469
3586
  await pubsub.psubscribe(lock_key_channel)
3470
- while not state_is_locked:
3471
- # wait for the lock to be released
3472
- while True:
3473
- if not await self.redis.exists(lock_key):
3474
- break # key was removed, try to get the lock again
3475
- message = await pubsub.get_message(
3476
- ignore_subscribe_messages=True,
3477
- timeout=self.lock_expiration / 1000.0,
3478
- )
3479
- if message is None:
3480
- continue
3481
- if message["data"] in self._redis_keyspace_lock_release_events:
3482
- break
3483
- state_is_locked = await self._try_get_lock(lock_key, lock_id)
3587
+ # wait for the lock to be released
3588
+ while True:
3589
+ # fast path
3590
+ if await self._try_get_lock(lock_key, lock_id):
3591
+ return
3592
+ # wait for lock events
3593
+ await self._get_pubsub_message(pubsub)
3484
3594
 
3485
3595
  @contextlib.asynccontextmanager
3486
3596
  async def _lock(self, token: str):
@@ -3539,33 +3649,30 @@ class MutableProxy(wrapt.ObjectProxy):
3539
3649
  """A proxy for a mutable object that tracks changes."""
3540
3650
 
3541
3651
  # Methods on wrapped objects which should mark the state as dirty.
3542
- __mark_dirty_attrs__ = set(
3543
- [
3544
- "add",
3545
- "append",
3546
- "clear",
3547
- "difference_update",
3548
- "discard",
3549
- "extend",
3550
- "insert",
3551
- "intersection_update",
3552
- "pop",
3553
- "popitem",
3554
- "remove",
3555
- "reverse",
3556
- "setdefault",
3557
- "sort",
3558
- "symmetric_difference_update",
3559
- "update",
3560
- ]
3561
- )
3652
+ __mark_dirty_attrs__ = {
3653
+ "add",
3654
+ "append",
3655
+ "clear",
3656
+ "difference_update",
3657
+ "discard",
3658
+ "extend",
3659
+ "insert",
3660
+ "intersection_update",
3661
+ "pop",
3662
+ "popitem",
3663
+ "remove",
3664
+ "reverse",
3665
+ "setdefault",
3666
+ "sort",
3667
+ "symmetric_difference_update",
3668
+ "update",
3669
+ }
3670
+
3562
3671
  # Methods on wrapped objects might return mutable objects that should be tracked.
3563
- __wrap_mutable_attrs__ = set(
3564
- [
3565
- "get",
3566
- "setdefault",
3567
- ]
3568
- )
3672
+ __wrap_mutable_attrs__ = {
3673
+ "get",
3674
+ "setdefault",
3675
+ }
3569
3676
 
3570
3677
  # These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
3571
3678
  __never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
@@ -3596,11 +3703,19 @@ class MutableProxy(wrapt.ObjectProxy):
3596
3703
  self._self_state = state
3597
3704
  self._self_field_name = field_name
3598
3705
 
3706
+ def __repr__(self) -> str:
3707
+ """Get the representation of the wrapped object.
3708
+
3709
+ Returns:
3710
+ The representation of the wrapped object.
3711
+ """
3712
+ return f"{type(self).__name__}({self.__wrapped__})"
3713
+
3599
3714
  def _mark_dirty(
3600
3715
  self,
3601
3716
  wrapped=None,
3602
3717
  instance=None,
3603
- args=tuple(),
3718
+ args=(),
3604
3719
  kwargs=None,
3605
3720
  ) -> Any:
3606
3721
  """Mark the state as dirty, then call a wrapped function.
@@ -3856,7 +3971,7 @@ class ImmutableMutableProxy(MutableProxy):
3856
3971
  self,
3857
3972
  wrapped=None,
3858
3973
  instance=None,
3859
- args=tuple(),
3974
+ args=(),
3860
3975
  kwargs=None,
3861
3976
  ) -> Any:
3862
3977
  """Raise an exception when an attempt is made to modify the object.
reflex/style.py CHANGED
@@ -74,7 +74,7 @@ def set_color_mode(
74
74
  new_color_mode = LiteralVar.create(new_color_mode)
75
75
 
76
76
  return Var(
77
- f"() => {str(base_setter)}({str(new_color_mode)})",
77
+ f"() => {base_setter!s}({new_color_mode!s})",
78
78
  _var_data=VarData.merge(
79
79
  base_setter._get_all_var_data(), new_color_mode._get_all_var_data()
80
80
  ),
@@ -138,9 +138,6 @@ def convert_item(
138
138
  if isinstance(style_item, Var):
139
139
  return style_item, style_item._get_all_var_data()
140
140
 
141
- # if isinstance(style_item, str) and REFLEX_VAR_OPENING_TAG not in style_item:
142
- # return style_item, None
143
-
144
141
  # Otherwise, convert to Var to collapse VarData encoded in f-string.
145
142
  new_var = LiteralVar.create(style_item)
146
143
  var_data = new_var._get_all_var_data() if new_var is not None else None
reflex/testing.py CHANGED
@@ -8,7 +8,6 @@ import dataclasses
8
8
  import functools
9
9
  import inspect
10
10
  import os
11
- import pathlib
12
11
  import platform
13
12
  import re
14
13
  import signal
@@ -20,6 +19,7 @@ import threading
20
19
  import time
21
20
  import types
22
21
  from http.server import SimpleHTTPRequestHandler
22
+ from pathlib import Path
23
23
  from typing import (
24
24
  TYPE_CHECKING,
25
25
  Any,
@@ -100,7 +100,7 @@ class chdir(contextlib.AbstractContextManager):
100
100
 
101
101
  def __enter__(self):
102
102
  """Save current directory and perform chdir."""
103
- self._old_cwd.append(os.getcwd())
103
+ self._old_cwd.append(Path.cwd())
104
104
  os.chdir(self.path)
105
105
 
106
106
  def __exit__(self, *excinfo):
@@ -120,8 +120,8 @@ class AppHarness:
120
120
  app_source: Optional[
121
121
  Callable[[], None] | types.ModuleType | str | functools.partial[Any]
122
122
  ]
123
- app_path: pathlib.Path
124
- app_module_path: pathlib.Path
123
+ app_path: Path
124
+ app_module_path: Path
125
125
  app_module: Optional[types.ModuleType] = None
126
126
  app_instance: Optional[reflex.App] = None
127
127
  frontend_process: Optional[subprocess.Popen] = None
@@ -136,7 +136,7 @@ class AppHarness:
136
136
  @classmethod
137
137
  def create(
138
138
  cls,
139
- root: pathlib.Path,
139
+ root: Path,
140
140
  app_source: Optional[
141
141
  Callable[[], None] | types.ModuleType | str | functools.partial[Any]
142
142
  ] = None,
@@ -206,7 +206,7 @@ class AppHarness:
206
206
  The full state name
207
207
  """
208
208
  # NOTE: using State.get_name() somehow causes trouble here
209
- # path = [State.get_name()] + [self.get_state_name(p) for p in path]
209
+ # path = [State.get_name()] + [self.get_state_name(p) for p in path] # noqa: ERA001
210
210
  path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
211
211
  return ".".join(path)
212
212
 
@@ -436,7 +436,6 @@ class AppHarness:
436
436
 
437
437
  Returns:
438
438
  The rendered app global code.
439
-
440
439
  """
441
440
  if not inspect.isclass(value) and not inspect.isfunction(value):
442
441
  return f"{key} = {value!r}"
@@ -815,7 +814,7 @@ class AppHarness:
815
814
  class SimpleHTTPRequestHandlerCustomErrors(SimpleHTTPRequestHandler):
816
815
  """SimpleHTTPRequestHandler with custom error page handling."""
817
816
 
818
- def __init__(self, *args, error_page_map: dict[int, pathlib.Path], **kwargs):
817
+ def __init__(self, *args, error_page_map: dict[int, Path], **kwargs):
819
818
  """Initialize the handler.
820
819
 
821
820
  Args:
@@ -858,8 +857,8 @@ class Subdir404TCPServer(socketserver.TCPServer):
858
857
  def __init__(
859
858
  self,
860
859
  *args,
861
- root: pathlib.Path,
862
- error_page_map: dict[int, pathlib.Path] | None,
860
+ root: Path,
861
+ error_page_map: dict[int, Path] | None,
863
862
  **kwargs,
864
863
  ):
865
864
  """Initialize the server.
@@ -879,7 +878,7 @@ class Subdir404TCPServer(socketserver.TCPServer):
879
878
 
880
879
  Args:
881
880
  request: the requesting socket
882
- client_address: (host, port) referring to the clients address.
881
+ client_address: (host, port) referring to the client's address.
883
882
  """
884
883
  self.RequestHandlerClass(
885
884
  request,
reflex/utils/build.py CHANGED
@@ -150,7 +150,7 @@ def zip_app(
150
150
  _zip(
151
151
  component_name=constants.ComponentName.BACKEND,
152
152
  target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
153
- root_dir=Path("."),
153
+ root_dir=Path.cwd(),
154
154
  dirs_to_exclude={"__pycache__"},
155
155
  files_to_exclude=files_to_exclude,
156
156
  top_level_dirs_to_exclude={"assets"},