Skip to content

Commit 5139bbd

Browse files
Carreauryanking13pre-commit-ci[bot]
authored
BUG: fetch_string_and_headers compat: raise in and out of pyodide (#129)
* fetch_string_and_headers compat: raise in and out of pyodide Currently only the not_in_pyodide will raise on non-success, because this is the default behavior of urllib, the in_pyodide will not, so I added a raise_for_status. It is better to raise, as otherwise the package parser will potentially get proper URL and not manage to parse it, and decide there is no wheels, while we actually just got an error (404, or maybe 500). In addition wraps both case in a custom local HttpStatusError, so that we can actually catch these errors in the right places when we encounter them. Also add handling for PyPI 404 Now that warehouse set cors to 404, (pypi/warehouse#16339) we need to change the checked exceptions as there is no more network errors. * Remove compat with Pyodide < 0.27 * Update test_compat.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Gyeongjae Choi <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4a3984a commit 5139bbd

File tree

8 files changed

+87
-9
lines changed

8 files changed

+87
-9
lines changed

micropip/_compat/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
to_js = compatibility_layer.to_js
3636

37+
HttpStatusError = compatibility_layer.HttpStatusError
38+
3739

3840
__all__ = [
3941
"REPODATA_INFO",
@@ -45,4 +47,5 @@
4547
"loadPackage",
4648
"get_dynlibs",
4749
"to_js",
50+
"HttpStatusError",
4851
]

micropip/_compat/_compat_in_pyodide.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from pyodide._package_loader import get_dynlibs
99
from pyodide.ffi import IN_BROWSER, to_js
10-
from pyodide.http import pyfetch
10+
from pyodide.http import HttpStatusError, pyfetch
1111

1212
from .compatibility_layer import CompatibilityLayer
1313

@@ -28,6 +28,15 @@
2828

2929

3030
class CompatibilityInPyodide(CompatibilityLayer):
31+
class HttpStatusError(Exception):
32+
status_code: int
33+
message: str
34+
35+
def __init__(self, status_code: int, message: str):
36+
self.status_code = status_code
37+
self.message = message
38+
super().__init__(message)
39+
3140
@staticmethod
3241
def repodata_info() -> dict[str, str]:
3342
return REPODATA_INFO
@@ -50,7 +59,11 @@ async def fetch_bytes(url: str, kwargs: dict[str, str]) -> bytes:
5059
async def fetch_string_and_headers(
5160
url: str, kwargs: dict[str, str]
5261
) -> tuple[str, dict[str, str]]:
53-
response = await pyfetch(url, **kwargs)
62+
try:
63+
response = await pyfetch(url, **kwargs)
64+
response.raise_for_status()
65+
except HttpStatusError as e:
66+
raise CompatibilityInPyodide.HttpStatusError(e.status, str(e)) from e
5467

5568
content = await response.string()
5669
headers: dict[str, str] = response.headers

micropip/_compat/_compat_not_in_pyodide.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
from pathlib import Path
33
from typing import IO, TYPE_CHECKING, Any
4+
from urllib.error import HTTPError
45
from urllib.request import Request, urlopen
56
from urllib.response import addinfourl
67

@@ -15,6 +16,15 @@ class CompatibilityNotInPyodide(CompatibilityLayer):
1516
# Vendored from packaging
1617
_canonicalize_regex = re.compile(r"[-_.]+")
1718

19+
class HttpStatusError(Exception):
20+
status_code: int
21+
message: str
22+
23+
def __init__(self, status_code: int, message: str):
24+
self.status_code = status_code
25+
self.message = message
26+
super().__init__(message)
27+
1828
class loadedPackages(CompatibilityLayer.loadedPackages):
1929
@staticmethod
2030
def to_py():
@@ -40,7 +50,11 @@ async def fetch_bytes(url: str, kwargs: dict[str, Any]) -> bytes:
4050
async def fetch_string_and_headers(
4151
url: str, kwargs: dict[str, Any]
4252
) -> tuple[str, dict[str, str]]:
43-
response = CompatibilityNotInPyodide._fetch(url, kwargs=kwargs)
53+
try:
54+
response = CompatibilityNotInPyodide._fetch(url, kwargs=kwargs)
55+
except HTTPError as e:
56+
raise CompatibilityNotInPyodide.HttpStatusError(e.code, str(e)) from e
57+
4458
headers = {k.lower(): v for k, v in response.headers.items()}
4559
return response.read().decode(), headers
4660

micropip/_compat/compatibility_layer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ class CompatibilityLayer(ABC):
1313
All of the following methods / properties must be implemented for use both inside and outside of pyodide.
1414
"""
1515

16+
class HttpStatusError(ABC, Exception):
17+
status_code: int
18+
message: str
19+
20+
@abstractmethod
21+
def __init__(self, status_code: int, message: str):
22+
pass
23+
1624
class loadedPackages(ABC):
1725
@staticmethod
1826
@abstractmethod

micropip/package_index.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from packaging.utils import InvalidWheelFilename
1111
from packaging.version import InvalidVersion, Version
1212

13-
from ._compat import fetch_string_and_headers
13+
from ._compat import HttpStatusError, fetch_string_and_headers
1414
from ._utils import is_package_compatible, parse_version
1515
from .externals.mousebender.simple import from_project_details_html
1616
from .wheelinfo import WheelInfo
@@ -276,11 +276,16 @@ async def query_package(
276276

277277
try:
278278
metadata, headers = await fetch_string_and_headers(url, _fetch_kwargs)
279-
except OSError:
280-
continue
279+
except HttpStatusError as e:
280+
if e.status_code == 404:
281+
continue
282+
raise
281283

282284
content_type = headers.get("content-type", "").lower()
283-
parser = _select_parser(content_type, name)
285+
try:
286+
parser = _select_parser(content_type, name)
287+
except ValueError as e:
288+
raise ValueError(f"Error trying to decode url: {url}") from e
284289
return parser(metadata)
285290
else:
286291
raise ValueError(

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ def mock_fetch(monkeypatch, mock_importlib):
340340
def _mock_package_index_gen(
341341
httpserver,
342342
pkgs=("black", "pytest", "numpy", "pytz", "snowballstemmer"),
343+
pkgs_not_found=(),
343344
content_type="application/json",
344345
suffix="_json.json.gz",
345346
):
@@ -355,6 +356,10 @@ def _mock_package_index_gen(
355356
content_type=content_type,
356357
headers={"Access-Control-Allow-Origin": "*"},
357358
)
359+
for pkg in pkgs_not_found:
360+
httpserver.expect_request(f"/{base}/{pkg}/").respond_with_data(
361+
"Not found", status=404, content_type="text/plain"
362+
)
358363

359364
index_url = httpserver.url_for(base)
360365

tests/test_compat.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
test that function in compati behave the same
3+
4+
"""
5+
6+
import pytest
7+
from pytest_pyodide import run_in_pyodide
8+
9+
10+
@pytest.mark.driver_timeout(10)
11+
def test_404(selenium_standalone_micropip, httpserver, request):
12+
selenium_standalone_micropip.set_script_timeout(11)
13+
14+
@run_in_pyodide(packages=["micropip", "packaging"])
15+
async def _inner_test_404_raise(selenium, url):
16+
import pytest
17+
18+
from micropip._compat import HttpStatusError, fetch_string_and_headers
19+
20+
with pytest.raises(HttpStatusError):
21+
await fetch_string_and_headers(url, {})
22+
23+
httpserver.expect_request("/404").respond_with_data(
24+
"Not found",
25+
status=404,
26+
content_type="text/plain",
27+
headers={"Access-Control-Allow-Origin": "*"},
28+
)
29+
url_404 = httpserver.url_for("/404")
30+
_inner_test_404_raise(selenium_standalone_micropip, url_404)

tests/test_package_index.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ def test_contain_placeholder():
135135
)
136136
async def test_query_package(mock_fixture, pkg1, pkg2, request):
137137
gen_mock_server = request.getfixturevalue(mock_fixture)
138-
pkg1_index_url = gen_mock_server(pkgs=[pkg1])
139-
pkg2_index_url = gen_mock_server(pkgs=[pkg2])
138+
pkg1_index_url = gen_mock_server(pkgs=[pkg1], pkgs_not_found=[pkg2])
139+
pkg2_index_url = gen_mock_server(pkgs=[pkg2], pkgs_not_found=[pkg1])
140140

141141
for _index_urls in (
142142
pkg1_index_url,

0 commit comments

Comments
 (0)