Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix upload metadata #1576

Merged
merged 19 commits into from
Nov 12, 2018
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1576.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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.
151 changes: 150 additions & 1 deletion setuptools/command/upload.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import io
import os
import hashlib
import getpass
import platform

from base64 import standard_b64encode

from distutils import log
from distutils.command import upload as orig
from distutils.spawn import spawn

from distutils.errors import DistutilsError

from six.moves.urllib.request import urlopen, Request
from six.moves.urllib.error import HTTPError
from six.moves.urllib.parse import urlparse

class upload(orig.upload):
"""
Override default upload behavior to obtain password
in a variety of different ways.
"""

def run(self):
try:
orig.upload.run(self)
Expand All @@ -33,6 +45,143 @@ def finalize_options(self):
self._prompt_for_password()
)

def upload_file(self, command, pyversion, filename):
# Makes sure the repository URL is compliant
schema, netloc, url, params, query, fragments = \
urlparse(self.repository)
if params or query or fragments:
raise AssertionError("Incompatible url %s" % self.repository)

if schema not in ('http', 'https'):
raise AssertionError("unsupported schema " + schema)

# Sign if requested
if self.sign:
gpg_args = ["gpg", "--detach-sign", "-a", filename]
if self.identity:
gpg_args[2:2] = ["--local-user", self.identity]
spawn(gpg_args,
dry_run=self.dry_run)

# Fill in the data - send all the meta-data in case we need to
# register a new release
with open(filename, 'rb') as f:
content = f.read()

meta = self.distribution.metadata

data = {
# action
':action': 'file_upload',
'protocol_version': '1',

# identify release
'name': meta.get_name(),
'version': meta.get_version(),

# file content
'content': (os.path.basename(filename),content),
'filetype': command,
'pyversion': pyversion,
'md5_digest': hashlib.md5(content).hexdigest(),

# additional meta-data
'metadata_version': str(meta.get_metadata_version()),
'summary': meta.get_description(),
'home_page': meta.get_url(),
'author': meta.get_contact(),
'author_email': meta.get_contact_email(),
'license': meta.get_licence(),
'description': meta.get_long_description(),
'keywords': meta.get_keywords(),
'platform': meta.get_platforms(),
'classifiers': meta.get_classifiers(),
'download_url': meta.get_download_url(),
# PEP 314
'provides': meta.get_provides(),
'requires': meta.get_requires(),
'obsoletes': meta.get_obsoletes(),
}
comment = ''
if command == 'bdist_rpm':
dist, version, id = platform.dist()
if dist:
comment = 'built for %s %s' % (dist, version)
elif command == 'bdist_dumb':
comment = 'built for %s' % platform.platform(terse=1)
data['comment'] = comment

if self.sign:
data['gpg_signature'] = (os.path.basename(filename) + ".asc",
open(filename+".asc", "rb").read())

# set up the authentication
user_pass = (self.username + ":" + self.password).encode('ascii')
# The exact encoding of the authentication string is debated.
# Anyway PyPI only accepts ascii for both username or password.
auth = "Basic " + standard_b64encode(user_pass).decode('ascii')

# Build up the MIME payload for the POST data
boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
sep_boundary = b'\r\n--' + boundary.encode('ascii')
end_boundary = sep_boundary + b'--\r\n'
body = io.BytesIO()
for key, value in data.items():
title = '\r\nContent-Disposition: form-data; name="%s"' % key
# handle multiple entries for the same name
if not isinstance(value, list):
value = [value]
for value in value:
if type(value) is tuple:
title += '; filename="%s"' % value[0]
value = value[1]
else:
value = str(value).encode('utf-8')
body.write(sep_boundary)
body.write(title.encode('utf-8'))
body.write(b"\r\n\r\n")
body.write(value)
body.write(end_boundary)
body = body.getvalue()

msg = "Submitting %s to %s" % (filename, self.repository)
self.announce(msg, log.INFO)

# build the Request
headers = {
'Content-type': 'multipart/form-data; boundary=%s' % boundary,
'Content-length': str(len(body)),
'Authorization': auth,
}

request = Request(self.repository, data=body,
headers=headers)
# send the data
try:
result = urlopen(request)
status = result.getcode()
reason = result.msg
except HTTPError as e:
status = e.code
reason = e.msg
except OSError as e:
self.announce(str(e), log.ERROR)
raise

if status == 200:
self.announce('Server response (%s): %s' % (status, reason),
log.INFO)
if self.show_response:
text = getattr(self, '_read_pypi_response',
lambda x: None)(result)
if text is not None:
msg = '\n'.join(('-' * 75, text, '-' * 75))
self.announce(msg, log.INFO)
else:
msg = 'Upload failed (%s): %s' % (status, reason)
self.announce(msg, log.ERROR)
raise DistutilsError(msg)

def _load_password_from_keyring(self):
"""
Attempt to load password from keyring. Suppress Exceptions.
Expand Down
134 changes: 102 additions & 32 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
import distutils.cmd
import distutils.dist
import itertools


from collections import defaultdict
from email import message_from_file

from distutils.errors import (
DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,
)
Expand Down Expand Up @@ -39,35 +43,103 @@ def _get_unpatched(cls):
return get_unpatched(cls)


def get_metadata_version(dist_md):
if dist_md.long_description_content_type or dist_md.provides_extras:
return StrictVersion('2.1')
elif (dist_md.maintainer is not None or
dist_md.maintainer_email is not None or
getattr(dist_md, 'python_requires', None) is not None):
return StrictVersion('1.2')
elif (dist_md.provides or dist_md.requires or dist_md.obsoletes or
dist_md.classifiers or dist_md.download_url):
return StrictVersion('1.1')
def get_metadata_version(self):
mv = getattr(self, 'metadata_version', None)

if mv is None:
if self.long_description_content_type or self.provides_extras:
mv = StrictVersion('2.1')
elif (self.maintainer is not None or
self.maintainer_email is not None or
getattr(self, 'python_requires', None) is not None):
mv = StrictVersion('1.2')
elif (self.provides or self.requires or self.obsoletes or
self.classifiers or self.download_url):
mv = StrictVersion('1.1')
else:
mv = StrictVersion('1.0')

self.metadata_version = mv

return mv


def read_pkg_file(self, file):
"""Reads the metadata values from a file object."""
msg = message_from_file(file)

def _read_field(name):
value = msg[name]
if value == 'UNKNOWN':
return None
return value

def _read_list(name):
values = msg.get_all(name, None)
if values == []:
return None
return values

self.metadata_version = StrictVersion(msg['metadata-version'])
self.name = _read_field('name')
self.version = _read_field('version')
self.description = _read_field('summary')
# we are filling author only.
self.author = _read_field('author')
self.maintainer = None
self.author_email = _read_field('author-email')
self.maintainer_email = None
self.url = _read_field('home-page')
self.license = _read_field('license')

if 'download-url' in msg:
self.download_url = _read_field('download-url')
else:
self.download_url = None

self.long_description = _read_field('description')
self.description = _read_field('summary')

return StrictVersion('1.0')
if 'keywords' in msg:
self.keywords = _read_field('keywords').split(',')

self.platforms = _read_list('platform')
self.classifiers = _read_list('classifier')

# PEP 314 - these fields only exist in 1.1
if self.metadata_version == StrictVersion('1.1'):
self.requires = _read_list('requires')
self.provides = _read_list('provides')
self.obsoletes = _read_list('obsoletes')
else:
self.requires = None
self.provides = None
self.obsoletes = None


# Based on Python 3.5 version
def write_pkg_file(self, file):
"""Write the PKG-INFO format data to a file object.
"""
version = get_metadata_version(self)
version = self.get_metadata_version()

if six.PY2:
def write_field(key, value):
file.write("%s: %s\n" % (key, self._encode_field(value)))
else:
def write_field(key, value):
file.write("%s: %s\n" % (key, value))


file.write('Metadata-Version: %s\n' % version)
file.write('Name: %s\n' % self.get_name())
file.write('Version: %s\n' % self.get_version())
file.write('Summary: %s\n' % self.get_description())
file.write('Home-page: %s\n' % self.get_url())
write_field('Metadata-Version', str(version))
write_field('Name', self.get_name())
write_field('Version', self.get_version())
write_field('Summary', self.get_description())
write_field('Home-page', self.get_url())

if version < StrictVersion('1.2'):
file.write('Author: %s\n' % self.get_contact())
file.write('Author-email: %s\n' % self.get_contact_email())
write_field('Author:', self.get_contact())
write_field('Author-email:', self.get_contact_email())
else:
optional_fields = (
('Author', 'author'),
Expand All @@ -78,28 +150,26 @@ def write_pkg_file(self, file):

for field, attr in optional_fields:
attr_val = getattr(self, attr)
if six.PY2:
attr_val = self._encode_field(attr_val)

if attr_val is not None:
file.write('%s: %s\n' % (field, attr_val))
write_field(field, attr_val)

file.write('License: %s\n' % self.get_license())
write_field('License', self.get_license())
if self.download_url:
file.write('Download-URL: %s\n' % self.download_url)
write_field('Download-URL', self.download_url)
for project_url in self.project_urls.items():
file.write('Project-URL: %s, %s\n' % project_url)
write_field('Project-URL', '%s, %s' % project_url)

long_desc = rfc822_escape(self.get_long_description())
file.write('Description: %s\n' % long_desc)
write_field('Description', long_desc)

keywords = ','.join(self.get_keywords())
if keywords:
file.write('Keywords: %s\n' % keywords)
write_field('Keywords', keywords)

if version >= StrictVersion('1.2'):
for platform in self.get_platforms():
file.write('Platform: %s\n' % platform)
write_field('Platform', platform)
else:
self._write_list(file, 'Platform', self.get_platforms())

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

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

# PEP 566
if self.long_description_content_type:
file.write(
'Description-Content-Type: %s\n' %
write_field(
'Description-Content-Type',
self.long_description_content_type
)
if self.provides_extras:
for extra in self.provides_extras:
file.write('Provides-Extra: %s\n' % extra)
write_field('Provides-Extra', extra)


sequence = tuple, list
Expand Down
12 changes: 6 additions & 6 deletions setuptools/monkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def patch_all():
warehouse = 'https://upload.pypi.org/legacy/'
distutils.config.PyPIRCCommand.DEFAULT_REPOSITORY = warehouse

_patch_distribution_metadata_write_pkg_file()
_patch_distribution_metadata()

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


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


def patch_func(replacement, target_mod, func_name):
Expand Down
Loading