Skip to content

Commit e572c7f

Browse files
authored
bpo-40698: Improve distutils upload hash digests (GH-20260)
- Fix upload test on systems that blocks MD5 - Add SHA2-256 and Blake2b-256 digests based on new Warehous and twine specs. Signed-off-by: Christian Heimes <[email protected]>
1 parent bac170c commit e572c7f

File tree

4 files changed

+50
-5
lines changed

4 files changed

+50
-5
lines changed

Doc/whatsnew/3.9.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,13 @@ and :meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime`
331331
methods now returns a :func:`~collections.namedtuple` instead of a :class:`tuple`.
332332
(Contributed by Dong-hee Na in :issue:`24416`.)
333333

334+
distutils
335+
---------
336+
337+
The :command:`upload` command now creates SHA2-256 and Blake2b-256 hash
338+
digests. It skips MD5 on platforms that block MD5 digest.
339+
(Contributed by Christian Heimes in :issue:`40698`.)
340+
334341
fcntl
335342
-----
336343

Lib/distutils/command/upload.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
from distutils.spawn import spawn
1717
from distutils import log
1818

19+
20+
# PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256)
21+
# https://bugs.python.org/issue40698
22+
_FILE_CONTENT_DIGESTS = {
23+
"md5_digest": getattr(hashlib, "md5", None),
24+
"sha256_digest": getattr(hashlib, "sha256", None),
25+
"blake2_256_digest": getattr(hashlib, "blake2b", None),
26+
}
27+
28+
1929
class upload(PyPIRCCommand):
2030

2131
description = "upload binary package to PyPI"
@@ -87,6 +97,7 @@ def upload_file(self, command, pyversion, filename):
8797
content = f.read()
8898
finally:
8999
f.close()
100+
90101
meta = self.distribution.metadata
91102
data = {
92103
# action
@@ -101,7 +112,6 @@ def upload_file(self, command, pyversion, filename):
101112
'content': (os.path.basename(filename),content),
102113
'filetype': command,
103114
'pyversion': pyversion,
104-
'md5_digest': hashlib.md5(content).hexdigest(),
105115

106116
# additional meta-data
107117
'metadata_version': '1.0',
@@ -123,6 +133,16 @@ def upload_file(self, command, pyversion, filename):
123133

124134
data['comment'] = ''
125135

136+
# file content digests
137+
for digest_name, digest_cons in _FILE_CONTENT_DIGESTS.items():
138+
if digest_cons is None:
139+
continue
140+
try:
141+
data[digest_name] = digest_cons(content).hexdigest()
142+
except ValueError:
143+
# hash digest not available or blocked by security policy
144+
pass
145+
126146
if self.sign:
127147
with open(filename + ".asc", "rb") as f:
128148
data['gpg_signature'] = (os.path.basename(filename) + ".asc",

Lib/distutils/tests/test_upload.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,30 @@ def test_upload(self):
130130

131131
# what did we send ?
132132
headers = dict(self.last_open.req.headers)
133-
self.assertEqual(headers['Content-length'], '2162')
133+
self.assertGreaterEqual(int(headers['Content-length']), 2162)
134134
content_type = headers['Content-type']
135135
self.assertTrue(content_type.startswith('multipart/form-data'))
136136
self.assertEqual(self.last_open.req.get_method(), 'POST')
137137
expected_url = 'https://upload.pypi.org/legacy/'
138138
self.assertEqual(self.last_open.req.get_full_url(), expected_url)
139-
self.assertTrue(b'xxx' in self.last_open.req.data)
140-
self.assertIn(b'protocol_version', self.last_open.req.data)
139+
data = self.last_open.req.data
140+
self.assertIn(b'xxx',data)
141+
self.assertIn(b'protocol_version', data)
142+
self.assertIn(b'sha256_digest', data)
143+
self.assertIn(
144+
b'cd2eb0837c9b4c962c22d2ff8b5441b7b45805887f051d39bf133b583baf'
145+
b'6860',
146+
data
147+
)
148+
if b'md5_digest' in data:
149+
self.assertIn(b'f561aaf6ef0bf14d4208bb46a4ccb3ad', data)
150+
if b'blake2_256_digest' in data:
151+
self.assertIn(
152+
b'b6f289a27d4fe90da63c503bfe0a9b761a8f76bb86148565065f040be'
153+
b'6d1c3044cf7ded78ef800509bccb4b648e507d88dc6383d67642aadcc'
154+
b'ce443f1534330a',
155+
data
156+
)
141157

142158
# The PyPI response body was echoed
143159
results = self.get_logs(INFO)
@@ -166,7 +182,7 @@ def test_upload_correct_cr(self):
166182
cmd.run()
167183

168184
headers = dict(self.last_open.req.headers)
169-
self.assertEqual(headers['Content-length'], '2172')
185+
self.assertGreaterEqual(int(headers['Content-length']), 2172)
170186
self.assertIn(b'long description\r', self.last_open.req.data)
171187

172188
def test_upload_fails(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`distutils` upload creates SHA2-256 and Blake2b-256 digests. MD5
2+
digests is skipped if platform blocks MD5.

0 commit comments

Comments
 (0)