reflex 0.7.3a2__py3-none-any.whl → 0.7.4a1__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/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:
@@ -14,7 +14,6 @@ import platform
14
14
  import random
15
15
  import re
16
16
  import shutil
17
- import stat
18
17
  import sys
19
18
  import tempfile
20
19
  import time
@@ -23,7 +22,7 @@ import zipfile
23
22
  from datetime import datetime
24
23
  from pathlib import Path
25
24
  from types import ModuleType
26
- from typing import Callable, NamedTuple
25
+ from typing import Callable, NamedTuple, Sequence
27
26
  from urllib.parse import urlparse
28
27
 
29
28
  import httpx
@@ -43,13 +42,11 @@ from reflex.utils.exceptions import (
43
42
  SystemPackageMissingError,
44
43
  )
45
44
  from reflex.utils.format import format_library_name
46
- from reflex.utils.registry import _get_npm_registry
45
+ from reflex.utils.registry import get_npm_registry
47
46
 
48
47
  if typing.TYPE_CHECKING:
49
48
  from reflex.app import App
50
49
 
51
- CURRENTLY_INSTALLING_NODE = False
52
-
53
50
 
54
51
  class AppInfo(NamedTuple):
55
52
  """A tuple containing the app instance and module."""
@@ -191,24 +188,6 @@ def get_node_version() -> version.Version | None:
191
188
  return None
192
189
 
193
190
 
194
- def get_fnm_version() -> version.Version | None:
195
- """Get the version of fnm.
196
-
197
- Returns:
198
- The version of FNM.
199
- """
200
- try:
201
- result = processes.new_process([constants.Fnm.EXE, "--version"], run=True)
202
- return version.parse(result.stdout.split(" ")[1]) # pyright: ignore [reportOptionalMemberAccess, reportAttributeAccessIssue]
203
- except (FileNotFoundError, TypeError):
204
- return None
205
- except version.InvalidVersion as e:
206
- console.warn(
207
- f"The detected fnm version ({e.args[0]}) is not valid. Defaulting to None."
208
- )
209
- return None
210
-
211
-
212
191
  def get_bun_version() -> version.Version | None:
213
192
  """Get the version of bun.
214
193
 
@@ -231,42 +210,107 @@ def get_bun_version() -> version.Version | None:
231
210
  return None
232
211
 
233
212
 
234
- def get_install_package_manager(on_failure_return_none: bool = False) -> str | None:
235
- """Get the package manager executable for installation.
236
- Currently, bun is used for installation only.
213
+ def prefer_npm_over_bun() -> bool:
214
+ """Check if npm should be preferred over bun.
215
+
216
+ Returns:
217
+ If npm should be preferred over bun.
218
+ """
219
+ return npm_escape_hatch() or (
220
+ constants.IS_WINDOWS and windows_check_onedrive_in_path()
221
+ )
222
+
223
+
224
+ def get_nodejs_compatible_package_managers(
225
+ raise_on_none: bool = True,
226
+ ) -> Sequence[str]:
227
+ """Get the package manager executable for installation. Typically, bun is used for installation.
237
228
 
238
229
  Args:
239
- on_failure_return_none: Whether to return None on failure.
230
+ raise_on_none: Whether to raise an error if the package manager is not found.
240
231
 
241
232
  Returns:
242
233
  The path to the package manager.
234
+
235
+ Raises:
236
+ FileNotFoundError: If the package manager is not found and raise_on_none is True.
237
+ """
238
+ bun_package_manager = (
239
+ str(bun_path) if (bun_path := path_ops.get_bun_path()) else None
240
+ )
241
+
242
+ npm_package_manager = (
243
+ str(npm_path) if (npm_path := path_ops.get_npm_path()) else None
244
+ )
245
+
246
+ if prefer_npm_over_bun():
247
+ package_managers = [npm_package_manager, bun_package_manager]
248
+ else:
249
+ package_managers = [bun_package_manager, npm_package_manager]
250
+
251
+ package_managers = list(filter(None, package_managers))
252
+
253
+ if not package_managers and not raise_on_none:
254
+ raise FileNotFoundError(
255
+ "Bun or npm not found. You might need to rerun `reflex init` or install either."
256
+ )
257
+
258
+ return package_managers
259
+
260
+
261
+ def is_outdated_nodejs_installed():
262
+ """Check if the installed Node.js version is outdated.
263
+
264
+ Returns:
265
+ If the installed Node.js version is outdated.
243
266
  """
244
- if constants.IS_WINDOWS and (
245
- windows_check_onedrive_in_path() or windows_npm_escape_hatch()
267
+ current_version = get_node_version()
268
+ if current_version is not None and current_version < version.parse(
269
+ constants.Node.MIN_VERSION
246
270
  ):
247
- return get_package_manager(on_failure_return_none)
248
- return str(get_config().bun_path)
271
+ console.warn(
272
+ f"Your version ({current_version}) of Node.js is out of date. Upgrade to {constants.Node.MIN_VERSION} or higher."
273
+ )
274
+ return True
275
+ return False
249
276
 
250
277
 
251
- def get_package_manager(on_failure_return_none: bool = False) -> str | None:
252
- """Get the package manager executable for running app.
253
- Currently on unix systems, npm is used for running the app only.
278
+ def get_js_package_executor(raise_on_none: bool = False) -> Sequence[Sequence[str]]:
279
+ """Get the paths to package managers for running commands. Ordered by preference.
280
+ This is currently identical to get_install_package_managers, but may change in the future.
254
281
 
255
282
  Args:
256
- on_failure_return_none: Whether to return None on failure.
283
+ raise_on_none: Whether to raise an error if no package managers is not found.
257
284
 
258
285
  Returns:
259
- The path to the package manager.
286
+ The paths to the package managers as a list of lists, where each list is the command to run and its arguments.
260
287
 
261
288
  Raises:
262
- FileNotFoundError: If the package manager is not found.
289
+ FileNotFoundError: If no package managers are found and raise_on_none is True.
263
290
  """
264
- npm_path = path_ops.get_npm_path()
265
- if npm_path is not None:
266
- return str(npm_path)
267
- if on_failure_return_none:
268
- return None
269
- raise FileNotFoundError("NPM not found. You may need to run `reflex init`.")
291
+ bun_package_manager = (
292
+ [str(bun_path)] + (["--bun"] if is_outdated_nodejs_installed() else [])
293
+ if (bun_path := path_ops.get_bun_path())
294
+ else None
295
+ )
296
+
297
+ npm_package_manager = (
298
+ [str(npm_path)] if (npm_path := path_ops.get_npm_path()) else None
299
+ )
300
+
301
+ if prefer_npm_over_bun():
302
+ package_managers = [npm_package_manager, bun_package_manager]
303
+ else:
304
+ package_managers = [bun_package_manager, npm_package_manager]
305
+
306
+ package_managers = list(filter(None, package_managers))
307
+
308
+ if not package_managers and raise_on_none:
309
+ raise FileNotFoundError(
310
+ "Bun or npm not found. You might need to rerun `reflex init` or install either."
311
+ )
312
+
313
+ return package_managers
270
314
 
271
315
 
272
316
  def windows_check_onedrive_in_path() -> bool:
@@ -278,8 +322,8 @@ def windows_check_onedrive_in_path() -> bool:
278
322
  return "onedrive" in str(Path.cwd()).lower()
279
323
 
280
324
 
281
- def windows_npm_escape_hatch() -> bool:
282
- """For windows, if the user sets REFLEX_USE_NPM, use npm instead of bun.
325
+ def npm_escape_hatch() -> bool:
326
+ """If the user sets REFLEX_USE_NPM, prefer npm over bun.
283
327
 
284
328
  Returns:
285
329
  If the user has set REFLEX_USE_NPM.
@@ -368,6 +412,15 @@ def get_and_validate_app(reload: bool = False) -> AppInfo:
368
412
  return AppInfo(app=app, module=app_module)
369
413
 
370
414
 
415
+ def validate_app(reload: bool = False) -> None:
416
+ """Validate the app instance based on the default config.
417
+
418
+ Args:
419
+ reload: Re-import the app module from disk
420
+ """
421
+ get_and_validate_app(reload=reload)
422
+
423
+
371
424
  def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
372
425
  """Get the app module based on the default config after first compiling it.
373
426
 
@@ -386,6 +439,16 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
386
439
  return app_module
387
440
 
388
441
 
442
+ def compile_app(reload: bool = False, export: bool = False) -> None:
443
+ """Compile the app module based on the default config.
444
+
445
+ Args:
446
+ reload: Re-import the app module from disk
447
+ export: Compile the app for export
448
+ """
449
+ get_compiled_app(reload=reload, export=export)
450
+
451
+
389
452
  def get_redis() -> Redis | None:
390
453
  """Get the asynchronous redis client.
391
454
 
@@ -862,7 +925,7 @@ def initialize_bun_config():
862
925
  bunfig_content = custom_bunfig.read_text()
863
926
  console.info(f"Copying custom bunfig.toml inside {get_web_dir()} folder")
864
927
  else:
865
- best_registry = _get_npm_registry()
928
+ best_registry = get_npm_registry()
866
929
  bunfig_content = constants.Bun.DEFAULT_CONFIG.format(registry=best_registry)
867
930
 
868
931
  bun_config_path.write_text(bunfig_content)
@@ -970,92 +1033,6 @@ def download_and_run(url: str, *args, show_status: bool = False, **env):
970
1033
  show(f"Installing {url}", process)
971
1034
 
972
1035
 
973
- def download_and_extract_fnm_zip():
974
- """Download and run a script.
975
-
976
- Raises:
977
- Exit: If an error occurs while downloading or extracting the FNM zip.
978
- """
979
- # Download the zip file
980
- url = constants.Fnm.INSTALL_URL
981
- console.debug(f"Downloading {url}")
982
- fnm_zip_file: Path = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip"
983
- # Function to download and extract the FNM zip release.
984
- try:
985
- # Download the FNM zip release.
986
- # TODO: show progress to improve UX
987
- response = net.get(url, follow_redirects=True)
988
- response.raise_for_status()
989
- with fnm_zip_file.open("wb") as output_file:
990
- for chunk in response.iter_bytes():
991
- output_file.write(chunk)
992
-
993
- # Extract the downloaded zip file.
994
- with zipfile.ZipFile(fnm_zip_file, "r") as zip_ref:
995
- zip_ref.extractall(constants.Fnm.DIR)
996
-
997
- console.debug("FNM package downloaded and extracted successfully.")
998
- except Exception as e:
999
- console.error(f"An error occurred while downloading fnm package: {e}")
1000
- raise typer.Exit(1) from e
1001
- finally:
1002
- # Clean up the downloaded zip file.
1003
- path_ops.rm(fnm_zip_file)
1004
-
1005
-
1006
- def install_node():
1007
- """Install fnm and nodejs for use by Reflex.
1008
- Independent of any existing system installations.
1009
- """
1010
- if not constants.Fnm.FILENAME:
1011
- # fnm only support Linux, macOS and Windows distros.
1012
- console.debug("")
1013
- return
1014
-
1015
- # Skip installation if check_node_version() checks out
1016
- if check_node_version():
1017
- console.debug("Skipping node installation as it is already installed.")
1018
- return
1019
-
1020
- path_ops.mkdir(constants.Fnm.DIR)
1021
- if not constants.Fnm.EXE.exists():
1022
- download_and_extract_fnm_zip()
1023
-
1024
- if constants.IS_WINDOWS:
1025
- # Install node
1026
- fnm_exe = Path(constants.Fnm.EXE).resolve()
1027
- fnm_dir = Path(constants.Fnm.DIR).resolve()
1028
- process = processes.new_process(
1029
- [
1030
- "powershell",
1031
- "-Command",
1032
- f'& "{fnm_exe}" install {constants.Node.VERSION} --fnm-dir "{fnm_dir}"',
1033
- ],
1034
- )
1035
- else: # All other platforms (Linux, MacOS).
1036
- # Add execute permissions to fnm executable.
1037
- constants.Fnm.EXE.chmod(stat.S_IXUSR)
1038
- # Install node.
1039
- # Specify arm64 arch explicitly for M1s and M2s.
1040
- architecture_arg = (
1041
- ["--arch=arm64"]
1042
- if platform.system() == "Darwin" and platform.machine() == "arm64"
1043
- else []
1044
- )
1045
-
1046
- process = processes.new_process(
1047
- [
1048
- constants.Fnm.EXE,
1049
- "install",
1050
- *architecture_arg,
1051
- constants.Node.VERSION,
1052
- "--fnm-dir",
1053
- constants.Fnm.DIR,
1054
- ],
1055
- )
1056
- processes.show_status("Installing node", process)
1057
-
1058
-
1059
1036
  def install_bun():
1060
1037
  """Install bun onto the user's system.
1061
1038
 
@@ -1069,7 +1046,9 @@ def install_bun():
1069
1046
  )
1070
1047
 
1071
1048
  # Skip if bun is already installed.
1072
- if get_bun_version() == version.parse(constants.Bun.VERSION):
1049
+ if (current_version := get_bun_version()) and current_version >= version.parse(
1050
+ constants.Bun.MIN_VERSION
1051
+ ):
1073
1052
  console.debug("Skipping bun installation as it is already installed.")
1074
1053
  return
1075
1054
 
@@ -1157,38 +1136,19 @@ def install_frontend_packages(packages: set[str], config: Config):
1157
1136
  packages: A list of package names to be installed.
1158
1137
  config: The config object.
1159
1138
 
1160
- Raises:
1161
- FileNotFoundError: If the package manager is not found.
1162
-
1163
1139
  Example:
1164
1140
  >>> install_frontend_packages(["react", "react-dom"], get_config())
1165
1141
  """
1166
- # unsupported archs(arm and 32bit machines) will use npm anyway. so we dont have to run npm twice
1167
- fallback_command = (
1168
- get_package_manager(on_failure_return_none=True)
1169
- if (
1170
- not constants.IS_WINDOWS
1171
- or (constants.IS_WINDOWS and not windows_check_onedrive_in_path())
1172
- )
1173
- else None
1174
- )
1175
-
1176
- install_package_manager = (
1177
- get_install_package_manager(on_failure_return_none=True) or fallback_command
1142
+ install_package_managers = get_nodejs_compatible_package_managers(
1143
+ raise_on_none=True
1178
1144
  )
1179
1145
 
1180
- if install_package_manager is None:
1181
- raise FileNotFoundError(
1182
- "Could not find a package manager to install frontend packages. You may need to run `reflex init`."
1183
- )
1184
-
1185
- fallback_command = (
1186
- fallback_command if fallback_command is not install_package_manager else None
1187
- )
1146
+ primary_package_manager = install_package_managers[0]
1147
+ fallbacks = install_package_managers[1:]
1188
1148
 
1189
- processes.run_process_with_fallback(
1190
- [install_package_manager, "install", "--legacy-peer-deps"],
1191
- fallback=fallback_command,
1149
+ processes.run_process_with_fallbacks(
1150
+ [primary_package_manager, "install", "--legacy-peer-deps"],
1151
+ fallbacks=fallbacks,
1192
1152
  analytics_enabled=True,
1193
1153
  show_status_message="Installing base frontend packages",
1194
1154
  cwd=get_web_dir(),
@@ -1196,16 +1156,16 @@ def install_frontend_packages(packages: set[str], config: Config):
1196
1156
  )
1197
1157
 
1198
1158
  if config.tailwind is not None:
1199
- processes.run_process_with_fallback(
1159
+ processes.run_process_with_fallbacks(
1200
1160
  [
1201
- install_package_manager,
1161
+ primary_package_manager,
1202
1162
  "add",
1203
1163
  "--legacy-peer-deps",
1204
1164
  "-d",
1205
1165
  constants.Tailwind.VERSION,
1206
1166
  *((config.tailwind or {}).get("plugins", [])),
1207
1167
  ],
1208
- fallback=fallback_command,
1168
+ fallbacks=fallbacks,
1209
1169
  analytics_enabled=True,
1210
1170
  show_status_message="Installing tailwind",
1211
1171
  cwd=get_web_dir(),
@@ -1214,9 +1174,9 @@ def install_frontend_packages(packages: set[str], config: Config):
1214
1174
 
1215
1175
  # Install custom packages defined in frontend_packages
1216
1176
  if len(packages) > 0:
1217
- processes.run_process_with_fallback(
1218
- [install_package_manager, "add", "--legacy-peer-deps", *packages],
1219
- fallback=fallback_command,
1177
+ processes.run_process_with_fallbacks(
1178
+ [primary_package_manager, "add", "--legacy-peer-deps", *packages],
1179
+ fallbacks=fallbacks,
1220
1180
  analytics_enabled=True,
1221
1181
  show_status_message="Installing frontend packages from config and components",
1222
1182
  cwd=get_web_dir(),
@@ -1338,24 +1298,19 @@ def validate_frontend_dependencies(init: bool = True):
1338
1298
  Exit: If the package manager is invalid.
1339
1299
  """
1340
1300
  if not init:
1341
- # we only need to validate the package manager when running app.
1342
- # `reflex init` will install the deps anyway(if applied).
1343
- package_manager = get_package_manager()
1344
- if not package_manager:
1345
- console.error(
1346
- "Could not find NPM package manager. Make sure you have node installed."
1347
- )
1348
- raise typer.Exit(1)
1301
+ try:
1302
+ get_js_package_executor(raise_on_none=True)
1303
+ except FileNotFoundError as e:
1304
+ raise typer.Exit(1) from e
1349
1305
 
1306
+ if prefer_npm_over_bun():
1350
1307
  if not check_node_version():
1351
1308
  node_version = get_node_version()
1352
1309
  console.error(
1353
1310
  f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
1354
1311
  )
1355
1312
  raise typer.Exit(1)
1356
-
1357
- if init:
1358
- # we only need bun for package install on `reflex init`.
1313
+ else:
1359
1314
  validate_bun()
1360
1315
 
1361
1316
 
@@ -1400,12 +1355,8 @@ def initialize_frontend_dependencies():
1400
1355
  """Initialize all the frontend dependencies."""
1401
1356
  # validate dependencies before install
1402
1357
  validate_frontend_dependencies()
1403
- # Avoid warning about Node installation while we're trying to install it.
1404
- global CURRENTLY_INSTALLING_NODE
1405
- CURRENTLY_INSTALLING_NODE = True
1406
1358
  # Install the frontend dependencies.
1407
- processes.run_concurrently(install_node, install_bun)
1408
- CURRENTLY_INSTALLING_NODE = False
1359
+ processes.run_concurrently(install_bun)
1409
1360
  # Set up the web directory.
1410
1361
  initialize_web_directory()
1411
1362
 
@@ -1610,7 +1561,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
1610
1561
  console.error(f"Failed to unzip the template: {uze}")
1611
1562
  raise typer.Exit(1) from uze
1612
1563
 
1613
- if len(subdirs := os.listdir(unzip_dir)) != 1:
1564
+ if len(subdirs := list(unzip_dir.iterdir())) != 1:
1614
1565
  console.error(f"Expected one directory in the zip, found {subdirs}")
1615
1566
  raise typer.Exit(1)
1616
1567
 
reflex/utils/processes.py CHANGED
@@ -10,7 +10,7 @@ import signal
10
10
  import subprocess
11
11
  from concurrent import futures
12
12
  from pathlib import Path
13
- from typing import Callable, Generator, Tuple
13
+ from typing import Any, Callable, Generator, Sequence, Tuple
14
14
 
15
15
  import psutil
16
16
  import typer
@@ -171,14 +171,9 @@ def new_process(
171
171
 
172
172
  # Add node_bin_path to the PATH environment variable.
173
173
  if not environment.REFLEX_BACKEND_ONLY.get():
174
- node_bin_path = str(path_ops.get_node_bin_path())
175
- if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE:
176
- console.warn(
177
- "The path to the Node binary could not be found. Please ensure that Node is properly "
178
- "installed and added to your system's PATH environment variable or try running "
179
- "`reflex init` again."
180
- )
181
- path_env = os.pathsep.join([node_bin_path, path_env])
174
+ node_bin_path = path_ops.get_node_bin_path()
175
+ if node_bin_path:
176
+ path_env = os.pathsep.join([str(node_bin_path), path_env])
182
177
 
183
178
  env: dict[str, str] = {
184
179
  **os.environ,
@@ -202,7 +197,7 @@ def new_process(
202
197
 
203
198
  @contextlib.contextmanager
204
199
  def run_concurrently_context(
205
- *fns: Callable | Tuple,
200
+ *fns: Callable[..., Any] | tuple[Callable[..., Any], ...],
206
201
  ) -> Generator[list[futures.Future], None, None]:
207
202
  """Run functions concurrently in a thread pool.
208
203
 
@@ -218,14 +213,14 @@ def run_concurrently_context(
218
213
  return
219
214
 
220
215
  # Convert the functions to tuples.
221
- fns = [fn if isinstance(fn, tuple) else (fn,) for fn in fns] # pyright: ignore [reportAssignmentType]
216
+ fns = tuple(fn if isinstance(fn, tuple) else (fn,) for fn in fns)
222
217
 
223
218
  # Run the functions concurrently.
224
219
  executor = None
225
220
  try:
226
221
  executor = futures.ThreadPoolExecutor(max_workers=len(fns))
227
222
  # Submit the tasks.
228
- tasks = [executor.submit(*fn) for fn in fns] # pyright: ignore [reportArgumentType]
223
+ tasks = [executor.submit(*fn) for fn in fns]
229
224
 
230
225
  # Yield control back to the main thread while tasks are running.
231
226
  yield tasks
@@ -380,11 +375,11 @@ def get_command_with_loglevel(command: list[str]) -> list[str]:
380
375
  return command
381
376
 
382
377
 
383
- def run_process_with_fallback(
378
+ def run_process_with_fallbacks(
384
379
  args: list[str],
385
380
  *,
386
381
  show_status_message: str,
387
- fallback: str | list | None = None,
382
+ fallbacks: str | Sequence[str] | Sequence[Sequence[str]] | None = None,
388
383
  analytics_enabled: bool = False,
389
384
  **kwargs,
390
385
  ):
@@ -393,12 +388,12 @@ def run_process_with_fallback(
393
388
  Args:
394
389
  args: A string, or a sequence of program arguments.
395
390
  show_status_message: The status message to be displayed in the console.
396
- fallback: The fallback command to run.
391
+ fallbacks: The fallback command to run if the initial command fails.
397
392
  analytics_enabled: Whether analytics are enabled for this command.
398
393
  kwargs: Kwargs to pass to new_process function.
399
394
  """
400
395
  process = new_process(get_command_with_loglevel(args), **kwargs)
401
- if fallback is None:
396
+ if not fallbacks:
402
397
  # No fallback given, or this _is_ the fallback command.
403
398
  show_status(
404
399
  show_status_message,
@@ -408,16 +403,24 @@ def run_process_with_fallback(
408
403
  else:
409
404
  # Suppress errors for initial command, because we will try to fallback
410
405
  show_status(show_status_message, process, suppress_errors=True)
406
+
407
+ current_fallback = fallbacks[0] if not isinstance(fallbacks, str) else fallbacks
408
+ next_fallbacks = fallbacks[1:] if not isinstance(fallbacks, str) else None
409
+
411
410
  if process.returncode != 0:
412
411
  # retry with fallback command.
413
- fallback_args = [fallback, *args[1:]]
412
+ fallback_with_args = (
413
+ [current_fallback, *args[1:]]
414
+ if isinstance(fallbacks, str)
415
+ else [*current_fallback, *args[1:]]
416
+ )
414
417
  console.warn(
415
- f"There was an error running command: {args}. Falling back to: {fallback_args}."
418
+ f"There was an error running command: {args}. Falling back to: {fallback_with_args}."
416
419
  )
417
- run_process_with_fallback(
418
- fallback_args,
420
+ run_process_with_fallbacks(
421
+ fallback_with_args,
419
422
  show_status_message=show_status_message,
420
- fallback=None,
423
+ fallbacks=next_fallbacks,
421
424
  analytics_enabled=analytics_enabled,
422
425
  **kwargs,
423
426
  )