[Python-checkins] gh-74166: Add option to get all errors from socket.create_connection (GH-91586)

iritkatriel webhook-mailer at python.org
Mon Apr 18 18:15:45 EDT 2022


https://github.com/python/cpython/commit/39a54ba63850e081a4a5551a773df5b4d5b1d3cd
commit: 39a54ba63850e081a4a5551a773df5b4d5b1d3cd
branch: main
author: Irit Katriel <1055913+iritkatriel at users.noreply.github.com>
committer: iritkatriel <1055913+iritkatriel at users.noreply.github.com>
date: 2022-04-18T23:15:41+01:00
summary:

gh-74166: Add option to get all errors from socket.create_connection (GH-91586)

files:
A Misc/NEWS.d/next/Library/2022-04-15-20-56-31.gh-issue-74166.70KlvL.rst
M Doc/library/socket.rst
M Doc/whatsnew/3.11.rst
M Lib/socket.py
M Lib/test/test_socket.py

diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst
index f8392ba1254b75..d7a440127ec930 100755
--- a/Doc/library/socket.rst
+++ b/Doc/library/socket.rst
@@ -660,7 +660,7 @@ The following functions all create :ref:`socket objects <socket-objects>`.
       Windows support added.
 
 
-.. function:: create_connection(address[, timeout[, source_address]])
+.. function:: create_connection(address[, timeout[, source_address[, all_errors]]])
 
    Connect to a TCP service listening on the internet *address* (a 2-tuple
    ``(host, port)``), and return the socket object.  This is a higher-level
@@ -679,9 +679,18 @@ The following functions all create :ref:`socket objects <socket-objects>`.
    socket to bind to as its source address before connecting.  If host or port
    are '' or 0 respectively the OS default behavior will be used.
 
+   When a connection cannot be created, an exception is raised. By default,
+   it is the exception from the last address in the list. If *all_errors*
+   is ``True``, it is an :exc:`ExceptionGroup` containing the errors of all
+   attempts.
+
    .. versionchanged:: 3.2
       *source_address* was added.
 
+   .. versionchanged:: 3.11
+      *all_errors* was added.
+
+
 .. function:: create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False)
 
    Convenience function which creates a TCP socket bound to *address* (a 2-tuple
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 27a5a0fc3d6871..baff6873991acb 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -357,6 +357,10 @@ socket
 * Add CAN Socket support for NetBSD.
   (Contributed by Thomas Klausner in :issue:`30512`.)
 
+* :meth:`~socket.create_connection` has an option to raise, in case of
+  failure to connect, an :exc:`ExceptionGroup` containing all errors
+  instead of only raising the last error.
+  (Contributed by Irit Katriel in :issue:`29980`).
 
 sqlite3
 -------
diff --git a/Lib/socket.py b/Lib/socket.py
index ef82c496cb85a4..97362d92f64656 100755
--- a/Lib/socket.py
+++ b/Lib/socket.py
@@ -806,7 +806,7 @@ def getfqdn(name=''):
 _GLOBAL_DEFAULT_TIMEOUT = object()
 
 def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
-                      source_address=None):
+                      source_address=None, all_errors=False):
     """Connect to *address* and return the socket object.
 
     Convenience function.  Connect to *address* (a 2-tuple ``(host,
@@ -816,11 +816,13 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
     global default timeout setting returned by :func:`getdefaulttimeout`
     is used.  If *source_address* is set it must be a tuple of (host, port)
     for the socket to bind as a source address before making the connection.
-    A host of '' or port 0 tells the OS to use the default.
+    A host of '' or port 0 tells the OS to use the default. When a connection
+    cannot be created, raises the last error if *all_errors* is False,
+    and an ExceptionGroup of all errors if *all_errors* is True.
     """
 
     host, port = address
-    err = None
+    exceptions = []
     for res in getaddrinfo(host, port, 0, SOCK_STREAM):
         af, socktype, proto, canonname, sa = res
         sock = None
@@ -832,20 +834,24 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
                 sock.bind(source_address)
             sock.connect(sa)
             # Break explicitly a reference cycle
-            err = None
+            exceptions.clear()
             return sock
 
-        except error as _:
-            err = _
+        except error as exc:
+            if not all_errors:
+                exceptions.clear()  # raise only the last error
+            exceptions.append(exc)
             if sock is not None:
                 sock.close()
 
-    if err is not None:
+    if len(exceptions):
         try:
-            raise err
+            if not all_errors:
+                raise exceptions[0]
+            raise ExceptionGroup("create_connection failed", exceptions)
         finally:
             # Break explicitly a reference cycle
-            err = None
+            exceptions = None
     else:
         raise error("getaddrinfo returns an empty list")
 
diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py
index 97cc6260c3c2a5..613363722cf028 100755
--- a/Lib/test/test_socket.py
+++ b/Lib/test/test_socket.py
@@ -5177,6 +5177,24 @@ def test_create_connection(self):
         expected_errnos = socket_helper.get_socket_conn_refused_errs()
         self.assertIn(cm.exception.errno, expected_errnos)
 
+    def test_create_connection_all_errors(self):
+        port = socket_helper.find_unused_port()
+        try:
+            socket.create_connection((HOST, port), all_errors=True)
+        except ExceptionGroup as e:
+            eg = e
+        else:
+            self.fail('expected connection to fail')
+
+        self.assertIsInstance(eg, ExceptionGroup)
+        for e in eg.exceptions:
+            self.assertIsInstance(e, OSError)
+
+        addresses = socket.getaddrinfo(
+            'localhost', port, 0, socket.SOCK_STREAM)
+        # assert that we got an exception for each address
+        self.assertEqual(len(addresses), len(eg.exceptions))
+
     def test_create_connection_timeout(self):
         # Issue #9792: create_connection() should not recast timeout errors
         # as generic socket errors.
diff --git a/Misc/NEWS.d/next/Library/2022-04-15-20-56-31.gh-issue-74166.70KlvL.rst b/Misc/NEWS.d/next/Library/2022-04-15-20-56-31.gh-issue-74166.70KlvL.rst
new file mode 100644
index 00000000000000..ddd4eb77e7c746
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-04-15-20-56-31.gh-issue-74166.70KlvL.rst
@@ -0,0 +1 @@
+Add option to raise all errors from :meth:`~socket.create_connection` in an :exc:`ExceptionGroup` when it fails to create a connection. The default remains to raise only the last error that had occurred when multiple addresses were tried.



More information about the Python-checkins mailing list