Skip to content

Commit 375138c

Browse files
authored
Merge pull request #1576 from pganssle/fix_upload_metadata
Fix upload metadata
2 parents 38f1e49 + 2b5b913 commit 375138c

File tree

6 files changed

+523
-39
lines changed

6 files changed

+523
-39
lines changed

changelog.d/1576.change.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Started monkey-patching ``get_metadata_version`` and ``read_pkg_file`` onto ``distutils.DistributionMetadata`` to retain the correct version on the ``PKG-INFO`` file in the (deprecated) ``upload`` command.

setuptools/command/upload.py

+144-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1+
import io
2+
import os
3+
import hashlib
14
import getpass
5+
import platform
6+
7+
from base64 import standard_b64encode
8+
29
from distutils import log
310
from distutils.command import upload as orig
11+
from distutils.spawn import spawn
12+
13+
from distutils.errors import DistutilsError
414

15+
from six.moves.urllib.request import urlopen, Request
16+
from six.moves.urllib.error import HTTPError
17+
from six.moves.urllib.parse import urlparse
518

619
class upload(orig.upload):
720
"""
821
Override default upload behavior to obtain password
922
in a variety of different ways.
1023
"""
11-
1224
def run(self):
1325
try:
1426
orig.upload.run(self)
@@ -33,6 +45,137 @@ def finalize_options(self):
3345
self._prompt_for_password()
3446
)
3547

48+
def upload_file(self, command, pyversion, filename):
49+
# Makes sure the repository URL is compliant
50+
schema, netloc, url, params, query, fragments = \
51+
urlparse(self.repository)
52+
if params or query or fragments:
53+
raise AssertionError("Incompatible url %s" % self.repository)
54+
55+
if schema not in ('http', 'https'):
56+
raise AssertionError("unsupported schema " + schema)
57+
58+
# Sign if requested
59+
if self.sign:
60+
gpg_args = ["gpg", "--detach-sign", "-a", filename]
61+
if self.identity:
62+
gpg_args[2:2] = ["--local-user", self.identity]
63+
spawn(gpg_args,
64+
dry_run=self.dry_run)
65+
66+
# Fill in the data - send all the meta-data in case we need to
67+
# register a new release
68+
with open(filename, 'rb') as f:
69+
content = f.read()
70+
71+
meta = self.distribution.metadata
72+
73+
data = {
74+
# action
75+
':action': 'file_upload',
76+
'protocol_version': '1',
77+
78+
# identify release
79+
'name': meta.get_name(),
80+
'version': meta.get_version(),
81+
82+
# file content
83+
'content': (os.path.basename(filename),content),
84+
'filetype': command,
85+
'pyversion': pyversion,
86+
'md5_digest': hashlib.md5(content).hexdigest(),
87+
88+
# additional meta-data
89+
'metadata_version': str(meta.get_metadata_version()),
90+
'summary': meta.get_description(),
91+
'home_page': meta.get_url(),
92+
'author': meta.get_contact(),
93+
'author_email': meta.get_contact_email(),
94+
'license': meta.get_licence(),
95+
'description': meta.get_long_description(),
96+
'keywords': meta.get_keywords(),
97+
'platform': meta.get_platforms(),
98+
'classifiers': meta.get_classifiers(),
99+
'download_url': meta.get_download_url(),
100+
# PEP 314
101+
'provides': meta.get_provides(),
102+
'requires': meta.get_requires(),
103+
'obsoletes': meta.get_obsoletes(),
104+
}
105+
106+
data['comment'] = ''
107+
108+
if self.sign:
109+
data['gpg_signature'] = (os.path.basename(filename) + ".asc",
110+
open(filename+".asc", "rb").read())
111+
112+
# set up the authentication
113+
user_pass = (self.username + ":" + self.password).encode('ascii')
114+
# The exact encoding of the authentication string is debated.
115+
# Anyway PyPI only accepts ascii for both username or password.
116+
auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
117+
118+
# Build up the MIME payload for the POST data
119+
boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
120+
sep_boundary = b'\r\n--' + boundary.encode('ascii')
121+
end_boundary = sep_boundary + b'--\r\n'
122+
body = io.BytesIO()
123+
for key, value in data.items():
124+
title = '\r\nContent-Disposition: form-data; name="%s"' % key
125+
# handle multiple entries for the same name
126+
if not isinstance(value, list):
127+
value = [value]
128+
for value in value:
129+
if type(value) is tuple:
130+
title += '; filename="%s"' % value[0]
131+
value = value[1]
132+
else:
133+
value = str(value).encode('utf-8')
134+
body.write(sep_boundary)
135+
body.write(title.encode('utf-8'))
136+
body.write(b"\r\n\r\n")
137+
body.write(value)
138+
body.write(end_boundary)
139+
body = body.getvalue()
140+
141+
msg = "Submitting %s to %s" % (filename, self.repository)
142+
self.announce(msg, log.INFO)
143+
144+
# build the Request
145+
headers = {
146+
'Content-type': 'multipart/form-data; boundary=%s' % boundary,
147+
'Content-length': str(len(body)),
148+
'Authorization': auth,
149+
}
150+
151+
request = Request(self.repository, data=body,
152+
headers=headers)
153+
# send the data
154+
try:
155+
result = urlopen(request)
156+
status = result.getcode()
157+
reason = result.msg
158+
except HTTPError as e:
159+
status = e.code
160+
reason = e.msg
161+
except OSError as e:
162+
self.announce(str(e), log.ERROR)
163+
raise
164+
165+
if status == 200:
166+
self.announce('Server response (%s): %s' % (status, reason),
167+
log.INFO)
168+
if self.show_response:
169+
text = getattr(self, '_read_pypi_response',
170+
lambda x: None)(result)
171+
if text is not None:
172+
msg = '\n'.join(('-' * 75, text, '-' * 75))
173+
self.announce(msg, log.INFO)
174+
else:
175+
msg = 'Upload failed (%s): %s' % (status, reason)
176+
self.announce(msg, log.ERROR)
177+
raise DistutilsError(msg)
178+
36179
def _load_password_from_keyring(self):
37180
"""
38181
Attempt to load password from keyring. Suppress Exceptions.

setuptools/dist.py

+102-32
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
import distutils.cmd
1111
import distutils.dist
1212
import itertools
13+
14+
1315
from collections import defaultdict
16+
from email import message_from_file
17+
1418
from distutils.errors import (
1519
DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,
1620
)
@@ -39,35 +43,103 @@ def _get_unpatched(cls):
3943
return get_unpatched(cls)
4044

4145

42-
def get_metadata_version(dist_md):
43-
if dist_md.long_description_content_type or dist_md.provides_extras:
44-
return StrictVersion('2.1')
45-
elif (dist_md.maintainer is not None or
46-
dist_md.maintainer_email is not None or
47-
getattr(dist_md, 'python_requires', None) is not None):
48-
return StrictVersion('1.2')
49-
elif (dist_md.provides or dist_md.requires or dist_md.obsoletes or
50-
dist_md.classifiers or dist_md.download_url):
51-
return StrictVersion('1.1')
46+
def get_metadata_version(self):
47+
mv = getattr(self, 'metadata_version', None)
48+
49+
if mv is None:
50+
if self.long_description_content_type or self.provides_extras:
51+
mv = StrictVersion('2.1')
52+
elif (self.maintainer is not None or
53+
self.maintainer_email is not None or
54+
getattr(self, 'python_requires', None) is not None):
55+
mv = StrictVersion('1.2')
56+
elif (self.provides or self.requires or self.obsoletes or
57+
self.classifiers or self.download_url):
58+
mv = StrictVersion('1.1')
59+
else:
60+
mv = StrictVersion('1.0')
61+
62+
self.metadata_version = mv
63+
64+
return mv
65+
66+
67+
def read_pkg_file(self, file):
68+
"""Reads the metadata values from a file object."""
69+
msg = message_from_file(file)
70+
71+
def _read_field(name):
72+
value = msg[name]
73+
if value == 'UNKNOWN':
74+
return None
75+
return value
76+
77+
def _read_list(name):
78+
values = msg.get_all(name, None)
79+
if values == []:
80+
return None
81+
return values
82+
83+
self.metadata_version = StrictVersion(msg['metadata-version'])
84+
self.name = _read_field('name')
85+
self.version = _read_field('version')
86+
self.description = _read_field('summary')
87+
# we are filling author only.
88+
self.author = _read_field('author')
89+
self.maintainer = None
90+
self.author_email = _read_field('author-email')
91+
self.maintainer_email = None
92+
self.url = _read_field('home-page')
93+
self.license = _read_field('license')
94+
95+
if 'download-url' in msg:
96+
self.download_url = _read_field('download-url')
97+
else:
98+
self.download_url = None
99+
100+
self.long_description = _read_field('description')
101+
self.description = _read_field('summary')
52102

53-
return StrictVersion('1.0')
103+
if 'keywords' in msg:
104+
self.keywords = _read_field('keywords').split(',')
105+
106+
self.platforms = _read_list('platform')
107+
self.classifiers = _read_list('classifier')
108+
109+
# PEP 314 - these fields only exist in 1.1
110+
if self.metadata_version == StrictVersion('1.1'):
111+
self.requires = _read_list('requires')
112+
self.provides = _read_list('provides')
113+
self.obsoletes = _read_list('obsoletes')
114+
else:
115+
self.requires = None
116+
self.provides = None
117+
self.obsoletes = None
54118

55119

56120
# Based on Python 3.5 version
57121
def write_pkg_file(self, file):
58122
"""Write the PKG-INFO format data to a file object.
59123
"""
60-
version = get_metadata_version(self)
124+
version = self.get_metadata_version()
125+
126+
if six.PY2:
127+
def write_field(key, value):
128+
file.write("%s: %s\n" % (key, self._encode_field(value)))
129+
else:
130+
def write_field(key, value):
131+
file.write("%s: %s\n" % (key, value))
132+
61133

62-
file.write('Metadata-Version: %s\n' % version)
63-
file.write('Name: %s\n' % self.get_name())
64-
file.write('Version: %s\n' % self.get_version())
65-
file.write('Summary: %s\n' % self.get_description())
66-
file.write('Home-page: %s\n' % self.get_url())
134+
write_field('Metadata-Version', str(version))
135+
write_field('Name', self.get_name())
136+
write_field('Version', self.get_version())
137+
write_field('Summary', self.get_description())
138+
write_field('Home-page', self.get_url())
67139

68140
if version < StrictVersion('1.2'):
69-
file.write('Author: %s\n' % self.get_contact())
70-
file.write('Author-email: %s\n' % self.get_contact_email())
141+
write_field('Author:', self.get_contact())
142+
write_field('Author-email:', self.get_contact_email())
71143
else:
72144
optional_fields = (
73145
('Author', 'author'),
@@ -78,28 +150,26 @@ def write_pkg_file(self, file):
78150

79151
for field, attr in optional_fields:
80152
attr_val = getattr(self, attr)
81-
if six.PY2:
82-
attr_val = self._encode_field(attr_val)
83153

84154
if attr_val is not None:
85-
file.write('%s: %s\n' % (field, attr_val))
155+
write_field(field, attr_val)
86156

87-
file.write('License: %s\n' % self.get_license())
157+
write_field('License', self.get_license())
88158
if self.download_url:
89-
file.write('Download-URL: %s\n' % self.download_url)
159+
write_field('Download-URL', self.download_url)
90160
for project_url in self.project_urls.items():
91-
file.write('Project-URL: %s, %s\n' % project_url)
161+
write_field('Project-URL', '%s, %s' % project_url)
92162

93163
long_desc = rfc822_escape(self.get_long_description())
94-
file.write('Description: %s\n' % long_desc)
164+
write_field('Description', long_desc)
95165

96166
keywords = ','.join(self.get_keywords())
97167
if keywords:
98-
file.write('Keywords: %s\n' % keywords)
168+
write_field('Keywords', keywords)
99169

100170
if version >= StrictVersion('1.2'):
101171
for platform in self.get_platforms():
102-
file.write('Platform: %s\n' % platform)
172+
write_field('Platform', platform)
103173
else:
104174
self._write_list(file, 'Platform', self.get_platforms())
105175

@@ -112,17 +182,17 @@ def write_pkg_file(self, file):
112182

113183
# Setuptools specific for PEP 345
114184
if hasattr(self, 'python_requires'):
115-
file.write('Requires-Python: %s\n' % self.python_requires)
185+
write_field('Requires-Python', self.python_requires)
116186

117187
# PEP 566
118188
if self.long_description_content_type:
119-
file.write(
120-
'Description-Content-Type: %s\n' %
189+
write_field(
190+
'Description-Content-Type',
121191
self.long_description_content_type
122192
)
123193
if self.provides_extras:
124194
for extra in self.provides_extras:
125-
file.write('Provides-Extra: %s\n' % extra)
195+
write_field('Provides-Extra', extra)
126196

127197

128198
sequence = tuple, list

setuptools/monkey.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def patch_all():
8484
warehouse = 'https://upload.pypi.org/legacy/'
8585
distutils.config.PyPIRCCommand.DEFAULT_REPOSITORY = warehouse
8686

87-
_patch_distribution_metadata_write_pkg_file()
87+
_patch_distribution_metadata()
8888

8989
# Install Distribution throughout the distutils
9090
for module in distutils.dist, distutils.core, distutils.cmd:
@@ -101,11 +101,11 @@ def patch_all():
101101
patch_for_msvc_specialized_compiler()
102102

103103

104-
def _patch_distribution_metadata_write_pkg_file():
105-
"""Patch write_pkg_file to also write Requires-Python/Requires-External"""
106-
distutils.dist.DistributionMetadata.write_pkg_file = (
107-
setuptools.dist.write_pkg_file
108-
)
104+
def _patch_distribution_metadata():
105+
"""Patch write_pkg_file and read_pkg_file for higher metadata standards"""
106+
for attr in ('write_pkg_file', 'read_pkg_file', 'get_metadata_version'):
107+
new_val = getattr(setuptools.dist, attr)
108+
setattr(distutils.dist.DistributionMetadata, attr, new_val)
109109

110110

111111
def patch_func(replacement, target_mod, func_name):

0 commit comments

Comments
 (0)