reflex 0.4.4a2__py3-none-any.whl → 0.4.5__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.

reflex/state.py CHANGED
@@ -472,7 +472,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
472
472
  events[name] = value
473
473
 
474
474
  for name, fn in events.items():
475
- handler = EventHandler(fn=fn)
475
+ handler = cls._create_event_handler(fn)
476
476
  cls.event_handlers[name] = handler
477
477
  setattr(cls, name, handler)
478
478
 
@@ -677,7 +677,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
677
677
 
678
678
  @classmethod
679
679
  @functools.lru_cache()
680
- def get_class_substate(cls, path: Sequence[str]) -> Type[BaseState]:
680
+ def get_class_substate(cls, path: Sequence[str] | str) -> Type[BaseState]:
681
681
  """Get the class substate.
682
682
 
683
683
  Args:
@@ -689,6 +689,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
689
689
  Raises:
690
690
  ValueError: If the substate is not found.
691
691
  """
692
+ if isinstance(path, str):
693
+ path = tuple(path.split("."))
694
+
692
695
  if len(path) == 0:
693
696
  return cls
694
697
  if path[0] == cls.get_name():
@@ -789,6 +792,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
789
792
  """
790
793
  setattr(cls, prop._var_name, prop)
791
794
 
795
+ @classmethod
796
+ def _create_event_handler(cls, fn):
797
+ """Create an event handler for the given function.
798
+
799
+ Args:
800
+ fn: The function to create an event handler for.
801
+
802
+ Returns:
803
+ The event handler.
804
+ """
805
+ return EventHandler(fn=fn, state_full_name=cls.get_full_name())
806
+
792
807
  @classmethod
793
808
  def _create_setter(cls, prop: BaseVar):
794
809
  """Create a setter for the var.
@@ -798,7 +813,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
798
813
  """
799
814
  setter_name = prop.get_setter_name(include_state=False)
800
815
  if setter_name not in cls.__dict__:
801
- event_handler = EventHandler(fn=prop.get_setter())
816
+ event_handler = cls._create_event_handler(prop.get_setter())
802
817
  cls.event_handlers[setter_name] = event_handler
803
818
  setattr(cls, setter_name, event_handler)
804
819
 
@@ -1752,7 +1767,7 @@ class UpdateVarsInternalState(State):
1752
1767
  """
1753
1768
  for var, value in vars.items():
1754
1769
  state_name, _, var_name = var.rpartition(".")
1755
- var_state_cls = State.get_class_substate(tuple(state_name.split(".")))
1770
+ var_state_cls = State.get_class_substate(state_name)
1756
1771
  var_state = await self.get_state(var_state_cls)
1757
1772
  setattr(var_state, var_name, value)
1758
1773
 
@@ -2268,7 +2283,7 @@ class StateManagerRedis(StateManager):
2268
2283
  _, state_path = _split_substate_key(token)
2269
2284
  if state_path:
2270
2285
  # Get the State class associated with the given path.
2271
- state_cls = self.state.get_class_substate(tuple(state_path.split(".")))
2286
+ state_cls = self.state.get_class_substate(state_path)
2272
2287
  else:
2273
2288
  raise RuntimeError(
2274
2289
  "StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
reflex/testing.py CHANGED
@@ -211,7 +211,9 @@ class AppHarness:
211
211
  # get the source from a function or module object
212
212
  source_code = "\n".join(
213
213
  [
214
- "\n".join(f"{k} = {v!r}" for k, v in app_globals.items()),
214
+ "\n".join(
215
+ self.get_app_global_source(k, v) for k, v in app_globals.items()
216
+ ),
215
217
  self._get_source_from_app_source(self.app_source),
216
218
  ]
217
219
  )
@@ -331,6 +333,24 @@ class AppHarness:
331
333
  self._wait_frontend()
332
334
  return self
333
335
 
336
+ @staticmethod
337
+ def get_app_global_source(key, value):
338
+ """Get the source code of a global object.
339
+ If value is a function or class we render the actual
340
+ source of value otherwise we assign value to key.
341
+
342
+ Args:
343
+ key: variable name to assign value to.
344
+ value: value of the global variable.
345
+
346
+ Returns:
347
+ The rendered app global code.
348
+
349
+ """
350
+ if not inspect.isclass(value) and not inspect.isfunction(value):
351
+ return f"{key} = {value!r}"
352
+ return inspect.getsource(value)
353
+
334
354
  def __enter__(self) -> "AppHarness":
335
355
  """Contextmanager protocol for `start()`.
336
356
 
reflex/utils/exec.py CHANGED
@@ -307,3 +307,12 @@ def is_prod_mode() -> bool:
307
307
  constants.Env.DEV.value,
308
308
  )
309
309
  return current_mode == constants.Env.PROD.value
310
+
311
+
312
+ def should_skip_compile() -> bool:
313
+ """Whether the app should skip compile.
314
+
315
+ Returns:
316
+ True if the app should skip compile.
317
+ """
318
+ return os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes"
reflex/utils/format.py CHANGED
@@ -6,8 +6,7 @@ import inspect
6
6
  import json
7
7
  import os
8
8
  import re
9
- import sys
10
- from typing import TYPE_CHECKING, Any, List, Union
9
+ from typing import TYPE_CHECKING, Any, List, Optional, Union
11
10
 
12
11
  from reflex import constants
13
12
  from reflex.utils import exceptions, serializers, types
@@ -161,16 +160,17 @@ def to_camel_case(text: str, allow_hyphens: bool = False) -> str:
161
160
  return leading_underscores_or_hyphens + converted_word
162
161
 
163
162
 
164
- def to_title_case(text: str) -> str:
163
+ def to_title_case(text: str, sep: str = "") -> str:
165
164
  """Convert a string from snake case to title case.
166
165
 
167
166
  Args:
168
167
  text: The string to convert.
168
+ sep: The separator to use to join the words.
169
169
 
170
170
  Returns:
171
171
  The title case string.
172
172
  """
173
- return "".join(word.capitalize() for word in text.split("_"))
173
+ return sep.join(word.title() for word in text.split("_"))
174
174
 
175
175
 
176
176
  def to_kebab_case(text: str) -> str:
@@ -188,6 +188,20 @@ def to_kebab_case(text: str) -> str:
188
188
  return to_snake_case(text).replace("_", "-")
189
189
 
190
190
 
191
+ def make_default_page_title(app_name: str, route: str) -> str:
192
+ """Make a default page title from a route.
193
+
194
+ Args:
195
+ app_name: The name of the app owning the page.
196
+ route: The route to make the title from.
197
+
198
+ Returns:
199
+ The default page title.
200
+ """
201
+ title = constants.DefaultPage.TITLE.format(app_name, route)
202
+ return to_title_case(title, " ")
203
+
204
+
191
205
  def _escape_js_string(string: str) -> str:
192
206
  """Escape the string for use as a JS string literal.
193
207
 
@@ -470,18 +484,18 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
470
484
  if len(parts) == 1:
471
485
  return ("", parts[-1])
472
486
 
473
- # Get the state and the function name.
474
- state_name, name = parts[-2:]
487
+ # Get the state full name
488
+ state_full_name = handler.state_full_name
475
489
 
476
- # Construct the full event handler name.
477
- try:
478
- # Try to get the state from the module.
479
- state = vars(sys.modules[handler.fn.__module__])[state_name]
480
- except Exception:
481
- # If the state isn't in the module, just return the function name.
490
+ # Get the function name
491
+ name = parts[-1]
492
+
493
+ from reflex.state import State
494
+
495
+ if state_full_name == "state" and name not in State.__dict__:
482
496
  return ("", to_snake_case(handler.fn.__qualname__))
483
497
 
484
- return (state.get_full_name(), name)
498
+ return (state_full_name, name)
485
499
 
486
500
 
487
501
  def format_event_handler(handler: EventHandler) -> str:
@@ -513,9 +527,14 @@ def format_event(event_spec: EventSpec) -> str:
513
527
  ":".join(
514
528
  (
515
529
  name._var_name,
516
- wrap(json.dumps(val._var_name).strip('"').replace("`", "\\`"), "`")
517
- if val._var_is_string
518
- else val._var_full_name,
530
+ (
531
+ wrap(
532
+ json.dumps(val._var_name).strip('"').replace("`", "\\`"),
533
+ "`",
534
+ )
535
+ if val._var_is_string
536
+ else val._var_full_name
537
+ ),
519
538
  )
520
539
  )
521
540
  for name, val in event_spec.args
@@ -584,11 +603,12 @@ def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
584
603
  return {k.replace("-", "_"): v for k, v in params.items()}
585
604
 
586
605
 
587
- def format_state(value: Any) -> Any:
606
+ def format_state(value: Any, key: Optional[str] = None) -> Any:
588
607
  """Recursively format values in the given state.
589
608
 
590
609
  Args:
591
610
  value: The state to format.
611
+ key: The key associated with the value (optional).
592
612
 
593
613
  Returns:
594
614
  The formatted state.
@@ -598,7 +618,7 @@ def format_state(value: Any) -> Any:
598
618
  """
599
619
  # Handle dicts.
600
620
  if isinstance(value, dict):
601
- return {k: format_state(v) for k, v in value.items()}
621
+ return {k: format_state(v, k) for k, v in value.items()}
602
622
 
603
623
  # Handle lists, sets, typles.
604
624
  if isinstance(value, types.StateIterBases):
@@ -613,7 +633,14 @@ def format_state(value: Any) -> Any:
613
633
  if serialized is not None:
614
634
  return serialized
615
635
 
616
- raise TypeError(f"No JSON serializer found for var {value} of type {type(value)}.")
636
+ if key is None:
637
+ raise TypeError(
638
+ f"No JSON serializer found for var {value} of type {type(value)}."
639
+ )
640
+ else:
641
+ raise TypeError(
642
+ f"No JSON serializer found for State Var '{key}' of value {value} of type {type(value)}."
643
+ )
617
644
 
618
645
 
619
646
  def format_state_name(state_name: str) -> str:
@@ -34,6 +34,8 @@ from reflex.compiler import templates
34
34
  from reflex.config import Config, get_config
35
35
  from reflex.utils import console, path_ops, processes
36
36
 
37
+ CURRENTLY_INSTALLING_NODE = False
38
+
37
39
 
38
40
  def check_latest_package_version(package_name: str):
39
41
  """Check if the latest version of the package is installed.
@@ -103,8 +105,11 @@ def get_node_version() -> version.Version | None:
103
105
  Returns:
104
106
  The version of node.
105
107
  """
108
+ node_path = path_ops.get_node_path()
109
+ if node_path is None:
110
+ return None
106
111
  try:
107
- result = processes.new_process([path_ops.get_node_path(), "-v"], run=True)
112
+ result = processes.new_process([node_path, "-v"], run=True)
108
113
  # The output will be in the form "vX.Y.Z", but version.parse() can handle it
109
114
  return version.parse(result.stdout) # type: ignore
110
115
  except (FileNotFoundError, TypeError):
@@ -211,7 +216,11 @@ def get_compiled_app(reload: bool = False) -> ModuleType:
211
216
  The compiled app based on the default config.
212
217
  """
213
218
  app_module = get_app(reload=reload)
214
- getattr(app_module, constants.CompileVars.APP).compile_()
219
+ app = getattr(app_module, constants.CompileVars.APP)
220
+ # For py3.8 and py3.9 compatibility when redis is used, we MUST add any decorator pages
221
+ # before compiling the app in a thread to avoid event loop error (REF-2172).
222
+ app._apply_decorated_pages()
223
+ app.compile_()
215
224
  return app_module
216
225
 
217
226
 
@@ -425,19 +434,21 @@ def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
425
434
  )
426
435
 
427
436
 
428
- def get_project_hash() -> int | None:
437
+ def get_project_hash(raise_on_fail: bool = False) -> int | None:
429
438
  """Get the project hash from the reflex.json file if the file exists.
430
439
 
440
+ Args:
441
+ raise_on_fail: Whether to raise an error if the file does not exist.
442
+
431
443
  Returns:
432
444
  project_hash: The app hash.
433
445
  """
434
- if not os.path.exists(constants.Reflex.JSON):
446
+ if not os.path.exists(constants.Reflex.JSON) and not raise_on_fail:
435
447
  return None
436
448
  # Open and read the file
437
449
  with open(constants.Reflex.JSON, "r") as file:
438
450
  data = json.load(file)
439
- project_hash = data["project_hash"]
440
- return project_hash
451
+ return data.get("project_hash")
441
452
 
442
453
 
443
454
  def initialize_web_directory():
@@ -611,6 +622,11 @@ def install_node():
611
622
  console.debug("")
612
623
  return
613
624
 
625
+ # Skip installation if check_node_version() checks out
626
+ if check_node_version():
627
+ console.debug("Skipping node installation as it is already installed.")
628
+ return
629
+
614
630
  path_ops.mkdir(constants.Fnm.DIR)
615
631
  if not os.path.exists(constants.Fnm.EXE):
616
632
  download_and_extract_fnm_zip()
@@ -627,10 +643,6 @@ def install_node():
627
643
  ],
628
644
  )
629
645
  else: # All other platforms (Linux, MacOS).
630
- # TODO we can skip installation if check_node_version() checks out
631
- if check_node_version():
632
- console.debug("Skipping node installation as it is already installed.")
633
- return
634
646
  # Add execute permissions to fnm executable.
635
647
  os.chmod(constants.Fnm.EXE, stat.S_IXUSR)
636
648
  # Install node.
@@ -812,6 +824,11 @@ def check_initialized(frontend: bool = True):
812
824
  console.warn(
813
825
  """Windows Subsystem for Linux (WSL) is recommended for improving initial install times."""
814
826
  )
827
+ if sys.version_info >= (3, 12):
828
+ console.warn(
829
+ "Python 3.12 on Windows has known issues with hot reload (reflex-dev/reflex#2335). "
830
+ "Python 3.11 is recommended with this release of Reflex."
831
+ )
815
832
 
816
833
 
817
834
  def is_latest_template() -> bool:
@@ -931,8 +948,12 @@ def initialize_frontend_dependencies():
931
948
  """Initialize all the frontend dependencies."""
932
949
  # validate dependencies before install
933
950
  validate_frontend_dependencies()
951
+ # Avoid warning about Node installation while we're trying to install it.
952
+ global CURRENTLY_INSTALLING_NODE
953
+ CURRENTLY_INSTALLING_NODE = True
934
954
  # Install the frontend dependencies.
935
955
  processes.run_concurrently(install_node, install_bun)
956
+ CURRENTLY_INSTALLING_NODE = False
936
957
  # Set up the web directory.
937
958
  initialize_web_directory()
938
959
 
reflex/utils/processes.py CHANGED
@@ -135,13 +135,20 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
135
135
 
136
136
  Returns:
137
137
  Execute a child program in a new process.
138
+
139
+ Raises:
140
+ Exit: When attempting to run a command with a None value.
138
141
  """
139
142
  node_bin_path = path_ops.get_node_bin_path()
140
- if not node_bin_path:
143
+ if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE:
141
144
  console.warn(
142
145
  "The path to the Node binary could not be found. Please ensure that Node is properly "
143
- "installed and added to your system's PATH environment variable."
146
+ "installed and added to your system's PATH environment variable or try running "
147
+ "`reflex init` again."
144
148
  )
149
+ if None in args:
150
+ console.error(f"Invalid command: {args}")
151
+ raise typer.Exit(1)
145
152
  # Add the node bin path to the PATH environment variable.
146
153
  env = {
147
154
  **os.environ,
reflex/utils/telemetry.py CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  import multiprocessing
7
6
  import platform
8
7
  from datetime import datetime
9
8
 
9
+ import httpx
10
10
  import psutil
11
11
 
12
12
  from reflex import constants
13
- from reflex.utils.prerequisites import ensure_reflex_installation_id
13
+ from reflex.utils import console
14
+ from reflex.utils.exec import should_skip_compile
15
+ from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash
14
16
 
15
17
  POSTHOG_API_URL: str = "https://app.posthog.com/capture/"
16
18
 
@@ -57,7 +59,68 @@ def get_memory() -> int:
57
59
  Returns:
58
60
  The total memory in MB.
59
61
  """
60
- return psutil.virtual_memory().total >> 20
62
+ try:
63
+ return psutil.virtual_memory().total >> 20
64
+ except ValueError: # needed to pass ubuntu test
65
+ return 0
66
+
67
+
68
+ def _raise_on_missing_project_hash() -> bool:
69
+ """Check if an error should be raised when project hash is missing.
70
+
71
+ When running reflex with --backend-only, or doing database migration
72
+ operations, there is no requirement for a .web directory, so the reflex.json
73
+ file may not exist, and this should not be considered an error.
74
+
75
+ Returns:
76
+ False when compilation should be skipped (i.e. no .web directory is required).
77
+ Otherwise return True.
78
+ """
79
+ if should_skip_compile():
80
+ return False
81
+ return True
82
+
83
+
84
+ def _prepare_event(event: str) -> dict:
85
+ """Prepare the event to be sent to the PostHog server.
86
+
87
+ Args:
88
+ event: The event name.
89
+
90
+ Returns:
91
+ The event data.
92
+ """
93
+ installation_id = ensure_reflex_installation_id()
94
+ project_hash = get_project_hash(raise_on_fail=_raise_on_missing_project_hash())
95
+
96
+ if installation_id is None or project_hash is None:
97
+ console.debug(
98
+ f"Could not get installation_id or project_hash: {installation_id}, {project_hash}"
99
+ )
100
+ return {}
101
+
102
+ return {
103
+ "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
104
+ "event": event,
105
+ "properties": {
106
+ "distinct_id": installation_id,
107
+ "distinct_app_id": project_hash,
108
+ "user_os": get_os(),
109
+ "reflex_version": get_reflex_version(),
110
+ "python_version": get_python_version(),
111
+ "cpu_count": get_cpu_count(),
112
+ "memory": get_memory(),
113
+ },
114
+ "timestamp": datetime.utcnow().isoformat(),
115
+ }
116
+
117
+
118
+ def _send_event(event_data: dict) -> bool:
119
+ try:
120
+ httpx.post(POSTHOG_API_URL, json=event_data)
121
+ return True
122
+ except Exception:
123
+ return False
61
124
 
62
125
 
63
126
  def send(event: str, telemetry_enabled: bool | None = None) -> bool:
@@ -70,8 +133,6 @@ def send(event: str, telemetry_enabled: bool | None = None) -> bool:
70
133
  Returns:
71
134
  Whether the telemetry was sent successfully.
72
135
  """
73
- import httpx
74
-
75
136
  from reflex.config import get_config
76
137
 
77
138
  # Get the telemetry_enabled from the config if it is not specified.
@@ -82,29 +143,8 @@ def send(event: str, telemetry_enabled: bool | None = None) -> bool:
82
143
  if not telemetry_enabled:
83
144
  return False
84
145
 
85
- installation_id = ensure_reflex_installation_id()
86
- if installation_id is None:
146
+ event_data = _prepare_event(event)
147
+ if not event_data:
87
148
  return False
88
149
 
89
- try:
90
- with open(constants.Dirs.REFLEX_JSON) as f:
91
- reflex_json = json.load(f)
92
- project_hash = reflex_json["project_hash"]
93
- post_hog = {
94
- "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
95
- "event": event,
96
- "properties": {
97
- "distinct_id": installation_id,
98
- "distinct_app_id": project_hash,
99
- "user_os": get_os(),
100
- "reflex_version": get_reflex_version(),
101
- "python_version": get_python_version(),
102
- "cpu_count": get_cpu_count(),
103
- "memory": get_memory(),
104
- },
105
- "timestamp": datetime.utcnow().isoformat(),
106
- }
107
- httpx.post(POSTHOG_API_URL, json=post_hog)
108
- return True
109
- except Exception:
110
- return False
150
+ return _send_event(event_data)
reflex/vars.py CHANGED
@@ -633,7 +633,7 @@ class Var:
633
633
  if types.is_generic_alias(self._var_type):
634
634
  index = i if not isinstance(i, Var) else 0
635
635
  type_ = types.get_args(self._var_type)
636
- type_ = type_[index % len(type_)]
636
+ type_ = type_[index % len(type_)] if type_ else Any
637
637
  elif types._issubclass(self._var_type, str):
638
638
  type_ = str
639
639
 
@@ -1449,7 +1449,7 @@ class Var:
1449
1449
  return self._replace(
1450
1450
  _var_name=f"{self._var_name}.split({other._var_full_name})",
1451
1451
  _var_is_string=False,
1452
- _var_type=list[str],
1452
+ _var_type=List[str],
1453
1453
  merge_var_data=other._var_data,
1454
1454
  )
1455
1455
 
@@ -1555,7 +1555,7 @@ class Var:
1555
1555
 
1556
1556
  return BaseVar(
1557
1557
  _var_name=f"Array.from(range({v1._var_full_name}, {v2._var_full_name}, {step._var_name}))",
1558
- _var_type=list[int],
1558
+ _var_type=List[int],
1559
1559
  _var_is_local=False,
1560
1560
  _var_data=VarData.merge(
1561
1561
  v1._var_data,
@@ -1861,6 +1861,8 @@ class ComputedVar(Var, property):
1861
1861
  # handle caching
1862
1862
  if not hasattr(instance, self._cache_attr):
1863
1863
  setattr(instance, self._cache_attr, super().__get__(instance, owner))
1864
+ # Ensure the computed var gets serialized to redis.
1865
+ instance._was_touched = True
1864
1866
  return getattr(instance, self._cache_attr)
1865
1867
 
1866
1868
  def _deps(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reflex
3
- Version: 0.4.4a2
3
+ Version: 0.4.5
4
4
  Summary: Web apps in pure Python.
5
5
  Home-page: https://reflex.dev
6
6
  License: Apache-2.0