Path: blob/main/test/lib/python3.9/site-packages/setuptools/_distutils/command/upload.py
4804 views
"""1distutils.command.upload23Implements the Distutils 'upload' subcommand (upload package to a package4index).5"""67import os8import io9import hashlib10from base64 import standard_b64encode11from urllib.request import urlopen, Request, HTTPError12from urllib.parse import urlparse13from distutils.errors import DistutilsError, DistutilsOptionError14from distutils.core import PyPIRCCommand15from distutils.spawn import spawn16from distutils import log171819# PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256)20# https://bugs.python.org/issue4069821_FILE_CONTENT_DIGESTS = {22"md5_digest": getattr(hashlib, "md5", None),23"sha256_digest": getattr(hashlib, "sha256", None),24"blake2_256_digest": getattr(hashlib, "blake2b", None),25}262728class upload(PyPIRCCommand):2930description = "upload binary package to PyPI"3132user_options = PyPIRCCommand.user_options + [33('sign', 's',34'sign files to upload using gpg'),35('identity=', 'i', 'GPG identity used to sign files'),36]3738boolean_options = PyPIRCCommand.boolean_options + ['sign']3940def initialize_options(self):41PyPIRCCommand.initialize_options(self)42self.username = ''43self.password = ''44self.show_response = 045self.sign = False46self.identity = None4748def finalize_options(self):49PyPIRCCommand.finalize_options(self)50if self.identity and not self.sign:51raise DistutilsOptionError(52"Must use --sign for --identity to have meaning"53)54config = self._read_pypirc()55if config != {}:56self.username = config['username']57self.password = config['password']58self.repository = config['repository']59self.realm = config['realm']6061# getting the password from the distribution62# if previously set by the register command63if not self.password and self.distribution.password:64self.password = self.distribution.password6566def run(self):67if not self.distribution.dist_files:68msg = ("Must create and upload files in one command "69"(e.g. setup.py sdist upload)")70raise DistutilsOptionError(msg)71for command, pyversion, filename in self.distribution.dist_files:72self.upload_file(command, pyversion, filename)7374def upload_file(self, command, pyversion, filename):75# Makes sure the repository URL is compliant76schema, netloc, url, params, query, fragments = \77urlparse(self.repository)78if params or query or fragments:79raise AssertionError("Incompatible url %s" % self.repository)8081if schema not in ('http', 'https'):82raise AssertionError("unsupported schema " + schema)8384# Sign if requested85if self.sign:86gpg_args = ["gpg", "--detach-sign", "-a", filename]87if self.identity:88gpg_args[2:2] = ["--local-user", self.identity]89spawn(gpg_args,90dry_run=self.dry_run)9192# Fill in the data - send all the meta-data in case we need to93# register a new release94f = open(filename,'rb')95try:96content = f.read()97finally:98f.close()99100meta = self.distribution.metadata101data = {102# action103':action': 'file_upload',104'protocol_version': '1',105106# identify release107'name': meta.get_name(),108'version': meta.get_version(),109110# file content111'content': (os.path.basename(filename),content),112'filetype': command,113'pyversion': pyversion,114115# additional meta-data116'metadata_version': '1.0',117'summary': meta.get_description(),118'home_page': meta.get_url(),119'author': meta.get_contact(),120'author_email': meta.get_contact_email(),121'license': meta.get_licence(),122'description': meta.get_long_description(),123'keywords': meta.get_keywords(),124'platform': meta.get_platforms(),125'classifiers': meta.get_classifiers(),126'download_url': meta.get_download_url(),127# PEP 314128'provides': meta.get_provides(),129'requires': meta.get_requires(),130'obsoletes': meta.get_obsoletes(),131}132133data['comment'] = ''134135# file content digests136for digest_name, digest_cons in _FILE_CONTENT_DIGESTS.items():137if digest_cons is None:138continue139try:140data[digest_name] = digest_cons(content).hexdigest()141except ValueError:142# hash digest not available or blocked by security policy143pass144145if self.sign:146with open(filename + ".asc", "rb") as f:147data['gpg_signature'] = (os.path.basename(filename) + ".asc",148f.read())149150# set up the authentication151user_pass = (self.username + ":" + self.password).encode('ascii')152# The exact encoding of the authentication string is debated.153# Anyway PyPI only accepts ascii for both username or password.154auth = "Basic " + standard_b64encode(user_pass).decode('ascii')155156# Build up the MIME payload for the POST data157boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'158sep_boundary = b'\r\n--' + boundary.encode('ascii')159end_boundary = sep_boundary + b'--\r\n'160body = io.BytesIO()161for key, value in data.items():162title = '\r\nContent-Disposition: form-data; name="%s"' % key163# handle multiple entries for the same name164if not isinstance(value, list):165value = [value]166for value in value:167if type(value) is tuple:168title += '; filename="%s"' % value[0]169value = value[1]170else:171value = str(value).encode('utf-8')172body.write(sep_boundary)173body.write(title.encode('utf-8'))174body.write(b"\r\n\r\n")175body.write(value)176body.write(end_boundary)177body = body.getvalue()178179msg = "Submitting %s to %s" % (filename, self.repository)180self.announce(msg, log.INFO)181182# build the Request183headers = {184'Content-type': 'multipart/form-data; boundary=%s' % boundary,185'Content-length': str(len(body)),186'Authorization': auth,187}188189request = Request(self.repository, data=body,190headers=headers)191# send the data192try:193result = urlopen(request)194status = result.getcode()195reason = result.msg196except HTTPError as e:197status = e.code198reason = e.msg199except OSError as e:200self.announce(str(e), log.ERROR)201raise202203if status == 200:204self.announce('Server response (%s): %s' % (status, reason),205log.INFO)206if self.show_response:207text = self._read_pypi_response(result)208msg = '\n'.join(('-' * 75, text, '-' * 75))209self.announce(msg, log.INFO)210else:211msg = 'Upload failed (%s): %s' % (status, reason)212self.announce(msg, log.ERROR)213raise DistutilsError(msg)214215216