Skip to content

Commit 0ee0104

Browse files
authored
Allow empty hosts for schemes that do not require them (#1136)
1 parent 1e969ab commit 0ee0104

File tree

3 files changed

+52
-16
lines changed

3 files changed

+52
-16
lines changed

CHANGES/1136.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow empty host for URL schemes other than the special schemes listed in the WHATWG URL spec -- by :user:`bdraco`.

tests/test_url_parsing.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,19 +210,14 @@ def test_no_path(self):
210210
assert u.query_string == ""
211211
assert u.fragment == ""
212212

213-
@pytest.mark.xfail(
214-
# FIXME: remove "no cover" pragmas upon xfail marker deletion
215-
reason="https://github.com/aio-libs/yarl/issues/821",
216-
raises=ValueError,
217-
)
218213
def test_no_host(self):
219-
u = URL("//:80")
220-
assert u.scheme == "" # pragma: no cover
221-
assert u.host == "" # pragma: no cover
222-
assert u.port == 80 # pragma: no cover
223-
assert u.path == "/" # pragma: no cover
224-
assert u.query_string == "" # pragma: no cover
225-
assert u.fragment == "" # pragma: no cover
214+
u = URL("//:77")
215+
assert u.scheme == ""
216+
assert u.host == ""
217+
assert u.port == 77
218+
assert u.path == "/"
219+
assert u.query_string == ""
220+
assert u.fragment == ""
226221

227222
def test_double_port(self):
228223
with pytest.raises(ValueError):
@@ -457,9 +452,19 @@ def test_complex_frag(self):
457452

458453

459454
class TestStripEmptyParts:
460-
def test_all_empty(self):
455+
def test_all_empty_http(self):
461456
with pytest.raises(ValueError):
462-
URL("//@:?#")
457+
URL("http://@:?#")
458+
459+
def test_all_empty(self):
460+
u = URL("//@:?#")
461+
assert u.scheme == ""
462+
assert u.user is None
463+
assert u.password is None
464+
assert u.host == ""
465+
assert u.path == ""
466+
assert u.query_string == ""
467+
assert u.fragment == ""
463468

464469
def test_path_only(self):
465470
u = URL("///path")
@@ -580,3 +585,22 @@ def test_empty_path(self):
580585
assert u.path == ""
581586
assert u.query_string == ""
582587
assert u.fragment == ""
588+
589+
590+
@pytest.mark.parametrize(
591+
("scheme"),
592+
[
593+
("http"),
594+
("https"),
595+
("ws"),
596+
("wss"),
597+
("ftp"),
598+
],
599+
)
600+
def test_schemes_that_require_host(scheme: str) -> None:
601+
"""Verify that schemes that require a host raise with empty host."""
602+
expect = (
603+
"Invalid URL: host is required for " f"absolute urls with the {scheme} scheme"
604+
)
605+
with pytest.raises(ValueError, match=expect):
606+
URL(f"{scheme}://:1")

yarl/_url.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@
3434
from ._helpers import cached_property
3535
from ._quoting import _Quoter, _Unquoter
3636

37-
DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443}
37+
DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443, "ftp": 21}
3838
USES_AUTHORITY = frozenset(uses_netloc)
3939
USES_RELATIVE = frozenset(uses_relative)
4040

41+
# Special schemes https://url.spec.whatwg.org/#special-scheme
42+
# are not allowed to have an empty host https://url.spec.whatwg.org/#url-representation
43+
SCHEME_REQUIRES_HOST = frozenset(("http", "https", "ws", "wss", "ftp"))
44+
4145
sentinel = object()
4246

4347
SimpleQuery = Union[str, int, float]
@@ -255,7 +259,14 @@ def __new__(
255259
else:
256260
username, password, host, port = cls._split_netloc(val[1])
257261
if host is None:
258-
raise ValueError("Invalid URL: host is required for absolute urls")
262+
if scheme in SCHEME_REQUIRES_HOST:
263+
msg = (
264+
"Invalid URL: host is required for "
265+
f"absolute urls with the {scheme} scheme"
266+
)
267+
raise ValueError(msg)
268+
else:
269+
host = ""
259270
host = cls._encode_host(host)
260271
raw_user = None if username is None else cls._REQUOTER(username)
261272
raw_password = None if password is None else cls._REQUOTER(password)

0 commit comments

Comments
 (0)