forked from pypa/setuptools
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Remove "upload" and "register" commands.
The upload and register commands were deprecated over a year ago, in July 2019 (PR pypaGH-1410, discussed in issue pypaGH-1381). It is time to actively remove them in favor of twine.
- Loading branch information
Showing
8 changed files
with
57 additions
and
445 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Removed the "upload" and "register" commands in favor of `twine <https://pypi.org/p/twine>`_. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,196 +1,17 @@ | ||
import io | ||
import os | ||
import hashlib | ||
import getpass | ||
|
||
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 setuptools.extern.six.moves.urllib.request import urlopen, Request | ||
from setuptools.extern.six.moves.urllib.error import HTTPError | ||
from setuptools.extern.six.moves.urllib.parse import urlparse | ||
from setuptools.errors import RemovedCommandError | ||
|
||
|
||
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) | ||
finally: | ||
self.announce( | ||
"WARNING: Uploading via this command is deprecated, use twine " | ||
"to upload instead (https://pypi.org/p/twine/)", | ||
log.WARN | ||
) | ||
"""Formerly used to upload packages to PyPI.""" | ||
|
||
def finalize_options(self): | ||
orig.upload.finalize_options(self) | ||
self.username = ( | ||
self.username or | ||
getpass.getuser() | ||
) | ||
# Attempt to obtain password. Short circuit evaluation at the first | ||
# sign of success. | ||
self.password = ( | ||
self.password or | ||
self._load_password_from_keyring() or | ||
self._prompt_for_password() | ||
def run(self): | ||
msg = ( | ||
"The upload command has been removed, use twine to upload " | ||
+ "instead (https://pypi.org/p/twine)" | ||
) | ||
|
||
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(), | ||
} | ||
|
||
data['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. | ||
""" | ||
try: | ||
keyring = __import__('keyring') | ||
return keyring.get_password(self.repository, self.username) | ||
except Exception: | ||
pass | ||
|
||
def _prompt_for_password(self): | ||
""" | ||
Prompt for a password on the tty. Suppress Exceptions. | ||
""" | ||
try: | ||
return getpass.getpass() | ||
except (Exception, KeyboardInterrupt): | ||
pass | ||
self.announce("ERROR: " + msg, log.ERROR) | ||
raise RemovedCommandError(msg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
"""setuptools.errors | ||
Provides exceptions used by setuptools modules. | ||
""" | ||
|
||
from distutils.errors import DistutilsError | ||
|
||
|
||
class RemovedCommandError(DistutilsError, RuntimeError): | ||
"""Error used for commands that have been removed in setuptools. | ||
Since ``setuptools`` is built on ``distutils``, simply removing a command | ||
from ``setuptools`` will make the behavior fall back to ``distutils``; this | ||
error is raised if a command exists in ``distutils`` but has been actively | ||
removed in ``setuptools``. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,21 @@ | ||
import mock | ||
from distutils import log | ||
|
||
import pytest | ||
|
||
from setuptools.command.register import register | ||
from setuptools.dist import Distribution | ||
from setuptools.errors import RemovedCommandError | ||
|
||
try: | ||
from unittest import mock | ||
except ImportError: | ||
import mock | ||
|
||
class TestRegisterTest: | ||
def test_warns_deprecation(self): | ||
dist = Distribution() | ||
|
||
cmd = register(dist) | ||
cmd.run_command = mock.Mock() | ||
cmd.send_metadata = mock.Mock() | ||
cmd.announce = mock.Mock() | ||
|
||
cmd.run() | ||
import pytest | ||
|
||
cmd.announce.assert_called_with( | ||
"WARNING: Registering is deprecated, use twine to upload instead " | ||
"(https://pypi.org/p/twine/)", | ||
log.WARN | ||
) | ||
|
||
def test_warns_deprecation_when_raising(self): | ||
class RegisterTest: | ||
def test_register_exception(self): | ||
""" Ensure that the register command has been properly removed.""" | ||
dist = Distribution() | ||
dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] | ||
|
||
cmd = register(dist) | ||
cmd.run_command = mock.Mock() | ||
cmd.send_metadata = mock.Mock() | ||
cmd.send_metadata.side_effect = Exception | ||
cmd.announce = mock.Mock() | ||
|
||
with pytest.raises(Exception): | ||
with pytest.Raises(RemovedCommandError): | ||
cmd.run() | ||
|
||
cmd.announce.assert_called_with( | ||
"WARNING: Registering is deprecated, use twine to upload instead " | ||
"(https://pypi.org/p/twine/)", | ||
log.WARN | ||
) |
Oops, something went wrong.