Skip to content

Commit e5000ed

Browse files
committed
feat: better errors when unsupported packages are requested
1 parent f019906 commit e5000ed

File tree

2 files changed

+48
-7
lines changed

2 files changed

+48
-7
lines changed

src/pywrangler/sync.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def _install_requirements_to_vendor(requirements: list[str]) -> None:
177177
extra={"markup": True},
178178
)
179179
with temp_requirements_file(requirements) as requirements_file:
180-
run_command(
180+
result = run_command(
181181
[
182182
"uv",
183183
"pip",
@@ -190,8 +190,31 @@ def _install_requirements_to_vendor(requirements: list[str]) -> None:
190190
"--index-strategy",
191191
"unsafe-best-match",
192192
],
193+
capture_output=True,
194+
check=False,
193195
env=os.environ | {"VIRTUAL_ENV": get_pyodide_venv_path()},
194196
)
197+
if result.returncode != 0:
198+
logger.warning(result.stdout.strip())
199+
# Handle some common failures and give nicer error messages for them.
200+
lowered_stdout = result.stdout.lower()
201+
if "invalid peer certificate" in lowered_stdout:
202+
logger.error(
203+
"Installation failed because of an invalid peer certificate. Are your systems certificates correctly installed? Do you have an Enterprise VPN enabled?"
204+
)
205+
elif "failed to fetch" in lowered_stdout:
206+
logger.error(
207+
"Installation failed because of a failed fetch. Is your network connection working?"
208+
)
209+
elif "no solution found when resolving dependencies" in lowered_stdout:
210+
logger.error(
211+
"Installation failed because the packages you requested are not supported by Python Workers. See above for details."
212+
)
213+
else:
214+
logger.error(
215+
"Installation of packages into the Python Worker failed. Possibly because these packages are not currently supported. See above for details."
216+
)
217+
raise click.exceptions.Exit(code=result.returncode)
195218
pyv = get_python_version()
196219
shutil.rmtree(vendor_path)
197220
shutil.copytree(
@@ -221,16 +244,24 @@ def _install_requirements_to_venv(requirements: list[str]) -> None:
221244
extra={"markup": True},
222245
)
223246
with temp_requirements_file(requirements) as requirements_file:
224-
run_command(
247+
result = run_command(
225248
[
226249
"uv",
227250
"pip",
228251
"install",
229252
"-r",
230253
requirements_file,
231254
],
255+
check=False,
232256
env=os.environ | {"VIRTUAL_ENV": venv_workers_path},
257+
capture_output=True,
233258
)
259+
if result.returncode != 0:
260+
logger.warning(result.stdout.strip())
261+
logger.error(
262+
"Failed to install the requirements defined in your pyproject.toml file. See above for details."
263+
)
264+
raise click.exceptions.Exit(code=result.returncode)
234265

235266
get_venv_workers_token_path().touch()
236267
logger.info(
@@ -240,8 +271,14 @@ def _install_requirements_to_venv(requirements: list[str]) -> None:
240271

241272

242273
def install_requirements(requirements: list[str]) -> None:
243-
_install_requirements_to_vendor(requirements)
274+
# Note: the order these are executed is important.
275+
# We need to install to .venv-workers first, so that we can determine if the packages requested
276+
# by the user are valid.
244277
_install_requirements_to_venv(requirements)
278+
# Then we install the same requirements to the vendor directory. If this installation
279+
# fails while the above succeeded, it implies that Pyodide does not support these package
280+
# requirements which allows us to give a nicer error message to the user.
281+
_install_requirements_to_vendor(requirements)
245282

246283

247284
def _is_out_of_date(token: Path, time: float) -> bool:

src/pywrangler/utils.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,24 @@ def run_command(
7979
"""
8080
logger.log(RUNNING_LEVEL, f"{' '.join(str(arg) for arg in command)}")
8181
try:
82+
kwargs = {}
83+
if capture_output:
84+
kwargs = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
85+
8286
process = subprocess.run(
8387
command,
8488
cwd=cwd,
8589
env=env,
8690
check=check,
87-
capture_output=capture_output,
8891
text=True,
89-
)
92+
**kwargs,
93+
) # type: ignore[call-overload]
9094
if process.stdout and not capture_output:
9195
logger.log(OUTPUT_LEVEL, f"{process.stdout.strip()}")
92-
return process
96+
return process # type: ignore[no-any-return]
9397
except subprocess.CalledProcessError as e:
9498
logger.error(
95-
f"Error running command: {' '.join(str(arg) for arg in command)}\nExit code: {e.returncode}\nOutput:\n{e.stdout.strip() if e.stdout else ''}{e.stderr.strip() if e.stderr else ''}"
99+
f"Error running command: {' '.join(str(arg) for arg in command)}\nExit code: {e.returncode}\nOutput:\n{e.stdout.strip() if e.stdout else ''}"
96100
)
97101
raise click.exceptions.Exit(code=e.returncode) from None
98102
except FileNotFoundError:

0 commit comments

Comments
 (0)