reflex 0.7.3a1__py3-none-any.whl → 0.7.4__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 (50) hide show
  1. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +9 -1
  2. reflex/.templates/web/utils/state.js +1 -1
  3. reflex/app.py +21 -5
  4. reflex/app_mixins/middleware.py +2 -3
  5. reflex/base.py +3 -3
  6. reflex/compiler/compiler.py +68 -8
  7. reflex/components/component.py +6 -3
  8. reflex/components/core/client_side_routing.py +3 -3
  9. reflex/components/core/cond.py +20 -12
  10. reflex/components/core/upload.py +1 -1
  11. reflex/components/dynamic.py +2 -4
  12. reflex/components/lucide/icon.py +20 -27
  13. reflex/components/plotly/plotly.py +9 -9
  14. reflex/components/recharts/recharts.py +2 -2
  15. reflex/components/sonner/toast.py +1 -1
  16. reflex/config.py +23 -23
  17. reflex/constants/__init__.py +1 -2
  18. reflex/constants/base.py +3 -0
  19. reflex/constants/installer.py +8 -105
  20. reflex/custom_components/custom_components.py +8 -3
  21. reflex/reflex.py +22 -4
  22. reflex/state.py +9 -1
  23. reflex/testing.py +7 -1
  24. reflex/utils/build.py +3 -4
  25. reflex/utils/exec.py +156 -75
  26. reflex/utils/net.py +107 -18
  27. reflex/utils/path_ops.py +15 -25
  28. reflex/utils/prerequisites.py +225 -189
  29. reflex/utils/processes.py +70 -35
  30. reflex/utils/redir.py +3 -1
  31. reflex/utils/registry.py +16 -8
  32. reflex/vars/base.py +2 -38
  33. reflex/vars/datetime.py +10 -34
  34. reflex/vars/number.py +16 -112
  35. reflex/vars/sequence.py +99 -108
  36. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/METADATA +32 -23
  37. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/RECORD +58 -68
  38. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/WHEEL +1 -1
  39. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/entry_points.txt +0 -3
  40. benchmarks/__init__.py +0 -3
  41. benchmarks/benchmark_compile_times.py +0 -147
  42. benchmarks/benchmark_imports.py +0 -128
  43. benchmarks/benchmark_lighthouse.py +0 -75
  44. benchmarks/benchmark_package_size.py +0 -135
  45. benchmarks/benchmark_web_size.py +0 -106
  46. benchmarks/conftest.py +0 -20
  47. benchmarks/lighthouse.sh +0 -77
  48. benchmarks/utils.py +0 -74
  49. reflex/app_module_for_backend.py +0 -33
  50. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/licenses/LICENSE +0 -0
reflex/utils/exec.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
+ import importlib.util
6
7
  import json
7
8
  import os
8
9
  import platform
@@ -19,6 +20,7 @@ from reflex import constants
19
20
  from reflex.config import environment, get_config
20
21
  from reflex.constants.base import LogLevel
21
22
  from reflex.utils import console, path_ops
23
+ from reflex.utils.decorator import once
22
24
  from reflex.utils.prerequisites import get_web_dir
23
25
 
24
26
  # For uvicorn windows bug fix (#2335)
@@ -154,7 +156,11 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
154
156
  console.rule("[bold green]App Running")
155
157
  os.environ["PORT"] = str(get_config().frontend_port if port is None else port)
156
158
  run_process_and_launch_url(
157
- [prerequisites.get_package_manager(), "run", "dev"],
159
+ [
160
+ *prerequisites.get_js_package_executor(raise_on_none=True)[0],
161
+ "run",
162
+ "dev",
163
+ ],
158
164
  backend_present,
159
165
  )
160
166
 
@@ -176,18 +182,35 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
176
182
  # Run the frontend in production mode.
177
183
  console.rule("[bold green]App Running")
178
184
  run_process_and_launch_url(
179
- [prerequisites.get_package_manager(), "run", "prod"],
185
+ [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
180
186
  backend_present,
181
187
  )
182
188
 
183
189
 
190
+ @once
191
+ def _warn_user_about_uvicorn():
192
+ # When we eventually switch to Granian by default, we should enable this warning.
193
+ if False:
194
+ console.warn(
195
+ "Using Uvicorn for backend as it is installed. This behavior will change in 0.8.0 to use Granian by default."
196
+ )
197
+
198
+
184
199
  def should_use_granian():
185
200
  """Whether to use Granian for backend.
186
201
 
187
202
  Returns:
188
203
  True if Granian should be used.
189
204
  """
190
- return environment.REFLEX_USE_GRANIAN.get()
205
+ if environment.REFLEX_USE_GRANIAN.get():
206
+ return True
207
+ if (
208
+ importlib.util.find_spec("uvicorn") is None
209
+ or importlib.util.find_spec("gunicorn") is None
210
+ ):
211
+ return True
212
+ _warn_user_about_uvicorn()
213
+ return False
191
214
 
192
215
 
193
216
  def get_app_module():
@@ -196,22 +219,9 @@ def get_app_module():
196
219
  Returns:
197
220
  The app module for the backend.
198
221
  """
199
- return f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
200
-
201
-
202
- def get_granian_target():
203
- """Get the Granian target for the backend.
204
-
205
- Returns:
206
- The Granian target for the backend.
207
- """
208
- import reflex
209
-
210
- app_module_path = Path(reflex.__file__).parent / "app_module_for_backend.py"
222
+ config = get_config()
211
223
 
212
- return (
213
- f"{app_module_path!s}:{constants.CompileVars.APP}.{constants.CompileVars.API}"
214
- )
224
+ return f"{config.module}:{constants.CompileVars.APP}"
215
225
 
216
226
 
217
227
  def run_backend(
@@ -313,7 +323,8 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel):
313
323
  import uvicorn
314
324
 
315
325
  uvicorn.run(
316
- app=f"{get_app_module()}.{constants.CompileVars.API}",
326
+ app=f"{get_app_module()}",
327
+ factory=True,
317
328
  host=host,
318
329
  port=port,
319
330
  log_level=loglevel.value,
@@ -331,36 +342,91 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
331
342
  loglevel: The log level.
332
343
  """
333
344
  console.debug("Using Granian for backend")
334
- try:
335
- from granian.constants import Interfaces
336
- from granian.log import LogLevels
337
- from granian.server import Server as Granian
338
-
339
- Granian(
340
- target=get_granian_target(),
341
- address=host,
342
- port=port,
343
- interface=Interfaces.ASGI,
344
- log_level=LogLevels(loglevel.value),
345
- reload=True,
346
- reload_paths=get_reload_paths(),
347
- ).serve()
348
- except ImportError:
349
- console.error(
350
- 'InstallError: REFLEX_USE_GRANIAN is set but `granian` is not installed. (run `pip install "granian[reload]>=1.6.0"`)'
345
+
346
+ from granian.constants import Interfaces
347
+ from granian.log import LogLevels
348
+ from granian.server import MPServer as Granian
349
+
350
+ Granian(
351
+ target=get_app_module(),
352
+ factory=True,
353
+ address=host,
354
+ port=port,
355
+ interface=Interfaces.ASGI,
356
+ log_level=LogLevels(loglevel.value),
357
+ reload=True,
358
+ reload_paths=get_reload_paths(),
359
+ ).serve()
360
+
361
+
362
+ def _deprecate_asgi_config(
363
+ config_name: str,
364
+ reason: str = "",
365
+ ):
366
+ # When we eventually switch to Granian by default, we should enable this deprecation.
367
+ if False:
368
+ console.deprecate(
369
+ f"config.{config_name}",
370
+ reason=reason,
371
+ deprecation_version="0.7.5",
372
+ removal_version="0.8.0",
351
373
  )
352
- os._exit(1)
353
374
 
354
375
 
376
+ @once
355
377
  def _get_backend_workers():
356
378
  from reflex.utils import processes
357
379
 
358
380
  config = get_config()
359
- return (
360
- processes.get_num_workers()
361
- if not config.gunicorn_workers
362
- else config.gunicorn_workers
363
- )
381
+
382
+ gunicorn_workers = config.gunicorn_workers or 0
383
+
384
+ if config.gunicorn_workers is not None:
385
+ _deprecate_asgi_config(
386
+ "gunicorn_workers",
387
+ "If you're using Granian, use GRANIAN_WORKERS instead.",
388
+ )
389
+
390
+ return gunicorn_workers if gunicorn_workers else processes.get_num_workers()
391
+
392
+
393
+ @once
394
+ def _get_backend_timeout():
395
+ config = get_config()
396
+
397
+ timeout = config.timeout or 120
398
+
399
+ if config.timeout is not None:
400
+ _deprecate_asgi_config(
401
+ "timeout",
402
+ "If you're using Granian, use GRANIAN_WORKERS_LIFETIME instead.",
403
+ )
404
+
405
+ return timeout
406
+
407
+
408
+ @once
409
+ def _get_backend_max_requests():
410
+ config = get_config()
411
+
412
+ gunicorn_max_requests = config.gunicorn_max_requests or 120
413
+
414
+ if config.gunicorn_max_requests is not None:
415
+ _deprecate_asgi_config("gunicorn_max_requests")
416
+
417
+ return gunicorn_max_requests
418
+
419
+
420
+ @once
421
+ def _get_backend_max_requests_jitter():
422
+ config = get_config()
423
+
424
+ gunicorn_max_requests_jitter = config.gunicorn_max_requests_jitter or 25
425
+
426
+ if config.gunicorn_max_requests_jitter is not None:
427
+ _deprecate_asgi_config("gunicorn_max_requests_jitter")
428
+
429
+ return gunicorn_max_requests_jitter
364
430
 
365
431
 
366
432
  def run_backend_prod(
@@ -404,17 +470,25 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
404
470
  [
405
471
  "uvicorn",
406
472
  *(
407
- [
473
+ (
408
474
  "--limit-max-requests",
409
- str(config.gunicorn_max_requests),
410
- ]
411
- if config.gunicorn_max_requests > 0
412
- else []
475
+ str(max_requessts),
476
+ )
477
+ if (
478
+ (max_requessts := _get_backend_max_requests()) is not None
479
+ and max_requessts > 0
480
+ )
481
+ else ()
482
+ ),
483
+ *(
484
+ ("--timeout-keep-alive", str(timeout))
485
+ if (timeout := _get_backend_timeout()) is not None
486
+ else ()
413
487
  ),
414
- *("--timeout-keep-alive", str(config.timeout)),
415
488
  *("--host", host),
416
489
  *("--port", str(port)),
417
490
  *("--workers", str(_get_backend_workers())),
491
+ "--factory",
418
492
  app_module,
419
493
  ]
420
494
  if constants.IS_WINDOWS
@@ -422,17 +496,34 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
422
496
  "gunicorn",
423
497
  *("--worker-class", config.gunicorn_worker_class),
424
498
  *(
425
- [
499
+ (
426
500
  "--max-requests",
427
- str(config.gunicorn_max_requests),
501
+ str(max_requessts),
502
+ )
503
+ if (
504
+ (max_requessts := _get_backend_max_requests()) is not None
505
+ and max_requessts > 0
506
+ )
507
+ else ()
508
+ ),
509
+ *(
510
+ (
428
511
  "--max-requests-jitter",
429
- str(config.gunicorn_max_requests_jitter),
430
- ]
431
- if config.gunicorn_max_requests > 0
432
- else []
512
+ str(max_requessts_jitter),
513
+ )
514
+ if (
515
+ (max_requessts_jitter := _get_backend_max_requests_jitter())
516
+ is not None
517
+ and max_requessts_jitter > 0
518
+ )
519
+ else ()
433
520
  ),
434
521
  "--preload",
435
- *("--timeout", str(config.timeout)),
522
+ *(
523
+ ("--timeout", str(timeout))
524
+ if (timeout := _get_backend_timeout()) is not None
525
+ else ()
526
+ ),
436
527
  *("--bind", f"{host}:{port}"),
437
528
  *("--threads", str(_get_backend_workers())),
438
529
  f"{app_module}()",
@@ -468,17 +559,12 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel):
468
559
 
469
560
  command = [
470
561
  "granian",
471
- "--workers",
472
- str(_get_backend_workers()),
473
- "--log-level",
474
- "critical",
475
- "--host",
476
- host,
477
- "--port",
478
- str(port),
479
- "--interface",
480
- str(Interfaces.ASGI),
481
- get_granian_target(),
562
+ *("--workers", str(_get_backend_workers())),
563
+ *("--log-level", "critical"),
564
+ *("--host", host),
565
+ *("--port", str(port)),
566
+ *("--interface", str(Interfaces.ASGI)),
567
+ *("--factory", get_app_module()),
482
568
  ]
483
569
  processes.new_process(
484
570
  command,
@@ -518,13 +604,8 @@ def output_system_info():
518
604
 
519
605
  system = platform.system()
520
606
 
521
- fnm_info = f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]"
522
-
523
- dependencies.extend(
524
- [
525
- fnm_info,
526
- f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {path_ops.get_bun_path()})]",
527
- ],
607
+ dependencies.append(
608
+ f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {path_ops.get_bun_path()})]"
528
609
  )
529
610
 
530
611
  if system == "Linux":
@@ -540,10 +621,10 @@ def output_system_info():
540
621
  console.debug(f"{dep}")
541
622
 
542
623
  console.debug(
543
- f"Using package installer at: {prerequisites.get_install_package_manager(on_failure_return_none=True)}"
624
+ f"Using package installer at: {prerequisites.get_nodejs_compatible_package_managers(raise_on_none=False)}"
544
625
  )
545
626
  console.debug(
546
- f"Using package executer at: {prerequisites.get_package_manager(on_failure_return_none=True)}"
627
+ f"Using package executer at: {prerequisites.get_js_package_executor(raise_on_none=False)}"
547
628
  )
548
629
  if system != "Windows":
549
630
  console.debug(f"Unzip path: {path_ops.which('unzip')}")
reflex/utils/net.py CHANGED
@@ -1,8 +1,13 @@
1
1
  """Helpers for downloading files from the network."""
2
2
 
3
+ import functools
4
+ import time
5
+ from typing import Callable, ParamSpec, TypeVar
6
+
3
7
  import httpx
4
8
 
5
- from ..config import environment
9
+ from reflex.utils.decorator import once
10
+
6
11
  from . import console
7
12
 
8
13
 
@@ -12,30 +17,114 @@ def _httpx_verify_kwarg() -> bool:
12
17
  Returns:
13
18
  True if SSL verification is enabled, False otherwise
14
19
  """
20
+ from ..config import environment
21
+
15
22
  return not environment.SSL_NO_VERIFY.get()
16
23
 
17
24
 
18
- def get(url: str, **kwargs) -> httpx.Response:
19
- """Make an HTTP GET request.
25
+ _P = ParamSpec("_P")
26
+ _T = TypeVar("_T")
27
+
28
+
29
+ def _wrap_https_func(
30
+ func: Callable[_P, _T],
31
+ ) -> Callable[_P, _T]:
32
+ """Wrap an HTTPS function with logging.
20
33
 
21
34
  Args:
22
- url: The URL to request.
23
- **kwargs: Additional keyword arguments to pass to httpx.get.
35
+ func: The function to wrap.
24
36
 
25
37
  Returns:
26
- The response object.
38
+ The wrapped function.
39
+ """
40
+
41
+ @functools.wraps(func)
42
+ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
43
+ url = args[0]
44
+ console.debug(f"Sending HTTPS request to {args[0]}")
45
+ initial_time = time.time()
46
+ try:
47
+ response = func(*args, **kwargs)
48
+ except httpx.ConnectError as err:
49
+ if "CERTIFICATE_VERIFY_FAILED" in str(err):
50
+ # If the error is a certificate verification error, recommend mitigating steps.
51
+ console.error(
52
+ f"Certificate verification failed for {url}. Set environment variable SSL_CERT_FILE to the "
53
+ "path of the certificate file or SSL_NO_VERIFY=1 to disable verification."
54
+ )
55
+ raise
56
+ else:
57
+ console.debug(
58
+ f"Received response from {url} in {time.time() - initial_time:.3f} seconds"
59
+ )
60
+ return response
61
+
62
+ return wrapper
63
+
27
64
 
28
- Raises:
29
- httpx.ConnectError: If the connection cannot be established.
65
+ def _is_ipv4_supported() -> bool:
66
+ """Determine if the system supports IPv4.
67
+
68
+ Returns:
69
+ True if the system supports IPv4, False otherwise.
30
70
  """
31
- kwargs.setdefault("verify", _httpx_verify_kwarg())
32
71
  try:
33
- return httpx.get(url, **kwargs)
34
- except httpx.ConnectError as err:
35
- if "CERTIFICATE_VERIFY_FAILED" in str(err):
36
- # If the error is a certificate verification error, recommend mitigating steps.
37
- console.error(
38
- f"Certificate verification failed for {url}. Set environment variable SSL_CERT_FILE to the "
39
- "path of the certificate file or SSL_NO_VERIFY=1 to disable verification."
40
- )
41
- raise
72
+ httpx.head("http://1.1.1.1", timeout=3)
73
+ except httpx.RequestError:
74
+ return False
75
+ else:
76
+ return True
77
+
78
+
79
+ def _is_ipv6_supported() -> bool:
80
+ """Determine if the system supports IPv6.
81
+
82
+ Returns:
83
+ True if the system supports IPv6, False otherwise.
84
+ """
85
+ try:
86
+ httpx.head("http://[2606:4700:4700::1111]", timeout=3)
87
+ except httpx.RequestError:
88
+ return False
89
+ else:
90
+ return True
91
+
92
+
93
+ def _should_use_ipv6() -> bool:
94
+ """Determine if the system supports IPv6.
95
+
96
+ Returns:
97
+ True if the system supports IPv6, False otherwise.
98
+ """
99
+ return not _is_ipv4_supported() and _is_ipv6_supported()
100
+
101
+
102
+ def _httpx_local_address_kwarg() -> str:
103
+ """Get the value of the HTTPX local_address keyword argument.
104
+
105
+ Returns:
106
+ The local address to bind to
107
+ """
108
+ from ..config import environment
109
+
110
+ return environment.REFLEX_HTTP_CLIENT_BIND_ADDRESS.get() or (
111
+ "::" if _should_use_ipv6() else "0.0.0.0"
112
+ )
113
+
114
+
115
+ @once
116
+ def _httpx_client() -> httpx.Client:
117
+ """Get an HTTPX client.
118
+
119
+ Returns:
120
+ An HTTPX client.
121
+ """
122
+ return httpx.Client(
123
+ transport=httpx.HTTPTransport(
124
+ local_address=_httpx_local_address_kwarg(),
125
+ verify=_httpx_verify_kwarg(),
126
+ )
127
+ )
128
+
129
+
130
+ get = _wrap_https_func(_httpx_client().get)
reflex/utils/path_ops.py CHANGED
@@ -9,7 +9,6 @@ import shutil
9
9
  import stat
10
10
  from pathlib import Path
11
11
 
12
- from reflex import constants
13
12
  from reflex.config import environment, get_config
14
13
 
15
14
  # Shorthand for join.
@@ -43,13 +42,19 @@ def rm(path: str | Path):
43
42
  path.unlink()
44
43
 
45
44
 
46
- def cp(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
45
+ def cp(
46
+ src: str | Path,
47
+ dest: str | Path,
48
+ overwrite: bool = True,
49
+ ignore: tuple[str, ...] | None = None,
50
+ ) -> bool:
47
51
  """Copy a file or directory.
48
52
 
49
53
  Args:
50
54
  src: The path to the file or directory.
51
55
  dest: The path to the destination.
52
56
  overwrite: Whether to overwrite the destination.
57
+ ignore: Ignoring files and directories that match one of the glob-style patterns provided
53
58
 
54
59
  Returns:
55
60
  Whether the copy was successful.
@@ -61,7 +66,11 @@ def cp(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
61
66
  return False
62
67
  if src.is_dir():
63
68
  rm(dest)
64
- shutil.copytree(src, dest)
69
+ shutil.copytree(
70
+ src,
71
+ dest,
72
+ ignore=shutil.ignore_patterns(*ignore) if ignore is not None else ignore,
73
+ )
65
74
  else:
66
75
  shutil.copyfile(src, dest)
67
76
  return True
@@ -146,15 +155,6 @@ def which(program: str | Path) -> Path | None:
146
155
  return Path(which_result) if which_result else None
147
156
 
148
157
 
149
- def use_system_node() -> bool:
150
- """Check if the system node should be used.
151
-
152
- Returns:
153
- Whether the system node should be used.
154
- """
155
- return environment.REFLEX_USE_SYSTEM_NODE.get()
156
-
157
-
158
158
  def use_system_bun() -> bool:
159
159
  """Check if the system bun should be used.
160
160
 
@@ -170,11 +170,7 @@ def get_node_bin_path() -> Path | None:
170
170
  Returns:
171
171
  The path to the node bin folder.
172
172
  """
173
- bin_path = Path(constants.Node.BIN_PATH)
174
- if not bin_path.exists():
175
- path = which("node")
176
- return path.parent.absolute() if path else None
177
- return bin_path.absolute()
173
+ return bin_path.parent.absolute() if (bin_path := get_node_path()) else None
178
174
 
179
175
 
180
176
  def get_node_path() -> Path | None:
@@ -183,10 +179,7 @@ def get_node_path() -> Path | None:
183
179
  Returns:
184
180
  The path to the node binary file.
185
181
  """
186
- node_path = Path(constants.Node.PATH)
187
- if use_system_node() or not node_path.exists():
188
- node_path = which("node")
189
- return node_path
182
+ return which("node")
190
183
 
191
184
 
192
185
  def get_npm_path() -> Path | None:
@@ -195,10 +188,7 @@ def get_npm_path() -> Path | None:
195
188
  Returns:
196
189
  The path to the npm binary file.
197
190
  """
198
- npm_path = Path(constants.Node.NPM_PATH)
199
- if use_system_node() or not npm_path.exists():
200
- npm_path = which("npm")
201
- return npm_path.absolute() if npm_path else None
191
+ return npm_path.absolute() if (npm_path := which("npm")) else None
202
192
 
203
193
 
204
194
  def get_bun_path() -> Path | None: