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 0ac959f8..91490568 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,8 @@ def resolve( req_type=req_type, ignore_platform=ignore_platform, ) + 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( @@ -365,7 +391,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") @@ -379,8 +406,10 @@ def get_project_from_pypi( package.status_reason, ) case pypi_simple.ProjectStatus.QUARANTINED: - raise ValueError( - f"project {project!r} is quarantined: {package.status_reason}" + logger.debug( + "project %r is quarantined on PyPI, check was skipped: %s", + project, + package.status_reason, ) case _: logger.warning( diff --git a/src/fromager/sources.py b/src/fromager/sources.py index bd308adb..26157b67 100644 --- a/src/fromager/sources.py +++ b/src/fromager/sources.py @@ -180,6 +180,10 @@ 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. + 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) results = resolver.find_all_matching_from_provider( 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 690a1288..86d47af4 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -1278,3 +1278,98 @@ 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 = """ + + +
+ + + +