Skip to content

Commit a11df56

Browse files
authored
Implement PEP 658 (#13649)
* add database model for storing calculated hashes of wheel METADATA files * implement a helper for extracting METADATA file contents from wheels * Store the digest of metadata file if exists, push to object storage * don't store md5 * expose data-dist-info-metadata on simple api * fail if unable to extract metadata file from wheels * ensure metadata and pgp files are archived along with the actual distribution file also resolves #13414
1 parent f567980 commit a11df56

File tree

11 files changed

+320
-33
lines changed

11 files changed

+320
-33
lines changed

tests/unit/api/test_simple.py

+8
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
277277
"yanked": False,
278278
"size": f.size,
279279
"upload-time": f.upload_time.isoformat() + "Z",
280+
"data-dist-info-metadata": False,
280281
}
281282
for f in files
282283
],
@@ -323,6 +324,7 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
323324
"yanked": False,
324325
"size": f.size,
325326
"upload-time": f.upload_time.isoformat() + "Z",
327+
"data-dist-info-metadata": False,
326328
}
327329
for f in files
328330
],
@@ -370,6 +372,7 @@ def test_with_files_with_version_multi_digit(
370372
release=r,
371373
filename=f"{project.name}-{r.version}.whl",
372374
packagetype="bdist_wheel",
375+
metadata_file_sha256_digest="deadbeefdeadbeefdeadbeefdeadbeef",
373376
)
374377
for r in releases
375378
]
@@ -405,6 +408,11 @@ def test_with_files_with_version_multi_digit(
405408
"yanked": False,
406409
"size": f.size,
407410
"upload-time": f.upload_time.isoformat() + "Z",
411+
"data-dist-info-metadata": {
412+
"sha256": "deadbeefdeadbeefdeadbeefdeadbeef"
413+
}
414+
if f.metadata_file_sha256_digest is not None
415+
else False,
408416
}
409417
for f in files
410418
],

tests/unit/forklift/test_legacy.py

+122-22
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,26 @@ def _get_tar_testdata(compression_type=""):
7272
return temp_f.getvalue()
7373

7474

75+
def _get_whl_testdata(name="fake_package", version="1.0"):
76+
temp_f = io.BytesIO()
77+
with zipfile.ZipFile(file=temp_f, mode="w") as zfp:
78+
zfp.writestr(f"{name}-{version}.dist-info/METADATA", "Fake metadata")
79+
return temp_f.getvalue()
80+
81+
82+
def _storage_hash(data):
83+
return hashlib.blake2b(data, digest_size=256 // 8).hexdigest()
84+
85+
7586
_TAR_GZ_PKG_TESTDATA = _get_tar_testdata("gz")
7687
_TAR_GZ_PKG_MD5 = hashlib.md5(_TAR_GZ_PKG_TESTDATA).hexdigest()
7788
_TAR_GZ_PKG_SHA256 = hashlib.sha256(_TAR_GZ_PKG_TESTDATA).hexdigest()
78-
_TAR_GZ_PKG_STORAGE_HASH = hashlib.blake2b(
79-
_TAR_GZ_PKG_TESTDATA, digest_size=256 // 8
80-
).hexdigest()
89+
_TAR_GZ_PKG_STORAGE_HASH = _storage_hash(_TAR_GZ_PKG_TESTDATA)
8190

8291
_TAR_BZ2_PKG_TESTDATA = _get_tar_testdata("bz2")
8392
_TAR_BZ2_PKG_MD5 = hashlib.md5(_TAR_BZ2_PKG_TESTDATA).hexdigest()
8493
_TAR_BZ2_PKG_SHA256 = hashlib.sha256(_TAR_BZ2_PKG_TESTDATA).hexdigest()
85-
_TAR_BZ2_PKG_STORAGE_HASH = hashlib.blake2b(
86-
_TAR_BZ2_PKG_TESTDATA, digest_size=256 // 8
87-
).hexdigest()
94+
_TAR_BZ2_PKG_STORAGE_HASH = _storage_hash(_TAR_BZ2_PKG_TESTDATA)
8895

8996

9097
class TestExcWithMessage:
@@ -2771,6 +2778,8 @@ def test_upload_succeeds_with_wheel(
27712778
RoleFactory.create(user=user, project=project)
27722779

27732780
filename = f"{project.name}-{release.version}-cp34-none-{plat}.whl"
2781+
filebody = _get_whl_testdata(name=project.name, version=release.version)
2782+
filestoragehash = _storage_hash(filebody)
27742783

27752784
pyramid_config.testing_securitypolicy(identity=user)
27762785
db_request.user = user
@@ -2782,19 +2791,22 @@ def test_upload_succeeds_with_wheel(
27822791
"version": release.version,
27832792
"filetype": "bdist_wheel",
27842793
"pyversion": "cp34",
2785-
"md5_digest": _TAR_GZ_PKG_MD5,
2794+
"md5_digest": hashlib.md5(filebody).hexdigest(),
27862795
"content": pretend.stub(
27872796
filename=filename,
2788-
file=io.BytesIO(_TAR_GZ_PKG_TESTDATA),
2789-
type="application/tar",
2797+
file=io.BytesIO(filebody),
2798+
type="application/zip",
27902799
),
27912800
}
27922801
)
27932802

27942803
@pretend.call_recorder
27952804
def storage_service_store(path, file_path, *, meta):
27962805
with open(file_path, "rb") as fp:
2797-
assert fp.read() == _TAR_GZ_PKG_TESTDATA
2806+
if file_path.endswith(".metadata"):
2807+
assert fp.read() == b"Fake metadata"
2808+
else:
2809+
assert fp.read() == filebody
27982810

27992811
storage_service = pretend.stub(store=storage_service_store)
28002812

@@ -2818,9 +2830,9 @@ def storage_service_store(path, file_path, *, meta):
28182830
pretend.call(
28192831
"/".join(
28202832
[
2821-
_TAR_GZ_PKG_STORAGE_HASH[:2],
2822-
_TAR_GZ_PKG_STORAGE_HASH[2:4],
2823-
_TAR_GZ_PKG_STORAGE_HASH[4:],
2833+
filestoragehash[:2],
2834+
filestoragehash[2:4],
2835+
filestoragehash[4:],
28242836
filename,
28252837
]
28262838
),
@@ -2831,7 +2843,24 @@ def storage_service_store(path, file_path, *, meta):
28312843
"package-type": "bdist_wheel",
28322844
"python-version": "cp34",
28332845
},
2834-
)
2846+
),
2847+
pretend.call(
2848+
"/".join(
2849+
[
2850+
filestoragehash[:2],
2851+
filestoragehash[2:4],
2852+
filestoragehash[4:],
2853+
filename + ".metadata",
2854+
]
2855+
),
2856+
mock.ANY,
2857+
meta={
2858+
"project": project.normalized_name,
2859+
"version": release.version,
2860+
"package-type": "bdist_wheel",
2861+
"python-version": "cp34",
2862+
},
2863+
),
28352864
]
28362865

28372866
# Ensure that a File object has been created.
@@ -2884,6 +2913,8 @@ def test_upload_succeeds_with_wheel_after_sdist(
28842913
RoleFactory.create(user=user, project=project)
28852914

28862915
filename = f"{project.name}-{release.version}-cp34-none-any.whl"
2916+
filebody = _get_whl_testdata(name=project.name, version=release.version)
2917+
filestoragehash = _storage_hash(filebody)
28872918

28882919
pyramid_config.testing_securitypolicy(identity=user)
28892920
db_request.user = user
@@ -2895,19 +2926,22 @@ def test_upload_succeeds_with_wheel_after_sdist(
28952926
"version": release.version,
28962927
"filetype": "bdist_wheel",
28972928
"pyversion": "cp34",
2898-
"md5_digest": "335c476dc930b959dda9ec82bd65ef19",
2929+
"md5_digest": hashlib.md5(filebody).hexdigest(),
28992930
"content": pretend.stub(
29002931
filename=filename,
2901-
file=io.BytesIO(b"A fake file."),
2902-
type="application/tar",
2932+
file=io.BytesIO(filebody),
2933+
type="application/zip",
29032934
),
29042935
}
29052936
)
29062937

29072938
@pretend.call_recorder
29082939
def storage_service_store(path, file_path, *, meta):
29092940
with open(file_path, "rb") as fp:
2910-
assert fp.read() == b"A fake file."
2941+
if file_path.endswith(".metadata"):
2942+
assert fp.read() == b"Fake metadata"
2943+
else:
2944+
assert fp.read() == filebody
29112945

29122946
storage_service = pretend.stub(store=storage_service_store)
29132947
db_request.find_service = pretend.call_recorder(
@@ -2930,9 +2964,9 @@ def storage_service_store(path, file_path, *, meta):
29302964
pretend.call(
29312965
"/".join(
29322966
[
2933-
"4e",
2934-
"6e",
2935-
"fa4c0ee2bbad071b4f5b5ea68f1aea89fa716e7754eb13e2314d45a5916e",
2967+
filestoragehash[:2],
2968+
filestoragehash[2:4],
2969+
filestoragehash[4:],
29362970
filename,
29372971
]
29382972
),
@@ -2943,7 +2977,24 @@ def storage_service_store(path, file_path, *, meta):
29432977
"package-type": "bdist_wheel",
29442978
"python-version": "cp34",
29452979
},
2946-
)
2980+
),
2981+
pretend.call(
2982+
"/".join(
2983+
[
2984+
filestoragehash[:2],
2985+
filestoragehash[2:4],
2986+
filestoragehash[4:],
2987+
filename + ".metadata",
2988+
]
2989+
),
2990+
mock.ANY,
2991+
meta={
2992+
"project": project.normalized_name,
2993+
"version": release.version,
2994+
"package-type": "bdist_wheel",
2995+
"python-version": "cp34",
2996+
},
2997+
),
29472998
]
29482999

29493000
# Ensure that a File object has been created.
@@ -3025,6 +3076,55 @@ def test_upload_fails_with_unsupported_wheel_plat(
30253076
"400 Binary wheel .* has an unsupported platform tag .*", resp.status
30263077
)
30273078

3079+
def test_upload_fails_with_missing_metadata_wheel(
3080+
self, monkeypatch, pyramid_config, db_request
3081+
):
3082+
user = UserFactory.create()
3083+
pyramid_config.testing_securitypolicy(identity=user)
3084+
db_request.user = user
3085+
EmailFactory.create(user=user)
3086+
project = ProjectFactory.create()
3087+
release = ReleaseFactory.create(project=project, version="1.0")
3088+
RoleFactory.create(user=user, project=project)
3089+
3090+
temp_f = io.BytesIO()
3091+
with zipfile.ZipFile(file=temp_f, mode="w") as zfp:
3092+
zfp.writestr(
3093+
f"{project.name.lower()}-{release.version}.dist-info/METADATA",
3094+
"Fake metadata",
3095+
)
3096+
3097+
filename = f"{project.name}-{release.version}-cp34-none-any.whl"
3098+
filebody = temp_f.getvalue()
3099+
3100+
db_request.POST = MultiDict(
3101+
{
3102+
"metadata_version": "1.2",
3103+
"name": project.name,
3104+
"version": release.version,
3105+
"filetype": "bdist_wheel",
3106+
"pyversion": "cp34",
3107+
"md5_digest": hashlib.md5(filebody).hexdigest(),
3108+
"content": pretend.stub(
3109+
filename=filename,
3110+
file=io.BytesIO(filebody),
3111+
type="application/zip",
3112+
),
3113+
}
3114+
)
3115+
3116+
monkeypatch.setattr(legacy, "_is_valid_dist_file", lambda *a, **kw: True)
3117+
3118+
with pytest.raises(HTTPBadRequest) as excinfo:
3119+
legacy.file_upload(db_request)
3120+
3121+
resp = excinfo.value
3122+
3123+
assert resp.status_code == 400
3124+
assert re.match(
3125+
"400 Wheel .* does not contain the required METADATA file: .*", resp.status
3126+
)
3127+
30283128
def test_upload_updates_existing_project_name(
30293129
self, pyramid_config, db_request, metrics
30303130
):

tests/unit/packaging/test_models.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ def test_compute_paths(self, db_session):
551551

552552
assert rfile.path == expected
553553
assert rfile.pgp_path == expected + ".asc"
554+
assert rfile.metadata_path == expected + ".metadata"
554555

555556
def test_query_paths(self, db_session):
556557
project = DBProjectFactory.create()
@@ -571,10 +572,10 @@ def test_query_paths(self, db_session):
571572
)
572573

573574
results = (
574-
db_session.query(File.path, File.pgp_path)
575+
db_session.query(File.path, File.pgp_path, File.metadata_path)
575576
.filter(File.id == rfile.id)
576577
.limit(1)
577578
.one()
578579
)
579580

580-
assert results == (expected, expected + ".asc")
581+
assert results == (expected, expected + ".asc", expected + ".metadata")

tests/unit/packaging/test_tasks.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,65 @@ def mock_named_temporary_file():
8484
assert primary_stub.get_metadata.calls == [pretend.call(file.path)]
8585
assert primary_stub.get.calls == [pretend.call(file.path)]
8686
assert archive_stub.store.calls == [
87-
pretend.call(file.path, "/tmp/wutang", meta={"fizz": "buzz"})
87+
pretend.call(file.path, "/tmp/wutang", meta={"fizz": "buzz"}),
88+
]
89+
else:
90+
assert primary_stub.get_metadata.calls == []
91+
assert primary_stub.get.calls == []
92+
assert archive_stub.store.calls == []
93+
94+
95+
@pytest.mark.parametrize("archived", [True, False])
96+
def test_sync_file_to_archive_includes_bonus_files(db_request, monkeypatch, archived):
97+
file = FileFactory(
98+
archived=archived,
99+
has_signature=True,
100+
metadata_file_sha256_digest="deadbeefdeadbeefdeadbeefdeadbeef",
101+
)
102+
primary_stub = pretend.stub(
103+
get_metadata=pretend.call_recorder(lambda path: {"fizz": "buzz"}),
104+
get=pretend.call_recorder(
105+
lambda path: pretend.stub(read=lambda: b"my content")
106+
),
107+
)
108+
archive_stub = pretend.stub(
109+
store=pretend.call_recorder(lambda filename, path, meta=None: None)
110+
)
111+
db_request.find_service = pretend.call_recorder(
112+
lambda iface, name=None: {"primary": primary_stub, "archive": archive_stub}[
113+
name
114+
]
115+
)
116+
117+
@contextmanager
118+
def mock_named_temporary_file():
119+
yield pretend.stub(
120+
name="/tmp/wutang",
121+
write=lambda bites: None,
122+
flush=lambda: None,
123+
)
124+
125+
monkeypatch.setattr(tempfile, "NamedTemporaryFile", mock_named_temporary_file)
126+
127+
sync_file_to_archive(db_request, file.id)
128+
129+
assert file.archived
130+
131+
if not archived:
132+
assert primary_stub.get_metadata.calls == [
133+
pretend.call(file.path),
134+
pretend.call(file.metadata_path),
135+
pretend.call(file.pgp_path),
136+
]
137+
assert primary_stub.get.calls == [
138+
pretend.call(file.path),
139+
pretend.call(file.metadata_path),
140+
pretend.call(file.pgp_path),
141+
]
142+
assert archive_stub.store.calls == [
143+
pretend.call(file.path, "/tmp/wutang", meta={"fizz": "buzz"}),
144+
pretend.call(file.metadata_path, "/tmp/wutang", meta={"fizz": "buzz"}),
145+
pretend.call(file.pgp_path, "/tmp/wutang", meta={"fizz": "buzz"}),
88146
]
89147
else:
90148
assert primary_stub.get_metadata.calls == []

tests/unit/packaging/test_utils.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def test_render_simple_detail(db_request, monkeypatch, jinja):
2626
release1 = ReleaseFactory.create(project=project, version="1.0")
2727
release2 = ReleaseFactory.create(project=project, version="dog")
2828
FileFactory.create(release=release1)
29-
FileFactory.create(release=release2)
29+
FileFactory.create(
30+
release=release2, metadata_file_sha256_digest="beefdeadbeefdeadbeefdeadbeefdead"
31+
)
3032

3133
fake_hasher = pretend.stub(
3234
update=pretend.call_recorder(lambda x: None),

0 commit comments

Comments
 (0)