Skip to content

Commit 445f9b6

Browse files
pavank63claude
andcommitted
feat(resolver): enforce PyPI quarantine check for all resolver types
The PEP 792 quarantine check previously only ran inside get_project_from_pypi(), which is only called by PyPIProvider. Packages resolved via GitHubTagProvider, GitLabTagProvider, or PyPIProvider with a custom index URL bypassed the check entirely. Add a standalone check_pypi_quarantine_status() function that queries pypi.org for quarantine status and call it unconditionally from both resolve() and resolve_source() entry points. Remove the quarantine check from get_project_from_pypi() to avoid split responsibility. Signed-off-by: Pavan Kalyan Reddy Cherupally <pcherupa@redhat.com> Co-Authored-By: Claude <claude@anthropic.com>
1 parent 06e6317 commit 445f9b6

3 files changed

Lines changed: 88 additions & 5 deletions

File tree

src/fromager/resolver.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,30 @@ def match_py_req(py_req: str, *, python_version: Version = PYTHON_VERSION) -> bo
7777
return python_version in SpecifierSet(py_req)
7878

7979

80+
def check_pypi_quarantine_status(project: str) -> None:
81+
"""Check if a project is quarantined on PyPI (PEP 792).
82+
83+
Raises ValueError if the project is quarantined.
84+
"""
85+
client = pypi_simple.PyPISimple(
86+
endpoint=PYPI_SERVER_URL,
87+
session=session,
88+
accept=pypi_simple.ACCEPT_JSON_PREFERRED,
89+
)
90+
try:
91+
package = client.get_project_page(project)
92+
except Exception:
93+
logger.debug(
94+
"failed to check quarantine status for %s on PyPI, skipping check",
95+
project,
96+
)
97+
return
98+
if package.status == pypi_simple.ProjectStatus.QUARANTINED:
99+
raise ValueError(
100+
f"project {project!r} is quarantined on PyPI: {package.status_reason}"
101+
)
102+
103+
80104
def resolve(
81105
*,
82106
ctx: context.WorkContext,
@@ -104,6 +128,7 @@ def resolve(
104128
req_type=req_type,
105129
ignore_platform=ignore_platform,
106130
)
131+
check_pypi_quarantine_status(req.name)
107132
provider.cooldown = resolve_package_cooldown(ctx, req)
108133
max_age_cutoff = _compute_max_age_cutoff(ctx)
109134
results = find_all_matching_from_provider(
@@ -349,7 +374,8 @@ def get_project_from_pypi(
349374
)
350375
raise
351376

352-
# PEP 792 package status
377+
# PEP 792 package status (quarantine is checked separately
378+
# via check_pypi_quarantine_status at the resolution entry points)
353379
match package.status:
354380
case None:
355381
logger.debug("no package status")
@@ -362,10 +388,6 @@ def get_project_from_pypi(
362388
package.status,
363389
package.status_reason,
364390
)
365-
case pypi_simple.ProjectStatus.QUARANTINED:
366-
raise ValueError(
367-
f"project {project!r} is quarantined: {package.status_reason}"
368-
)
369391
case _:
370392
logger.warning(
371393
"project %r has unknown status %r: %s",

src/fromager/sources.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ def resolve_source(
180180
ctx=ctx, req=req, sdist_server_url=sdist_server_url, req_type=req_type
181181
)
182182

183+
# PEP 792: check quarantine status on PyPI regardless of resolver type.
184+
resolver.check_pypi_quarantine_status(req.name)
185+
183186
# Get all matching candidates from provider
184187
max_age_cutoff = resolver._compute_max_age_cutoff(ctx)
185188
results = resolver.find_all_matching_from_provider(

tests/test_resolver.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,3 +1278,61 @@ def test_cli_package_resolver(
12781278
assert "- PyPI versions: 1.2.2, 1.3.1+local, 1.3.2, 2.0.0a1" in result.stdout
12791279
assert "- only wheels on PyPI: 1.3.1+local, 2.0.0a1" in result.stdout
12801280
assert "- missing from Fromager: 1.3.1+local, 2.0.0a1" in result.stdout
1281+
1282+
1283+
_quarantined_simple_response = """
1284+
<!DOCTYPE html>
1285+
<html>
1286+
<head>
1287+
<meta name="pypi:repository-version" content="1.2">
1288+
<meta name="pypi:project-status" content="quarantined">
1289+
<meta name="pypi:project-status-reason" content="security concern">
1290+
<title>Links for testpkg</title>
1291+
</head>
1292+
<body>
1293+
<h1>Links for testpkg</h1>
1294+
</body>
1295+
</html>
1296+
"""
1297+
1298+
_active_simple_response = """
1299+
<!DOCTYPE html>
1300+
<html>
1301+
<head>
1302+
<meta name="pypi:repository-version" content="1.2">
1303+
<meta name="pypi:project-status" content="active">
1304+
<title>Links for testpkg</title>
1305+
</head>
1306+
<body>
1307+
<h1>Links for testpkg</h1>
1308+
</body>
1309+
</html>
1310+
"""
1311+
1312+
1313+
def test_check_pypi_quarantine_status_raises_for_quarantined() -> None:
1314+
with requests_mock.Mocker() as m:
1315+
m.get(
1316+
"https://pypi.org/simple/testpkg/",
1317+
text=_quarantined_simple_response,
1318+
)
1319+
with pytest.raises(ValueError, match="quarantined"):
1320+
resolver.check_pypi_quarantine_status("testpkg")
1321+
1322+
1323+
def test_check_pypi_quarantine_status_passes_for_active() -> None:
1324+
with requests_mock.Mocker() as m:
1325+
m.get(
1326+
"https://pypi.org/simple/testpkg/",
1327+
text=_active_simple_response,
1328+
)
1329+
resolver.check_pypi_quarantine_status("testpkg")
1330+
1331+
1332+
def test_check_pypi_quarantine_status_handles_fetch_failure() -> None:
1333+
with requests_mock.Mocker() as m:
1334+
m.get(
1335+
"https://pypi.org/simple/testpkg/",
1336+
status_code=404,
1337+
)
1338+
resolver.check_pypi_quarantine_status("testpkg")

0 commit comments

Comments
 (0)