Skip to content

Commit f413f95

Browse files
committed
Remove "upload" and "register" commands.
The upload and register commands were deprecated over a year ago, in July 2018 (PR pypaGH-1410, discussed in issue pypaGH-1381). It is time to actively remove them in favor of twine.
1 parent 6ac7b4e commit f413f95

File tree

8 files changed

+64
-442
lines changed

8 files changed

+64
-442
lines changed

changelog.d/1898.breaking.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Removed the "upload" and "register" commands in favor of `twine <https://pypi.org/p/twine>`_.

docs/setuptools.txt

+6-9
Original file line numberDiff line numberDiff line change
@@ -2087,16 +2087,13 @@ New in 41.5.0: Deprecated the test command.
20872087
``upload`` - Upload source and/or egg distributions to PyPI
20882088
===========================================================
20892089

2090-
.. warning::
2091-
**upload** is deprecated in favor of using `twine
2092-
<https://pypi.org/p/twine>`_
2093-
2094-
The ``upload`` command is implemented and `documented
2095-
<https://docs.python.org/3.1/distutils/uploading.html>`_
2096-
in distutils.
2090+
The ``upload`` command was deprecated in version 40.0 and removed in version
2091+
42.0. Use `twine <https://pypi.org/p/twine>`_ instead.
20972092

2098-
New in 20.1: Added keyring support.
2099-
New in 40.0: Deprecated the upload command.
2093+
For more information on the current best practices in uploading your packages
2094+
to PyPI, see the Python Packaging User Guide's "Packaging Python Projects"
2095+
tutorial specifically the section on `uploading the distribution archives
2096+
<https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives>`_.
21002097

21012098

21022099
-----------------------------------------

setuptools/command/__init__.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop',
33
'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',
44
'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts',
5-
'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib',
6-
'dist_info',
5+
'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info',
76
]
87

98
from distutils.command.bdist import bdist

setuptools/command/register.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from distutils import log
22
import distutils.command.register as orig
33

4+
from setuptools.errors import RemovedCommandError
5+
46

57
class register(orig.register):
6-
__doc__ = orig.register.__doc__
8+
"""Formerly used to register packages on PyPI."""
79

810
def run(self):
9-
try:
10-
# Make sure that we are using valid current name/version info
11-
self.run_command('egg_info')
12-
orig.register.run(self)
13-
finally:
14-
self.announce(
15-
"WARNING: Registering is deprecated, use twine to "
16-
"upload instead (https://pypi.org/p/twine/)",
17-
log.WARN
18-
)
11+
msg = (
12+
"The register command has been removed, use twine to upload "
13+
+ "instead (https://pypi.org/p/twine)"
14+
)
15+
16+
self.announce("ERROR: " + msg, log.ERROR)
17+
18+
raise RemovedCommandError(msg)

setuptools/command/upload.py

+8-187
Original file line numberDiff line numberDiff line change
@@ -1,196 +1,17 @@
1-
import io
2-
import os
3-
import hashlib
4-
import getpass
5-
6-
from base64 import standard_b64encode
7-
81
from distutils import log
92
from distutils.command import upload as orig
10-
from distutils.spawn import spawn
11-
12-
from distutils.errors import DistutilsError
133

14-
from setuptools.extern.six.moves.urllib.request import urlopen, Request
15-
from setuptools.extern.six.moves.urllib.error import HTTPError
16-
from setuptools.extern.six.moves.urllib.parse import urlparse
4+
from setuptools.errors import RemovedCommandError
175

186

197
class upload(orig.upload):
20-
"""
21-
Override default upload behavior to obtain password
22-
in a variety of different ways.
23-
"""
24-
def run(self):
25-
try:
26-
orig.upload.run(self)
27-
finally:
28-
self.announce(
29-
"WARNING: Uploading via this command is deprecated, use twine "
30-
"to upload instead (https://pypi.org/p/twine/)",
31-
log.WARN
32-
)
8+
"""Formerly used to upload packages to PyPI."""
339

34-
def finalize_options(self):
35-
orig.upload.finalize_options(self)
36-
self.username = (
37-
self.username or
38-
getpass.getuser()
39-
)
40-
# Attempt to obtain password. Short circuit evaluation at the first
41-
# sign of success.
42-
self.password = (
43-
self.password or
44-
self._load_password_from_keyring() or
45-
self._prompt_for_password()
10+
def run(self):
11+
msg = (
12+
"The upload command has been removed, use twine to upload "
13+
+ "instead (https://pypi.org/p/twine)"
4614
)
4715

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-
179-
def _load_password_from_keyring(self):
180-
"""
181-
Attempt to load password from keyring. Suppress Exceptions.
182-
"""
183-
try:
184-
keyring = __import__('keyring')
185-
return keyring.get_password(self.repository, self.username)
186-
except Exception:
187-
pass
188-
189-
def _prompt_for_password(self):
190-
"""
191-
Prompt for a password on the tty. Suppress Exceptions.
192-
"""
193-
try:
194-
return getpass.getpass()
195-
except (Exception, KeyboardInterrupt):
196-
pass
16+
self.announce("ERROR: " + msg, log.ERROR)
17+
raise RemovedCommandError(msg)

setuptools/errors.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""setuptools.errors
2+
3+
Provides exceptions used by setuptools modules.
4+
"""
5+
6+
from distutils.errors import DistutilsError
7+
8+
9+
class RemovedCommandError(DistutilsError, RuntimeError):
10+
"""Error used for commands that have been removed in setuptools.
11+
12+
Since ``setuptools`` is built on ``distutils``, simply removing a command
13+
from ``setuptools`` will make the behavior fall back to ``distutils``; this
14+
error is raised if a command exists in ``distutils`` but has been actively
15+
removed in ``setuptools``.
16+
"""

setuptools/tests/test_register.py

+11-32
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,22 @@
1-
import mock
2-
from distutils import log
3-
4-
import pytest
5-
61
from setuptools.command.register import register
72
from setuptools.dist import Distribution
3+
from setuptools.errors import RemovedCommandError
84

5+
try:
6+
from unittest import mock
7+
except ImportError:
8+
import mock
99

10-
class TestRegisterTest:
11-
def test_warns_deprecation(self):
12-
dist = Distribution()
13-
14-
cmd = register(dist)
15-
cmd.run_command = mock.Mock()
16-
cmd.send_metadata = mock.Mock()
17-
cmd.announce = mock.Mock()
18-
19-
cmd.run()
10+
import pytest
2011

21-
cmd.announce.assert_called_with(
22-
"WARNING: Registering is deprecated, use twine to upload instead "
23-
"(https://pypi.org/p/twine/)",
24-
log.WARN
25-
)
2612

27-
def test_warns_deprecation_when_raising(self):
13+
class TestRegister:
14+
def test_register_exception(self):
15+
"""Ensure that the register command has been properly removed."""
2816
dist = Distribution()
17+
dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())]
2918

3019
cmd = register(dist)
31-
cmd.run_command = mock.Mock()
32-
cmd.send_metadata = mock.Mock()
33-
cmd.send_metadata.side_effect = Exception
34-
cmd.announce = mock.Mock()
3520

36-
with pytest.raises(Exception):
21+
with pytest.raises(RemovedCommandError):
3722
cmd.run()
38-
39-
cmd.announce.assert_called_with(
40-
"WARNING: Registering is deprecated, use twine to upload instead "
41-
"(https://pypi.org/p/twine/)",
42-
log.WARN
43-
)

0 commit comments

Comments
 (0)