reflex 0.4.9a2__py3-none-any.whl → 0.5.0a1__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 (131) hide show
  1. reflex/.templates/apps/blank/code/blank.py +19 -16
  2. reflex/.templates/apps/demo/code/pages/datatable.py +4 -4
  3. reflex/.templates/apps/demo/code/pages/forms.py +2 -2
  4. reflex/.templates/web/utils/helpers/debounce.js +17 -0
  5. reflex/.templates/web/utils/helpers/throttle.js +22 -0
  6. reflex/.templates/web/utils/state.js +21 -3
  7. reflex/__init__.py +6 -1
  8. reflex/__init__.pyi +4 -1
  9. reflex/app.py +157 -140
  10. reflex/app_module_for_backend.py +1 -1
  11. reflex/base.py +13 -15
  12. reflex/compiler/compiler.py +10 -1
  13. reflex/compiler/utils.py +3 -30
  14. reflex/components/__init__.py +1 -0
  15. reflex/components/chakra/datadisplay/list.py +1 -3
  16. reflex/components/chakra/datadisplay/list.pyi +3 -3
  17. reflex/components/chakra/disclosure/accordion.py +1 -1
  18. reflex/components/chakra/forms/pininput.pyi +1 -1
  19. reflex/components/chakra/media/icon.py +2 -2
  20. reflex/components/component.py +279 -32
  21. reflex/components/core/__init__.py +2 -2
  22. reflex/components/core/cond.py +1 -10
  23. reflex/components/core/debounce.py +5 -2
  24. reflex/components/core/debounce.pyi +4 -2
  25. reflex/components/core/foreach.py +1 -16
  26. reflex/components/core/html.py +6 -0
  27. reflex/components/core/match.py +2 -17
  28. reflex/components/core/upload.py +42 -1
  29. reflex/components/core/upload.pyi +199 -1
  30. reflex/components/datadisplay/code.py +7 -3
  31. reflex/components/datadisplay/code.pyi +3 -1
  32. reflex/components/el/elements/forms.py +1 -1
  33. reflex/components/el/elements/forms.pyi +1 -1
  34. reflex/components/lucide/icon.py +5 -13
  35. reflex/components/lucide/icon.pyi +0 -1
  36. reflex/components/markdown/markdown.py +5 -23
  37. reflex/components/markdown/markdown.pyi +1 -4
  38. reflex/components/radix/primitives/accordion.py +227 -406
  39. reflex/components/radix/primitives/accordion.pyi +369 -28
  40. reflex/components/radix/primitives/form.py +33 -29
  41. reflex/components/radix/primitives/form.pyi +7 -2
  42. reflex/components/radix/primitives/progress.py +17 -9
  43. reflex/components/radix/primitives/progress.pyi +2 -0
  44. reflex/components/radix/primitives/slider.py +30 -18
  45. reflex/components/radix/primitives/slider.pyi +4 -0
  46. reflex/components/radix/themes/base.py +8 -1
  47. reflex/components/radix/themes/base.pyi +79 -1
  48. reflex/components/radix/themes/color_mode.py +74 -30
  49. reflex/components/radix/themes/color_mode.pyi +26 -185
  50. reflex/components/radix/themes/components/__init__.py +17 -0
  51. reflex/components/radix/themes/components/badge.py +2 -1
  52. reflex/components/radix/themes/components/badge.pyi +3 -1
  53. reflex/components/radix/themes/components/button.py +3 -1
  54. reflex/components/radix/themes/components/button.pyi +4 -1
  55. reflex/components/radix/themes/components/checkbox_cards.py +48 -0
  56. reflex/components/radix/themes/components/checkbox_cards.pyi +264 -0
  57. reflex/components/radix/themes/components/checkbox_group.py +42 -0
  58. reflex/components/radix/themes/components/checkbox_group.pyi +253 -0
  59. reflex/components/radix/themes/components/data_list.py +63 -0
  60. reflex/components/radix/themes/components/data_list.pyi +426 -0
  61. reflex/components/radix/themes/components/icon_button.py +20 -17
  62. reflex/components/radix/themes/components/icon_button.pyi +5 -1
  63. reflex/components/radix/themes/components/progress.py +55 -0
  64. reflex/components/radix/themes/components/progress.pyi +180 -0
  65. reflex/components/radix/themes/components/radio.py +31 -0
  66. reflex/components/radix/themes/components/radio.pyi +169 -0
  67. reflex/components/radix/themes/components/radio_cards.py +48 -0
  68. reflex/components/radix/themes/components/radio_cards.pyi +264 -0
  69. reflex/components/radix/themes/components/radio_group.py +2 -4
  70. reflex/components/radix/themes/components/segmented_control.py +48 -0
  71. reflex/components/radix/themes/components/segmented_control.pyi +262 -0
  72. reflex/components/radix/themes/components/skeleton.py +32 -0
  73. reflex/components/radix/themes/components/skeleton.pyi +106 -0
  74. reflex/components/radix/themes/components/spinner.py +26 -0
  75. reflex/components/radix/themes/components/spinner.pyi +101 -0
  76. reflex/components/radix/themes/components/tabs.py +26 -1
  77. reflex/components/radix/themes/components/tabs.pyi +69 -9
  78. reflex/components/radix/themes/components/text_field.py +101 -71
  79. reflex/components/radix/themes/components/text_field.pyi +81 -499
  80. reflex/components/radix/themes/layout/base.py +2 -2
  81. reflex/components/radix/themes/layout/base.pyi +4 -4
  82. reflex/components/radix/themes/layout/center.py +8 -3
  83. reflex/components/radix/themes/layout/center.pyi +2 -1
  84. reflex/components/radix/themes/layout/container.py +30 -2
  85. reflex/components/radix/themes/layout/container.pyi +9 -30
  86. reflex/components/radix/themes/layout/list.py +10 -5
  87. reflex/components/radix/themes/layout/list.pyi +5 -21
  88. reflex/components/radix/themes/layout/spacer.py +8 -3
  89. reflex/components/radix/themes/layout/spacer.pyi +2 -1
  90. reflex/components/radix/themes/layout/stack.py +7 -1
  91. reflex/components/radix/themes/layout/stack.pyi +3 -3
  92. reflex/components/radix/themes/typography/link.py +10 -2
  93. reflex/components/radix/themes/typography/link.pyi +5 -4
  94. reflex/components/sonner/__init__.py +3 -0
  95. reflex/components/sonner/toast.py +267 -0
  96. reflex/components/sonner/toast.pyi +205 -0
  97. reflex/components/tags/iter_tag.py +9 -6
  98. reflex/config.py +30 -54
  99. reflex/constants/__init__.py +0 -2
  100. reflex/constants/base.py +0 -5
  101. reflex/constants/colors.py +2 -0
  102. reflex/constants/installer.py +5 -1
  103. reflex/constants/route.py +4 -0
  104. reflex/custom_components/custom_components.py +22 -1
  105. reflex/event.py +75 -30
  106. reflex/experimental/__init__.py +5 -0
  107. reflex/experimental/layout.py +24 -6
  108. reflex/model.py +2 -1
  109. reflex/page.py +7 -4
  110. reflex/reflex.py +8 -3
  111. reflex/route.py +39 -0
  112. reflex/state.py +128 -131
  113. reflex/style.py +20 -1
  114. reflex/testing.py +10 -6
  115. reflex/utils/console.py +3 -1
  116. reflex/utils/exec.py +20 -7
  117. reflex/utils/format.py +1 -1
  118. reflex/utils/imports.py +3 -1
  119. reflex/utils/prerequisites.py +141 -20
  120. reflex/utils/processes.py +21 -1
  121. reflex/utils/pyi_generator.py +95 -5
  122. reflex/utils/serializers.py +1 -1
  123. reflex/utils/telemetry.py +26 -4
  124. reflex/utils/types.py +62 -18
  125. reflex/vars.py +11 -5
  126. {reflex-0.4.9a2.dist-info → reflex-0.5.0a1.dist-info}/METADATA +16 -4
  127. {reflex-0.4.9a2.dist-info → reflex-0.5.0a1.dist-info}/RECORD +130 -108
  128. {reflex-0.4.9a2.dist-info → reflex-0.5.0a1.dist-info}/WHEEL +1 -1
  129. reflex/app.pyi +0 -149
  130. {reflex-0.4.9a2.dist-info → reflex-0.5.0a1.dist-info}/LICENSE +0 -0
  131. {reflex-0.4.9a2.dist-info → reflex-0.5.0a1.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import functools
5
6
  import glob
6
7
  import importlib
7
8
  import inspect
@@ -49,6 +50,14 @@ class Template(Base):
49
50
  demo_url: str
50
51
 
51
52
 
53
+ class CpuInfo(Base):
54
+ """Model to save cpu info."""
55
+
56
+ manufacturer_id: Optional[str]
57
+ model_name: Optional[str]
58
+ address_width: Optional[int]
59
+
60
+
52
61
  def check_latest_package_version(package_name: str):
53
62
  """Check if the latest version of the package is installed.
54
63
 
@@ -172,7 +181,7 @@ def get_install_package_manager() -> str | None:
172
181
  Returns:
173
182
  The path to the package manager.
174
183
  """
175
- if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE:
184
+ if constants.IS_WINDOWS and not is_windows_bun_supported():
176
185
  return get_package_manager()
177
186
  return get_config().bun_path
178
187
 
@@ -240,7 +249,7 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
240
249
  # For py3.8 and py3.9 compatibility when redis is used, we MUST add any decorator pages
241
250
  # before compiling the app in a thread to avoid event loop error (REF-2172).
242
251
  app._apply_decorated_pages()
243
- app.compile_(export=export)
252
+ app._compile(export=export)
244
253
  return app_module
245
254
 
246
255
 
@@ -285,7 +294,7 @@ def parse_redis_url() -> str | dict | None:
285
294
  feature_name="host[:port] style redis urls",
286
295
  reason="redis-py url syntax is now being used",
287
296
  deprecation_version="0.3.6",
288
- removal_version="0.5.0",
297
+ removal_version="0.6.0",
289
298
  )
290
299
  redis_url, has_port, redis_port = config.redis_url.partition(":")
291
300
  if not has_port:
@@ -325,7 +334,7 @@ def validate_app_name(app_name: str | None = None) -> str:
325
334
  app_name if app_name else os.getcwd().split(os.path.sep)[-1].replace("-", "_")
326
335
  )
327
336
  # Make sure the app is not named "reflex".
328
- if app_name == constants.Reflex.MODULE_NAME:
337
+ if app_name.lower() == constants.Reflex.MODULE_NAME:
329
338
  console.error(
330
339
  f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
331
340
  )
@@ -728,7 +737,7 @@ def install_bun():
728
737
  Raises:
729
738
  FileNotFoundError: If required packages are not found.
730
739
  """
731
- if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE:
740
+ if constants.IS_WINDOWS and not is_windows_bun_supported():
732
741
  console.warn(
733
742
  "Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm."
734
743
  )
@@ -743,8 +752,15 @@ def install_bun():
743
752
  # if unzip is installed
744
753
  if constants.IS_WINDOWS:
745
754
  processes.new_process(
746
- ["powershell", "-c", f"irm {constants.Bun.INSTALL_URL}.ps1|iex"],
747
- env={"BUN_INSTALL": constants.Bun.ROOT_PATH},
755
+ [
756
+ "powershell",
757
+ "-c",
758
+ f"irm {constants.Bun.WINDOWS_INSTALL_URL}|iex",
759
+ ],
760
+ env={
761
+ "BUN_INSTALL": constants.Bun.ROOT_PATH,
762
+ "BUN_VERSION": constants.Bun.VERSION,
763
+ },
748
764
  shell=True,
749
765
  run=True,
750
766
  show_logs=console.is_debug(),
@@ -823,10 +839,10 @@ def install_frontend_packages(packages: set[str], config: Config):
823
839
  """
824
840
  # unsupported archs(arm and 32bit machines) will use npm anyway. so we dont have to run npm twice
825
841
  fallback_command = (
826
- get_install_package_manager()
842
+ get_package_manager()
827
843
  if not constants.IS_WINDOWS
828
844
  or constants.IS_WINDOWS
829
- and constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE
845
+ and is_windows_bun_supported()
830
846
  else None
831
847
  )
832
848
  processes.run_process_with_fallback(
@@ -893,10 +909,26 @@ def needs_reinit(frontend: bool = True) -> bool:
893
909
  if not os.path.exists(constants.Dirs.WEB):
894
910
  return True
895
911
 
912
+ # If the template is out of date, then we need to re-init
913
+ if not is_latest_template():
914
+ return True
915
+
896
916
  if constants.IS_WINDOWS:
917
+ import uvicorn
918
+
919
+ uvi_ver = uvicorn.__version__
897
920
  console.warn(
898
921
  """Windows Subsystem for Linux (WSL) is recommended for improving initial install times."""
899
922
  )
923
+ if sys.version_info >= (3, 12) and uvi_ver != "0.24.0.post1":
924
+ console.warn(
925
+ f"""On Python 3.12, `uvicorn==0.24.0.post1` is recommended for improved hot reload times. Found {uvi_ver} instead."""
926
+ )
927
+
928
+ if sys.version_info < (3, 12) and uvi_ver != "0.20.0":
929
+ console.warn(
930
+ f"""On Python < 3.12, `uvicorn==0.20.0` is recommended for improved hot reload times. Found {uvi_ver} instead."""
931
+ )
900
932
  # No need to reinitialize if the app is already initialized.
901
933
  return False
902
934
 
@@ -1182,18 +1214,18 @@ def _get_rx_chakra_component_to_migrate() -> set[str]:
1182
1214
  rx_chakra_object = getattr(reflex.chakra, rx_chakra_name)
1183
1215
  try:
1184
1216
  if (
1185
- inspect.ismethod(rx_chakra_object)
1186
- and inspect.isclass(rx_chakra_object.__self__)
1187
- and issubclass(rx_chakra_object.__self__, ChakraComponent)
1188
- ):
1189
- names_to_migrate.add(rx_chakra_name)
1190
-
1191
- elif inspect.isclass(rx_chakra_object) and issubclass(
1192
- rx_chakra_object, ChakraComponent
1217
+ (
1218
+ inspect.ismethod(rx_chakra_object)
1219
+ and inspect.isclass(rx_chakra_object.__self__)
1220
+ and issubclass(rx_chakra_object.__self__, ChakraComponent)
1221
+ )
1222
+ or (
1223
+ inspect.isclass(rx_chakra_object)
1224
+ and issubclass(rx_chakra_object, ChakraComponent)
1225
+ )
1226
+ or rx_chakra_name in whitelist
1193
1227
  ):
1194
1228
  names_to_migrate.add(rx_chakra_name)
1195
- elif rx_chakra_name in whitelist:
1196
- names_to_migrate.add(rx_chakra_name)
1197
1229
 
1198
1230
  except Exception:
1199
1231
  raise
@@ -1406,4 +1438,93 @@ def initialize_app(app_name: str, template: str | None = None):
1406
1438
  template_url=template_url,
1407
1439
  )
1408
1440
 
1409
- telemetry.send("init")
1441
+ telemetry.send("init", template=template)
1442
+
1443
+
1444
+ def format_address_width(address_width) -> int | None:
1445
+ """Cast address width to an int.
1446
+
1447
+ Args:
1448
+ address_width: The address width.
1449
+
1450
+ Returns:
1451
+ Address width int
1452
+ """
1453
+ try:
1454
+ return int(address_width) if address_width else None
1455
+ except ValueError:
1456
+ return None
1457
+
1458
+
1459
+ @functools.lru_cache(maxsize=None)
1460
+ def get_cpu_info() -> CpuInfo | None:
1461
+ """Get the CPU info of the underlining host.
1462
+
1463
+ Returns:
1464
+ The CPU info.
1465
+ """
1466
+ platform_os = platform.system()
1467
+ cpuinfo = {}
1468
+ try:
1469
+ if platform_os == "Windows":
1470
+ cmd = "wmic cpu get addresswidth,caption,manufacturer /FORMAT:csv"
1471
+ output = processes.execute_command_and_return_output(cmd)
1472
+ if output:
1473
+ val = output.splitlines()[-1].split(",")[1:]
1474
+ cpuinfo["manufacturer_id"] = val[2]
1475
+ cpuinfo["model_name"] = val[1].split("Family")[0].strip()
1476
+ cpuinfo["address_width"] = format_address_width(val[0])
1477
+ elif platform_os == "Linux":
1478
+ output = processes.execute_command_and_return_output("lscpu")
1479
+ if output:
1480
+ lines = output.split("\n")
1481
+ for line in lines:
1482
+ if "Architecture" in line:
1483
+ cpuinfo["address_width"] = (
1484
+ 64 if line.split(":")[1].strip() == "x86_64" else 32
1485
+ )
1486
+ if "Vendor ID:" in line:
1487
+ cpuinfo["manufacturer_id"] = line.split(":")[1].strip()
1488
+ if "Model name" in line:
1489
+ cpuinfo["model_name"] = line.split(":")[1].strip()
1490
+ elif platform_os == "Darwin":
1491
+ cpuinfo["address_width"] = format_address_width(
1492
+ processes.execute_command_and_return_output("getconf LONG_BIT")
1493
+ )
1494
+ cpuinfo["manufacturer_id"] = processes.execute_command_and_return_output(
1495
+ "sysctl -n machdep.cpu.brand_string"
1496
+ )
1497
+ cpuinfo["model_name"] = processes.execute_command_and_return_output(
1498
+ "uname -m"
1499
+ )
1500
+ except Exception as err:
1501
+ console.error(f"Failed to retrieve CPU info. {err}")
1502
+ return None
1503
+
1504
+ return (
1505
+ CpuInfo(
1506
+ manufacturer_id=cpuinfo.get("manufacturer_id"),
1507
+ model_name=cpuinfo.get("model_name"),
1508
+ address_width=cpuinfo.get("address_width"),
1509
+ )
1510
+ if cpuinfo
1511
+ else None
1512
+ )
1513
+
1514
+
1515
+ @functools.lru_cache(maxsize=None)
1516
+ def is_windows_bun_supported() -> bool:
1517
+ """Check whether the underlining host running windows qualifies to run bun.
1518
+ We typically do not run bun on ARM or 32 bit devices that use windows.
1519
+
1520
+ Returns:
1521
+ Whether the host is qualified to use bun.
1522
+ """
1523
+ cpu_info = get_cpu_info()
1524
+ return (
1525
+ constants.IS_WINDOWS
1526
+ and cpu_info is not None
1527
+ and cpu_info.address_width == 64
1528
+ and cpu_info.model_name is not None
1529
+ and "ARM" not in cpu_info.model_name
1530
+ )
reflex/utils/processes.py CHANGED
@@ -137,7 +137,9 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
137
137
  # Add the node bin path to the PATH environment variable.
138
138
  env = {
139
139
  **os.environ,
140
- "PATH": os.pathsep.join([node_bin_path if node_bin_path else "", os.environ["PATH"]]), # type: ignore
140
+ "PATH": os.pathsep.join(
141
+ [node_bin_path if node_bin_path else "", os.environ["PATH"]]
142
+ ), # type: ignore
141
143
  **kwargs.pop("env", {}),
142
144
  }
143
145
  kwargs = {
@@ -345,3 +347,21 @@ def run_process_with_fallback(args, *, show_status_message, fallback=None, **kwa
345
347
  fallback=None,
346
348
  **kwargs,
347
349
  )
350
+
351
+
352
+ def execute_command_and_return_output(command) -> str | None:
353
+ """Execute a command and return the output.
354
+
355
+ Args:
356
+ command: The command to run.
357
+
358
+ Returns:
359
+ The output of the command.
360
+ """
361
+ try:
362
+ return subprocess.check_output(command, shell=True).decode().strip()
363
+ except subprocess.SubprocessError as err:
364
+ console.error(
365
+ f"The command `{command}` failed with error: {err}. This will return None."
366
+ )
367
+ return None
@@ -34,7 +34,7 @@ PWD = Path(".").resolve()
34
34
 
35
35
  EXCLUDED_FILES = [
36
36
  "__init__.py",
37
- "app.py",
37
+ # "app.py",
38
38
  "component.py",
39
39
  "bare.py",
40
40
  "foreach.py",
@@ -117,6 +117,29 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
117
117
  """
118
118
  res = ""
119
119
  args = get_args(value)
120
+
121
+ if value is type(None):
122
+ return "None"
123
+
124
+ if rx_types.is_union(value):
125
+ if type(None) in value.__args__:
126
+ res_args = [
127
+ _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
128
+ for arg in value.__args__
129
+ if arg is not type(None)
130
+ ]
131
+ if len(res_args) == 1:
132
+ return f"Optional[{res_args[0]}]"
133
+ else:
134
+ res = f"Union[{', '.join(res_args)}]"
135
+ return f"Optional[{res}]"
136
+
137
+ res_args = [
138
+ _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
139
+ for arg in value.__args__
140
+ ]
141
+ return f"Union[{', '.join(res_args)}]"
142
+
120
143
  if args:
121
144
  inner_container_type_args = (
122
145
  [repr(arg) for arg in args]
@@ -141,6 +164,20 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
141
164
  res = f"Union[{res}]"
142
165
  elif isinstance(value, str):
143
166
  ev = eval(value, type_hint_globals)
167
+ if rx_types.is_optional(ev):
168
+ # hints = {
169
+ # _get_type_hint(arg, type_hint_globals, is_optional=False)
170
+ # for arg in ev.__args__
171
+ # }
172
+ return _get_type_hint(ev, type_hint_globals, is_optional=False)
173
+ # return f"Optional[{', '.join(hints)}]"
174
+
175
+ if rx_types.is_union(ev):
176
+ res = [
177
+ _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
178
+ for arg in ev.__args__
179
+ ]
180
+ return f"Union[{', '.join(res)}]"
144
181
  res = (
145
182
  _get_type_hint(ev, type_hint_globals, is_optional=False)
146
183
  if ev.__name__ == "Var"
@@ -424,7 +461,58 @@ def _generate_component_create_functiondef(
424
461
  return definition
425
462
 
426
463
 
464
+ def _generate_staticmethod_call_functiondef(
465
+ node: ast.FunctionDef | None,
466
+ clz: type[Component] | type[SimpleNamespace],
467
+ type_hint_globals: dict[str, Any],
468
+ ) -> ast.FunctionDef | None:
469
+ ...
470
+
471
+ fullspec = getfullargspec(clz.__call__)
472
+
473
+ call_args = ast.arguments(
474
+ args=[
475
+ ast.arg(
476
+ name,
477
+ annotation=ast.Name(
478
+ id=_get_type_hint(
479
+ anno := fullspec.annotations[name],
480
+ type_hint_globals,
481
+ is_optional=rx_types.is_optional(anno),
482
+ )
483
+ ),
484
+ )
485
+ for name in fullspec.args
486
+ ],
487
+ posonlyargs=[],
488
+ kwonlyargs=[],
489
+ kw_defaults=[],
490
+ kwarg=ast.arg(arg="props"),
491
+ defaults=[],
492
+ )
493
+ definition = ast.FunctionDef(
494
+ name="__call__",
495
+ args=call_args,
496
+ body=[
497
+ ast.Expr(value=ast.Constant(value=clz.__call__.__doc__)),
498
+ ast.Expr(
499
+ value=ast.Constant(...),
500
+ ),
501
+ ],
502
+ decorator_list=[ast.Name(id="staticmethod")],
503
+ lineno=node.lineno if node is not None else None,
504
+ returns=ast.Constant(
505
+ value=_get_type_hint(
506
+ typing.get_type_hints(clz.__call__).get("return", None),
507
+ type_hint_globals,
508
+ )
509
+ ),
510
+ )
511
+ return definition
512
+
513
+
427
514
  def _generate_namespace_call_functiondef(
515
+ node: ast.ClassDef | None,
428
516
  clz_name: str,
429
517
  classes: dict[str, type[Component] | type[SimpleNamespace]],
430
518
  type_hint_globals: dict[str, Any],
@@ -432,6 +520,7 @@ def _generate_namespace_call_functiondef(
432
520
  """Generate the __call__ function definition for a SimpleNamespace.
433
521
 
434
522
  Args:
523
+ node: The existing __call__ classdef parent node from the ast
435
524
  clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for.
436
525
  classes: Map name to actual class definition.
437
526
  type_hint_globals: The globals to use to resolving a type hint str.
@@ -446,10 +535,12 @@ def _generate_namespace_call_functiondef(
446
535
 
447
536
  clz = classes[clz_name]
448
537
 
538
+ if not hasattr(clz.__call__, "__self__"):
539
+ return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) # type: ignore
540
+
449
541
  # Determine which class is wrapped by the namespace __call__ method
450
542
  component_clz = clz.__call__.__self__
451
543
 
452
- # Only generate for create functions
453
544
  if clz.__call__.__func__.__name__ != "create":
454
545
  return None
455
546
 
@@ -603,6 +694,7 @@ class StubGenerator(ast.NodeTransformer):
603
694
  if not child.targets[:]:
604
695
  node.body.remove(child)
605
696
  call_definition = _generate_namespace_call_functiondef(
697
+ node,
606
698
  self.current_class,
607
699
  self.classes,
608
700
  type_hint_globals=self.type_hint_globals,
@@ -738,9 +830,7 @@ class PyiGenerator:
738
830
  mode=black.mode.Mode(is_pyi=True),
739
831
  ).splitlines():
740
832
  # Bit of a hack here, since the AST cannot represent comments.
741
- if "def create(" in formatted_line:
742
- pyi_content.append(formatted_line + " # type: ignore")
743
- elif "Figure" in formatted_line:
833
+ if "def create(" in formatted_line or "Figure" in formatted_line:
744
834
  pyi_content.append(formatted_line + " # type: ignore")
745
835
  else:
746
836
  pyi_content.append(formatted_line)
@@ -359,7 +359,7 @@ try:
359
359
  mime_type = MIME[image_format]
360
360
  except KeyError:
361
361
  # Unknown mime_type: warn and return image/png and hope the browser can sort it out.
362
- warnings.warn(
362
+ warnings.warn( # noqa: B028
363
363
  f"Unknown mime type for {image} {image_format}. Defaulting to image/png"
364
364
  )
365
365
  mime_type = "image/png"
reflex/utils/telemetry.py CHANGED
@@ -32,6 +32,15 @@ def get_os() -> str:
32
32
  return platform.system()
33
33
 
34
34
 
35
+ def get_detailed_platform_str() -> str:
36
+ """Get the detailed os/platform string.
37
+
38
+ Returns:
39
+ The platform string
40
+ """
41
+ return platform.platform()
42
+
43
+
35
44
  def get_python_version() -> str:
36
45
  """Get the Python version.
37
46
 
@@ -87,15 +96,18 @@ def _raise_on_missing_project_hash() -> bool:
87
96
  return True
88
97
 
89
98
 
90
- def _prepare_event(event: str) -> dict:
99
+ def _prepare_event(event: str, **kwargs) -> dict:
91
100
  """Prepare the event to be sent to the PostHog server.
92
101
 
93
102
  Args:
94
103
  event: The event name.
104
+ kwargs: Additional data to send with the event.
95
105
 
96
106
  Returns:
97
107
  The event data.
98
108
  """
109
+ from reflex.utils.prerequisites import get_cpu_info
110
+
99
111
  installation_id = ensure_reflex_installation_id()
100
112
  project_hash = get_project_hash(raise_on_fail=_raise_on_missing_project_hash())
101
113
 
@@ -111,6 +123,9 @@ def _prepare_event(event: str) -> dict:
111
123
  else:
112
124
  # for python 3.11 & 3.12
113
125
  stamp = datetime.now(UTC).isoformat()
126
+
127
+ cpuinfo = get_cpu_info()
128
+
114
129
  return {
115
130
  "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
116
131
  "event": event,
@@ -118,10 +133,17 @@ def _prepare_event(event: str) -> dict:
118
133
  "distinct_id": installation_id,
119
134
  "distinct_app_id": project_hash,
120
135
  "user_os": get_os(),
136
+ "user_os_detail": get_detailed_platform_str(),
121
137
  "reflex_version": get_reflex_version(),
122
138
  "python_version": get_python_version(),
123
139
  "cpu_count": get_cpu_count(),
124
140
  "memory": get_memory(),
141
+ "cpu_info": dict(cpuinfo) if cpuinfo else {},
142
+ **(
143
+ {"template": template}
144
+ if (template := kwargs.get("template")) is not None
145
+ else {}
146
+ ),
125
147
  },
126
148
  "timestamp": stamp,
127
149
  }
@@ -135,12 +157,13 @@ def _send_event(event_data: dict) -> bool:
135
157
  return False
136
158
 
137
159
 
138
- def send(event: str, telemetry_enabled: bool | None = None) -> bool:
160
+ def send(event: str, telemetry_enabled: bool | None = None, **kwargs) -> bool:
139
161
  """Send anonymous telemetry for Reflex.
140
162
 
141
163
  Args:
142
164
  event: The event name.
143
165
  telemetry_enabled: Whether to send the telemetry (If None, get from config).
166
+ kwargs: Additional data to send with the event.
144
167
 
145
168
  Returns:
146
169
  Whether the telemetry was sent successfully.
@@ -155,8 +178,7 @@ def send(event: str, telemetry_enabled: bool | None = None) -> bool:
155
178
  if not telemetry_enabled:
156
179
  return False
157
180
 
158
- event_data = _prepare_event(event)
181
+ event_data = _prepare_event(event, **kwargs)
159
182
  if not event_data:
160
183
  return False
161
-
162
184
  return _send_event(event_data)
reflex/utils/types.py CHANGED
@@ -4,16 +4,18 @@ from __future__ import annotations
4
4
 
5
5
  import contextlib
6
6
  import inspect
7
+ import sys
7
8
  import types
8
9
  from functools import wraps
9
10
  from typing import (
10
- TYPE_CHECKING,
11
11
  Any,
12
12
  Callable,
13
+ Dict,
13
14
  Iterable,
14
15
  List,
15
16
  Literal,
16
17
  Optional,
18
+ Tuple,
17
19
  Type,
18
20
  Union,
19
21
  _GenericAlias, # type: ignore
@@ -25,23 +27,22 @@ from typing import (
25
27
  import sqlalchemy
26
28
 
27
29
  try:
28
- # TODO The type checking guard can be removed once
29
- # reflex-hosting-cli tools are compatible with pydantic v2
30
-
31
- if not TYPE_CHECKING:
32
- from pydantic.v1.fields import ModelField
33
- else:
34
- raise ModuleNotFoundError
30
+ from pydantic.v1.fields import ModelField
35
31
  except ModuleNotFoundError:
36
- from pydantic.fields import ModelField
32
+ from pydantic.fields import ModelField # type: ignore
37
33
 
38
34
  from sqlalchemy.ext.associationproxy import AssociationProxyInstance
39
35
  from sqlalchemy.ext.hybrid import hybrid_property
40
- from sqlalchemy.orm import DeclarativeBase, Mapped, QueryableAttribute, Relationship
36
+ from sqlalchemy.orm import (
37
+ DeclarativeBase,
38
+ Mapped,
39
+ QueryableAttribute,
40
+ Relationship,
41
+ )
41
42
 
42
43
  from reflex import constants
43
44
  from reflex.base import Base
44
- from reflex.utils import serializers
45
+ from reflex.utils import console, serializers
45
46
 
46
47
  # Potential GenericAlias types for isinstance checks.
47
48
  GenericAliasTypes = [_GenericAlias]
@@ -76,6 +77,13 @@ StateIterVar = Union[list, set, tuple]
76
77
  ArgsSpec = Callable
77
78
 
78
79
 
80
+ PrimitiveToAnnotation = {
81
+ list: List,
82
+ tuple: Tuple,
83
+ dict: Dict,
84
+ }
85
+
86
+
79
87
  class Unset:
80
88
  """A class to represent an unset value.
81
89
 
@@ -111,6 +119,18 @@ def is_generic_alias(cls: GenericType) -> bool:
111
119
  return isinstance(cls, GenericAliasTypes)
112
120
 
113
121
 
122
+ def is_none(cls: GenericType) -> bool:
123
+ """Check if a class is None.
124
+
125
+ Args:
126
+ cls: The class to check.
127
+
128
+ Returns:
129
+ Whether the class is None.
130
+ """
131
+ return cls is type(None) or cls is None
132
+
133
+
114
134
  def is_union(cls: GenericType) -> bool:
115
135
  """Check if a class is a Union.
116
136
 
@@ -192,8 +212,20 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
192
212
  elif isinstance(cls, type) and issubclass(cls, DeclarativeBase):
193
213
  insp = sqlalchemy.inspect(cls)
194
214
  if name in insp.columns:
195
- return insp.columns[name].type.python_type
196
- if name not in insp.all_orm_descriptors.keys():
215
+ # check for list types
216
+ column = insp.columns[name]
217
+ column_type = column.type
218
+ type_ = insp.columns[name].type.python_type
219
+ if hasattr(column_type, "item_type") and (
220
+ item_type := column_type.item_type.python_type # type: ignore
221
+ ):
222
+ if type_ in PrimitiveToAnnotation:
223
+ type_ = PrimitiveToAnnotation[type_] # type: ignore
224
+ type_ = type_[item_type] # type: ignore
225
+ if column.nullable:
226
+ type_ = Optional[type_]
227
+ return type_
228
+ if name not in insp.all_orm_descriptors:
197
229
  return None
198
230
  descriptor = insp.all_orm_descriptors[name]
199
231
  if hint := get_property_hint(descriptor):
@@ -202,11 +234,10 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
202
234
  prop = descriptor.property
203
235
  if not isinstance(prop, Relationship):
204
236
  return None
205
- class_ = prop.mapper.class_
206
- if prop.uselist:
207
- return List[class_]
208
- else:
209
- return class_
237
+ type_ = prop.mapper.class_
238
+ # TODO: check for nullable?
239
+ type_ = List[type_] if prop.uselist else Optional[type_]
240
+ return type_
210
241
  if isinstance(attr, AssociationProxyInstance):
211
242
  return List[
212
243
  get_attribute_access_type(
@@ -232,6 +263,19 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
232
263
  if type_ is not None:
233
264
  # Return the first attribute type that is accessible.
234
265
  return type_
266
+ elif isinstance(cls, type):
267
+ # Bare class
268
+ if sys.version_info >= (3, 10):
269
+ exceptions = NameError
270
+ else:
271
+ exceptions = (NameError, TypeError)
272
+ try:
273
+ hints = get_type_hints(cls)
274
+ if name in hints:
275
+ return hints[name]
276
+ except exceptions as e:
277
+ console.warn(f"Failed to resolve ForwardRefs for {cls}.{name} due to {e}")
278
+ pass
235
279
  return None # Attribute is not accessible.
236
280
 
237
281