reflex 0.5.2a1__py3-none-any.whl → 0.5.3a1__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 (163) hide show
  1. reflex/.templates/web/postcss.config.js +1 -0
  2. reflex/.templates/web/styles/tailwind.css +4 -1
  3. reflex/__init__.py +298 -204
  4. reflex/__init__.pyi +196 -157
  5. reflex/app.py +13 -2
  6. reflex/components/__init__.py +31 -17
  7. reflex/components/__init__.pyi +26 -0
  8. reflex/components/base/__init__.py +25 -9
  9. reflex/components/base/__init__.pyi +26 -0
  10. reflex/components/base/fragment.py +3 -0
  11. reflex/components/base/fragment.pyi +2 -0
  12. reflex/components/base/head.py +3 -0
  13. reflex/components/base/head.pyi +2 -0
  14. reflex/components/base/script.py +3 -0
  15. reflex/components/base/script.pyi +2 -0
  16. reflex/components/core/__init__.py +51 -37
  17. reflex/components/core/__init__.pyi +39 -0
  18. reflex/components/core/banner.py +7 -1
  19. reflex/components/core/banner.pyi +6 -1
  20. reflex/components/core/debounce.py +3 -0
  21. reflex/components/core/debounce.pyi +2 -0
  22. reflex/components/core/foreach.py +3 -0
  23. reflex/components/core/html.py +3 -0
  24. reflex/components/core/html.pyi +2 -0
  25. reflex/components/core/match.py +3 -0
  26. reflex/components/core/responsive.py +1 -1
  27. reflex/components/core/upload.py +5 -2
  28. reflex/components/core/upload.pyi +4 -2
  29. reflex/components/datadisplay/__init__.py +17 -8
  30. reflex/components/datadisplay/__init__.pyi +14 -0
  31. reflex/components/datadisplay/code.py +3 -0
  32. reflex/components/datadisplay/code.pyi +2 -0
  33. reflex/components/datadisplay/dataeditor.py +4 -0
  34. reflex/components/datadisplay/dataeditor.pyi +3 -0
  35. reflex/components/el/__init__.py +15 -1
  36. reflex/components/el/__init__.pyi +227 -0
  37. reflex/components/el/elements/__init__.py +129 -220
  38. reflex/components/el/elements/__init__.pyi +341 -0
  39. reflex/components/el/elements/forms.py +15 -0
  40. reflex/components/el/elements/forms.pyi +14 -0
  41. reflex/components/el/elements/inline.py +30 -0
  42. reflex/components/el/elements/inline.pyi +29 -0
  43. reflex/components/el/elements/media.py +16 -0
  44. reflex/components/el/elements/media.pyi +15 -0
  45. reflex/components/el/elements/metadata.py +7 -0
  46. reflex/components/el/elements/metadata.pyi +6 -0
  47. reflex/components/el/elements/other.py +9 -0
  48. reflex/components/el/elements/other.pyi +8 -0
  49. reflex/components/el/elements/scripts.py +5 -0
  50. reflex/components/el/elements/scripts.pyi +4 -0
  51. reflex/components/el/elements/sectioning.py +17 -0
  52. reflex/components/el/elements/sectioning.pyi +16 -0
  53. reflex/components/el/elements/tables.py +12 -0
  54. reflex/components/el/elements/tables.pyi +11 -0
  55. reflex/components/el/elements/typography.py +16 -0
  56. reflex/components/el/elements/typography.pyi +15 -0
  57. reflex/components/moment/__init__.py +1 -1
  58. reflex/components/plotly/plotly.py +184 -6
  59. reflex/components/plotly/plotly.pyi +62 -4
  60. reflex/components/radix/__init__.py +14 -2
  61. reflex/components/radix/__init__.pyi +73 -0
  62. reflex/components/radix/primitives/__init__.py +13 -5
  63. reflex/components/radix/primitives/__init__.pyi +11 -0
  64. reflex/components/radix/themes/__init__.py +20 -6
  65. reflex/components/radix/themes/__init__.pyi +13 -0
  66. reflex/components/radix/themes/base.py +26 -20
  67. reflex/components/radix/themes/base.pyi +4 -1
  68. reflex/components/radix/themes/color_mode.py +3 -1
  69. reflex/components/radix/themes/color_mode.pyi +3 -1
  70. reflex/components/radix/themes/components/__init__.py +11 -79
  71. reflex/components/radix/themes/components/__init__.pyi +44 -0
  72. reflex/components/radix/themes/components/alert_dialog.py +2 -2
  73. reflex/components/radix/themes/components/alert_dialog.pyi +2 -2
  74. reflex/components/radix/themes/components/badge.py +2 -2
  75. reflex/components/radix/themes/components/badge.pyi +2 -2
  76. reflex/components/radix/themes/components/button.py +2 -2
  77. reflex/components/radix/themes/components/button.pyi +2 -2
  78. reflex/components/radix/themes/components/callout.py +4 -4
  79. reflex/components/radix/themes/components/callout.pyi +4 -4
  80. reflex/components/radix/themes/components/card.py +2 -2
  81. reflex/components/radix/themes/components/card.pyi +2 -2
  82. reflex/components/radix/themes/components/dialog.py +2 -2
  83. reflex/components/radix/themes/components/dialog.pyi +2 -2
  84. reflex/components/radix/themes/components/hover_card.py +2 -2
  85. reflex/components/radix/themes/components/hover_card.pyi +2 -2
  86. reflex/components/radix/themes/components/icon_button.py +2 -2
  87. reflex/components/radix/themes/components/icon_button.pyi +2 -2
  88. reflex/components/radix/themes/components/inset.py +2 -2
  89. reflex/components/radix/themes/components/inset.pyi +2 -2
  90. reflex/components/radix/themes/components/popover.py +2 -2
  91. reflex/components/radix/themes/components/popover.pyi +2 -2
  92. reflex/components/radix/themes/components/table.py +8 -8
  93. reflex/components/radix/themes/components/table.pyi +8 -8
  94. reflex/components/radix/themes/components/text_area.py +11 -2
  95. reflex/components/radix/themes/components/text_area.pyi +18 -3
  96. reflex/components/radix/themes/components/text_field.py +3 -3
  97. reflex/components/radix/themes/components/text_field.pyi +3 -3
  98. reflex/components/radix/themes/layout/__init__.py +12 -38
  99. reflex/components/radix/themes/layout/__init__.pyi +21 -0
  100. reflex/components/radix/themes/layout/box.py +5 -2
  101. reflex/components/radix/themes/layout/box.pyi +4 -2
  102. reflex/components/radix/themes/layout/center.py +3 -0
  103. reflex/components/radix/themes/layout/center.pyi +2 -0
  104. reflex/components/radix/themes/layout/container.py +5 -2
  105. reflex/components/radix/themes/layout/container.pyi +4 -2
  106. reflex/components/radix/themes/layout/flex.py +5 -2
  107. reflex/components/radix/themes/layout/flex.pyi +4 -2
  108. reflex/components/radix/themes/layout/grid.py +5 -2
  109. reflex/components/radix/themes/layout/grid.pyi +4 -2
  110. reflex/components/radix/themes/layout/list.py +14 -0
  111. reflex/components/radix/themes/layout/list.pyi +3 -0
  112. reflex/components/radix/themes/layout/section.py +7 -4
  113. reflex/components/radix/themes/layout/section.pyi +5 -3
  114. reflex/components/radix/themes/layout/spacer.py +3 -0
  115. reflex/components/radix/themes/layout/spacer.pyi +2 -0
  116. reflex/components/radix/themes/layout/stack.py +5 -0
  117. reflex/components/radix/themes/layout/stack.pyi +4 -0
  118. reflex/components/radix/themes/typography/__init__.py +11 -16
  119. reflex/components/radix/themes/typography/__init__.pyi +12 -0
  120. reflex/components/radix/themes/typography/blockquote.py +5 -2
  121. reflex/components/radix/themes/typography/blockquote.pyi +4 -2
  122. reflex/components/radix/themes/typography/code.py +5 -2
  123. reflex/components/radix/themes/typography/code.pyi +4 -2
  124. reflex/components/radix/themes/typography/heading.py +5 -2
  125. reflex/components/radix/themes/typography/heading.pyi +4 -2
  126. reflex/components/radix/themes/typography/link.py +3 -0
  127. reflex/components/radix/themes/typography/link.pyi +2 -0
  128. reflex/components/radix/themes/typography/text.py +6 -6
  129. reflex/components/radix/themes/typography/text.pyi +6 -6
  130. reflex/components/recharts/__init__.py +114 -104
  131. reflex/components/recharts/__init__.pyi +106 -0
  132. reflex/components/recharts/cartesian.py +17 -0
  133. reflex/components/recharts/cartesian.pyi +16 -0
  134. reflex/components/recharts/charts.py +12 -0
  135. reflex/components/recharts/charts.pyi +11 -0
  136. reflex/components/recharts/general.py +7 -0
  137. reflex/components/recharts/general.pyi +6 -0
  138. reflex/components/recharts/polar.py +11 -0
  139. reflex/components/recharts/polar.pyi +9 -0
  140. reflex/config.py +3 -0
  141. reflex/constants/__init__.py +0 -2
  142. reflex/constants/base.py +2 -0
  143. reflex/constants/base.pyi +5 -0
  144. reflex/constants/installer.py +2 -1
  145. reflex/experimental/__init__.py +2 -0
  146. reflex/experimental/assets.py +56 -0
  147. reflex/experimental/client_state.py +4 -2
  148. reflex/experimental/hooks.py +8 -6
  149. reflex/experimental/layout.py +3 -1
  150. reflex/state.py +54 -4
  151. reflex/utils/exec.py +8 -0
  152. reflex/utils/lazy_loader.py +33 -0
  153. reflex/utils/prerequisites.py +1 -14
  154. reflex/utils/pyi_generator.py +71 -20
  155. reflex/utils/serializers.py +3 -3
  156. reflex/vars.py +79 -5
  157. reflex/vars.pyi +16 -0
  158. {reflex-0.5.2a1.dist-info → reflex-0.5.3a1.dist-info}/METADATA +2 -1
  159. {reflex-0.5.2a1.dist-info → reflex-0.5.3a1.dist-info}/RECORD +162 -148
  160. reflex/config.pyi +0 -112
  161. {reflex-0.5.2a1.dist-info → reflex-0.5.3a1.dist-info}/LICENSE +0 -0
  162. {reflex-0.5.2a1.dist-info → reflex-0.5.3a1.dist-info}/WHEEL +0 -0
  163. {reflex-0.5.2a1.dist-info → reflex-0.5.3a1.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,12 @@
1
1
  """Add standard Hooks wrapper for React."""
2
2
 
3
+ from typing import Optional, Union
4
+
3
5
  from reflex.utils.imports import ImportVar
4
6
  from reflex.vars import Var, VarData
5
7
 
6
8
 
7
- def _add_react_import(v: Var | None, tags: str | list):
9
+ def _add_react_import(v: Optional[Var], tags: Union[str, list]):
8
10
  if v is None:
9
11
  return
10
12
 
@@ -16,7 +18,7 @@ def _add_react_import(v: Var | None, tags: str | list):
16
18
  )
17
19
 
18
20
 
19
- def const(name, value) -> Var | None:
21
+ def const(name, value) -> Optional[Var]:
20
22
  """Create a constant Var.
21
23
 
22
24
  Args:
@@ -31,7 +33,7 @@ def const(name, value) -> Var | None:
31
33
  return Var.create(f"const {name} = {value}")
32
34
 
33
35
 
34
- def useCallback(func, deps) -> Var | None:
36
+ def useCallback(func, deps) -> Optional[Var]:
35
37
  """Create a useCallback hook with a function and dependencies.
36
38
 
37
39
  Args:
@@ -49,7 +51,7 @@ def useCallback(func, deps) -> Var | None:
49
51
  return v
50
52
 
51
53
 
52
- def useContext(context) -> Var | None:
54
+ def useContext(context) -> Optional[Var]:
53
55
  """Create a useContext hook with a context.
54
56
 
55
57
  Args:
@@ -63,7 +65,7 @@ def useContext(context) -> Var | None:
63
65
  return v
64
66
 
65
67
 
66
- def useRef(default) -> Var | None:
68
+ def useRef(default) -> Optional[Var]:
67
69
  """Create a useRef hook with a default value.
68
70
 
69
71
  Args:
@@ -77,7 +79,7 @@ def useRef(default) -> Var | None:
77
79
  return v
78
80
 
79
81
 
80
- def useState(var_name, default=None) -> Var | None:
82
+ def useState(var_name, default=None) -> Optional[Var]:
81
83
  """Create a useState hook with a variable name and setter name.
82
84
 
83
85
  Args:
@@ -9,7 +9,9 @@ from reflex.components.base.fragment import Fragment
9
9
  from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf
10
10
  from reflex.components.radix.primitives.drawer import DrawerRoot, drawer
11
11
  from reflex.components.radix.themes.components.icon_button import IconButton
12
- from reflex.components.radix.themes.layout import Box, Container, HStack
12
+ from reflex.components.radix.themes.layout.box import Box
13
+ from reflex.components.radix.themes.layout.container import Container
14
+ from reflex.components.radix.themes.layout.stack import HStack
13
15
  from reflex.event import call_script
14
16
  from reflex.experimental import hooks
15
17
  from reflex.state import ComponentState
reflex/state.py CHANGED
@@ -7,6 +7,7 @@ import contextlib
7
7
  import copy
8
8
  import functools
9
9
  import inspect
10
+ import os
10
11
  import traceback
11
12
  import uuid
12
13
  from abc import ABC, abstractmethod
@@ -35,6 +36,7 @@ except ModuleNotFoundError:
35
36
 
36
37
  import wrapt
37
38
  from redis.asyncio import Redis
39
+ from redis.exceptions import ResponseError
38
40
 
39
41
  from reflex import constants
40
42
  from reflex.base import Base
@@ -1536,6 +1538,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1536
1538
  if actual_var is not None:
1537
1539
  actual_var.mark_dirty(instance=self)
1538
1540
 
1541
+ def _expired_computed_vars(self) -> set[str]:
1542
+ """Determine ComputedVars that need to be recalculated based on the expiration time.
1543
+
1544
+ Returns:
1545
+ Set of computed vars to include in the delta.
1546
+ """
1547
+ return set(
1548
+ cvar
1549
+ for cvar in self.computed_vars
1550
+ if self.computed_vars[cvar].needs_update(instance=self)
1551
+ )
1552
+
1539
1553
  def _dirty_computed_vars(self, from_vars: set[str] | None = None) -> set[str]:
1540
1554
  """Determine ComputedVars that need to be recalculated based on the given vars.
1541
1555
 
@@ -1588,6 +1602,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1588
1602
  # and always dirty computed vars (cache=False)
1589
1603
  delta_vars = (
1590
1604
  self.dirty_vars.intersection(self.base_vars)
1605
+ .union(self.dirty_vars.intersection(self.computed_vars))
1591
1606
  .union(self._dirty_computed_vars())
1592
1607
  .union(self._always_dirty_computed_vars)
1593
1608
  )
@@ -1621,6 +1636,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1621
1636
  self.parent_state.dirty_substates.add(self.get_name())
1622
1637
  self.parent_state._mark_dirty()
1623
1638
 
1639
+ # Append expired computed vars to dirty_vars to trigger recalculation
1640
+ self.dirty_vars.update(self._expired_computed_vars())
1641
+
1624
1642
  # have to mark computed vars dirty to allow access to newly computed
1625
1643
  # values within the same ComputedVar function
1626
1644
  self._mark_dirty_computed_vars()
@@ -2622,13 +2640,26 @@ class StateManagerRedis(StateManager):
2622
2640
  Args:
2623
2641
  lock_key: The redis key for the lock.
2624
2642
  lock_id: The ID of the lock.
2643
+
2644
+ Raises:
2645
+ ResponseError: when the keyspace config cannot be set.
2625
2646
  """
2626
2647
  state_is_locked = False
2627
2648
  lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
2628
2649
  # Enable keyspace notifications for the lock key, so we know when it is available.
2629
- await self.redis.config_set(
2630
- "notify-keyspace-events", self._redis_notify_keyspace_events
2631
- )
2650
+ try:
2651
+ await self.redis.config_set(
2652
+ "notify-keyspace-events",
2653
+ self._redis_notify_keyspace_events,
2654
+ )
2655
+ except ResponseError:
2656
+ # Some redis servers only allow out-of-band configuration, so ignore errors here.
2657
+ ignore_config_error = os.environ.get(
2658
+ "REFLEX_IGNORE_REDIS_CONFIG_ERROR",
2659
+ None,
2660
+ )
2661
+ if not ignore_config_error:
2662
+ raise
2632
2663
  async with self.redis.pubsub() as pubsub:
2633
2664
  await pubsub.psubscribe(lock_key_channel)
2634
2665
  while not state_is_locked:
@@ -2836,6 +2867,11 @@ class MutableProxy(wrapt.ObjectProxy):
2836
2867
  ]
2837
2868
  )
2838
2869
 
2870
+ # These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
2871
+ __never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
2872
+ pydantic.BaseModel.__dict__
2873
+ )
2874
+
2839
2875
  __mutable_types__ = (list, dict, set, Base)
2840
2876
 
2841
2877
  def __init__(self, wrapped: Any, state: BaseState, field_name: str):
@@ -2885,7 +2921,10 @@ class MutableProxy(wrapt.ObjectProxy):
2885
2921
  Returns:
2886
2922
  The wrapped value.
2887
2923
  """
2888
- if isinstance(value, self.__mutable_types__):
2924
+ # Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
2925
+ if isinstance(value, self.__mutable_types__) and not isinstance(
2926
+ value, MutableProxy
2927
+ ):
2889
2928
  return type(self)(
2890
2929
  wrapped=value,
2891
2930
  state=self._self_state,
@@ -2932,6 +2971,17 @@ class MutableProxy(wrapt.ObjectProxy):
2932
2971
  self._wrap_recursive_decorator,
2933
2972
  )
2934
2973
 
2974
+ if (
2975
+ isinstance(self.__wrapped__, Base)
2976
+ and __name not in self.__never_wrap_base_attrs__
2977
+ and hasattr(value, "__func__")
2978
+ ):
2979
+ # Wrap methods called on Base subclasses, which might do _anything_
2980
+ return wrapt.FunctionWrapper(
2981
+ functools.partial(value.__func__, self),
2982
+ self._wrap_recursive_decorator,
2983
+ )
2984
+
2935
2985
  if isinstance(value, self.__mutable_types__) and __name not in (
2936
2986
  "__wrapped__",
2937
2987
  "_self_state",
reflex/utils/exec.py CHANGED
@@ -113,6 +113,14 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True):
113
113
  else:
114
114
  console.print("New packages detected: Updating app...")
115
115
  else:
116
+ if any(
117
+ [x in line for x in ("bin executable does not exist on disk",)]
118
+ ):
119
+ console.error(
120
+ "Try setting `REFLEX_USE_NPM=1` and re-running `reflex init` and `reflex run` to use npm instead of bun:\n"
121
+ "`REFLEX_USE_NPM=1 reflex init`\n"
122
+ "`REFLEX_USE_NPM=1 reflex run`"
123
+ )
116
124
  new_hash = detect_package_change(json_file_path)
117
125
  if new_hash != last_hash:
118
126
  last_hash = new_hash
@@ -0,0 +1,33 @@
1
+ """Module to implement lazy loading in reflex."""
2
+ import copy
3
+
4
+ import lazy_loader as lazy
5
+
6
+
7
+ def attach(package_name, submodules=None, submod_attrs=None):
8
+ """Replaces a package's __getattr__, __dir__, and __all__ attributes using lazy.attach.
9
+ The lazy loader __getattr__ doesn't support tuples as list values. We needed to add
10
+ this functionality (tuples) in Reflex to support 'import as _' statements. This function
11
+ reformats the submod_attrs dictionary to flatten the module list before passing it to
12
+ lazy_loader.
13
+
14
+ Args:
15
+ package_name: name of the package.
16
+ submodules : List of submodules to attach.
17
+ submod_attrs : Dictionary of submodule -> list of attributes / functions.
18
+ These attributes are imported as they are used.
19
+
20
+ Returns:
21
+ __getattr__, __dir__, __all__
22
+ """
23
+ _submod_attrs = copy.deepcopy(submod_attrs)
24
+ if _submod_attrs:
25
+ for k, v in _submod_attrs.items():
26
+ # when flattening the list, only keep the alias in the tuple(mod[1])
27
+ _submod_attrs[k] = [
28
+ mod if not isinstance(mod, tuple) else mod[1] for mod in v
29
+ ]
30
+
31
+ return lazy.attach(
32
+ package_name=package_name, submodules=submodules, submod_attrs=_submod_attrs
33
+ )
@@ -332,19 +332,6 @@ def parse_redis_url() -> str | dict | None:
332
332
  return dict(host=redis_url, port=int(redis_port), db=0)
333
333
 
334
334
 
335
- def get_production_backend_url() -> str:
336
- """Get the production backend URL.
337
-
338
- Returns:
339
- The production backend URL.
340
- """
341
- config = get_config()
342
- return constants.PRODUCTION_BACKEND_URL.format(
343
- username=config.username,
344
- app_name=config.app_name,
345
- )
346
-
347
-
348
335
  def validate_app_name(app_name: str | None = None) -> str:
349
336
  """Validate the app name.
350
337
 
@@ -625,7 +612,7 @@ def _update_next_config(
625
612
  next_config = {
626
613
  "basePath": config.frontend_path or "",
627
614
  "compress": config.next_compression,
628
- "reactStrictMode": True,
615
+ "reactStrictMode": config.react_strict_mode,
629
616
  "trailingSlash": True,
630
617
  }
631
618
  if transpile_packages:
@@ -29,11 +29,9 @@ from reflex.vars import Var
29
29
 
30
30
  logger = logging.getLogger("pyi_generator")
31
31
 
32
- INIT_FILE = Path("reflex/__init__.pyi").resolve()
33
32
  PWD = Path(".").resolve()
34
33
 
35
34
  EXCLUDED_FILES = [
36
- "__init__.py",
37
35
  # "app.py",
38
36
  "component.py",
39
37
  "bare.py",
@@ -322,6 +320,7 @@ def _extract_class_props_as_ast_nodes(
322
320
  all_props = []
323
321
  kwargs = []
324
322
  for target_class in clzs:
323
+ event_triggers = target_class().get_event_triggers()
325
324
  # Import from the target class to ensure type hints are resolvable.
326
325
  exec(f"from {target_class.__module__} import *", type_hint_globals)
327
326
  for name, value in target_class.__annotations__.items():
@@ -329,6 +328,7 @@ def _extract_class_props_as_ast_nodes(
329
328
  name in spec.kwonlyargs
330
329
  or name in EXCLUDED_PROPS
331
330
  or name in all_props
331
+ or name in event_triggers
332
332
  or (isinstance(value, str) and "ClassVar" in value)
333
333
  ):
334
334
  continue
@@ -775,6 +775,13 @@ class StubGenerator(ast.NodeTransformer):
775
775
  # Remove annotated assignments in Component classes (props)
776
776
  return None
777
777
 
778
+ # remove dunder method assignments for lazy_loader.attach
779
+ for target in node.targets:
780
+ if isinstance(target, ast.Tuple):
781
+ for name in target.elts:
782
+ if isinstance(name, ast.Name) and name.id.startswith("_"):
783
+ return
784
+
778
785
  return node
779
786
 
780
787
  def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign | None:
@@ -805,6 +812,23 @@ class StubGenerator(ast.NodeTransformer):
805
812
  return node
806
813
 
807
814
 
815
+ class InitStubGenerator(StubGenerator):
816
+ """A node transformer that will generate the stubs for a given init file."""
817
+
818
+ def visit_Import(
819
+ self, node: ast.Import | ast.ImportFrom
820
+ ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom]:
821
+ """Collect import statements from the init module.
822
+
823
+ Args:
824
+ node: The import node to visit.
825
+
826
+ Returns:
827
+ The modified import node(s).
828
+ """
829
+ return [node]
830
+
831
+
808
832
  class PyiGenerator:
809
833
  """A .pyi file generator that will scan all defined Component in Reflex and
810
834
  generate the approriate stub.
@@ -842,6 +866,37 @@ class PyiGenerator:
842
866
  pyi_path.write_text("\n".join(pyi_content))
843
867
  logger.info(f"Wrote {relpath}")
844
868
 
869
+ def _get_init_lazy_imports(self, mod, new_tree):
870
+ # retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present.
871
+ sub_mods = getattr(mod, "_SUBMODULES", None)
872
+ sub_mod_attrs = getattr(mod, "_SUBMOD_ATTRS", None)
873
+
874
+ if not sub_mods and not sub_mod_attrs:
875
+ return
876
+ sub_mods_imports = []
877
+ sub_mod_attrs_imports = []
878
+
879
+ if sub_mods:
880
+ sub_mods_imports = [
881
+ f"from . import {mod} as {mod}" for mod in sorted(sub_mods)
882
+ ]
883
+ sub_mods_imports.append("")
884
+
885
+ if sub_mod_attrs:
886
+ sub_mod_attrs = {
887
+ attr: mod for mod, attrs in sub_mod_attrs.items() for attr in attrs
888
+ }
889
+ # construct the import statement and handle special cases for aliases
890
+ sub_mod_attrs_imports = [
891
+ f"from .{path} import {mod if not isinstance(mod, tuple) else mod[0]} as {mod if not isinstance(mod, tuple) else mod[1]}"
892
+ for mod, path in sub_mod_attrs.items()
893
+ ]
894
+ sub_mod_attrs_imports.append("")
895
+
896
+ text = "\n" + "\n".join([*sub_mods_imports, *sub_mod_attrs_imports])
897
+ text += ast.unparse(new_tree) + "\n"
898
+ return text
899
+
845
900
  def _scan_file(self, module_path: Path):
846
901
  module_import = (
847
902
  _relative_to_pwd(module_path)
@@ -860,13 +915,22 @@ class PyiGenerator:
860
915
  and obj != Component
861
916
  and inspect.getmodule(obj) == module
862
917
  }
863
- if not class_names:
918
+ is_init_file = _relative_to_pwd(module_path).name == "__init__.py"
919
+ if not class_names and not is_init_file:
864
920
  return
865
921
 
866
- new_tree = StubGenerator(module, class_names).visit(
867
- ast.parse(inspect.getsource(module))
868
- )
869
- self._write_pyi_file(module_path, ast.unparse(new_tree))
922
+ if is_init_file:
923
+ new_tree = InitStubGenerator(module, class_names).visit(
924
+ ast.parse(inspect.getsource(module))
925
+ )
926
+ init_imports = self._get_init_lazy_imports(module, new_tree)
927
+ if init_imports:
928
+ self._write_pyi_file(module_path, init_imports)
929
+ else:
930
+ new_tree = StubGenerator(module, class_names).visit(
931
+ ast.parse(inspect.getsource(module))
932
+ )
933
+ self._write_pyi_file(module_path, ast.unparse(new_tree))
870
934
 
871
935
  def _scan_files_multiprocess(self, files: list[Path]):
872
936
  with Pool(processes=cpu_count()) as pool:
@@ -922,16 +986,3 @@ class PyiGenerator:
922
986
  self._scan_files(file_targets)
923
987
  else:
924
988
  self._scan_files_multiprocess(file_targets)
925
-
926
-
927
- def generate_init():
928
- """Generate a pyi file for the main __init__.py."""
929
- from reflex import _MAPPING # type: ignore
930
-
931
- imports = [
932
- f"from {path if mod != path.rsplit('.')[-1] or mod == 'page' else '.'.join(path.rsplit('.')[:-1])} import {mod} as {mod}"
933
- for mod, path in _MAPPING.items()
934
- ]
935
- imports.append("")
936
- with contextlib.suppress(Exception):
937
- INIT_FILE.write_text("\n".join(imports))
@@ -255,7 +255,7 @@ def serialize_enum(en: Enum) -> str:
255
255
  en: The enum to serialize.
256
256
 
257
257
  Returns:
258
- The serialized enum.
258
+ The serialized enum.
259
259
  """
260
260
  return en.value
261
261
 
@@ -313,7 +313,7 @@ try:
313
313
  from plotly.io import to_json
314
314
 
315
315
  @serializer
316
- def serialize_figure(figure: Figure) -> list:
316
+ def serialize_figure(figure: Figure) -> dict:
317
317
  """Serialize a plotly figure.
318
318
 
319
319
  Args:
@@ -322,7 +322,7 @@ try:
322
322
  Returns:
323
323
  The serialized figure.
324
324
  """
325
- return json.loads(str(to_json(figure)))["data"]
325
+ return json.loads(str(to_json(figure)))
326
326
 
327
327
  except ImportError:
328
328
  pass
reflex/vars.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import contextlib
6
6
  import dataclasses
7
+ import datetime
7
8
  import dis
8
9
  import functools
9
10
  import inspect
@@ -1873,13 +1874,26 @@ class ComputedVar(Var, property):
1873
1874
  # Whether to track dependencies and cache computed values
1874
1875
  _cache: bool = dataclasses.field(default=False)
1875
1876
 
1876
- _initial_value: Any | types.Unset = dataclasses.field(default_factory=types.Unset)
1877
+ # The initial value of the computed var
1878
+ _initial_value: Any | types.Unset = dataclasses.field(default=types.Unset())
1879
+
1880
+ # Explicit var dependencies to track
1881
+ _static_deps: set[str] = dataclasses.field(default_factory=set)
1882
+
1883
+ # Whether var dependencies should be auto-determined
1884
+ _auto_deps: bool = dataclasses.field(default=True)
1885
+
1886
+ # Interval at which the computed var should be updated
1887
+ _update_interval: Optional[datetime.timedelta] = dataclasses.field(default=None)
1877
1888
 
1878
1889
  def __init__(
1879
1890
  self,
1880
1891
  fget: Callable[[BaseState], Any],
1881
1892
  initial_value: Any | types.Unset = types.Unset(),
1882
1893
  cache: bool = False,
1894
+ deps: Optional[List[Union[str, Var]]] = None,
1895
+ auto_deps: bool = True,
1896
+ interval: Optional[Union[int, datetime.timedelta]] = None,
1883
1897
  **kwargs,
1884
1898
  ):
1885
1899
  """Initialize a ComputedVar.
@@ -1888,10 +1902,22 @@ class ComputedVar(Var, property):
1888
1902
  fget: The getter function.
1889
1903
  initial_value: The initial value of the computed var.
1890
1904
  cache: Whether to cache the computed value.
1905
+ deps: Explicit var dependencies to track.
1906
+ auto_deps: Whether var dependencies should be auto-determined.
1907
+ interval: Interval at which the computed var should be updated.
1891
1908
  **kwargs: additional attributes to set on the instance
1892
1909
  """
1893
1910
  self._initial_value = initial_value
1894
1911
  self._cache = cache
1912
+ if isinstance(interval, int):
1913
+ interval = datetime.timedelta(seconds=interval)
1914
+ self._update_interval = interval
1915
+ if deps is None:
1916
+ deps = []
1917
+ self._static_deps = {
1918
+ dep._var_name if isinstance(dep, Var) else dep for dep in deps
1919
+ }
1920
+ self._auto_deps = auto_deps
1895
1921
  property.__init__(self, fget)
1896
1922
  kwargs["_var_name"] = kwargs.pop("_var_name", fget.__name__)
1897
1923
  kwargs["_var_type"] = kwargs.pop("_var_type", self._determine_var_type())
@@ -1912,6 +1938,9 @@ class ComputedVar(Var, property):
1912
1938
  fget=kwargs.get("fget", self.fget),
1913
1939
  initial_value=kwargs.get("initial_value", self._initial_value),
1914
1940
  cache=kwargs.get("cache", self._cache),
1941
+ deps=kwargs.get("deps", self._static_deps),
1942
+ auto_deps=kwargs.get("auto_deps", self._auto_deps),
1943
+ interval=kwargs.get("interval", self._update_interval),
1915
1944
  _var_name=kwargs.get("_var_name", self._var_name),
1916
1945
  _var_type=kwargs.get("_var_type", self._var_type),
1917
1946
  _var_is_local=kwargs.get("_var_is_local", self._var_is_local),
@@ -1932,7 +1961,32 @@ class ComputedVar(Var, property):
1932
1961
  """
1933
1962
  return f"__cached_{self._var_name}"
1934
1963
 
1935
- def __get__(self, instance, owner):
1964
+ @property
1965
+ def _last_updated_attr(self) -> str:
1966
+ """Get the attribute used to store the last updated timestamp.
1967
+
1968
+ Returns:
1969
+ An attribute name.
1970
+ """
1971
+ return f"__last_updated_{self._var_name}"
1972
+
1973
+ def needs_update(self, instance: BaseState) -> bool:
1974
+ """Check if the computed var needs to be updated.
1975
+
1976
+ Args:
1977
+ instance: The state instance that the computed var is attached to.
1978
+
1979
+ Returns:
1980
+ True if the computed var needs to be updated, False otherwise.
1981
+ """
1982
+ if self._update_interval is None:
1983
+ return False
1984
+ last_updated = getattr(instance, self._last_updated_attr, None)
1985
+ if last_updated is None:
1986
+ return True
1987
+ return datetime.datetime.now() - last_updated > self._update_interval
1988
+
1989
+ def __get__(self, instance: BaseState | None, owner):
1936
1990
  """Get the ComputedVar value.
1937
1991
 
1938
1992
  If the value is already cached on the instance, return the cached value.
@@ -1948,10 +2002,13 @@ class ComputedVar(Var, property):
1948
2002
  return super().__get__(instance, owner)
1949
2003
 
1950
2004
  # handle caching
1951
- if not hasattr(instance, self._cache_attr):
2005
+ if not hasattr(instance, self._cache_attr) or self.needs_update(instance):
2006
+ # Set cache attr on state instance.
1952
2007
  setattr(instance, self._cache_attr, super().__get__(instance, owner))
1953
2008
  # Ensure the computed var gets serialized to redis.
1954
2009
  instance._was_touched = True
2010
+ # Set the last updated timestamp on the state instance.
2011
+ setattr(instance, self._last_updated_attr, datetime.datetime.now())
1955
2012
  return getattr(instance, self._cache_attr)
1956
2013
 
1957
2014
  def _deps(
@@ -1978,7 +2035,9 @@ class ComputedVar(Var, property):
1978
2035
  VarValueError: if the function references the get_state, parent_state, or substates attributes
1979
2036
  (cannot track deps in a related state, only implicitly via parent state).
1980
2037
  """
1981
- d = set()
2038
+ if not self._auto_deps:
2039
+ return self._static_deps
2040
+ d = self._static_deps.copy()
1982
2041
  if obj is None:
1983
2042
  fget = property.__getattribute__(self, "fget")
1984
2043
  if fget is not None:
@@ -2076,6 +2135,9 @@ def computed_var(
2076
2135
  fget: Callable[[BaseState], Any] | None = None,
2077
2136
  initial_value: Any | None = None,
2078
2137
  cache: bool = False,
2138
+ deps: Optional[List[Union[str, Var]]] = None,
2139
+ auto_deps: bool = True,
2140
+ interval: Optional[Union[datetime.timedelta, int]] = None,
2079
2141
  **kwargs,
2080
2142
  ) -> ComputedVar | Callable[[Callable[[BaseState], Any]], ComputedVar]:
2081
2143
  """A ComputedVar decorator with or without kwargs.
@@ -2084,19 +2146,31 @@ def computed_var(
2084
2146
  fget: The getter function.
2085
2147
  initial_value: The initial value of the computed var.
2086
2148
  cache: Whether to cache the computed value.
2149
+ deps: Explicit var dependencies to track.
2150
+ auto_deps: Whether var dependencies should be auto-determined.
2151
+ interval: Interval at which the computed var should be updated.
2087
2152
  **kwargs: additional attributes to set on the instance
2088
2153
 
2089
2154
  Returns:
2090
2155
  A ComputedVar instance.
2156
+
2157
+ Raises:
2158
+ ValueError: If caching is disabled and an update interval is set.
2091
2159
  """
2160
+ if cache is False and interval is not None:
2161
+ raise ValueError("Cannot set update interval without caching.")
2162
+
2092
2163
  if fget is not None:
2093
2164
  return ComputedVar(fget=fget, cache=cache)
2094
2165
 
2095
- def wrapper(fget):
2166
+ def wrapper(fget: Callable[[BaseState], Any]) -> ComputedVar:
2096
2167
  return ComputedVar(
2097
2168
  fget=fget,
2098
2169
  initial_value=initial_value,
2099
2170
  cache=cache,
2171
+ deps=deps,
2172
+ auto_deps=auto_deps,
2173
+ interval=interval,
2100
2174
  **kwargs,
2101
2175
  )
2102
2176
 
reflex/vars.pyi CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import datetime
5
6
  from dataclasses import dataclass
6
7
  from _typeshed import Incomplete
7
8
  from reflex import constants as constants
@@ -141,6 +142,7 @@ class ComputedVar(Var):
141
142
  def _deps(self, objclass: Type, obj: Optional[FunctionType] = ...) -> Set[str]: ...
142
143
  def _replace(self, merge_var_data=None, **kwargs: Any) -> ComputedVar: ...
143
144
  def mark_dirty(self, instance) -> None: ...
145
+ def needs_update(self, instance) -> bool: ...
144
146
  def _determine_var_type(self) -> Type: ...
145
147
  @overload
146
148
  def __init__(
@@ -155,10 +157,24 @@ class ComputedVar(Var):
155
157
  def computed_var(
156
158
  fget: Callable[[BaseState], Any] | None = None,
157
159
  initial_value: Any | None = None,
160
+ cache: bool = False,
161
+ deps: Optional[List[Union[str, Var]]] = None,
162
+ auto_deps: bool = True,
163
+ interval: Optional[Union[datetime.timedelta, int]] = None,
158
164
  **kwargs,
159
165
  ) -> Callable[[Callable[[Any], Any]], ComputedVar]: ...
160
166
  @overload
161
167
  def computed_var(fget: Callable[[Any], Any]) -> ComputedVar: ...
168
+ @overload
169
+ def cached_var(
170
+ fget: Callable[[BaseState], Any] | None = None,
171
+ initial_value: Any | None = None,
172
+ deps: Optional[List[Union[str, Var]]] = None,
173
+ auto_deps: bool = True,
174
+ interval: Optional[Union[datetime.timedelta, int]] = None,
175
+ **kwargs,
176
+ ) -> Callable[[Callable[[Any], Any]], ComputedVar]: ...
177
+ @overload
162
178
  def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ...
163
179
 
164
180
  class CallableVar(BaseVar):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reflex
3
- Version: 0.5.2a1
3
+ Version: 0.5.3a1
4
4
  Summary: Web apps in pure Python.
5
5
  Home-page: https://reflex.dev
6
6
  License: Apache-2.0
@@ -25,6 +25,7 @@ Requires-Dist: fastapi (>=0.96.0,<1.0)
25
25
  Requires-Dist: gunicorn (>=20.1.0,<23.0)
26
26
  Requires-Dist: httpx (>=0.25.1,<1.0)
27
27
  Requires-Dist: jinja2 (>=3.1.2,<4.0)
28
+ Requires-Dist: lazy_loader (>=0.4)
28
29
  Requires-Dist: packaging (>=23.1,<25.0)
29
30
  Requires-Dist: platformdirs (>=3.10.0,<5.0)
30
31
  Requires-Dist: psutil (>=5.9.4,<6.0)