Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions src/fromager/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ def match_py_req(py_req: str, *, python_version: Version = PYTHON_VERSION) -> bo
return python_version in SpecifierSet(py_req)


def check_pypi_quarantine_status(project: str) -> None:
"""Check if a project is quarantined on PyPI (PEP 792).

Raises ValueError if the project is quarantined.
"""
client = pypi_simple.PyPISimple(
endpoint=PYPI_SERVER_URL,
session=session,
accept=pypi_simple.ACCEPT_JSON_PREFERRED,
)
try:
package = client.get_project_page(project)
except Exception:
logger.debug(
"failed to check quarantine status for %s on PyPI, skipping check",
project,
)
return
if package.status == pypi_simple.ProjectStatus.QUARANTINED:
raise ValueError(
f"project {project!r} is quarantined on PyPI: {package.status_reason}"
)


def resolve(
*,
ctx: context.WorkContext,
Expand Down Expand Up @@ -104,6 +128,7 @@ def resolve(
req_type=req_type,
ignore_platform=ignore_platform,
)
check_pypi_quarantine_status(req.name)
provider.cooldown = resolve_package_cooldown(ctx, req)
max_age_cutoff = _compute_max_age_cutoff(ctx)
results = find_all_matching_from_provider(
Expand Down Expand Up @@ -349,7 +374,8 @@ def get_project_from_pypi(
)
raise

# PEP 792 package status
# PEP 792 package status (quarantine is checked separately
# via check_pypi_quarantine_status at the resolution entry points)
match package.status:
case None:
logger.debug("no package status")
Expand All @@ -362,10 +388,6 @@ def get_project_from_pypi(
package.status,
package.status_reason,
)
case pypi_simple.ProjectStatus.QUARANTINED:
raise ValueError(
f"project {project!r} is quarantined: {package.status_reason}"
)
case _:
logger.warning(
"project %r has unknown status %r: %s",
Expand Down
3 changes: 3 additions & 0 deletions src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ def resolve_source(
ctx=ctx, req=req, sdist_server_url=sdist_server_url, req_type=req_type
)

# PEP 792: check quarantine status on PyPI regardless of resolver type.
resolver.check_pypi_quarantine_status(req.name)

# Get all matching candidates from provider
max_age_cutoff = resolver._compute_max_age_cutoff(ctx)
results = resolver.find_all_matching_from_provider(
Expand Down
58 changes: 58 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,3 +1278,61 @@ def test_cli_package_resolver(
assert "- PyPI versions: 1.2.2, 1.3.1+local, 1.3.2, 2.0.0a1" in result.stdout
assert "- only wheels on PyPI: 1.3.1+local, 2.0.0a1" in result.stdout
assert "- missing from Fromager: 1.3.1+local, 2.0.0a1" in result.stdout


_quarantined_simple_response = """
<!DOCTYPE html>
<html>
<head>
<meta name="pypi:repository-version" content="1.2">
<meta name="pypi:project-status" content="quarantined">
<meta name="pypi:project-status-reason" content="security concern">
<title>Links for testpkg</title>
</head>
<body>
<h1>Links for testpkg</h1>
</body>
</html>
"""

_active_simple_response = """
<!DOCTYPE html>
<html>
<head>
<meta name="pypi:repository-version" content="1.2">
<meta name="pypi:project-status" content="active">
<title>Links for testpkg</title>
</head>
<body>
<h1>Links for testpkg</h1>
</body>
</html>
"""


def test_check_pypi_quarantine_status_raises_for_quarantined() -> None:
with requests_mock.Mocker() as m:
m.get(
"https://pypi.org/simple/testpkg/",
text=_quarantined_simple_response,
)
with pytest.raises(ValueError, match="quarantined"):
resolver.check_pypi_quarantine_status("testpkg")


def test_check_pypi_quarantine_status_passes_for_active() -> None:
with requests_mock.Mocker() as m:
m.get(
"https://pypi.org/simple/testpkg/",
text=_active_simple_response,
)
resolver.check_pypi_quarantine_status("testpkg")


def test_check_pypi_quarantine_status_handles_fetch_failure() -> None:
with requests_mock.Mocker() as m:
m.get(
"https://pypi.org/simple/testpkg/",
status_code=404,
)
resolver.check_pypi_quarantine_status("testpkg")
Loading