From 2e4d3b133a8fe7be5469bc8f0d1a0f80a57f608c Mon Sep 17 00:00:00 2001
From: Antoine Pitrou <solipsis@pitrou.net>
Date: Wed, 18 Jun 2014 23:07:46 -0400
Subject: [PATCH] Issue #21722: The distutils "upload" command now exits with a
 non-zero return code when uploading fails. Patch by Martin Dengler.

---
 Lib/distutils/command/upload.py    | 15 ++++++++-------
 Lib/distutils/tests/test_upload.py | 16 ++++++++++++----
 Misc/ACKS                          |  1 +
 Misc/NEWS                          |  3 +++
 4 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/Lib/distutils/command/upload.py b/Lib/distutils/command/upload.py
index d6762e46fd..180be7c750 100644
--- a/Lib/distutils/command/upload.py
+++ b/Lib/distutils/command/upload.py
@@ -2,10 +2,6 @@
 
 Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
 
-from distutils.errors import *
-from distutils.core import PyPIRCCommand
-from distutils.spawn import spawn
-from distutils import log
 import sys
 import os, io
 import socket
@@ -13,6 +9,10 @@ import platform
 from base64 import standard_b64encode
 from urllib.request import urlopen, Request, HTTPError
 from urllib.parse import urlparse
+from distutils.errors import DistutilsError, DistutilsOptionError
+from distutils.core import PyPIRCCommand
+from distutils.spawn import spawn
+from distutils import log
 
 # this keeps compatibility for 2.3 and 2.4
 if sys.version < "2.5":
@@ -184,7 +184,7 @@ class upload(PyPIRCCommand):
             reason = result.msg
         except OSError as e:
             self.announce(str(e), log.ERROR)
-            return
+            raise
         except HTTPError as e:
             status = e.code
             reason = e.msg
@@ -193,8 +193,9 @@ class upload(PyPIRCCommand):
             self.announce('Server response (%s): %s' % (status, reason),
                           log.INFO)
         else:
-            self.announce('Upload failed (%s): %s' % (status, reason),
-                          log.ERROR)
+            msg = 'Upload failed (%s): %s' % (status, reason)
+            self.announce(msg, log.ERROR)
+            raise DistutilsError(msg)
         if self.show_response:
             text = self._read_pypi_response(result)
             msg = '\n'.join(('-' * 75, text, '-' * 75))
diff --git a/Lib/distutils/tests/test_upload.py b/Lib/distutils/tests/test_upload.py
index f53eb266ed..0380f97944 100644
--- a/Lib/distutils/tests/test_upload.py
+++ b/Lib/distutils/tests/test_upload.py
@@ -6,6 +6,7 @@ from test.support import run_unittest
 from distutils.command import upload as upload_mod
 from distutils.command.upload import upload
 from distutils.core import Distribution
+from distutils.errors import DistutilsError
 from distutils.log import INFO
 
 from distutils.tests.test_config import PYPIRC, PyPIRCCommandTestCase
@@ -41,13 +42,14 @@ username:me
 
 class FakeOpen(object):
 
-    def __init__(self, url):
+    def __init__(self, url, msg=None, code=None):
         self.url = url
         if not isinstance(url, str):
             self.req = url
         else:
             self.req = None
-        self.msg = 'OK'
+        self.msg = msg or 'OK'
+        self.code = code or 200
 
     def getheader(self, name, default=None):
         return {
@@ -58,7 +60,7 @@ class FakeOpen(object):
         return b'xyzzy'
 
     def getcode(self):
-        return 200
+        return self.code
 
 
 class uploadTestCase(PyPIRCCommandTestCase):
@@ -68,13 +70,15 @@ class uploadTestCase(PyPIRCCommandTestCase):
         self.old_open = upload_mod.urlopen
         upload_mod.urlopen = self._urlopen
         self.last_open = None
+        self.next_msg = None
+        self.next_code = None
 
     def tearDown(self):
         upload_mod.urlopen = self.old_open
         super(uploadTestCase, self).tearDown()
 
     def _urlopen(self, url):
-        self.last_open = FakeOpen(url)
+        self.last_open = FakeOpen(url, msg=self.next_msg, code=self.next_code)
         return self.last_open
 
     def test_finalize_options(self):
@@ -135,6 +139,10 @@ class uploadTestCase(PyPIRCCommandTestCase):
         results = self.get_logs(INFO)
         self.assertIn('xyzzy\n', results[-1])
 
+    def test_upload_fails(self):
+        self.next_msg = "Not Found"
+        self.next_code = 404
+        self.assertRaises(DistutilsError, self.test_upload)
 
 def test_suite():
     return unittest.makeSuite(uploadTestCase)
diff --git a/Misc/ACKS b/Misc/ACKS
index 2f4a3fb804..c42b02b9ef 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -308,6 +308,7 @@ Vincent Delft
 Arnaud Delobelle
 Konrad Delong
 Erik Demaine
+Martin Dengler
 John Dennis
 L. Peter Deutsch
 Roger Dev
diff --git a/Misc/NEWS b/Misc/NEWS
index 6e333a4944..e6c94694df 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -27,6 +27,9 @@ Core and Builtins
 Library
 -------
 
+- Issue #21722: The distutils "upload" command now exits with a non-zero
+  return code when uploading fails.  Patch by Martin Dengler.
+
 - Issue #21723: asyncio.Queue: support any type of number (ex: float) for the
   maximum size. Patch written by Vajrasky Kok.
 
-- 
2.40.0