Skip to content

Commit 4f096a2

Browse files
committed
fixup! cli: Support friendlier syntax for verify pypi command
1 parent 126eb9a commit 4f096a2

File tree

4 files changed

+66
-23
lines changed

4 files changed

+66
-23
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- The CLI subcommand `verify attestation` now supports `.slsa.attestation`
1313
files. When verifying an artifact, both `.publish.attestation` and
1414
`.slsa.attestation` files are used (if present).
15-
- The CLI subcommand `verify attestation` now supports a friendlier
15+
- The CLI subcommand `verify pypi` now supports a friendlier
1616
syntax to specify the artifact to verify. The artifact can now be
17-
specified as `$PKG_NAME/$FILE_NAME`, e.g:
18-
`sampleproject/sampleproject-1.0.0.tar.gz`. The old way (passing
17+
specified with a `pypi:` prefix followed by the filename, e.g:
18+
`pypi:sampleproject-1.0.0.tar.gz`. The old way (passing
1919
the direct URL) is still supported.
2020

2121
## [0.0.21]

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,12 @@ pypi-attestations verify attestation \
142142
143143
### Verifying a PyPI package
144144
> [!NOTE]
145-
> The package to verify can be passed either as $PKG_NAME/$FILE_NAME (e.g:
146-
> 'sampleproject/sampleproject-1.0.0-py3-none-any.whl'), or as a direct URL
145+
> The package to verify can be passed either as a `pypi:` prefixed filename (e.g:
146+
> 'pypi:sampleproject-1.0.0-py3-none-any.whl'), or as a direct URL
147147
> to the artifact hosted by PyPI.
148148
```bash
149149
pypi-attestations verify pypi --repository https://github.com/sigstore/sigstore-python \
150-
sigstore/sigstore-3.6.1-py3-none-any.whl
150+
pypi:sigstore-3.6.1-py3-none-any.whl
151151
152152
# or alternatively:
153153
pypi-attestations verify pypi --repository https://github.com/sigstore/sigstore-python \

src/pypi_attestations/_cli.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,8 @@ def _parser() -> argparse.ArgumentParser:
139139
"distribution_file",
140140
metavar="PYPI_FILE",
141141
type=str,
142-
help="PyPI file to verify formatted as $PKG_NAME/$FILE_NAME, e.g: "
143-
"sampleproject/sampleproject-1.0.0.tar.gz. Direct URLs to the hosted file are also "
144-
"supported.",
142+
help="PyPI file to verify, can be either: (1) pypi:$FILE_NAME (e.g. "
143+
"pypi:sampleproject-1.0.0.tar.gz) or (2) A direct URL to files.pythonhosted.org",
145144
)
146145

147146
verify_pypi_command.add_argument(
@@ -237,14 +236,23 @@ def _download_file(url: str, dest: Path) -> None:
237236
def _get_direct_url_from_arg(arg: str) -> URIReference:
238237
"""Parse the artifact argument for the `verify pypi` subcommand.
239238
240-
The argument can be either a direct URL to a PyPI-hosted artifact,
241-
or a friendly-formatted alternative: '$PKG_NAME/$FILE_NAME'.
239+
The argument can be:
240+
- A pypi: prefixed filename (e.g. pypi:sampleproject-1.0.0.tar.gz)
241+
- A direct URL to a PyPI-hosted artifact
242242
"""
243243
direct_url = None
244-
components = arg.split("/")
245-
# We support passing the file argument as $PKG_NAME/$FILE_NAME
246-
if len(components) == 2:
247-
pkg_name, file_name = components
244+
245+
if arg.startswith("pypi:"):
246+
file_name = arg[5:]
247+
try:
248+
if file_name.endswith(".tar.gz") or file_name.endswith(".zip"):
249+
pkg_name, _ = parse_sdist_filename(file_name)
250+
elif file_name.endswith(".whl"):
251+
pkg_name, _, _, _ = parse_wheel_filename(file_name)
252+
else:
253+
_die("File should be a wheel (*.whl) or a source distribution (*.zip or *.tar.gz)")
254+
except (InvalidSdistFilename, InvalidWheelFilename) as e:
255+
_die(f"Invalid distribution filename: {e}")
248256

249257
provenance_url = f"https://pypi.org/simple/{pkg_name}"
250258
response = requests.get(
@@ -261,7 +269,7 @@ def _get_direct_url_from_arg(arg: str) -> URIReference:
261269
direct_url = file_json.get("url", "")
262270
break
263271
if not direct_url:
264-
_die(f"Could not find the artifact '{file_name}' for '{pkg_name}'")
272+
_die(f"Could not find the artifact '{file_name}' on PyPI")
265273
else:
266274
direct_url = arg
267275

test/test_cli.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,8 @@ def test_validate_files(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> Non
373373
[
374374
(pypi_wheel_url, pypi_wheel_filename),
375375
(pypi_sdist_url, pypi_sdist_filename),
376-
(pypi_wheel_abbrev, pypi_wheel_filename),
377-
(pypi_sdist_abbrev, pypi_sdist_filename),
376+
(f"pypi:{pypi_wheel_filename}", pypi_wheel_filename),
377+
(f"pypi:{pypi_sdist_filename}", pypi_sdist_filename),
378378
],
379379
)
380380
def test_verify_pypi_command(
@@ -468,7 +468,7 @@ def test_verify_pypi_invalid_url(
468468
assert "Unsupported/invalid URL" in caplog.text
469469

470470

471-
def test_verify_pypi_invalid_file_name(
471+
def test_verify_pypi_invalid_file_name_url(
472472
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
473473
) -> None:
474474
# Failure because file is neither a wheer nor a sdist
@@ -503,6 +503,41 @@ def test_verify_pypi_invalid_file_name(
503503
assert "Invalid wheel filename" in caplog.text
504504

505505

506+
def test_verify_pypi_invalid_sdist_filename_pypi(
507+
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
508+
) -> None:
509+
# Failure because file is neither a wheer nor a sdist
510+
monkeypatch.setattr(pypi_attestations._cli, "_download_file", lambda url, dest: None)
511+
with pytest.raises(SystemExit):
512+
run_main_with_command(
513+
[
514+
"verify",
515+
"pypi",
516+
"--repository",
517+
"https://github.com/sigstore/sigstore-python",
518+
f"pypi:{pypi_wheel_filename}.invalid_ext",
519+
]
520+
)
521+
assert (
522+
"File should be a wheel (*.whl) or a source distribution (*.zip or *.tar.gz)" in caplog.text
523+
)
524+
525+
caplog.clear()
526+
527+
"""Test that invalid sdist filenames are properly handled."""
528+
with pytest.raises(SystemExit):
529+
run_main_with_command(
530+
[
531+
"verify",
532+
"pypi",
533+
"--repository",
534+
"https://github.com/sigstore/sigstore-python",
535+
"pypi:invalid-sdist-name.tar.gz", # Invalid sdist filename format
536+
]
537+
)
538+
assert "Invalid distribution filename:" in caplog.text
539+
540+
506541
@online
507542
def test_verify_pypi_validation_fails(
508543
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
@@ -575,10 +610,10 @@ def test_verify_pypi_error_finding_package_info(
575610
"pypi",
576611
"--repository",
577612
"https://github.com/sigstore/sigstore-python",
578-
"somepkg/somefile",
613+
"pypi:somefile-1.0.0.tar.gz",
579614
]
580615
)
581-
assert "Error trying to get information for 'somepkg' from PyPI: myerror" in caplog.text
616+
assert "Error trying to get information for 'somefile' from PyPI: myerror" in caplog.text
582617

583618

584619
def test_verify_pypi_error_finding_artifact_url(
@@ -594,10 +629,10 @@ def test_verify_pypi_error_finding_artifact_url(
594629
"pypi",
595630
"--repository",
596631
"https://github.com/sigstore/sigstore-python",
597-
"somepkg/somefile",
632+
"pypi:somefile-1.0.0.tar.gz",
598633
]
599634
)
600-
assert "Could not find the artifact 'somefile' for 'somepkg'" in caplog.text
635+
assert "Could not find the artifact 'somefile-1.0.0.tar.gz' on PyPI" in caplog.text
601636

602637

603638
def test_verify_pypi_error_validating_provenance(

0 commit comments

Comments
 (0)