reflex 0.6.6.post3__py3-none-any.whl → 0.6.7__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 +200 -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.7.dist-info}/METADATA +2 -2
  138. {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/RECORD +141 -140
  139. {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/LICENSE +0 -0
  140. {reflex-0.6.6.post3.dist-info → reflex-0.6.7.dist-info}/WHEEL +0 -0
  141. {reflex-0.6.6.post3.dist-info → reflex-0.6.7.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.
@@ -1310,8 +1329,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1310
1329
 
1311
1330
  if name in fields:
1312
1331
  field = fields[name]
1313
- field_type = field.outer_type_
1314
- if field.allow_none:
1332
+ field_type = _unwrap_field_type(field.outer_type_)
1333
+ if field.allow_none and not is_optional(field_type):
1315
1334
  field_type = Union[field_type, None]
1316
1335
  if not _isinstance(value, field_type):
1317
1336
  console.deprecate(
@@ -1836,11 +1855,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1836
1855
  Returns:
1837
1856
  Set of computed vars to include in the delta.
1838
1857
  """
1839
- return set(
1858
+ return {
1840
1859
  cvar
1841
1860
  for cvar in self.computed_vars
1842
1861
  if self.computed_vars[cvar].needs_update(instance=self)
1843
- )
1862
+ }
1844
1863
 
1845
1864
  def _dirty_computed_vars(
1846
1865
  self, from_vars: set[str] | None = None, include_backend: bool = True
@@ -1854,12 +1873,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1854
1873
  Returns:
1855
1874
  Set of computed vars to include in the delta.
1856
1875
  """
1857
- return set(
1876
+ return {
1858
1877
  cvar
1859
1878
  for dirty_var in from_vars or self.dirty_vars
1860
1879
  for cvar in self._computed_var_dependencies[dirty_var]
1861
1880
  if include_backend or not self.computed_vars[cvar]._backend
1862
- )
1881
+ }
1863
1882
 
1864
1883
  @classmethod
1865
1884
  def _potentially_dirty_substates(cls) -> set[Type[BaseState]]:
@@ -1869,16 +1888,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1869
1888
  Set of State classes that may need to be fetched to recalc computed vars.
1870
1889
  """
1871
1890
  # _always_dirty_substates need to be fetched to recalc computed vars.
1872
- fetch_substates = set(
1891
+ fetch_substates = {
1873
1892
  cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
1874
1893
  for substate_name in cls._always_dirty_substates
1875
- )
1894
+ }
1876
1895
  for dependent_substates in cls._substate_var_dependencies.values():
1877
1896
  fetch_substates.update(
1878
- set(
1897
+ {
1879
1898
  cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
1880
1899
  for substate_name in dependent_substates
1881
- )
1900
+ }
1882
1901
  )
1883
1902
  return fetch_substates
1884
1903
 
@@ -2105,14 +2124,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2105
2124
  state["__dict__"].pop("router", None)
2106
2125
  state["__dict__"].pop("router_data", None)
2107
2126
  # Never serialize parent_state or substates.
2108
- state["__dict__"]["parent_state"] = None
2109
- state["__dict__"]["substates"] = {}
2127
+ state["__dict__"].pop("parent_state", None)
2128
+ state["__dict__"].pop("substates", None)
2110
2129
  state["__dict__"].pop("_was_touched", None)
2111
2130
  # Remove all inherited vars.
2112
2131
  for inherited_var_name in self.inherited_vars:
2113
2132
  state["__dict__"].pop(inherited_var_name, None)
2114
2133
  return state
2115
2134
 
2135
+ def __setstate__(self, state: dict[str, Any]):
2136
+ """Set the state from redis deserialization.
2137
+
2138
+ This method is called by pickle to deserialize the object.
2139
+
2140
+ Args:
2141
+ state: The state dict for deserialization.
2142
+ """
2143
+ state["__dict__"]["parent_state"] = None
2144
+ state["__dict__"]["substates"] = {}
2145
+ super().__setstate__(state)
2146
+
2116
2147
  def _check_state_size(
2117
2148
  self,
2118
2149
  pickle_state_size: int,
@@ -2168,7 +2199,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2168
2199
 
2169
2200
  return md5(
2170
2201
  pickle.dumps(
2171
- list(sorted(_field_tuple(field_name) for field_name in cls.base_vars))
2202
+ sorted(_field_tuple(field_name) for field_name in cls.base_vars)
2172
2203
  )
2173
2204
  ).hexdigest()
2174
2205
 
@@ -2177,12 +2208,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2177
2208
 
2178
2209
  Returns:
2179
2210
  The serialized state.
2211
+
2212
+ Raises:
2213
+ StateSerializationError: If the state cannot be serialized.
2180
2214
  """
2215
+ payload = b""
2216
+ error = ""
2181
2217
  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
2218
+ payload = pickle.dumps((self._to_schema(), self))
2186
2219
  except HANDLED_PICKLE_ERRORS as og_pickle_error:
2187
2220
  error = (
2188
2221
  f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
@@ -2191,7 +2224,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2191
2224
  try:
2192
2225
  import dill
2193
2226
 
2194
- return dill.dumps((self._to_schema(), self))
2227
+ payload = dill.dumps((self._to_schema(), self))
2195
2228
  except ImportError:
2196
2229
  error += (
2197
2230
  f"Pickle error: {og_pickle_error}. "
@@ -2199,8 +2232,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
2199
2232
  )
2200
2233
  except HANDLED_PICKLE_ERRORS as ex:
2201
2234
  error += f"Dill was also unable to pickle the state: {ex}"
2202
- console.warn(error)
2203
- return b""
2235
+ console.warn(error)
2236
+
2237
+ if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
2238
+ self._check_state_size(len(payload))
2239
+
2240
+ if not payload:
2241
+ raise StateSerializationError(error)
2242
+
2243
+ return payload
2204
2244
 
2205
2245
  @classmethod
2206
2246
  def _deserialize(
@@ -2793,6 +2833,7 @@ class StateManager(Base, ABC):
2793
2833
  redis=redis,
2794
2834
  token_expiration=config.redis_token_expiration,
2795
2835
  lock_expiration=config.redis_lock_expiration,
2836
+ lock_warning_threshold=config.redis_lock_warning_threshold,
2796
2837
  )
2797
2838
  raise InvalidStateManagerMode(
2798
2839
  f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
@@ -3162,6 +3203,15 @@ def _default_lock_expiration() -> int:
3162
3203
  return get_config().redis_lock_expiration
3163
3204
 
3164
3205
 
3206
+ def _default_lock_warning_threshold() -> int:
3207
+ """Get the default lock warning threshold.
3208
+
3209
+ Returns:
3210
+ The default lock warning threshold.
3211
+ """
3212
+ return get_config().redis_lock_warning_threshold
3213
+
3214
+
3165
3215
  class StateManagerRedis(StateManager):
3166
3216
  """A state manager that stores states in redis."""
3167
3217
 
@@ -3174,6 +3224,11 @@ class StateManagerRedis(StateManager):
3174
3224
  # The maximum time to hold a lock (ms).
3175
3225
  lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
3176
3226
 
3227
+ # The maximum time to hold a lock (ms) before warning.
3228
+ lock_warning_threshold: int = pydantic.Field(
3229
+ default_factory=_default_lock_warning_threshold
3230
+ )
3231
+
3177
3232
  # The keyspace subscription string when redis is waiting for lock to be released
3178
3233
  _redis_notify_keyspace_events: str = (
3179
3234
  "K" # Enable keyspace notifications (target a particular key)
@@ -3292,7 +3347,7 @@ class StateManagerRedis(StateManager):
3292
3347
  state_cls = self.state.get_class_substate(state_path)
3293
3348
  else:
3294
3349
  raise RuntimeError(
3295
- "StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
3350
+ f"StateManagerRedis requires token to be specified in the form of {{token}}_{{state_full_name}}, but got {token}"
3296
3351
  )
3297
3352
 
3298
3353
  # The deserialized or newly created (sub)state instance.
@@ -3361,6 +3416,17 @@ class StateManagerRedis(StateManager):
3361
3416
  f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
3362
3417
  "or use `@rx.event(background=True)` decorator for long-running tasks."
3363
3418
  )
3419
+ elif lock_id is not None:
3420
+ time_taken = self.lock_expiration / 1000 - (
3421
+ await self.redis.ttl(self._lock_key(token))
3422
+ )
3423
+ if time_taken > self.lock_warning_threshold / 1000:
3424
+ console.warn(
3425
+ f"Lock for token {token} was held too long {time_taken=}s, "
3426
+ f"use `@rx.event(background=True)` decorator for long-running tasks.",
3427
+ dedupe=True,
3428
+ )
3429
+
3364
3430
  client_token, substate_name = _split_substate_key(token)
3365
3431
  # If the substate name on the token doesn't match the instance name, it cannot have a parent.
3366
3432
  if state.parent_state is not None and state.get_full_name() != substate_name:
@@ -3369,17 +3435,16 @@ class StateManagerRedis(StateManager):
3369
3435
  )
3370
3436
 
3371
3437
  # 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
- )
3438
+ tasks = [
3439
+ asyncio.create_task(
3440
+ self.set_state(
3441
+ _substate_key(client_token, substate),
3442
+ substate,
3443
+ lock_id,
3381
3444
  )
3382
3445
  )
3446
+ for substate in state.substates.values()
3447
+ ]
3383
3448
  # Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
3384
3449
  if state._get_was_touched():
3385
3450
  pickle_state = state._serialize()
@@ -3410,6 +3475,27 @@ class StateManagerRedis(StateManager):
3410
3475
  yield state
3411
3476
  await self.set_state(token, state, lock_id)
3412
3477
 
3478
+ @validator("lock_warning_threshold")
3479
+ @classmethod
3480
+ def validate_lock_warning_threshold(cls, lock_warning_threshold: int, values):
3481
+ """Validate the lock warning threshold.
3482
+
3483
+ Args:
3484
+ lock_warning_threshold: The lock warning threshold.
3485
+ values: The validated attributes.
3486
+
3487
+ Returns:
3488
+ The lock warning threshold.
3489
+
3490
+ Raises:
3491
+ InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
3492
+ """
3493
+ if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]):
3494
+ raise InvalidLockWarningThresholdError(
3495
+ f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
3496
+ )
3497
+ return lock_warning_threshold
3498
+
3413
3499
  @staticmethod
3414
3500
  def _lock_key(token: str) -> bytes:
3415
3501
  """Get the redis key for a token's lock.
@@ -3441,6 +3527,35 @@ class StateManagerRedis(StateManager):
3441
3527
  nx=True, # only set if it doesn't exist
3442
3528
  )
3443
3529
 
3530
+ async def _get_pubsub_message(
3531
+ self, pubsub: PubSub, timeout: float | None = None
3532
+ ) -> None:
3533
+ """Get lock release events from the pubsub.
3534
+
3535
+ Args:
3536
+ pubsub: The pubsub to get a message from.
3537
+ timeout: Remaining time to wait for a message.
3538
+
3539
+ Returns:
3540
+ The message.
3541
+ """
3542
+ if timeout is None:
3543
+ timeout = self.lock_expiration / 1000.0
3544
+
3545
+ started = time.time()
3546
+ message = await pubsub.get_message(
3547
+ ignore_subscribe_messages=True,
3548
+ timeout=timeout,
3549
+ )
3550
+ if (
3551
+ message is None
3552
+ or message["data"] not in self._redis_keyspace_lock_release_events
3553
+ ):
3554
+ remaining = timeout - (time.time() - started)
3555
+ if remaining <= 0:
3556
+ return
3557
+ await self._get_pubsub_message(pubsub, timeout=remaining)
3558
+
3444
3559
  async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
3445
3560
  """Wait for a redis lock to be released via pubsub.
3446
3561
 
@@ -3453,7 +3568,6 @@ class StateManagerRedis(StateManager):
3453
3568
  Raises:
3454
3569
  ResponseError: when the keyspace config cannot be set.
3455
3570
  """
3456
- state_is_locked = False
3457
3571
  lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
3458
3572
  # Enable keyspace notifications for the lock key, so we know when it is available.
3459
3573
  try:
@@ -3467,20 +3581,13 @@ class StateManagerRedis(StateManager):
3467
3581
  raise
3468
3582
  async with self.redis.pubsub() as pubsub:
3469
3583
  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)
3584
+ # wait for the lock to be released
3585
+ while True:
3586
+ # fast path
3587
+ if await self._try_get_lock(lock_key, lock_id):
3588
+ return
3589
+ # wait for lock events
3590
+ await self._get_pubsub_message(pubsub)
3484
3591
 
3485
3592
  @contextlib.asynccontextmanager
3486
3593
  async def _lock(self, token: str):
@@ -3539,33 +3646,30 @@ class MutableProxy(wrapt.ObjectProxy):
3539
3646
  """A proxy for a mutable object that tracks changes."""
3540
3647
 
3541
3648
  # 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
- )
3649
+ __mark_dirty_attrs__ = {
3650
+ "add",
3651
+ "append",
3652
+ "clear",
3653
+ "difference_update",
3654
+ "discard",
3655
+ "extend",
3656
+ "insert",
3657
+ "intersection_update",
3658
+ "pop",
3659
+ "popitem",
3660
+ "remove",
3661
+ "reverse",
3662
+ "setdefault",
3663
+ "sort",
3664
+ "symmetric_difference_update",
3665
+ "update",
3666
+ }
3667
+
3562
3668
  # Methods on wrapped objects might return mutable objects that should be tracked.
3563
- __wrap_mutable_attrs__ = set(
3564
- [
3565
- "get",
3566
- "setdefault",
3567
- ]
3568
- )
3669
+ __wrap_mutable_attrs__ = {
3670
+ "get",
3671
+ "setdefault",
3672
+ }
3569
3673
 
3570
3674
  # These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
3571
3675
  __never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
@@ -3596,11 +3700,19 @@ class MutableProxy(wrapt.ObjectProxy):
3596
3700
  self._self_state = state
3597
3701
  self._self_field_name = field_name
3598
3702
 
3703
+ def __repr__(self) -> str:
3704
+ """Get the representation of the wrapped object.
3705
+
3706
+ Returns:
3707
+ The representation of the wrapped object.
3708
+ """
3709
+ return f"{type(self).__name__}({self.__wrapped__})"
3710
+
3599
3711
  def _mark_dirty(
3600
3712
  self,
3601
3713
  wrapped=None,
3602
3714
  instance=None,
3603
- args=tuple(),
3715
+ args=(),
3604
3716
  kwargs=None,
3605
3717
  ) -> Any:
3606
3718
  """Mark the state as dirty, then call a wrapped function.
@@ -3856,7 +3968,7 @@ class ImmutableMutableProxy(MutableProxy):
3856
3968
  self,
3857
3969
  wrapped=None,
3858
3970
  instance=None,
3859
- args=tuple(),
3971
+ args=(),
3860
3972
  kwargs=None,
3861
3973
  ) -> Any:
3862
3974
  """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"},