Repository: encode/starlette

Issue:
scope["server"] can be None
From https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope

> server: A two-item iterable of [host, port], where host is the listening address for this server as a unicode string, and port is the integer listening port. Optional, defaults to None.

https://github.com/encode/starlette/blob/master/starlette/datastructures.py#L11 doesn't handle that option, it assumes scope["server"] is always a two-pair



Discussion:

Patch:
diff --git a/starlette/datastructures.py b/starlette/datastructures.py
index 558c8a9..2705fd3 100644
--- a/starlette/datastructures.py
+++ b/starlette/datastructures.py
@@ -7,16 +7,20 @@ class URL:
     def __init__(self, url: str = "", scope: Scope = None) -> None:
         if scope is not None:
             assert not url, 'Cannot set both "url" and "scope".'
-            scheme = scope["scheme"]
-            host, port = scope["server"]
+            scheme = scope.get("scheme", "http")
+            server = scope.get("server", None)
             path = scope.get("root_path", "") + scope["path"]
             query_string = scope["query_string"]
 
-            default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
-            if port == default_port:
-                url = "%s://%s%s" % (scheme, host, path)
+            if server is None:
+                url = path
             else:
-                url = "%s://%s:%s%s" % (scheme, host, port, path)
+                host, port = server
+                default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
+                if port == default_port:
+                    url = "%s://%s%s" % (scheme, host, path)
+                else:
+                    url = "%s://%s:%s%s" % (scheme, host, port, path)
 
             if query_string:
                 url += "?" + unquote(query_string.decode())
@@ -85,6 +89,9 @@ class URL:
     def __str__(self):
         return self._url
 
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, repr(self._url))
+
 
 # Type annotations for valid `__init__` values to QueryParams and Headers.
 StrPairs = typing.Sequence[typing.Tuple[str, str]]


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
index d6aa62f..4312e1c 100644
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -27,6 +27,23 @@ def test_url():
     assert new.hostname == "example.com"
 
 
+def test_url_from_scope():
+    u = URL(scope={"path": "/path/to/somewhere", "query_string": b"abc=123"})
+    assert u == "/path/to/somewhere?abc=123"
+    assert repr(u) == "URL('/path/to/somewhere?abc=123')"
+
+    u = URL(
+        scope={
+            "scheme": "https",
+            "server": ("example.org", 123),
+            "path": "/path/to/somewhere",
+            "query_string": b"abc=123",
+        }
+    )
+    assert u == "https://example.org:123/path/to/somewhere?abc=123"
+    assert repr(u) == "URL('https://example.org:123/path/to/somewhere?abc=123')"
+
+
 def test_headers():
     h = Headers([(b"a", b"123"), (b"a", b"456"), (b"b", b"789")])
     assert "a" in h