Repository: encode/uvicorn

Issue:
Uvicorn fails to resolve host names in version 0.12.2
### Checklist

<!-- Please make sure you check all these items before submitting your bug report. -->

- [x] The bug is reproducible against the latest release and/or `master`.
- [x] There are no similar issues or pull requests to fix it yet.

### Describe the bug

Since version 0.12.2 it is not possible to use host names (including localhost) with `uvicorn.run` 

### To reproduce

```
import uvicorn

async def app(scope, receive, send):
    assert scope['type'] == 'http'

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

uvicorn.run(app, host='localhost', port=8083)
```

### Expected behavior

The application should start

### Actual behavior

Uvicorn throws an error

### Debugging material

```
.py thon/debugpy/launcher 61232 -- /Users/paulafernandez/Sandbox/test/test/start 
INFO:     Started server process [43171]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
Traceback (most recent call last):
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Users/paulafernandez/.vscode/extensions/ms-python.python-2020.9.114305/pythonFiles/lib/python/debugpy/__main__.py", line 45, in <module>
    cli.main()
  File "/Users/paulafernandez/.vscode/extensions/ms-python.python-2020.9.114305/pythonFiles/lib/python/debugpy/../debugpy/server/cli.py", line 430, in main
    run()
  File "/Users/paulafernandez/.vscode/extensions/ms-python.python-2020.9.114305/pythonFiles/lib/python/debugpy/../debugpy/server/cli.py", line 267, in run_file
    runpy.run_path(options.target, run_name=compat.force_str("__main__"))
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 265, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 97, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Users/paulafernandez/Sandbox/test/test/start.py", line 18, in <module>
    uvicorn.run(
  File "/Users/paulafernandez/Library/Caches/pypoetry/virtualenvs/test-8kdwd4GS-py3.8/lib/python3.8/site-packages/uvicorn/main.py", line 391, in run
    server.run()
  File "/Users/paulafernandez/Library/Caches/pypoetry/virtualenvs/test-8kdwd4GS-py3.8/lib/python3.8/site-packages/uvicorn/main.py", line 419, in run
    loop.run_until_complete(self.serve(sockets=sockets))
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "/Users/paulafernandez/Library/Caches/pypoetry/virtualenvs/test-8kdwd4GS-py3.8/lib/python3.8/site-packages/uvicorn/main.py", line 436, in serve
    await self.startup(sockets=sockets)
  File "/Users/paulafernandez/Library/Caches/pypoetry/virtualenvs/test-8kdwd4GS-py3.8/lib/python3.8/site-packages/uvicorn/main.py", line 526, in startup
    if isinstance(ip_address(config.host), IPv6Address):
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ipaddress.py", line 53, in ip_address
    raise ValueError('%r does not appear to be an IPv4 or IPv6 address' %
ValueError: 'localhost' does not appear to be an IPv4 or IPv6 address
```

### Environment

- OS: macOS Catalina
- Python: 3.8.5
- Uvicorn version: 0.12.2
- The exact command you're running uvicorn with: `uvicorn.run(app, host='localhost', port=8083)`

### Additional context

The problem isn't present in earlier versions, including 0.12.1


Discussion:

Resolving patch:
diff --git a/uvicorn/config.py b/uvicorn/config.py
--- a/uvicorn/config.py
+++ b/uvicorn/config.py
@@ -7,7 +7,6 @@
 import socket
 import ssl
 import sys
-from enum import Enum
 from typing import List, Tuple
 
 import click
@@ -116,15 +115,8 @@ def create_ssl_context(
     return ctx
 
 
-class _IPKind(Enum):
-    IPv4 = "IPv4"
-    IPv6 = "IPv6"
-
-
-def _get_server_start_message(
-    host_ip_version: _IPKind = _IPKind.IPv4,
-) -> Tuple[str, str]:
-    if host_ip_version is _IPKind.IPv6:
+def _get_server_start_message(is_ipv6_message: bool = False) -> Tuple[str, str]:
+    if is_ipv6_message:
         ip_repr = "%s://[%s]:%d"
     else:
         ip_repr = "%s://%s:%d"
@@ -376,9 +368,9 @@ def bind_socket(self):
         sock.set_inheritable(True)
 
         if family == socket.AddressFamily.AF_INET6:
-            message, color_message = _get_server_start_message(_IPKind.IPv6)
+            message, color_message = _get_server_start_message(is_ipv6_message=True)
         else:
-            message, color_message = _get_server_start_message(_IPKind.IPv4)
+            message, color_message = _get_server_start_message()
         protocol_name = "https" if self.is_ssl else "http"
         logger.info(
             message,
diff --git a/uvicorn/main.py b/uvicorn/main.py
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -10,7 +10,7 @@
 import time
 import typing
 from email.utils import formatdate
-from ipaddress import IPv6Address, ip_address
+from ipaddress import IPv4Address, IPv6Address, ip_address
 
 import click
 
@@ -26,7 +26,6 @@
     WS_PROTOCOLS,
     Config,
     _get_server_start_message,
-    _IPKind,
 )
 from uvicorn.supervisors import ChangeReload, Multiprocess
 
@@ -523,10 +522,16 @@ def _share_socket(sock: socket) -> socket:
             if port == 0:
                 port = server.sockets[0].getsockname()[1]
             protocol_name = "https" if config.ssl else "http"
-            if isinstance(ip_address(config.host), IPv6Address):
-                message, color_message = _get_server_start_message(_IPKind.IPv6)
-            else:
-                message, color_message = _get_server_start_message(_IPKind.IPv4)
+            try:
+                addr = ip_address(config.host)
+                if isinstance(addr, IPv6Address):
+                    message, color_message = _get_server_start_message(
+                        is_ipv6_message=True
+                    )
+                elif isinstance(addr, IPv4Address):
+                    message, color_message = _get_server_start_message()
+            except ValueError:
+                message, color_message = _get_server_start_message()
 
             logger.info(
                 message,


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -33,6 +33,31 @@ def install_signal_handlers(self):
     thread.join()
 
 
+def test_run_hostname():
+    class App:
+        def __init__(self, scope):
+            if scope["type"] != "http":
+                raise Exception()
+
+        async def __call__(self, receive, send):
+            await send({"type": "http.response.start", "status": 204, "headers": []})
+            await send({"type": "http.response.body", "body": b"", "more_body": False})
+
+    class CustomServer(Server):
+        def install_signal_handlers(self):
+            pass
+
+    config = Config(app=App, host="localhost", loop="asyncio", limit_max_requests=1)
+    server = CustomServer(config=config)
+    thread = threading.Thread(target=server.run)
+    thread.start()
+    while not server.started:
+        time.sleep(0.01)
+    response = requests.get("http://localhost:8000")
+    assert response.status_code == 204
+    thread.join()
+
+
 def test_run_multiprocess():
     class App:
         def __init__(self, scope):