Skip to content

Commit b32be83

Browse files
authored
Merge pull request #48 from jwodder/up-740
Update for change in PEP 740
2 parents 6f70629 + 357a201 commit b32be83

File tree

9 files changed

+88
-115
lines changed

9 files changed

+88
-115
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
v1.8.0 (in development)
2+
-----------------------
3+
- Provenance support belatedly updated to match a change to PEP 740:
4+
- `DistributionPackage.provenance_sha256` is now deprecated and is always
5+
`None`
6+
- `DistributionPackage.provenance_url` is now determined correctly and is
7+
`None` when no provenance file is declared
8+
- `PyPISimple.get_provenance()` no longer verifies the provenance's digest,
9+
and the `verify` argument is now deprecated
10+
111
v1.7.0 (2025-07-28)
212
-------------------
313
- Support Python 3.13

docs/changelog.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
Changelog
44
=========
55

6+
v1.8.0 (in development)
7+
-----------------------
8+
- Provenance support belatedly updated to match a change to :pep:`740`:
9+
10+
- `DistributionPackage.provenance_sha256` is now deprecated and is always
11+
`None`
12+
- `DistributionPackage.provenance_url` is now determined correctly and is
13+
`None` when no provenance file is declared
14+
- `PyPISimple.get_provenance()` no longer verifies the provenance's digest,
15+
and the ``verify`` argument is now deprecated
16+
17+
618
v1.7.0 (2025-07-28)
719
-------------------
820
- Support Python 3.13

src/pypi_simple/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
for more information.
1515
"""
1616

17-
__version__ = "1.7.0"
17+
__version__ = "1.8.0.dev1"
1818
__author__ = "John Thorvald Wodder II"
1919
__author_email__ = "pypi-simple@varonathe.org"
2020
__license__ = "MIT"

src/pypi_simple/classes.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,19 @@ class DistributionPackage:
9797

9898
#: .. versionadded:: 1.6.0
9999
#:
100-
#: The SHA 256 digest of the package file's :pep:`740` ``.provenance``
101-
#: file.
100+
#: .. deprecated:: 1.8.0
102101
#:
103-
#: If `provenance_sha256` is non-`None`, then the package repository
104-
#: provides a ``.provenance`` file for the package. If it is `None`, no
105-
#: conclusions can be drawn.
106-
provenance_sha256: Optional[str] = None
102+
#: This attribute is deprecated; its value is always `None`.
103+
provenance_sha256: None = None
104+
105+
#: .. versionadded:: 1.6.0
106+
#:
107+
#: .. versionchanged:: 1.8.0
108+
#:
109+
#: ``provenance_url`` can now be `None`
110+
#:
111+
#: The URL of the package file's :pep:`740` provenance file, if any
112+
provenance_url: Optional[str] = None
107113

108114
@property
109115
def sig_url(self) -> str:
@@ -121,14 +127,6 @@ def metadata_url(self) -> str:
121127
"""
122128
return url_add_suffix(self.url, ".metadata")
123129

124-
@property
125-
def provenance_url(self) -> str:
126-
"""
127-
The URL of the package file's :pep:`740` ``.provenance`` file, if it
128-
exists; cf. `provenance_sha256`
129-
"""
130-
return url_add_suffix(self.url, ".provenance")
131-
132130
@classmethod
133131
def from_link(
134132
cls, link: Link, project_hint: Optional[str] = None
@@ -183,7 +181,7 @@ def from_link(
183181
digests=digests,
184182
metadata_digests=metadata_digests,
185183
has_metadata=has_metadata,
186-
provenance_sha256=link.get_str_attrib("data-provenance"),
184+
provenance_url=link.get_str_attrib("data-provenance"),
187185
)
188186

189187
@classmethod
@@ -237,7 +235,7 @@ def from_file(
237235
has_metadata=file.has_metadata,
238236
size=file.size,
239237
upload_time=file.upload_time,
240-
provenance_sha256=file.provenance,
238+
provenance_url=None if file.provenance is None else str(file.provenance),
241239
)
242240

243241

src/pypi_simple/client.py

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -498,56 +498,42 @@ def get_package_metadata(
498498
def get_provenance(
499499
self,
500500
pkg: DistributionPackage,
501-
verify: bool = True,
501+
verify: bool = True, # noqa: U100
502502
timeout: float | tuple[float, float] | None = None,
503503
headers: Optional[dict[str, str]] = None,
504504
) -> dict[str, Any]:
505505
"""
506506
.. versionadded:: 1.6.0
507507
508-
Retrieve the :pep:`740` ``.provenance`` file for the given
508+
.. versionchanged:: 1.8.0
509+
510+
The ``verify`` argument is now deprecated and does nothing.
511+
512+
Retrieve the :pep:`740` provenance file for the given
509513
`DistributionPackage` and decode it as JSON.
510514
511-
Not all packages have ``.provenance`` files available for download; cf.
512-
`DistributionPackage.provenance_sha256`. This method will always
513-
attempt to download the ``.provenance`` file regardless of the value of
514-
`DistributionPackage.provenance_sha256`; if the server replies with a
515-
404, a `NoProvenanceError` is raised.
515+
Not all packages have provenance files available for download. If
516+
`DistributionPackage.provenance_url` is `None` or if the server replies
517+
with a 404, a `NoProvenanceError` is raised.
516518
517519
:param DistributionPackage pkg:
518-
the distribution package to retrieve the ``.provenance`` file of
519-
:param bool verify:
520-
whether to verify the ``.provenance`` file's SHA 256 digest against
521-
the retrieved data
520+
the distribution package to retrieve the provenance file of
522521
:param timeout: optional timeout to pass to the ``requests`` call
523522
:type timeout: float | tuple[float,float] | None
524523
:param Optional[dict[str, str]] headers:
525524
Custom headers to provide for the request.
526525
:rtype: dict[str, Any]
527-
528526
:raises NoProvenanceError:
529-
if the repository responds with a 404 error code
527+
if ``provenance_url`` is `None` or the repository responds with a
528+
404 error code
530529
:raises requests.HTTPError: if the repository responds with an HTTP
531530
error code other than 404
532-
:raises NoDigestsError:
533-
if ``verify`` is true and ``pkg.provenance_sha256`` is `None`
534-
:raises DigestMismatchError:
535-
if ``verify`` is true and the digest of the downloaded data does
536-
not match the expected value
537531
"""
538-
digester: AbstractDigestChecker
539-
if verify:
540-
if pkg.provenance_sha256 is not None:
541-
digests = {"sha256": pkg.provenance_sha256}
542-
else:
543-
digests = {}
544-
digester = DigestChecker(digests, pkg.provenance_url)
545-
else:
546-
digester = NullDigestChecker()
547-
r = self.s.get(pkg.provenance_url, timeout=timeout, headers=headers)
532+
url = pkg.provenance_url
533+
if url is None:
534+
raise NoProvenanceError(pkg.filename, None)
535+
r = self.s.get(url, timeout=timeout, headers=headers)
548536
if r.status_code == 404:
549-
raise NoProvenanceError(pkg.filename, pkg.provenance_url)
537+
raise NoProvenanceError(pkg.filename, url)
550538
r.raise_for_status()
551-
digester.update(r.content)
552-
digester.finalize()
553539
return json.loads(r.content) # type: ignore[no-any-return]

src/pypi_simple/errors.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Optional
2+
3+
14
class UnsupportedRepoVersionError(Exception):
25
"""
36
Raised upon encountering a simple repository whose repository version
@@ -149,15 +152,24 @@ class NoProvenanceError(Exception):
149152
"""
150153
.. versionadded:: 1.6.0
151154
152-
Raised by `PyPISimple.get_provenance()` when a request for a
153-
``.provenance`` file fails with a 404 error code
155+
.. versionchanged:: 1.8.0
156+
157+
``url`` can now be `None`
158+
159+
Raised by `PyPISimple.get_provenance()` when passed a `DistributionPackage`
160+
with a `None` ``provenance_url`` or when a request for a provenance file
161+
fails with a 404 error code
154162
"""
155163

156-
def __init__(self, filename: str, url: str) -> None:
164+
def __init__(self, filename: str, url: Optional[str]) -> None:
157165
#: The filename of the package whose provenance was requested
158166
self.filename = filename
159-
#: The URL to which the failed request was made
167+
#: The URL to which the failed request was made, or `None` if
168+
#: ``provenance_url`` was `None`
160169
self.url = url
161170

162171
def __str__(self) -> str:
163-
return f"No .provenance file found for {self.filename} at {self.url}"
172+
if self.url is None:
173+
return f"No provenance file declared for {self.filename}"
174+
else:
175+
return f"No provenance file found for {self.filename} at {self.url}"

src/pypi_simple/pep691.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22
from datetime import datetime
33
from typing import Any, Dict, List, Optional, Union
4-
from pydantic import BaseModel, Field, StrictBool, field_validator
4+
from pydantic import BaseModel, Field, HttpUrl, StrictBool, field_validator
55
from .enums import ProjectStatus
66

77

@@ -41,7 +41,7 @@ class File(BaseModel, alias_generator=shishkebab, populate_by_name=True):
4141
yanked: Union[StrictBool, str] = False
4242
size: Optional[int] = None
4343
upload_time: Optional[datetime] = None
44-
provenance: Optional[str] = None
44+
provenance: Optional[HttpUrl] = None
4545

4646
@property
4747
def is_yanked(self) -> bool:

test/test_client.py

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22
import filecmp
3-
import hashlib
43
import json
54
from pathlib import Path
65
from types import TracebackType
@@ -324,19 +323,19 @@ def test_utf8_declarations(content_type: str, body_decl: bytes) -> None:
324323
method=responses.GET,
325324
url="https://test.nil/simple/project/",
326325
body=body_decl
327-
+ b'<a href="../files/project-0.1.0-p\xC3\xBF42-none-any.whl">project-0.1.0-p\xC3\xBF42-none-any.whl</a>',
326+
+ b'<a href="../files/project-0.1.0-p\xc3\xbf42-none-any.whl">project-0.1.0-p\xc3\xbf42-none-any.whl</a>',
328327
content_type=content_type,
329328
)
330329
with PyPISimple("https://test.nil/simple/") as simple:
331330
assert simple.get_project_page("project") == ProjectPage(
332331
project="project",
333332
packages=[
334333
DistributionPackage(
335-
filename="project-0.1.0-p\xFF42-none-any.whl",
334+
filename="project-0.1.0-p\xff42-none-any.whl",
336335
project="project",
337336
version="0.1.0",
338337
package_type="wheel",
339-
url="https://test.nil/simple/files/project-0.1.0-p\xFF42-none-any.whl",
338+
url="https://test.nil/simple/files/project-0.1.0-p\xff42-none-any.whl",
340339
digests={},
341340
requires_python=None,
342341
has_sig=None,
@@ -371,19 +370,19 @@ def test_latin2_declarations(content_type: str, body_decl: bytes) -> None:
371370
method=responses.GET,
372371
url="https://test.nil/simple/project/",
373372
body=body_decl
374-
+ b'<a href="../files/project-0.1.0-p\xC3\xBF42-none-any.whl">project-0.1.0-p\xC3\xBF42-none-any.whl</a>',
373+
+ b'<a href="../files/project-0.1.0-p\xc3\xbf42-none-any.whl">project-0.1.0-p\xc3\xbf42-none-any.whl</a>',
375374
content_type=content_type,
376375
)
377376
with PyPISimple("https://test.nil/simple/") as simple:
378377
assert simple.get_project_page("project") == ProjectPage(
379378
project="project",
380379
packages=[
381380
DistributionPackage(
382-
filename="project-0.1.0-p\u0102\u017C42-none-any.whl",
381+
filename="project-0.1.0-p\u0102\u017c42-none-any.whl",
383382
project="project",
384383
version="0.1.0",
385384
package_type="wheel",
386-
url="https://test.nil/simple/files/project-0.1.0-p\u0102\u017C42-none-any.whl",
385+
url="https://test.nil/simple/files/project-0.1.0-p\u0102\u017c42-none-any.whl",
387386
digests={},
388387
requires_python=None,
389388
has_sig=None,
@@ -1017,9 +1016,9 @@ def test_get_provenance() -> None:
10171016
digests={},
10181017
requires_python=None,
10191018
has_sig=None,
1020-
provenance_sha256=hashlib.sha256(provenance_bytes).hexdigest(),
1019+
provenance_url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance",
10211020
)
1022-
assert simple.get_provenance(pkg, verify=True) == provenance
1021+
assert simple.get_provenance(pkg) == provenance
10231022

10241023

10251024
@responses.activate
@@ -1040,6 +1039,7 @@ def test_get_provenance_404() -> None:
10401039
digests={},
10411040
requires_python=None,
10421041
has_sig=None,
1042+
provenance_url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance",
10431043
)
10441044
with pytest.raises(NoProvenanceError) as excinfo:
10451045
simple.get_provenance(pkg, verify=False)
@@ -1050,29 +1050,5 @@ def test_get_provenance_404() -> None:
10501050
)
10511051
assert (
10521052
str(excinfo.value)
1053-
== "No .provenance file found for sampleproject-1.2.3-py3-none-any.whl at https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance"
1053+
== "No provenance file found for sampleproject-1.2.3-py3-none-any.whl at https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance"
10541054
)
1055-
1056-
1057-
@responses.activate
1058-
def test_get_provenance_verify_no_digest() -> None:
1059-
responses.add(
1060-
method=responses.GET,
1061-
url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance",
1062-
body="Does not exist",
1063-
status=404,
1064-
)
1065-
with PyPISimple("https://test.nil/simple/") as simple:
1066-
pkg = DistributionPackage(
1067-
filename="sampleproject-1.2.3-py3-none-any.whl",
1068-
project="sampleproject",
1069-
version="1.2.3",
1070-
package_type="wheel",
1071-
url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl",
1072-
digests={},
1073-
requires_python=None,
1074-
has_sig=None,
1075-
provenance_sha256=None,
1076-
)
1077-
with pytest.raises(NoDigestsError):
1078-
simple.get_provenance(pkg, verify=True)

test/test_distrib_pkg.py

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def test_get_sig_url(has_sig: bool) -> None:
9696
"data-gpg-sig": "true",
9797
"data-core-metadata": "sha256=ae718719df4708f329d58ca4d5390c1206c4222ef7e62a3aa9844397c63de28b",
9898
"data-yanked": "Oopsy.",
99-
"data-provenance": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
99+
"data-provenance": "https://example.com/pypi-provenance/qypi-0.1.0-py3-none-any.whl.provenance",
100100
},
101101
),
102102
DistributionPackage(
@@ -116,7 +116,7 @@ def test_get_sig_url(has_sig: bool) -> None:
116116
"sha256": "ae718719df4708f329d58ca4d5390c1206c4222ef7e62a3aa9844397c63de28b"
117117
},
118118
has_metadata=True,
119-
provenance_sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
119+
provenance_url="https://example.com/pypi-provenance/qypi-0.1.0-py3-none-any.whl.provenance",
120120
),
121121
),
122122
(
@@ -199,27 +199,6 @@ def test_pep658_no_digests() -> None:
199199
)
200200

201201

202-
def test_provenance_url() -> None:
203-
pkg = DistributionPackage(
204-
filename="qypi-0.1.0-py3-none-any.whl",
205-
url="https://files.pythonhosted.org/packages/82/fc/9e25534641d7f63be93079bc07fa92bab136ddf5d4181059a1308a346f96/qypi-0.1.0-py3-none-any.whl",
206-
digests={
207-
"sha256": "da69d28dcd527c0e372b3fa7b92fc333b327f8470175f035abc4e351b539189f"
208-
},
209-
has_sig=True,
210-
requires_python="~= 3.6",
211-
project="qypi",
212-
version="0.1.0",
213-
package_type="wheel",
214-
is_yanked=False,
215-
yanked_reason=None,
216-
)
217-
assert (
218-
pkg.provenance_url
219-
== "https://files.pythonhosted.org/packages/82/fc/9e25534641d7f63be93079bc07fa92bab136ddf5d4181059a1308a346f96/qypi-0.1.0-py3-none-any.whl.provenance"
220-
)
221-
222-
223202
def test_from_json_data_no_metadata() -> None:
224203
pkg = DistributionPackage.from_json_data(
225204
{
@@ -276,10 +255,10 @@ def test_from_json_data_provenance() -> None:
276255
"requires-python": "~=3.6",
277256
"url": "https://files.pythonhosted.org/packages/b5/2b/7aa284f345e37f955d86e4cd57b1039b573552b0fc29d1a522ec05c1ee41/argset-0.1.0-py3-none-any.whl",
278257
"yanked": False,
279-
"provenance": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
258+
"provenance": "https://example.com/pypi-provenance/argset-0.1.0-py3-none-any.whl.provenance",
280259
}
281260
)
282261
assert (
283-
pkg.provenance_sha256
284-
== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
262+
pkg.provenance_url
263+
== "https://example.com/pypi-provenance/argset-0.1.0-py3-none-any.whl.provenance"
285264
)

0 commit comments

Comments
 (0)