reflex 0.5.6a1__py3-none-any.whl → 0.5.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 (43) hide show
  1. reflex/.templates/jinja/web/utils/context.js.jinja2 +2 -0
  2. reflex/.templates/web/utils/state.js +31 -2
  3. reflex/__init__.py +7 -1
  4. reflex/__init__.pyi +3 -0
  5. reflex/app.py +190 -4
  6. reflex/compiler/templates.py +1 -0
  7. reflex/components/base/__init__.py +4 -0
  8. reflex/components/base/__init__.pyi +2 -0
  9. reflex/components/base/error_boundary.py +78 -0
  10. reflex/components/base/error_boundary.pyi +98 -0
  11. reflex/components/core/foreach.py +3 -2
  12. reflex/components/core/upload.py +1 -1
  13. reflex/components/radix/primitives/form.py +3 -2
  14. reflex/components/recharts/cartesian.py +31 -16
  15. reflex/components/recharts/cartesian.pyi +46 -22
  16. reflex/components/recharts/charts.py +6 -6
  17. reflex/components/recharts/charts.pyi +10 -10
  18. reflex/components/recharts/general.py +7 -0
  19. reflex/components/recharts/general.pyi +5 -0
  20. reflex/components/recharts/recharts.py +16 -1
  21. reflex/components/recharts/recharts.pyi +1 -0
  22. reflex/config.py +2 -1
  23. reflex/constants/compiler.py +20 -2
  24. reflex/constants/config.py +1 -1
  25. reflex/event.py +9 -0
  26. reflex/experimental/__init__.py +2 -0
  27. reflex/experimental/vars/__init__.py +3 -0
  28. reflex/experimental/vars/base.py +210 -0
  29. reflex/state.py +105 -21
  30. reflex/testing.py +28 -0
  31. reflex/utils/compat.py +7 -5
  32. reflex/utils/console.py +10 -1
  33. reflex/utils/prerequisites.py +11 -5
  34. reflex/utils/processes.py +7 -2
  35. reflex/utils/pyi_generator.py +4 -1
  36. reflex/utils/types.py +35 -30
  37. reflex/vars.py +58 -23
  38. reflex/vars.pyi +4 -1
  39. {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/METADATA +1 -1
  40. {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/RECORD +43 -39
  41. {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/LICENSE +0 -0
  42. {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/WHEEL +0 -0
  43. {reflex-0.5.6a1.dist-info → reflex-0.5.7.dist-info}/entry_points.txt +0 -0
@@ -62,9 +62,17 @@ class CompileVars(SimpleNamespace):
62
62
  # The name of the function for converting a dict to an event.
63
63
  TO_EVENT = "Event"
64
64
  # The name of the internal on_load event.
65
- ON_LOAD_INTERNAL = "on_load_internal_state.on_load_internal"
65
+ ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal"
66
66
  # The name of the internal event to update generic state vars.
67
- UPDATE_VARS_INTERNAL = "update_vars_internal_state.update_vars_internal"
67
+ UPDATE_VARS_INTERNAL = (
68
+ "reflex___state____update_vars_internal_state.update_vars_internal"
69
+ )
70
+ # The name of the frontend event exception state
71
+ FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state"
72
+ # The full name of the frontend exception state
73
+ FRONTEND_EXCEPTION_STATE_FULL = (
74
+ f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}"
75
+ )
68
76
 
69
77
 
70
78
  class PageNames(SimpleNamespace):
@@ -124,6 +132,16 @@ class Hooks(SimpleNamespace):
124
132
  }
125
133
  })"""
126
134
 
135
+ FRONTEND_ERRORS = f"""
136
+ const logFrontendError = (error, info) => {{
137
+ if (process.env.NODE_ENV === "production") {{
138
+ addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE_FULL}.handle_frontend_exception", {{
139
+ stack: error.stack,
140
+ }})])
141
+ }}
142
+ }}
143
+ """
144
+
127
145
 
128
146
  class MemoizationDisposition(enum.Enum):
129
147
  """The conditions under which a component should be memoized."""
@@ -39,7 +39,7 @@ class GitIgnore(SimpleNamespace):
39
39
  # The gitignore file.
40
40
  FILE = ".gitignore"
41
41
  # Files to gitignore.
42
- DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]"}
42
+ DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]", "assets/external/"}
43
43
 
44
44
 
45
45
  class RequirementsTxt(SimpleNamespace):
reflex/event.py CHANGED
@@ -509,6 +509,15 @@ def console_log(message: str | Var[str]) -> EventSpec:
509
509
  return server_side("_console", get_fn_signature(console_log), message=message)
510
510
 
511
511
 
512
+ def back() -> EventSpec:
513
+ """Do a history.back on the browser.
514
+
515
+ Returns:
516
+ An event to go back one page.
517
+ """
518
+ return call_script("window.history.back()")
519
+
520
+
512
521
  def window_alert(message: str | Var[str]) -> EventSpec:
513
522
  """Create a window alert on the browser.
514
523
 
@@ -8,6 +8,7 @@ from reflex.components.sonner.toast import toast as toast
8
8
 
9
9
  from ..utils.console import warn
10
10
  from . import hooks as hooks
11
+ from . import vars as vars
11
12
  from .assets import asset as asset
12
13
  from .client_state import ClientStateVar as ClientStateVar
13
14
  from .layout import layout as layout
@@ -42,6 +43,7 @@ _x = ExperimentalNamespace(
42
43
  asset=asset,
43
44
  client_state=ClientStateVar.create,
44
45
  hooks=hooks,
46
+ vars=vars,
45
47
  layout=layout,
46
48
  progress=progress,
47
49
  PropsBase=PropsBase,
@@ -0,0 +1,3 @@
1
+ """Experimental Immutable-Based Var System."""
2
+
3
+ from .base import ImmutableVar as ImmutableVar
@@ -0,0 +1,210 @@
1
+ """Collection of base classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import sys
7
+ from typing import Any, Optional, Type
8
+
9
+ from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG
10
+ from reflex.utils import serializers, types
11
+ from reflex.utils.exceptions import VarTypeError
12
+ from reflex.vars import Var, VarData, _decode_var, _extract_var_data, _global_vars
13
+
14
+
15
+ @dataclasses.dataclass(
16
+ eq=False,
17
+ frozen=True,
18
+ **{"slots": True} if sys.version_info >= (3, 10) else {},
19
+ )
20
+ class ImmutableVar(Var):
21
+ """Base class for immutable vars."""
22
+
23
+ # The name of the var.
24
+ _var_name: str = dataclasses.field()
25
+
26
+ # The type of the var.
27
+ _var_type: Type = dataclasses.field(default=Any)
28
+
29
+ # Extra metadata associated with the Var
30
+ _var_data: Optional[VarData] = dataclasses.field(default=None)
31
+
32
+ @property
33
+ def _var_is_local(self) -> bool:
34
+ """Whether this is a local javascript variable.
35
+
36
+ Returns:
37
+ False
38
+ """
39
+ return False
40
+
41
+ @property
42
+ def _var_is_string(self) -> bool:
43
+ """Whether the var is a string literal.
44
+
45
+ Returns:
46
+ False
47
+ """
48
+ return False
49
+
50
+ @property
51
+ def _var_full_name_needs_state_prefix(self) -> bool:
52
+ """Whether the full name of the var needs a _var_state prefix.
53
+
54
+ Returns:
55
+ False
56
+ """
57
+ return False
58
+
59
+ def __post_init__(self):
60
+ """Post-initialize the var."""
61
+ # Decode any inline Var markup and apply it to the instance
62
+ _var_data, _var_name = _decode_var(self._var_name)
63
+ if _var_data:
64
+ self.__init__(
65
+ _var_name, self._var_type, VarData.merge(self._var_data, _var_data)
66
+ )
67
+
68
+ def _replace(self, merge_var_data=None, **kwargs: Any):
69
+ """Make a copy of this Var with updated fields.
70
+
71
+ Args:
72
+ merge_var_data: VarData to merge into the existing VarData.
73
+ **kwargs: Var fields to update.
74
+
75
+ Returns:
76
+ A new ImmutableVar with the updated fields overwriting the corresponding fields in this Var.
77
+
78
+ Raises:
79
+ TypeError: If _var_is_local, _var_is_string, or _var_full_name_needs_state_prefix is not None.
80
+ """
81
+ if kwargs.get("_var_is_local", False) is not False:
82
+ raise TypeError(
83
+ "The _var_is_local argument is not supported for ImmutableVar."
84
+ )
85
+
86
+ if kwargs.get("_var_is_string", False) is not False:
87
+ raise TypeError(
88
+ "The _var_is_string argument is not supported for ImmutableVar."
89
+ )
90
+
91
+ if kwargs.get("_var_full_name_needs_state_prefix", False) is not False:
92
+ raise TypeError(
93
+ "The _var_full_name_needs_state_prefix argument is not supported for ImmutableVar."
94
+ )
95
+
96
+ field_values = dict(
97
+ _var_name=kwargs.pop("_var_name", self._var_name),
98
+ _var_type=kwargs.pop("_var_type", self._var_type),
99
+ _var_data=VarData.merge(
100
+ kwargs.get("_var_data", self._var_data), merge_var_data
101
+ ),
102
+ )
103
+ return ImmutableVar(**field_values)
104
+
105
+ @classmethod
106
+ def create(
107
+ cls,
108
+ value: Any,
109
+ _var_is_local: bool | None = None,
110
+ _var_is_string: bool | None = None,
111
+ _var_data: VarData | None = None,
112
+ ) -> Var | None:
113
+ """Create a var from a value.
114
+
115
+ Args:
116
+ value: The value to create the var from.
117
+ _var_is_local: Whether the var is local. Deprecated.
118
+ _var_is_string: Whether the var is a string literal. Deprecated.
119
+ _var_data: Additional hooks and imports associated with the Var.
120
+
121
+ Returns:
122
+ The var.
123
+
124
+ Raises:
125
+ VarTypeError: If the value is JSON-unserializable.
126
+ TypeError: If _var_is_local or _var_is_string is not None.
127
+ """
128
+ if _var_is_local is not None:
129
+ raise TypeError(
130
+ "The _var_is_local argument is not supported for ImmutableVar."
131
+ )
132
+
133
+ if _var_is_string is not None:
134
+ raise TypeError(
135
+ "The _var_is_string argument is not supported for ImmutableVar."
136
+ )
137
+
138
+ from reflex.utils import format
139
+
140
+ # Check for none values.
141
+ if value is None:
142
+ return None
143
+
144
+ # If the value is already a var, do nothing.
145
+ if isinstance(value, Var):
146
+ return value
147
+
148
+ # Try to pull the imports and hooks from contained values.
149
+ if not isinstance(value, str):
150
+ _var_data = VarData.merge(*_extract_var_data(value), _var_data)
151
+
152
+ # Try to serialize the value.
153
+ type_ = type(value)
154
+ if type_ in types.JSONType:
155
+ name = value
156
+ else:
157
+ name, _serialized_type = serializers.serialize(value, get_type=True)
158
+ if name is None:
159
+ raise VarTypeError(
160
+ f"No JSON serializer found for var {value} of type {type_}."
161
+ )
162
+ name = name if isinstance(name, str) else format.json_dumps(name)
163
+
164
+ return ImmutableVar(
165
+ _var_name=name,
166
+ _var_type=type_,
167
+ _var_data=_var_data,
168
+ )
169
+
170
+ @classmethod
171
+ def create_safe(
172
+ cls,
173
+ value: Any,
174
+ _var_is_local: bool | None = None,
175
+ _var_is_string: bool | None = None,
176
+ _var_data: VarData | None = None,
177
+ ) -> Var:
178
+ """Create a var from a value, asserting that it is not None.
179
+
180
+ Args:
181
+ value: The value to create the var from.
182
+ _var_is_local: Whether the var is local. Deprecated.
183
+ _var_is_string: Whether the var is a string literal. Deprecated.
184
+ _var_data: Additional hooks and imports associated with the Var.
185
+
186
+ Returns:
187
+ The var.
188
+ """
189
+ var = cls.create(
190
+ value,
191
+ _var_is_local=_var_is_local,
192
+ _var_is_string=_var_is_string,
193
+ _var_data=_var_data,
194
+ )
195
+ assert var is not None
196
+ return var
197
+
198
+ def __format__(self, format_spec: str) -> str:
199
+ """Format the var into a Javascript equivalent to an f-string.
200
+
201
+ Args:
202
+ format_spec: The format specifier (Ignored for now).
203
+
204
+ Returns:
205
+ The formatted var.
206
+ """
207
+ _global_vars[hash(self)] = self
208
+
209
+ # Encode the _var_data into the formatted output for tracking purposes.
210
+ return f"{REFLEX_VAR_OPENING_TAG}{hash(self)}{REFLEX_VAR_CLOSING_TAG}{self._var_name}"
reflex/state.py CHANGED
@@ -8,7 +8,6 @@ import copy
8
8
  import functools
9
9
  import inspect
10
10
  import os
11
- import traceback
12
11
  import uuid
13
12
  from abc import ABC, abstractmethod
14
13
  from collections import defaultdict
@@ -25,9 +24,12 @@ from typing import (
25
24
  Sequence,
26
25
  Set,
27
26
  Type,
27
+ Union,
28
+ cast,
28
29
  )
29
30
 
30
31
  import dill
32
+ from sqlalchemy.orm import DeclarativeBase
31
33
 
32
34
  try:
33
35
  import pydantic.v1 as pydantic
@@ -47,7 +49,6 @@ from reflex.event import (
47
49
  EventHandler,
48
50
  EventSpec,
49
51
  fix_events,
50
- window_alert,
51
52
  )
52
53
  from reflex.utils import console, format, prerequisites, types
53
54
  from reflex.utils.exceptions import ImmutableStateError, LockExpiredError
@@ -61,7 +62,6 @@ if TYPE_CHECKING:
61
62
 
62
63
  Delta = Dict[str, Any]
63
64
  var = computed_var
64
- config = get_config()
65
65
 
66
66
 
67
67
  # If the state is this large, it's considered a performance issue.
@@ -426,6 +426,21 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
426
426
  if isinstance(v, ComputedVar)
427
427
  ]
428
428
 
429
+ @classmethod
430
+ def _validate_module_name(cls) -> None:
431
+ """Check if the module name is valid.
432
+
433
+ Reflex uses ___ as state name module separator.
434
+
435
+ Raises:
436
+ NameError: If the module name is invalid.
437
+ """
438
+ if "___" in cls.__module__:
439
+ raise NameError(
440
+ "The module name of a State class cannot contain '___'. "
441
+ "Please rename the module."
442
+ )
443
+
429
444
  @classmethod
430
445
  def __init_subclass__(cls, mixin: bool = False, **kwargs):
431
446
  """Do some magic for the subclass initialization.
@@ -445,8 +460,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
445
460
  if mixin:
446
461
  return
447
462
 
463
+ # Validate the module name.
464
+ cls._validate_module_name()
465
+
448
466
  # Event handlers should not shadow builtin state methods.
449
467
  cls._check_overridden_methods()
468
+
450
469
  # Computed vars should not shadow builtin state props.
451
470
  cls._check_overriden_basevars()
452
471
 
@@ -463,20 +482,22 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
463
482
  cls.inherited_backend_vars = parent_state.backend_vars
464
483
 
465
484
  # Check if another substate class with the same name has already been defined.
466
- if cls.__name__ in set(c.__name__ for c in parent_state.class_subclasses):
485
+ if cls.get_name() in set(
486
+ c.get_name() for c in parent_state.class_subclasses
487
+ ):
467
488
  if is_testing_env():
468
489
  # Clear existing subclass with same name when app is reloaded via
469
490
  # utils.prerequisites.get_app(reload=True)
470
491
  parent_state.class_subclasses = set(
471
492
  c
472
493
  for c in parent_state.class_subclasses
473
- if c.__name__ != cls.__name__
494
+ if c.get_name() != cls.get_name()
474
495
  )
475
496
  else:
476
497
  # During normal operation, subclasses cannot have the same name, even if they are
477
498
  # defined in different modules.
478
499
  raise StateValueError(
479
- f"The substate class '{cls.__name__}' has been defined multiple times. "
500
+ f"The substate class '{cls.get_name()}' has been defined multiple times. "
480
501
  "Shadowing substate classes is not allowed."
481
502
  )
482
503
  # Track this new subclass in the parent state's subclasses set.
@@ -759,7 +780,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
759
780
  Returns:
760
781
  The name of the state.
761
782
  """
762
- return format.to_snake_case(cls.__name__)
783
+ module = cls.__module__.replace(".", "___")
784
+ return format.to_snake_case(f"{module}___{cls.__name__}")
763
785
 
764
786
  @classmethod
765
787
  @functools.lru_cache()
@@ -1431,15 +1453,39 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1431
1453
  # Convert valid EventHandler and EventSpec into Event
1432
1454
  fixed_events = fix_events(self._check_valid(handler, events), token)
1433
1455
 
1434
- # Get the delta after processing the event.
1435
- delta = state.get_delta()
1436
- state._clean()
1456
+ try:
1457
+ # Get the delta after processing the event.
1458
+ delta = state.get_delta()
1459
+ state._clean()
1460
+
1461
+ return StateUpdate(
1462
+ delta=delta,
1463
+ events=fixed_events,
1464
+ final=final if not handler.is_background else True,
1465
+ )
1466
+ except Exception as ex:
1467
+ state._clean()
1437
1468
 
1438
- return StateUpdate(
1439
- delta=delta,
1440
- events=fixed_events,
1441
- final=final if not handler.is_background else True,
1442
- )
1469
+ app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
1470
+
1471
+ event_specs = app_instance.backend_exception_handler(ex)
1472
+
1473
+ if event_specs is None:
1474
+ return StateUpdate()
1475
+
1476
+ event_specs_correct_type = cast(
1477
+ Union[List[Union[EventSpec, EventHandler]], None],
1478
+ [event_specs] if isinstance(event_specs, EventSpec) else event_specs,
1479
+ )
1480
+ fixed_events = fix_events(
1481
+ event_specs_correct_type,
1482
+ token,
1483
+ router_data=state.router_data,
1484
+ )
1485
+ return StateUpdate(
1486
+ events=fixed_events,
1487
+ final=True,
1488
+ )
1443
1489
 
1444
1490
  async def _process_event(
1445
1491
  self, handler: EventHandler, state: BaseState | StateProxy, payload: Dict
@@ -1492,12 +1538,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1492
1538
 
1493
1539
  # If an error occurs, throw a window alert.
1494
1540
  except Exception as ex:
1495
- error = traceback.format_exc()
1496
- print(error)
1497
1541
  telemetry.send_error(ex, context="backend")
1542
+
1543
+ app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
1544
+
1545
+ event_specs = app_instance.backend_exception_handler(ex)
1546
+
1498
1547
  yield state._as_state_update(
1499
1548
  handler,
1500
- window_alert("An error occurred. See logs for details."),
1549
+ event_specs,
1501
1550
  final=True,
1502
1551
  )
1503
1552
 
@@ -1799,6 +1848,23 @@ class State(BaseState):
1799
1848
  is_hydrated: bool = False
1800
1849
 
1801
1850
 
1851
+ class FrontendEventExceptionState(State):
1852
+ """Substate for handling frontend exceptions."""
1853
+
1854
+ def handle_frontend_exception(self, stack: str) -> None:
1855
+ """Handle frontend exceptions.
1856
+
1857
+ If a frontend exception handler is provided, it will be called.
1858
+ Otherwise, the default frontend exception handler will be called.
1859
+
1860
+ Args:
1861
+ stack: The stack trace of the exception.
1862
+
1863
+ """
1864
+ app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
1865
+ app_instance.frontend_exception_handler(Exception(stack))
1866
+
1867
+
1802
1868
  class UpdateVarsInternalState(State):
1803
1869
  """Substate for handling internal state var updates."""
1804
1870
 
@@ -2319,6 +2385,24 @@ def _dill_reduce_state(pickler, obj):
2319
2385
  dill.Pickler.dispatch[type](pickler, obj)
2320
2386
 
2321
2387
 
2388
+ def _default_lock_expiration() -> int:
2389
+ """Get the default lock expiration time.
2390
+
2391
+ Returns:
2392
+ The default lock expiration time.
2393
+ """
2394
+ return get_config().redis_lock_expiration
2395
+
2396
+
2397
+ def _default_token_expiration() -> int:
2398
+ """Get the default token expiration time.
2399
+
2400
+ Returns:
2401
+ The default token expiration time.
2402
+ """
2403
+ return get_config().redis_token_expiration
2404
+
2405
+
2322
2406
  class StateManagerRedis(StateManager):
2323
2407
  """A state manager that stores states in redis."""
2324
2408
 
@@ -2326,10 +2410,10 @@ class StateManagerRedis(StateManager):
2326
2410
  redis: Redis
2327
2411
 
2328
2412
  # The token expiration time (s).
2329
- token_expiration: int = config.redis_token_expiration
2413
+ token_expiration: int = pydantic.Field(default_factory=_default_token_expiration)
2330
2414
 
2331
2415
  # The maximum time to hold a lock (ms).
2332
- lock_expiration: int = config.redis_lock_expiration
2416
+ lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
2333
2417
 
2334
2418
  # The keyspace subscription string when redis is waiting for lock to be released
2335
2419
  _redis_notify_keyspace_events: str = (
@@ -2902,7 +2986,7 @@ class MutableProxy(wrapt.ObjectProxy):
2902
2986
  pydantic.BaseModel.__dict__
2903
2987
  )
2904
2988
 
2905
- __mutable_types__ = (list, dict, set, Base)
2989
+ __mutable_types__ = (list, dict, set, Base, DeclarativeBase)
2906
2990
 
2907
2991
  def __init__(self, wrapped: Any, state: BaseState, field_name: str):
2908
2992
  """Create a proxy for a mutable object that tracks changes.
reflex/testing.py CHANGED
@@ -40,6 +40,7 @@ import reflex
40
40
  import reflex.reflex
41
41
  import reflex.utils.build
42
42
  import reflex.utils.exec
43
+ import reflex.utils.format
43
44
  import reflex.utils.prerequisites
44
45
  import reflex.utils.processes
45
46
  from reflex.state import (
@@ -177,6 +178,33 @@ class AppHarness:
177
178
  app_module_path=root / app_name / f"{app_name}.py",
178
179
  )
179
180
 
181
+ def get_state_name(self, state_cls_name: str) -> str:
182
+ """Get the state name for the given state class name.
183
+
184
+ Args:
185
+ state_cls_name: The state class name
186
+
187
+ Returns:
188
+ The state name
189
+ """
190
+ return reflex.utils.format.to_snake_case(
191
+ f"{self.app_name}___{self.app_name}___" + state_cls_name
192
+ )
193
+
194
+ def get_full_state_name(self, path: List[str]) -> str:
195
+ """Get the full state name for the given state class name.
196
+
197
+ Args:
198
+ path: A list of state class names
199
+
200
+ Returns:
201
+ The full state name
202
+ """
203
+ # NOTE: using State.get_name() somehow causes trouble here
204
+ # path = [State.get_name()] + [self.get_state_name(p) for p in path]
205
+ path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
206
+ return ".".join(path)
207
+
180
208
  def _get_globals_from_signature(self, func: Any) -> dict[str, Any]:
181
209
  """Get the globals from a function or module object.
182
210
 
reflex/utils/compat.py CHANGED
@@ -32,6 +32,13 @@ def pydantic_v1_patch():
32
32
  Yields:
33
33
  None when the Pydantic module is patched.
34
34
  """
35
+ import pydantic
36
+
37
+ if pydantic.__version__.startswith("1."):
38
+ # pydantic v1 is already installed
39
+ yield
40
+ return
41
+
35
42
  patched_modules = [
36
43
  "pydantic",
37
44
  "pydantic.fields",
@@ -42,11 +49,6 @@ def pydantic_v1_patch():
42
49
  try:
43
50
  import pydantic.v1 # type: ignore
44
51
 
45
- if pydantic.__version__.startswith("1."):
46
- # pydantic v1 is already installed
47
- yield
48
- return
49
-
50
52
  sys.modules["pydantic.fields"] = pydantic.v1.fields # type: ignore
51
53
  sys.modules["pydantic.main"] = pydantic.v1.main # type: ignore
52
54
  sys.modules["pydantic.errors"] = pydantic.v1.errors # type: ignore
reflex/utils/console.py CHANGED
@@ -17,6 +17,9 @@ _LOG_LEVEL = LogLevel.INFO
17
17
  # Deprecated features who's warning has been printed.
18
18
  _EMITTED_DEPRECATION_WARNINGS = set()
19
19
 
20
+ # Info messages which have been printed.
21
+ _EMITTED_INFO = set()
22
+
20
23
 
21
24
  def set_log_level(log_level: LogLevel):
22
25
  """Set the log level.
@@ -62,14 +65,20 @@ def debug(msg: str, **kwargs):
62
65
  print(msg_, **kwargs)
63
66
 
64
67
 
65
- def info(msg: str, **kwargs):
68
+ def info(msg: str, dedupe: bool = False, **kwargs):
66
69
  """Print an info message.
67
70
 
68
71
  Args:
69
72
  msg: The info message.
73
+ dedupe: If True, suppress multiple console logs of info message.
70
74
  kwargs: Keyword arguments to pass to the print function.
71
75
  """
72
76
  if _LOG_LEVEL <= LogLevel.INFO:
77
+ if dedupe:
78
+ if msg in _EMITTED_INFO:
79
+ return
80
+ else:
81
+ _EMITTED_INFO.add(msg)
73
82
  print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
74
83
 
75
84
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import functools
6
6
  import glob
7
7
  import importlib
8
+ import importlib.metadata
8
9
  import inspect
9
10
  import json
10
11
  import os
@@ -23,7 +24,6 @@ from types import ModuleType
23
24
  from typing import Callable, List, Optional
24
25
 
25
26
  import httpx
26
- import pkg_resources
27
27
  import typer
28
28
  from alembic.util.exc import CommandError
29
29
  from packaging import version
@@ -61,7 +61,7 @@ class CpuInfo(Base):
61
61
  def get_web_dir() -> Path:
62
62
  """Get the working directory for the next.js commands.
63
63
 
64
- Can be overriden with REFLEX_WEB_WORKDIR.
64
+ Can be overridden with REFLEX_WEB_WORKDIR.
65
65
 
66
66
  Returns:
67
67
  The working directory.
@@ -78,7 +78,7 @@ def check_latest_package_version(package_name: str):
78
78
  """
79
79
  try:
80
80
  # Get the latest version from PyPI
81
- current_version = pkg_resources.get_distribution(package_name).version
81
+ current_version = importlib.metadata.version(package_name)
82
82
  url = f"https://pypi.org/pypi/{package_name}/json"
83
83
  response = httpx.get(url)
84
84
  latest_version = response.json()["info"]["version"]
@@ -404,9 +404,15 @@ def initialize_gitignore(
404
404
  files_to_ignore: The files to add to the .gitignore file.
405
405
  """
406
406
  # Combine with the current ignored files.
407
+ current_ignore: set[str] = set()
407
408
  if os.path.exists(gitignore_file):
408
409
  with open(gitignore_file, "r") as f:
409
- files_to_ignore |= set([line.strip() for line in f.readlines()])
410
+ current_ignore |= set([line.strip() for line in f.readlines()])
411
+
412
+ if files_to_ignore == current_ignore:
413
+ console.debug(f"{gitignore_file} already up to date.")
414
+ return
415
+ files_to_ignore |= current_ignore
410
416
 
411
417
  # Write files to the .gitignore file.
412
418
  with open(gitignore_file, "w", newline="\n") as f:
@@ -970,7 +976,7 @@ def is_latest_template() -> bool:
970
976
  json_file = get_web_dir() / constants.Reflex.JSON
971
977
  if not json_file.exists():
972
978
  return False
973
- app_version = json.load(json_file.open()).get("version")
979
+ app_version = json.loads(json_file.read_text()).get("version")
974
980
  return app_version == constants.Reflex.VERSION
975
981
 
976
982