Skip to content

Commit

Permalink
Remove "upload" and "register" commands.
Browse files Browse the repository at this point in the history
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
pganssle committed Nov 1, 2019
1 parent a0fe403 commit 7b2e986
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 445 deletions.
1 change: 1 addition & 0 deletions changelog.d/1898.breaking.rst
@@ -0,0 +1 @@
Removed the "upload" and "register" commands in favor of `twine <https://pypi.org/p/twine>`_.
12 changes: 2 additions & 10 deletions docs/setuptools.txt
Expand Up @@ -2087,16 +2087,8 @@ New in 41.5.0: Deprecated the test command.
``upload`` - Upload source and/or egg distributions to PyPI
===========================================================

.. warning::
**upload** is deprecated in favor of using `twine
<https://pypi.org/p/twine>`_

The ``upload`` command is implemented and `documented
<https://docs.python.org/3.1/distutils/uploading.html>`_
in distutils.

New in 20.1: Added keyring support.
New in 40.0: Deprecated the upload command.
The ``upload`` command was deprecated in version 40.0 and removed in version
42.0. Use `twine <https://pypi.org/p/twine>`_ instead.


-----------------------------------------
Expand Down
3 changes: 1 addition & 2 deletions setuptools/command/__init__.py
Expand Up @@ -2,8 +2,7 @@
'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop',
'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',
'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts',
'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib',
'dist_info',
'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info',
]

from distutils.command.bdist import bdist
Expand Down
20 changes: 9 additions & 11 deletions setuptools/command/register.py
Expand Up @@ -3,16 +3,14 @@


class register(orig.register):
__doc__ = orig.register.__doc__
"""Formerly used to register packages on PyPI."""

def run(self):
try:
# Make sure that we are using valid current name/version info
self.run_command('egg_info')
orig.register.run(self)
finally:
self.announce(
"WARNING: Registering is deprecated, use twine to "
"upload instead (https://pypi.org/p/twine/)",
log.WARN
)
msg = (
"The register command has been removed, use twine to upload "
+ "instead (https://pypi.org/p/twine)"
)

self.announce("ERROR: " + msg, log.ERROR)

raise RemovedCommandError(msg)
195 changes: 8 additions & 187 deletions setuptools/command/upload.py
@@ -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)
16 changes: 16 additions & 0 deletions setuptools/errors.py
@@ -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``.
"""
44 changes: 11 additions & 33 deletions setuptools/tests/test_register.py
@@ -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
)

0 comments on commit 7b2e986

Please sign in to comment.