Repository: docker/docker-py

Issue:
DeprecationWarning: urllib.parse.splitnport() is deprecated as of 3.8, use urllib.parse.urlparse() instead
https://github.com/docker/docker-py/blob/9a24df5cdd03c679dc929735e4766e19ff1c2bdb/docker/utils/utils.py#L23
copy deprecated splitnport from stdlib Fixes #2566

Avoid deprecated splitnport function.
I saw three ways forward to avoid the use of the deprecated `splitnport` function.

* Just use the parsed results' hostname. I couldn't track down the actual reason why the stripping of brackets is **actually** problematic. So I figured that would be harder to get merged.
* Inline the use of `splitnport`. I see https://github.com/docker/docker-py/pull/2567, but also that it's been unchanged since 2020.
* Do something else. the behavior you're getting from `splitnport` is essentially just chopping off the port part to find the hostname. But the rest of the logic seems unnecessary because you've already run `parse` and have `parsed_url.port` handy. So I opted to just directly perform the behavior of chopping off the port instead.

I have no opinion on which of the three options are chosen, I'm just aiming to stop this warning on my own code. But figured that if copying the whole of `splitnport` was the reason for no action, that this might be preferable; or else might cause https://github.com/docker/docker-py/pull/2567 to be merged


Discussion:
Are we ever going to see a hotfix for this? Some of us have the "bad" habit of running CI/CD pipelines with warnings-as-errors.

This seems to be addressed in #2944 - can we get this merged into next release @aiordache @ulyssessouza ?
@ulyssessouza PTAL
and fwiw, the current failure seems to be the same on that's on master

Resolving patch:
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -1,4 +1,5 @@
 import base64
+import collections
 import json
 import os
 import os.path
@@ -8,15 +9,20 @@
 from distutils.version import StrictVersion
 
 from .. import errors
-from .. import tls
 from ..constants import DEFAULT_HTTP_HOST
 from ..constants import DEFAULT_UNIX_SOCKET
 from ..constants import DEFAULT_NPIPE
 from ..constants import BYTE_UNITS
+from ..tls import TLSConfig
 
-from urllib.parse import splitnport, urlparse
+from urllib.parse import urlparse, urlunparse
 
 
+URLComponents = collections.namedtuple(
+    'URLComponents',
+    'scheme netloc url params query fragment',
+)
+
 def create_ipam_pool(*args, **kwargs):
     raise errors.DeprecatedMethod(
         'utils.create_ipam_pool has been removed. Please use a '
@@ -201,10 +207,6 @@ def parse_repository_tag(repo_name):
 
 
 def parse_host(addr, is_win32=False, tls=False):
-    path = ''
-    port = None
-    host = None
-
     # Sensible defaults
     if not addr and is_win32:
         return DEFAULT_NPIPE
@@ -263,20 +265,20 @@ def parse_host(addr, is_win32=False, tls=False):
             # to be valid and equivalent to unix:///path
             path = '/'.join((parsed_url.hostname, path))
 
+    netloc = parsed_url.netloc
     if proto in ('tcp', 'ssh'):
-        # parsed_url.hostname strips brackets from IPv6 addresses,
-        # which can be problematic hence our use of splitnport() instead.
-        host, port = splitnport(parsed_url.netloc)
-        if port is None or port < 0:
+        port = parsed_url.port or 0
+        if port <= 0:
             if proto != 'ssh':
                 raise errors.DockerException(
                     'Invalid bind address format: port is required:'
                     ' {}'.format(addr)
                 )
             port = 22
+            netloc = f'{parsed_url.netloc}:{port}'
 
-        if not host:
-            host = DEFAULT_HTTP_HOST
+        if not parsed_url.hostname:
+            netloc = f'{DEFAULT_HTTP_HOST}:{port}'
 
     # Rewrite schemes to fit library internals (requests adapters)
     if proto == 'tcp':
@@ -286,7 +288,15 @@ def parse_host(addr, is_win32=False, tls=False):
 
     if proto in ('http+unix', 'npipe'):
         return f"{proto}://{path}".rstrip('/')
-    return f'{proto}://{host}:{port}{path}'.rstrip('/')
+
+    return urlunparse(URLComponents(
+        scheme=proto,
+        netloc=netloc,
+        url=path,
+        params='',
+        query='',
+        fragment='',
+    )).rstrip('/')
 
 
 def parse_devices(devices):
@@ -351,7 +361,7 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
         # so if it's not set already then set it to false.
         assert_hostname = False
 
-    params['tls'] = tls.TLSConfig(
+    params['tls'] = TLSConfig(
         client_cert=(os.path.join(cert_path, 'cert.pem'),
                      os.path.join(cert_path, 'key.pem')),
         ca_cert=os.path.join(cert_path, 'ca.pem'),


Test patch:
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -296,17 +296,24 @@ def test_parse_host(self):
             '[fd12::82d1]:2375/docker/engine': (
                 'http://[fd12::82d1]:2375/docker/engine'
             ),
+            'ssh://[fd12::82d1]': 'ssh://[fd12::82d1]:22',
+            'ssh://user@[fd12::82d1]:8765': 'ssh://user@[fd12::82d1]:8765',
             'ssh://': 'ssh://127.0.0.1:22',
             'ssh://user@localhost:22': 'ssh://user@localhost:22',
             'ssh://user@remote': 'ssh://user@remote:22',
         }
 
         for host in invalid_hosts:
-            with pytest.raises(DockerException):
+            msg = f'Should have failed to parse invalid host: {host}'
+            with self.assertRaises(DockerException, msg=msg):
                 parse_host(host, None)
 
         for host, expected in valid_hosts.items():
-            assert parse_host(host, None) == expected
+            self.assertEqual(
+                parse_host(host, None),
+                expected,
+                msg=f'Failed to parse valid host: {host}',
+            )
 
     def test_parse_host_empty_value(self):
         unix_socket = 'http+unix:///var/run/docker.sock'