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
9 changes: 9 additions & 0 deletions src/fromager/packagesettings/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions src/fromager/packagesettings/_pbi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +264 to +270
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a Sphinx version marker for the new public property.

resolver_skip_pypi_quarantine is a new public API but its docstring lacks a versionadded directive.

Suggested patch
 `@property`
 def resolver_skip_pypi_quarantine(self) -> bool:
-    """Skip PyPI quarantine status check for this package?"""
+    """Skip PyPI quarantine status check for this package?
+
+    .. versionadded:: 0.86
+    """
     return self._ps.resolver_dist.skip_pypi_quarantine

As per coding guidelines: "**/*.{rst,py}: Use Sphinx versionadded, versionremoved, versionchanged directives for user-facing changes".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@property
def resolver_skip_pypi_quarantine(self) -> bool:
"""Skip PyPI quarantine status check for this package?"""
return self._ps.resolver_dist.skip_pypi_quarantine
`@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
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_pbi.py` around lines 264 - 267, This new public
property resolver_skip_pypi_quarantine needs a Sphinx version marker in its
docstring: update the property docstring on resolver_skip_pypi_quarantine to
include a versionadded directive (e.g. :versionadded: X.Y) right after the
one-line description so the new API is documented properly; ensure you edit the
docstring for the resolver_skip_pypi_quarantine `@property` in the
packagesettings/_pbi module.


@property
def use_pypi_org_metadata(self) -> bool:
"""Can use metadata from pypi.org JSON / Simple API?
Expand Down
35 changes: 32 additions & 3 deletions src/fromager/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand All @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions tests/test_packagesettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"ignore_platform": True,
"use_pypi_org_metadata": True,
"min_release_age": None,
"skip_pypi_quarantine": False,
},
"variants": {
"cpu": {
Expand Down Expand Up @@ -149,6 +150,7 @@
"ignore_platform": False,
"use_pypi_org_metadata": None,
"min_release_age": None,
"skip_pypi_quarantine": False,
},
"variants": {},
}
Expand Down Expand Up @@ -190,6 +192,7 @@
"ignore_platform": False,
"use_pypi_org_metadata": None,
"min_release_age": None,
"skip_pypi_quarantine": False,
},
"variants": {
"cpu": {
Expand Down
95 changes: 95 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<!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")


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()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading