From ee7c8ca4ffcd0a0fb981ebcae20ca3a63a11e07d Mon Sep 17 00:00:00 2001 From: Pavan Kalyan Reddy Cherupally Date: Fri, 1 May 2026 08:54:01 -0500 Subject: [PATCH 1/2] 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 Co-Authored-By: Claude --- src/fromager/resolver.py | 32 ++++++++++++++++++---- src/fromager/sources.py | 3 +++ tests/test_resolver.py | 58 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/fromager/resolver.py b/src/fromager/resolver.py index 0ac959f8..db9dee12 100644 --- a/src/fromager/resolver.py +++ b/src/fromager/resolver.py @@ -76,6 +76,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, @@ -103,6 +127,7 @@ def resolve( req_type=req_type, ignore_platform=ignore_platform, ) + check_pypi_quarantine_status(req.name) provider.cooldown = resolve_package_cooldown(ctx, req, req_type=req_type) max_age_cutoff = _compute_max_age_cutoff(ctx) results = find_all_matching_from_provider( @@ -365,7 +390,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") @@ -378,10 +404,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", diff --git a/src/fromager/sources.py b/src/fromager/sources.py index bd308adb..f2b5a872 100644 --- a/src/fromager/sources.py +++ b/src/fromager/sources.py @@ -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( diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 690a1288..1955d41d 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -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 = """ + + + + + + +Links for testpkg + + +

Links for testpkg

+ + +""" + +_active_simple_response = """ + + + + + +Links for testpkg + + +

Links for testpkg

+ + +""" + + +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") From 865822b2d0115c6532ca6a829d80d69591dc88f3 Mon Sep 17 00:00:00 2001 From: Pavan Kalyan Reddy Cherupally Date: Wed, 27 May 2026 02:14:59 -0500 Subject: [PATCH 2/2] feat(resolver): add per-package `skip_pypi_quarantine` setting Packages resolved from non-PyPI sources (GitHub, GitLab) may share a name with an unrelated PyPI project. If that PyPI project gets quarantined, it would incorrectly block the legitimate package. Add `resolver_dist.skip_pypi_quarantine` (default false) to allow maintainers to disable the PEP 792 quarantine check for specific packages. Co-Authored-By: Claude Signed-off-by: Pavan Kalyan Reddy Cherupally --- src/fromager/packagesettings/_models.py | 9 ++++++ src/fromager/packagesettings/_pbi.py | 8 ++++++ src/fromager/resolver.py | 9 +++++- src/fromager/sources.py | 3 +- tests/test_packagesettings.py | 3 ++ tests/test_resolver.py | 37 +++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/fromager/packagesettings/_models.py b/src/fromager/packagesettings/_models.py index 64bc3e07..8d5cf4ab 100644 --- a/src/fromager/packagesettings/_models.py +++ b/src/fromager/packagesettings/_models.py @@ -170,6 +170,15 @@ class ResolverDist(pydantic.BaseModel): .. versionadded:: 0.82 """ + skip_pypi_quarantine: bool = False + """Skip PyPI quarantine status check for this package? (default: no) + + Use for packages resolved from non-PyPI sources (GitHub, GitLab) + where the PyPI name is a different, unrelated project. + + .. versionadded:: 0.86 + """ + @pydantic.model_validator(mode="after") def validate_ignore_platform(self) -> typing.Self: if self.ignore_platform and not self.include_wheels: diff --git a/src/fromager/packagesettings/_pbi.py b/src/fromager/packagesettings/_pbi.py index ff31cd0c..8f451874 100644 --- a/src/fromager/packagesettings/_pbi.py +++ b/src/fromager/packagesettings/_pbi.py @@ -261,6 +261,14 @@ def resolver_min_release_age(self) -> int | None: """ return self._ps.resolver_dist.min_release_age + @property + def resolver_skip_pypi_quarantine(self) -> bool: + """Skip PyPI quarantine status check for this package? + + .. versionadded:: 0.86 + """ + return self._ps.resolver_dist.skip_pypi_quarantine + @property def use_pypi_org_metadata(self) -> bool: """Can use metadata from pypi.org JSON / Simple API? diff --git a/src/fromager/resolver.py b/src/fromager/resolver.py index db9dee12..91490568 100644 --- a/src/fromager/resolver.py +++ b/src/fromager/resolver.py @@ -127,7 +127,8 @@ def resolve( req_type=req_type, ignore_platform=ignore_platform, ) - check_pypi_quarantine_status(req.name) + if not ctx.package_build_info(req).resolver_skip_pypi_quarantine: + check_pypi_quarantine_status(req.name) provider.cooldown = resolve_package_cooldown(ctx, req, req_type=req_type) max_age_cutoff = _compute_max_age_cutoff(ctx) results = find_all_matching_from_provider( @@ -404,6 +405,12 @@ def get_project_from_pypi( package.status, package.status_reason, ) + case pypi_simple.ProjectStatus.QUARANTINED: + logger.debug( + "project %r is quarantined on PyPI, check was skipped: %s", + project, + package.status_reason, + ) case _: logger.warning( "project %r has unknown status %r: %s", diff --git a/src/fromager/sources.py b/src/fromager/sources.py index f2b5a872..26157b67 100644 --- a/src/fromager/sources.py +++ b/src/fromager/sources.py @@ -181,7 +181,8 @@ def resolve_source( ) # PEP 792: check quarantine status on PyPI regardless of resolver type. - resolver.check_pypi_quarantine_status(req.name) + if not ctx.package_build_info(req).resolver_skip_pypi_quarantine: + resolver.check_pypi_quarantine_status(req.name) # Get all matching candidates from provider max_age_cutoff = resolver._compute_max_age_cutoff(ctx) diff --git a/tests/test_packagesettings.py b/tests/test_packagesettings.py index 5a14e4c5..c1fca01a 100644 --- a/tests/test_packagesettings.py +++ b/tests/test_packagesettings.py @@ -87,6 +87,7 @@ "ignore_platform": True, "use_pypi_org_metadata": True, "min_release_age": None, + "skip_pypi_quarantine": False, }, "variants": { "cpu": { @@ -149,6 +150,7 @@ "ignore_platform": False, "use_pypi_org_metadata": None, "min_release_age": None, + "skip_pypi_quarantine": False, }, "variants": {}, } @@ -190,6 +192,7 @@ "ignore_platform": False, "use_pypi_org_metadata": None, "min_release_age": None, + "skip_pypi_quarantine": False, }, "variants": { "cpu": { diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 1955d41d..86d47af4 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -1336,3 +1336,40 @@ def test_check_pypi_quarantine_status_handles_fetch_failure() -> None: status_code=404, ) resolver.check_pypi_quarantine_status("testpkg") + + +def test_check_pypi_quarantine_skipped_with_per_package_setting() -> None: + """resolve() skips quarantine check when skip_pypi_quarantine is True.""" + from unittest.mock import MagicMock, patch + + mock_ctx = MagicMock() + mock_pbi = MagicMock() + mock_pbi.resolver_skip_pypi_quarantine = True + mock_pbi.resolver_min_release_age = None + mock_ctx.package_build_info.return_value = mock_pbi + mock_ctx.cooldown = None + mock_ctx.max_release_age = None + + req = Requirement("testpkg") + + with ( + patch.object(resolver, "check_pypi_quarantine_status") as mock_check, + patch.object( + resolver, + "overrides", + ) as mock_overrides, + patch.object(resolver, "find_all_matching_from_provider") as mock_find, + ): + mock_provider = MagicMock() + mock_overrides.find_and_invoke.return_value = mock_provider + mock_find.return_value = [ + ("https://pypi.test/testpkg-1.0.tar.gz", Version("1.0")) + ] + + resolver.resolve( + ctx=mock_ctx, + req=req, + sdist_server_url="https://pypi.test/simple", + ) + + mock_check.assert_not_called()