Repository: cdent/gabbi

Issue:
Python 3.7.6 urllib.parse changes break gabbi tests
Python 3.7.6 was released with a change to how urllib.parse will deal with "localhost:80".

3.7.5:

```python
>>> from urllib import parse
>>> parse.urlparse("foobar.com:999")
ParseResult(scheme='', netloc='', path='foobar.com:999', params='', query='', fragment='')
```

3.7.6

```python
>>> from urllib import parse
>>> parse.urlparse("foobar.com:999")
ParseResult(scheme='foobar.com', netloc='', path='999', params='', query='', fragment='')
```

See this bug: https://bugs.python.org/issue27657 and a related pull request: https://github.com/python/cpython/pull/16839

This breaks [gabbi.utils:host_info_from_target](https://github.com/cdent/gabbi/blob/8e02d4d2df99999e0eb8022f8fe99d7796f52151/gabbi/utils.py#L147-L169). It also apparently break a whole ton of other software out there in the world. This a) seems like the wrong fix, b) if it is indeed correct, is bad for a minor release.

Since gabbi only talks http, it can probably be fixed by branch on whether the target starts with `http` but some discussion is probably worth having before going down that route.

Discussion:

Patch:
diff --git a/gabbi/utils.py b/gabbi/utils.py
index cc0285d..82fcb76 100644
--- a/gabbi/utils.py
+++ b/gabbi/utils.py
@@ -147,26 +147,16 @@ def parse_content_type(content_type, default_charset='utf-8'):
 def host_info_from_target(target, prefix=None):
     """Turn url or host:port and target into test destination."""
     force_ssl = False
+    # If we have a bare host prefix it with a scheme.
+    if '//' not in target and not target.startswith('http'):
+        target = 'http://' + target
+        if prefix:
+            target = target + prefix
     split_url = urlparse.urlparse(target)
 
-    if split_url.scheme:
-        if split_url.scheme == 'https':
-            force_ssl = True
-        return split_url.hostname, split_url.port, split_url.path, force_ssl
-    else:
-        target = target
-        prefix = prefix
-
-    if ':' in target and '[' not in target:
-        host, port = target.rsplit(':', 1)
-    elif ']:' in target:
-        host, port = target.rsplit(':', 1)
-    else:
-        host = target
-        port = None
-    host = host.replace('[', '').replace(']', '')
-
-    return host, port, prefix, force_ssl
+    if split_url.scheme == 'https':
+        force_ssl = True
+    return split_url.hostname, split_url.port, split_url.path, force_ssl
 
 
 def _colorize(color, message):
diff --git a/tox.ini b/tox.ini
index 67670a7..3fec99d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 minversion = 3.1.1
 skipsdist = True
-envlist = py35,py36,py37,py38,pypy3,pep8,limit,failskip,docs,py37-prefix,py37-limit,py37-verbosity,py37-failskip,py35-pytest,py36-pytest,py37-pytest
+envlist = py35,py36,py37,py38,py39,pypy3,pep8,limit,failskip,docs,py37-prefix,py37-limit,py37-verbosity,py37-failskip,py35-pytest,py36-pytest,py37-pytest
 
 [testenv]
 deps = -r{toxinidir}/requirements.txt


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index a0c8abb..395eb0e 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -18,6 +18,8 @@ jobs:
                   toxenv: py37
                 - python: 3.8
                   toxenv: py38
+                - python: 3.9
+                  toxenv: py39
                 - python: 3.5
                   toxenv: py35-pytest
                 - python: 3.6
diff --git a/gabbi/tests/test_utils.py b/gabbi/tests/test_utils.py
index 8f2c030..e623bcc 100644
--- a/gabbi/tests/test_utils.py
+++ b/gabbi/tests/test_utils.py
@@ -202,14 +202,14 @@ class UtilsHostInfoFromTarget(unittest.TestCase):
 
     def _test_hostport(self, url_or_host, expected_host,
                        provided_prefix=None, expected_port=None,
-                       expected_prefix=None, expected_ssl=False):
+                       expected_prefix='', expected_ssl=False):
         host, port, prefix, ssl = utils.host_info_from_target(
             url_or_host, provided_prefix)
 
         # normalize hosts, they are case insensitive
         self.assertEqual(expected_host.lower(), host.lower())
         # port can be a string or int depending on the inputs
-        self.assertEqual(expected_port, port)
+        self.assertEqual(str(expected_port), str(port))
         self.assertEqual(expected_prefix, prefix)
         self.assertEqual(expected_ssl, ssl)