from enum import Enum from urllib.parse import SplitResult, quote, unquote import pytest from yarl import URL def test_inheritance(): with pytest.raises(TypeError) as ctx: class MyURL(URL): pass assert ( "Inheriting a class " ".MyURL'> " "from URL is forbidden" == str(ctx.value) ) def test_str_subclass(): class S(str): pass assert str(URL(S("http://example.com"))) == "http://example.com" def test_is(): u1 = URL("http://example.com") u2 = URL(u1) assert u1 is u2 def test_bool(): assert URL("http://example.com") assert not URL() assert not URL("") def test_absolute_url_without_host(): with pytest.raises(ValueError): URL("http://:8080/") def test_url_is_not_str(): url = URL("http://example.com") assert not isinstance(url, str) def test_str(): url = URL("http://example.com:8888/path/to?a=1&b=2") assert str(url) == "http://example.com:8888/path/to?a=1&b=2" def test_repr(): url = URL("http://example.com") assert "URL('http://example.com')" == repr(url) def test_origin(): url = URL("http://user:password@example.com:8888/path/to?a=1&b=2") assert URL("http://example.com:8888") == url.origin() def test_origin_with_no_auth(): url = URL("http://example.com:8888/path/to?a=1&b=2") assert URL("http://example.com:8888") == url.origin() def test_origin_nonascii(): url = URL("http://user:password@оун-упа.укр:8888/path/to?a=1&b=2") assert str(url.origin()) == "http://xn----8sb1bdhvc.xn--j1amh:8888" def test_origin_ipv6(): url = URL("http://user:password@[::1]:8888/path/to?a=1&b=2") assert str(url.origin()) == "http://[::1]:8888" def test_origin_not_absolute_url(): url = URL("/path/to?a=1&b=2") with pytest.raises(ValueError): url.origin() def test_origin_no_scheme(): url = URL("//user:password@example.com:8888/path/to?a=1&b=2") with pytest.raises(ValueError): url.origin() def test_drop_dots(): u = URL("http://example.com/path/../to") assert str(u) == "http://example.com/to" def test_abs_cmp(): assert URL("http://example.com:8888") == URL("http://example.com:8888") assert URL("http://example.com:8888/") == URL("http://example.com:8888/") assert URL("http://example.com:8888/") == URL("http://example.com:8888") assert URL("http://example.com:8888") == URL("http://example.com:8888/") def test_abs_hash(): url = URL("http://example.com:8888") url_trailing = URL("http://example.com:8888/") assert hash(url) == hash(url_trailing) # properties def test_scheme(): url = URL("http://example.com") assert "http" == url.scheme def test_raw_user(): url = URL("http://user@example.com") assert "user" == url.raw_user assert url.raw_user == url._val.username def test_raw_user_non_ascii(): url = URL("http://бажан@example.com") assert "%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD" == url.raw_user assert url.raw_user == url._val.username def test_no_user(): url = URL("http://example.com") assert url.user is None def test_user_non_ascii(): url = URL("http://бажан@example.com") assert "бажан" == url.user def test_raw_password(): url = URL("http://user:password@example.com") assert "password" == url.raw_password assert url.raw_password == url._val.password def test_raw_password_non_ascii(): url = URL("http://user:пароль@example.com") assert "%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" == url.raw_password assert url.raw_password == url._val.password def test_password_non_ascii(): url = URL("http://user:пароль@example.com") assert "пароль" == url.password def test_password_without_user(): url = URL("http://:password@example.com") assert url.user is None assert "password" == url.password def test_empty_password_without_user(): url = URL("http://:@example.com") assert url.user is None assert url.password == "" assert url.raw_password == "" assert url.raw_password == url._val.password def test_user_empty_password(): url = URL("http://user:@example.com") assert "user" == url.user assert "" == url.password def test_raw_host(): url = URL("http://example.com") assert "example.com" == url.raw_host assert url.raw_host == url._val.hostname @pytest.mark.parametrize( ("host"), [ ("example.com"), ("[::1]"), ("xn--gnter-4ya.com"), ], ) def test_host_subcomponent(host: str): url = URL(f"http://{host}") assert url.host_subcomponent == host def test_host_subcomponent_return_idna_encoded_host(): url = URL("http://оун-упа.укр") assert url.host_subcomponent == "xn----8sb1bdhvc.xn--j1amh" def test_raw_host_non_ascii(): url = URL("http://оун-упа.укр") assert "xn----8sb1bdhvc.xn--j1amh" == url.raw_host assert url.raw_host == url._val.hostname def test_host_non_ascii(): url = URL("http://оун-упа.укр") assert "оун-упа.укр" == url.host def test_localhost(): url = URL("http://[::1]") assert "::1" == url.host def test_host_with_underscore(): url = URL("http://abc_def.com") assert "abc_def.com" == url.host def test_raw_host_when_port_is_specified(): url = URL("http://example.com:8888") assert "example.com" == url.raw_host assert url.raw_host == url._val.hostname def test_raw_host_from_str_with_ipv4(): url = URL("http://127.0.0.1:80") assert url.raw_host == "127.0.0.1" assert url.raw_host == url._val.hostname def test_raw_host_from_str_with_ipv6(): url = URL("http://[::1]:80") assert url.raw_host == "::1" assert url.raw_host == url._val.hostname def test_authority_full() -> None: url = URL("http://user:passwd@host.com:8080/path") assert url.raw_authority == "user:passwd@host.com:8080" assert url.authority == "user:passwd@host.com:8080" def test_authority_short() -> None: url = URL("http://host.com/path") assert url.raw_authority == "host.com" def test_authority_full_nonasci() -> None: url = URL("http://степан:пароль@слава.укр:8080/path") assert url.raw_authority == ( "%D1%81%D1%82%D0%B5%D0%BF%D0%B0%D0%BD:" "%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@" "xn--80aaf8a3a.xn--j1amh:8080" ) assert url.authority == "степан:пароль@слава.укр:8080" def test_authority_unknown_scheme() -> None: v = "scheme://user:password@example.com:43/path/to?a=1&b=2" url = URL(v) assert str(url) == v def test_lowercase(): url = URL("http://gitHUB.com") assert url.raw_host == "github.com" assert url.host == url.raw_host assert url.raw_host == url._val.hostname def test_lowercase_nonascii(): url = URL("http://Слава.Укр") assert url.raw_host == "xn--80aaf8a3a.xn--j1amh" assert url.raw_host == url._val.hostname assert url.host == "слава.укр" def test_compressed_ipv6(): url = URL("http://[1DEC:0:0:0::1]") assert url.raw_host == "1dec::1" assert url.host == url.raw_host assert url.raw_host == url._val.hostname def test_ipv4_zone(): # I'm unsure if it is correct. url = URL("http://1.2.3.4%тест%42:123") assert url.raw_host == "1.2.3.4%тест%42" assert url.host == url.raw_host assert url.raw_host == url._val.hostname def test_port_for_explicit_port(): url = URL("http://example.com:8888") assert 8888 == url.port assert url.explicit_port == url._val.port def test_port_for_implicit_port(): url = URL("http://example.com") assert 80 == url.port assert url.explicit_port == url._val.port def test_port_for_relative_url(): url = URL("/path/to") assert url.port is None assert url.explicit_port is None def test_port_for_unknown_scheme(): url = URL("unknown://example.com") assert url.port is None assert url.explicit_port is None def test_explicit_port_for_explicit_port(): url = URL("http://example.com:8888") assert 8888 == url.explicit_port assert url.explicit_port == url._val.port def test_explicit_port_for_implicit_port(): url = URL("http://example.com") assert url.explicit_port is None assert url.explicit_port == url._val.port def test_explicit_port_for_relative_url(): url = URL("/path/to") assert url.explicit_port is None assert url.explicit_port == url._val.port def test_explicit_port_for_unknown_scheme(): url = URL("unknown://example.com") assert url.explicit_port is None assert url.explicit_port == url._val.port def test_raw_path_string_empty(): url = URL("http://example.com") assert "/" == url.raw_path def test_raw_path(): url = URL("http://example.com/path/to") assert "/path/to" == url.raw_path def test_raw_path_non_ascii(): url = URL("http://example.com/шлях/сюди") assert "/%D1%88%D0%BB%D1%8F%D1%85/%D1%81%D1%8E%D0%B4%D0%B8" == url.raw_path def test_path_non_ascii(): url = URL("http://example.com/шлях/сюди") assert "/шлях/сюди" == url.path def test_path_with_spaces(): url = URL("http://example.com/a b/test") assert "/a b/test" == url.path url = URL("http://example.com/a b") assert "/a b" == url.path def test_path_with_2F(): """Path should decode %2F.""" url = URL("http://example.com/foo/bar%2fbaz") assert url.path == "/foo/bar/baz" def test_path_safe_with_2F(): """Path safe should not decode %2F, otherwise it may look like a path separator.""" url = URL("http://example.com/foo/bar%2fbaz") assert url.path_safe == "/foo/bar%2Fbaz" def test_path_safe_with_25(): """Path safe should not decode %25, otherwise it is prone to double unquoting.""" url = URL("http://example.com/foo/bar%252Fbaz") assert url.path_safe == "/foo/bar%252Fbaz" unquoted = url.path_safe.replace("%2F", "/").replace("%25", "%") assert unquoted == "/foo/bar%2Fbaz" @pytest.mark.parametrize( "original_path", [ "m+@bar/baz", "m%2B@bar/baz", "m%252B@bar/baz", "m%2F@bar/baz", ], ) def test_path_safe_only_round_trips(original_path: str) -> None: """Path safe can round trip with documented decode method.""" encoded_once = quote(original_path, safe="") encoded_twice = quote(encoded_once, safe="") url = URL(f"http://example.com/{encoded_twice}") unquoted = url.path_safe.replace("%2F", "/").replace("%25", "%") assert unquoted == f"/{encoded_once}" assert unquote(unquoted) == f"/{original_path}" def test_raw_path_for_empty_url(): url = URL() assert "" == url.raw_path def test_raw_path_for_colon_and_at(): url = URL("http://example.com/path:abc@123") assert url.raw_path == "/path:abc@123" def test_raw_query_string(): url = URL("http://example.com?a=1&b=2") assert url.raw_query_string == "a=1&b=2" def test_raw_query_string_non_ascii(): url = URL("http://example.com?б=в&ю=к") assert url.raw_query_string == "%D0%B1=%D0%B2&%D1%8E=%D0%BA" def test_query_string_non_ascii(): url = URL("http://example.com?б=в&ю=к") assert url.query_string == "б=в&ю=к" def test_path_qs(): url = URL("http://example.com/") assert url.path_qs == "/" url = URL("http://example.com/?б=в&ю=к") assert url.path_qs == "/?б=в&ю=к" url = URL("http://example.com/path?б=в&ю=к") assert url.path_qs == "/path?б=в&ю=к" def test_raw_path_qs(): url = URL("http://example.com/") assert url.raw_path_qs == "/" url = URL("http://example.com/?б=в&ю=к") assert url.raw_path_qs == "/?%D0%B1=%D0%B2&%D1%8E=%D0%BA" url = URL("http://example.com/path?б=в&ю=к") assert url.raw_path_qs == "/path?%D0%B1=%D0%B2&%D1%8E=%D0%BA" url = URL("http://example.com/шлях?a=1&b=2") assert url.raw_path_qs == "/%D1%88%D0%BB%D1%8F%D1%85?a=1&b=2" def test_query_string_spaces(): url = URL("http://example.com?a+b=c+d&e=f+g") assert url.query_string == "a b=c d&e=f g" # raw fragment def test_raw_fragment_empty(): url = URL("http://example.com") assert "" == url.raw_fragment def test_raw_fragment(): url = URL("http://example.com/path#anchor") assert "anchor" == url.raw_fragment def test_raw_fragment_non_ascii(): url = URL("http://example.com/path#якір") assert "%D1%8F%D0%BA%D1%96%D1%80" == url.raw_fragment def test_raw_fragment_safe(): url = URL("http://example.com/path#a?b/c:d@e") assert "a?b/c:d@e" == url.raw_fragment def test_fragment_non_ascii(): url = URL("http://example.com/path#якір") assert "якір" == url.fragment def test_raw_parts_empty(): url = URL("http://example.com") assert ("/",) == url.raw_parts def test_raw_parts(): url = URL("http://example.com/path/to") assert ("/", "path", "to") == url.raw_parts def test_raw_parts_without_path(): url = URL("http://example.com") assert ("/",) == url.raw_parts def test_raw_path_parts_with_2F_in_path(): url = URL("http://example.com/path%2Fto/three") assert ("/", "path%2Fto", "three") == url.raw_parts def test_raw_path_parts_with_2f_in_path(): url = URL("http://example.com/path%2fto/three") assert ("/", "path%2Fto", "three") == url.raw_parts def test_raw_parts_for_relative_path(): url = URL("path/to") assert ("path", "to") == url.raw_parts def test_raw_parts_for_relative_path_starting_from_slash(): url = URL("/path/to") assert ("/", "path", "to") == url.raw_parts def test_raw_parts_for_relative_double_path(): url = URL("path/to") assert ("path", "to") == url.raw_parts def test_parts_for_empty_url(): url = URL() assert ("",) == url.raw_parts def test_raw_parts_non_ascii(): url = URL("http://example.com/шлях/сюди") assert ( "/", "%D1%88%D0%BB%D1%8F%D1%85", "%D1%81%D1%8E%D0%B4%D0%B8", ) == url.raw_parts def test_parts_non_ascii(): url = URL("http://example.com/шлях/сюди") assert ("/", "шлях", "сюди") == url.parts def test_name_for_empty_url(): url = URL() assert "" == url.raw_name def test_raw_name(): url = URL("http://example.com/path/to#frag") assert "to" == url.raw_name def test_raw_name_root(): url = URL("http://example.com/#frag") assert "" == url.raw_name def test_raw_name_root2(): url = URL("http://example.com") assert "" == url.raw_name def test_raw_name_root3(): url = URL("http://example.com/") assert "" == url.raw_name def test_relative_raw_name(): url = URL("path/to") assert "to" == url.raw_name def test_relative_raw_name_starting_from_slash(): url = URL("/path/to") assert "to" == url.raw_name def test_relative_raw_name_slash(): url = URL("/") assert "" == url.raw_name def test_name_non_ascii(): url = URL("http://example.com/шлях") assert url.name == "шлях" def test_suffix_for_empty_url(): url = URL() assert "" == url.raw_suffix def test_raw_suffix(): url = URL("http://example.com/path/to.txt#frag") assert ".txt" == url.raw_suffix def test_raw_suffix_root(): url = URL("http://example.com/#frag") assert "" == url.raw_suffix def test_raw_suffix_root2(): url = URL("http://example.com") assert "" == url.raw_suffix def test_raw_suffix_root3(): url = URL("http://example.com/") assert "" == url.raw_suffix def test_relative_raw_suffix(): url = URL("path/to") assert "" == url.raw_suffix def test_relative_raw_suffix_starting_from_slash(): url = URL("/path/to") assert "" == url.raw_suffix def test_relative_raw_suffix_dot(): url = URL(".") assert "" == url.raw_suffix def test_suffix_non_ascii(): url = URL("http://example.com/шлях.суфікс") assert url.suffix == ".суфікс" def test_suffix_with_empty_name(): url = URL("http://example.com/.hgrc") assert "" == url.raw_suffix def test_suffix_multi_dot(): url = URL("http://example.com/doc.tar.gz") assert ".gz" == url.raw_suffix def test_suffix_with_dot_name(): url = URL("http://example.com/doc.") assert "" == url.raw_suffix def test_suffixes_for_empty_url(): url = URL() assert () == url.raw_suffixes def test_raw_suffixes(): url = URL("http://example.com/path/to.txt#frag") assert (".txt",) == url.raw_suffixes def test_raw_suffixes_root(): url = URL("http://example.com/#frag") assert () == url.raw_suffixes def test_raw_suffixes_root2(): url = URL("http://example.com") assert () == url.raw_suffixes def test_raw_suffixes_root3(): url = URL("http://example.com/") assert () == url.raw_suffixes def test_relative_raw_suffixes(): url = URL("path/to") assert () == url.raw_suffixes def test_relative_raw_suffixes_starting_from_slash(): url = URL("/path/to") assert () == url.raw_suffixes def test_relative_raw_suffixes_dot(): url = URL(".") assert () == url.raw_suffixes def test_suffixes_non_ascii(): url = URL("http://example.com/шлях.суфікс") assert url.suffixes == (".суфікс",) def test_suffixes_with_empty_name(): url = URL("http://example.com/.hgrc") assert () == url.raw_suffixes def test_suffixes_multi_dot(): url = URL("http://example.com/doc.tar.gz") assert (".tar", ".gz") == url.raw_suffixes def test_suffixes_with_dot_name(): url = URL("http://example.com/doc.") assert () == url.raw_suffixes def test_plus_in_path(): url = URL("http://example.com/test/x+y%2Bz/:+%2B/") assert "/test/x+y+z/:++/" == url.path def test_nonascii_in_qs(): url = URL("http://example.com") url2 = url.with_query({"f\xf8\xf8": "f\xf8\xf8"}) assert "http://example.com/?f%C3%B8%C3%B8=f%C3%B8%C3%B8" == str(url2) def test_percent_encoded_in_qs(): url = URL("http://example.com") url2 = url.with_query({"k%cf%80": "v%cf%80"}) assert str(url2) == "http://example.com/?k%25cf%2580=v%25cf%2580" assert url2.raw_query_string == "k%25cf%2580=v%25cf%2580" assert url2.query_string == "k%cf%80=v%cf%80" assert url2.query == {"k%cf%80": "v%cf%80"} # modifiers def test_parent_raw_path(): url = URL("http://example.com/path/to") assert url.parent.raw_path == "/path" def test_parent_raw_parts(): url = URL("http://example.com/path/to") assert url.parent.raw_parts == ("/", "path") def test_double_parent_raw_path(): url = URL("http://example.com/path/to") assert url.parent.parent.raw_path == "/" def test_empty_parent_raw_path(): url = URL("http://example.com/") assert url.parent.parent.raw_path == "/" def test_empty_parent_raw_path2(): url = URL("http://example.com") assert url.parent.parent.raw_path == "/" def test_clear_fragment_on_getting_parent(): url = URL("http://example.com/path/to#frag") assert URL("http://example.com/path") == url.parent def test_clear_fragment_on_getting_parent_toplevel(): url = URL("http://example.com/#frag") assert URL("http://example.com/") == url.parent def test_clear_query_on_getting_parent(): url = URL("http://example.com/path/to?a=b") assert URL("http://example.com/path") == url.parent def test_clear_query_on_getting_parent_toplevel(): url = URL("http://example.com/?a=b") assert URL("http://example.com/") == url.parent # truediv def test_div_root(): url = URL("http://example.com") / "path" / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div_root_with_slash(): url = URL("http://example.com/") / "path" / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div(): url = URL("http://example.com/path") / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div_with_slash(): url = URL("http://example.com/path/") / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div_path_starting_from_slash_is_forbidden(): url = URL("http://example.com/path/") with pytest.raises(ValueError): url / "/to/others" class StrEnum(str, Enum): spam = "ham" def __str__(self): return self.value def test_div_path_srting_subclass(): url = URL("http://example.com/path/") / StrEnum.spam assert str(url) == "http://example.com/path/ham" def test_div_bad_type(): url = URL("http://example.com/path/") with pytest.raises(TypeError): url / 3 def test_div_cleanup_query_and_fragment(): url = URL("http://example.com/path?a=1#frag") assert str(url / "to") == "http://example.com/path/to" def test_div_for_empty_url(): url = URL() / "a" assert url.raw_parts == ("a",) def test_div_for_relative_url(): url = URL("a") / "b" assert url.raw_parts == ("a", "b") def test_div_for_relative_url_started_with_slash(): url = URL("/a") / "b" assert url.raw_parts == ("/", "a", "b") def test_div_non_ascii(): url = URL("http://example.com/сюди") url2 = url / "туди" assert url2.path == "/сюди/туди" assert url2.raw_path == "/%D1%81%D1%8E%D0%B4%D0%B8/%D1%82%D1%83%D0%B4%D0%B8" assert url2.parts == ("/", "сюди", "туди") assert url2.raw_parts == ( "/", "%D1%81%D1%8E%D0%B4%D0%B8", "%D1%82%D1%83%D0%B4%D0%B8", ) def test_div_percent_encoded(): url = URL("http://example.com/path") url2 = url / "%cf%80" assert url2.path == "/path/%cf%80" assert url2.raw_path == "/path/%25cf%2580" assert url2.parts == ("/", "path", "%cf%80") assert url2.raw_parts == ("/", "path", "%25cf%2580") def test_div_with_colon_and_at(): url = URL("http://example.com/base") / "path:abc@123" assert url.raw_path == "/base/path:abc@123" def test_div_with_dots(): url = URL("http://example.com/base") / "../path/./to" assert url.raw_path == "/path/to" # joinpath @pytest.mark.parametrize( "base,to_join,expected", [ pytest.param("", ("path", "to"), "http://example.com/path/to", id="root"), pytest.param( "/", ("path", "to"), "http://example.com/path/to", id="root-with-slash" ), pytest.param("/path", ("to",), "http://example.com/path/to", id="path"), pytest.param( "/path/", ("to",), "http://example.com/path/to", id="path-with-slash" ), pytest.param( "/path", ("",), "http://example.com/path/", id="path-add-trailing-slash" ), pytest.param( "/path?a=1#frag", ("to",), "http://example.com/path/to", id="cleanup-query-and-fragment", ), pytest.param("", ("path/",), "http://example.com/path/", id="trailing-slash"), pytest.param( "", ( "path", "", ), "http://example.com/path/", id="trailing-slash-empty-string", ), pytest.param( "", ("path/", "to/"), "http://example.com/path/to/", id="duplicate-slash" ), pytest.param("", (), "http://example.com", id="empty-segments"), pytest.param( "/", ("path/",), "http://example.com/path/", id="base-slash-trailing-slash" ), pytest.param( "/", ("path/", "to/"), "http://example.com/path/to/", id="base-slash-duplicate-slash", ), pytest.param("/", (), "http://example.com", id="base-slash-empty-segments"), ], ) def test_joinpath(base, to_join, expected): url = URL(f"http://example.com{base}") assert str(url.joinpath(*to_join)) == expected @pytest.mark.parametrize( "base,to_join,expected", [ pytest.param("path", "a", "path/a", id="default_default"), pytest.param("path", "./a", "path/a", id="default_relative"), pytest.param("path/", "a", "path/a", id="empty-segment_default"), pytest.param("path/", "./a", "path/a", id="empty-segment_relative"), pytest.param("path", ".//a", "path//a", id="default_empty-segment"), pytest.param("path/", ".//a", "path//a", id="empty-segment_empty_segment"), pytest.param("path//", "a", "path//a", id="empty-segments_default"), pytest.param("path//", "./a", "path//a", id="empty-segments_relative"), pytest.param("path//", ".//a", "path///a", id="empty-segments_empty-segment"), pytest.param("path", "a/", "path/a/", id="default_trailing-empty-segment"), pytest.param("path", "a//", "path/a//", id="default_trailing-empty-segments"), pytest.param("path", "a//b", "path/a//b", id="default_embedded-empty-segment"), pytest.param( "path/a/b/c/d/e", "a/../../../../../../c", "path/c", id="long-backtrack" ), pytest.param( "path/a/b/c/d/e", "a/../../../././../../../c", "path/c", id="long-backtrack-with-dots", ), pytest.param("path/a/../../d/e", "a/../c", "d/e/c", id="backtrack-in-both"), ], ) def test_joinpath_empty_segments(base, to_join, expected): url = URL(f"http://example.com/{base}") assert ( f"http://example.com/{expected}" == str(url.joinpath(to_join)) and str(url / to_join) == f"http://example.com/{expected}" ) def test_joinpath_backtrack_to_base(): url = URL("http://example.com/../../c") new_url = url.joinpath("../../..") assert str(new_url) == "http://example.com" assert new_url.path == "/" assert new_url.raw_path == "/" def test_joinpath_single_empty_segments(): """joining standalone empty segments does not create empty segments""" a = URL("/1//2///3") assert a.parts == ("/", "1", "", "2", "", "", "3") b = URL("scheme://host").joinpath(*a.parts[1:]) assert b.path == "/1/2/3" @pytest.mark.parametrize( "url,to_join,expected", [ pytest.param(URL(), ("a",), ("a",), id="empty-url"), pytest.param(URL("a"), ("b",), ("a", "b"), id="relative-path"), pytest.param(URL("a"), ("b", "", "c"), ("a", "b", "c"), id="empty-element"), pytest.param(URL("/a"), ("b"), ("/", "a", "b"), id="absolute-path"), pytest.param(URL(), ("a/",), ("a", ""), id="trailing-slash"), pytest.param(URL(), ("a/", "b/"), ("a", "b", ""), id="duplicate-slash"), pytest.param(URL(), (), ("",), id="empty-segments"), ], ) def test_joinpath_relative(url, to_join, expected): assert url.joinpath(*to_join).raw_parts == expected @pytest.mark.parametrize( "url,to_join,encoded,e_path,e_raw_path,e_parts,e_raw_parts", [ pytest.param( "http://example.com/сюди", ("туди",), False, "/сюди/туди", "/%D1%81%D1%8E%D0%B4%D0%B8/%D1%82%D1%83%D0%B4%D0%B8", ("/", "сюди", "туди"), ("/", "%D1%81%D1%8E%D0%B4%D0%B8", "%D1%82%D1%83%D0%B4%D0%B8"), id="non-ascii", ), pytest.param( "http://example.com/path", ("%cf%80",), False, "/path/%cf%80", "/path/%25cf%2580", ("/", "path", "%cf%80"), ("/", "path", "%25cf%2580"), id="percent-encoded", ), pytest.param( "http://example.com/path", ("%cf%80",), True, "/path/π", "/path/%cf%80", ("/", "path", "π"), ("/", "path", "%cf%80"), id="encoded-percent-encoded", ), ], ) def test_joinpath_encoding( url, to_join, encoded, e_path, e_raw_path, e_parts, e_raw_parts ): joined = URL(url).joinpath(*to_join, encoded=encoded) assert joined.path == e_path assert joined.raw_path == e_raw_path assert joined.parts == e_parts assert joined.raw_parts == e_raw_parts @pytest.mark.parametrize( "to_join,expected", [ pytest.param(("path:abc@123",), "/base/path:abc@123", id="with-colon-and-at"), pytest.param(("..", "path", ".", "to"), "/path/to", id="with-dots"), ], ) def test_joinpath_edgecases(to_join, expected): url = URL("http://example.com/base").joinpath(*to_join) assert url.raw_path == expected def test_joinpath_path_starting_from_slash_is_forbidden(): url = URL("http://example.com/path/") with pytest.raises( ValueError, match="Appending path .* starting from slash is forbidden" ): assert url.joinpath("/to/others") PATHS = [ # No dots ("", ""), ("path", "path"), # Single-dot ("path/to", "path/to"), ("././path/to", "path/to"), ("path/./to", "path/to"), ("path/././to", "path/to"), ("path/to/.", "path/to/"), ("path/to/./.", "path/to/"), # Double-dots ("../path/to", "path/to"), ("path/../to", "to"), ("path/../../to", "to"), # Non-ASCII characters ("μονοπάτι/../../να/ᴜɴɪ/ᴄᴏᴅᴇ", "να/ᴜɴɪ/ᴄᴏᴅᴇ"), ("μονοπάτι/../../να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/.", "να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/"), ] @pytest.mark.parametrize("original,expected", PATHS) def test_join_path_normalized(original: str, expected: str) -> None: """Test that joinpath normalizes paths.""" base_url = URL("http://example.com") new_url = base_url.joinpath(original) assert new_url.path == f"/{expected}" # with_path def test_with_path(): url = URL("http://example.com") url2 = url.with_path("/test") assert str(url2) == "http://example.com/test" assert url2.raw_path == "/test" assert url2.path == "/test" def test_with_path_nonascii(): url = URL("http://example.com") url2 = url.with_path("/π") assert str(url2) == "http://example.com/%CF%80" assert url2.raw_path == "/%CF%80" assert url2.path == "/π" def test_with_path_percent_encoded(): url = URL("http://example.com") url2 = url.with_path("/%cf%80") assert str(url2) == "http://example.com/%25cf%2580" assert url2.raw_path == "/%25cf%2580" assert url2.path == "/%cf%80" def test_with_path_encoded(): url = URL("http://example.com") url2 = url.with_path("/test", encoded=True) assert str(url2) == "http://example.com/test" assert url2.raw_path == "/test" assert url2.path == "/test" def test_with_path_encoded_nonascii(): url = URL("http://example.com") url2 = url.with_path("/π", encoded=True) assert str(url2) == "http://example.com/π" assert url2.raw_path == "/π" assert url2.path == "/π" def test_with_path_encoded_percent_encoded(): url = URL("http://example.com") url2 = url.with_path("/%cf%80", encoded=True) assert str(url2) == "http://example.com/%cf%80" assert url2.raw_path == "/%cf%80" assert url2.path == "/π" def test_with_path_dots(): url = URL("http://example.com") assert str(url.with_path("/test/.")) == "http://example.com/test/" def test_with_path_relative(): url = URL("/path") assert str(url.with_path("/new")) == "/new" def test_with_path_query(): url = URL("http://example.com?a=b") assert str(url.with_path("/test")) == "http://example.com/test" def test_with_path_fragment(): url = URL("http://example.com#frag") assert str(url.with_path("/test")) == "http://example.com/test" def test_with_path_empty(): url = URL("http://example.com/test") assert str(url.with_path("")) == "http://example.com" def test_with_path_leading_slash(): url = URL("http://example.com") assert url.with_path("test").path == "/test" # with_fragment def test_with_fragment(): url = URL("http://example.com") url2 = url.with_fragment("frag") assert str(url2) == "http://example.com/#frag" assert url2.raw_fragment == "frag" assert url2.fragment == "frag" def test_with_fragment_safe(): url = URL("http://example.com") u2 = url.with_fragment("a:b?c@d/e") assert str(u2) == "http://example.com/#a:b?c@d/e" def test_with_fragment_non_ascii(): url = URL("http://example.com") url2 = url.with_fragment("фрагм") assert url2.raw_fragment == "%D1%84%D1%80%D0%B0%D0%B3%D0%BC" assert url2.fragment == "фрагм" def test_with_fragment_percent_encoded(): url = URL("http://example.com") url2 = url.with_fragment("%cf%80") assert str(url2) == "http://example.com/#%25cf%2580" assert url2.raw_fragment == "%25cf%2580" assert url2.fragment == "%cf%80" def test_with_fragment_None(): url = URL("http://example.com/path#frag") url2 = url.with_fragment(None) assert str(url2) == "http://example.com/path" def test_with_fragment_None_matching(): url = URL("http://example.com/path") url2 = url.with_fragment(None) assert url is url2 def test_with_fragment_matching(): url = URL("http://example.com/path#frag") url2 = url.with_fragment("frag") assert url is url2 def test_with_fragment_bad_type(): url = URL("http://example.com") with pytest.raises(TypeError): url.with_fragment(123) # with_name def test_with_name(): url = URL("http://example.com/a/b") assert url.raw_parts == ("/", "a", "b") url2 = url.with_name("c") assert url2.raw_parts == ("/", "a", "c") assert url2.parts == ("/", "a", "c") assert url2.raw_path == "/a/c" assert url2.path == "/a/c" def test_with_name_for_naked_path(): url = URL("http://example.com") url2 = url.with_name("a") assert url2.raw_parts == ("/", "a") def test_with_name_for_relative_path(): url = URL("a") url2 = url.with_name("b") assert url2.raw_parts == ("b",) def test_with_name_for_relative_path2(): url = URL("a/b") url2 = url.with_name("c") assert url2.raw_parts == ("a", "c") def test_with_name_for_relative_path_starting_from_slash(): url = URL("/a") url2 = url.with_name("b") assert url2.raw_parts == ("/", "b") def test_with_name_for_relative_path_starting_from_slash2(): url = URL("/a/b") url2 = url.with_name("c") assert url2.raw_parts == ("/", "a", "c") def test_with_name_empty(): url = URL("http://example.com/path/to").with_name("") assert str(url) == "http://example.com/path/" def test_with_name_non_ascii(): url = URL("http://example.com/path").with_name("шлях") assert url.path == "/шлях" assert url.raw_path == "/%D1%88%D0%BB%D1%8F%D1%85" assert url.parts == ("/", "шлях") assert url.raw_parts == ("/", "%D1%88%D0%BB%D1%8F%D1%85") def test_with_name_percent_encoded(): url = URL("http://example.com/path") url2 = url.with_name("%cf%80") assert url2.raw_parts == ("/", "%25cf%2580") assert url2.parts == ("/", "%cf%80") assert url2.raw_path == "/%25cf%2580" assert url2.path == "/%cf%80" def test_with_name_with_slash(): with pytest.raises(ValueError): URL("http://example.com").with_name("a/b") def test_with_name_non_str(): with pytest.raises(TypeError): URL("http://example.com").with_name(123) def test_with_name_within_colon_and_at(): url = URL("http://example.com/oldpath").with_name("path:abc@123") assert url.raw_path == "/path:abc@123" def test_with_name_dot(): with pytest.raises(ValueError): URL("http://example.com").with_name(".") def test_with_name_double_dot(): with pytest.raises(ValueError): URL("http://example.com").with_name("..") # with_suffix def test_with_suffix(): url = URL("http://example.com/a/b") assert url.raw_parts == ("/", "a", "b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("/", "a", "b.c") assert url2.parts == ("/", "a", "b.c") assert url2.raw_path == "/a/b.c" assert url2.path == "/a/b.c" def test_with_suffix_for_naked_path(): url = URL("http://example.com") with pytest.raises(ValueError) as excinfo: url.with_suffix(".a") (msg,) = excinfo.value.args assert msg == f"{url!r} has an empty name" def test_with_suffix_for_relative_path(): url = URL("a") url2 = url.with_suffix(".b") assert url2.raw_parts == ("a.b",) def test_with_suffix_for_relative_path2(): url = URL("a/b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("a", "b.c") def test_with_suffix_for_relative_path_starting_from_slash(): url = URL("/a") url2 = url.with_suffix(".b") assert url2.raw_parts == ("/", "a.b") def test_with_suffix_for_relative_path_starting_from_slash2(): url = URL("/a/b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("/", "a", "b.c") def test_with_suffix_empty(): url = URL("http://example.com/path/to").with_suffix("") assert str(url) == "http://example.com/path/to" def test_with_suffix_non_ascii(): url = URL("http://example.com/path").with_suffix(".шлях") assert url.path == "/path.шлях" assert url.raw_path == "/path.%D1%88%D0%BB%D1%8F%D1%85" assert url.parts == ("/", "path.шлях") assert url.raw_parts == ("/", "path.%D1%88%D0%BB%D1%8F%D1%85") def test_with_suffix_percent_encoded(): url = URL("http://example.com/path") url2 = url.with_suffix(".%cf%80") assert url2.raw_parts == ("/", "path.%25cf%2580") assert url2.parts == ("/", "path.%cf%80") assert url2.raw_path == "/path.%25cf%2580" assert url2.path == "/path.%cf%80" def test_with_suffix_without_dot(): with pytest.raises(ValueError) as excinfo: URL("http://example.com/a").with_suffix("b") (msg,) = excinfo.value.args assert msg == "Invalid suffix 'b'" def test_with_suffix_non_str(): with pytest.raises(TypeError) as excinfo: URL("http://example.com").with_suffix(123) (msg,) = excinfo.value.args assert msg == "Invalid suffix type" def test_with_suffix_dot(): with pytest.raises(ValueError) as excinfo: URL("http://example.com").with_suffix(".") (msg,) = excinfo.value.args assert msg == "Invalid suffix '.'" def test_with_suffix_with_slash(): with pytest.raises(ValueError) as excinfo: URL("http://example.com/a").with_suffix("/.b") (msg,) = excinfo.value.args assert msg == "Invalid suffix '/.b'" def test_with_suffix_with_slash2(): with pytest.raises(ValueError) as excinfo: URL("http://example.com/a").with_suffix(".b/.d") (msg,) = excinfo.value.args assert msg == "Slash in name is not allowed" def test_with_suffix_replace(): url = URL("/a.b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("/", "a.c") # is_absolute def test_is_absolute_for_relative_url(): url = URL("/path/to") assert not url.is_absolute() assert not url.absolute def test_is_absolute_for_absolute_url(): url = URL("http://example.com") assert url.is_absolute() assert url.absolute def test_is_non_absolute_for_empty_url(): url = URL() assert not url.is_absolute() assert not url.absolute def test_is_non_absolute_for_empty_url2(): url = URL("") assert not url.is_absolute() assert not url.absolute def test_is_absolute_path_starting_from_double_slash(): url = URL("//www.python.org") assert url.is_absolute() assert url.absolute # is_default_port def test_is_default_port_for_relative_url(): url = URL("/path/to") assert not url.is_default_port() def test_is_default_port_for_absolute_url_without_port(): url = URL("http://example.com") assert url.is_default_port() def test_is_default_port_for_absolute_url_with_default_port(): url = URL("http://example.com:80") assert url.is_default_port() assert str(url) == "http://example.com" def test_is_default_port_for_absolute_url_with_nondefault_port(): url = URL("http://example.com:8080") assert not url.is_default_port() def test_is_default_port_for_unknown_scheme(): url = URL("unknown://example.com:8080") assert not url.is_default_port() def test_handling_port_zero(): url = URL("http://example.com:0") assert url.explicit_port == 0 assert url.explicit_port == url._val.port assert str(url) == "http://example.com:0" assert not url.is_default_port() # def test_no_scheme(): url = URL("example.com") assert url.raw_host is None assert url.raw_path == "example.com" assert str(url) == "example.com" def test_no_scheme2(): url = URL("example.com/a/b") assert url.raw_host is None assert url.raw_path == "example.com/a/b" assert str(url) == "example.com/a/b" def test_from_non_allowed(): with pytest.raises(TypeError): URL(1234) def test_from_idna(): url = URL("http://xn--jxagkqfkduily1i.eu") assert "http://xn--jxagkqfkduily1i.eu" == str(url) url = URL("http://xn--einla-pqa.de/") # needs idna 2008 assert "http://xn--einla-pqa.de/" == str(url) def test_to_idna(): url = URL("http://εμπορικόσήμα.eu") assert "http://xn--jxagkqfkduily1i.eu" == str(url) url = URL("http://einlaß.de/") assert "http://xn--einla-pqa.de/" == str(url) def test_from_ascii_login(): url = URL("http://" "%D0%B2%D0%B0%D1%81%D1%8F" "@host:1234/") assert ("http://" "%D0%B2%D0%B0%D1%81%D1%8F" "@host:1234/") == str(url) def test_from_non_ascii_login(): url = URL("http://бажан@host:1234/") assert ("http://%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD@host:1234/") == str(url) def test_from_ascii_login_and_password(): url = URL( "http://" "%D0%B2%D0%B0%D1%81%D1%8F" ":%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" "@host:1234/" ) assert ( "http://" "%D0%B2%D0%B0%D1%81%D1%8F" ":%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" "@host:1234/" ) == str(url) def test_from_non_ascii_login_and_password(): url = URL("http://бажан:пароль@host:1234/") assert ( "http://" "%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD" ":%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" "@host:1234/" ) == str(url) def test_from_ascii_path(): url = URL("http://example.com/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0") assert ( "http://example.com/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0" ) == str(url) def test_from_ascii_path_lower_case(): url = URL("http://example.com/" "%d0%bf%d1%83%d1%82%d1%8c/%d1%82%d1%83%d0%b4%d0%b0") assert ( "http://example.com/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0" ) == str(url) def test_from_non_ascii_path(): url = URL("http://example.com/шлях/туди") assert ( "http://example.com/%D1%88%D0%BB%D1%8F%D1%85/%D1%82%D1%83%D0%B4%D0%B8" ) == str(url) def test_bytes(): url = URL("http://example.com/шлях/туди") assert ( b"http://example.com/%D1%88%D0%BB%D1%8F%D1%85/%D1%82%D1%83%D0%B4%D0%B8" == bytes(url) ) def test_from_ascii_query_parts(): url = URL( "http://example.com/" "?%D0%BF%D0%B0%D1%80%D0%B0%D0%BC" "=%D0%B7%D0%BD%D0%B0%D1%87" ) assert ( "http://example.com/" "?%D0%BF%D0%B0%D1%80%D0%B0%D0%BC" "=%D0%B7%D0%BD%D0%B0%D1%87" ) == str(url) def test_from_non_ascii_query_parts(): url = URL("http://example.com/?парам=знач") assert ( "http://example.com/" "?%D0%BF%D0%B0%D1%80%D0%B0%D0%BC" "=%D0%B7%D0%BD%D0%B0%D1%87" ) == str(url) def test_from_non_ascii_query_parts2(): url = URL("http://example.com/?п=з&ю=б") assert "http://example.com/?%D0%BF=%D0%B7&%D1%8E=%D0%B1" == str(url) def test_from_ascii_fragment(): url = URL("http://example.com/" "#%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82") assert ( "http://example.com/" "#%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82" ) == str(url) def test_from_bytes_with_non_ascii_fragment(): url = URL("http://example.com/#фрагмент") assert ( "http://example.com/" "#%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82" ) == str(url) def test_to_str(): url = URL("http://εμπορικόσήμα.eu/") assert "http://xn--jxagkqfkduily1i.eu/" == str(url) def test_to_str_long(): url = URL( "https://host-12345678901234567890123456789012345678901234567890" "-name:8888/" ) expected = ( "https://host-" "12345678901234567890123456789012345678901234567890" "-name:8888/" ) assert expected == str(url) def test_decoding_with_2F_in_path(): url = URL("http://example.com/path%2Fto") assert "http://example.com/path%2Fto" == str(url) assert url == URL(str(url)) def test_decoding_with_26_and_3D_in_query(): url = URL("http://example.com/?%26=%3D") assert "http://example.com/?%26=%3D" == str(url) assert url == URL(str(url)) def test_fragment_only_url(): url = URL("#frag") assert str(url) == "#frag" def test_url_from_url(): url = URL("http://example.com") assert URL(url) == url assert URL(url).raw_parts == ("/",) def test_lowercase_scheme(): url = URL("HTTP://example.com") assert str(url) == "http://example.com" def test_str_for_empty_url(): url = URL() assert "" == str(url) def test_parent_for_empty_url(): url = URL() assert url is url.parent def test_empty_value_for_query(): url = URL("http://example.com/path").with_query({"a": ""}) assert str(url) == "http://example.com/path?a=" def test_none_value_for_query(): with pytest.raises(TypeError): URL("http://example.com/path").with_query({"a": None}) def test_decode_pct_in_path(): url = URL("http://www.python.org/%7Eguido") assert "http://www.python.org/~guido" == str(url) def test_decode_pct_in_path_lower_case(): url = URL("http://www.python.org/%7eguido") assert "http://www.python.org/~guido" == str(url) # join def test_join(): base = URL("http://www.cwi.nl/%7Eguido/Python.html") url = URL("FAQ.html") url2 = base.join(url) assert str(url2) == "http://www.cwi.nl/~guido/FAQ.html" def test_join_absolute(): base = URL("http://www.cwi.nl/%7Eguido/Python.html") url = URL("//www.python.org/%7Eguido") url2 = base.join(url) assert str(url2) == "http://www.python.org/~guido" def test_join_non_url(): base = URL("http://example.com") with pytest.raises(TypeError): base.join("path/to") NORMAL = [ ("g:h", "g:h"), ("g", "http://a/b/c/g"), ("./g", "http://a/b/c/g"), ("g/", "http://a/b/c/g/"), ("/g", "http://a/g"), ("//g", "http://g"), ("?y", "http://a/b/c/d;p?y"), ("g?y", "http://a/b/c/g?y"), ("#s", "http://a/b/c/d;p?q#s"), ("g#s", "http://a/b/c/g#s"), ("g?y#s", "http://a/b/c/g?y#s"), (";x", "http://a/b/c/;x"), ("g;x", "http://a/b/c/g;x"), ("g;x?y#s", "http://a/b/c/g;x?y#s"), ("", "http://a/b/c/d;p?q"), (".", "http://a/b/c/"), ("./", "http://a/b/c/"), ("..", "http://a/b/"), ("../", "http://a/b/"), ("../g", "http://a/b/g"), ("../..", "http://a/"), ("../../", "http://a/"), ("../../g", "http://a/g"), ] @pytest.mark.parametrize("url,expected", NORMAL) def test_join_from_rfc_3986_normal(url, expected): # test case from https://tools.ietf.org/html/rfc3986.html#section-5.4 base = URL("http://a/b/c/d;p?q") url = URL(url) expected = URL(expected) assert base.join(url) == expected ABNORMAL = [ ("../../../g", "http://a/g"), ("../../../../g", "http://a/g"), ("/./g", "http://a/g"), ("/../g", "http://a/g"), ("g.", "http://a/b/c/g."), (".g", "http://a/b/c/.g"), ("g..", "http://a/b/c/g.."), ("..g", "http://a/b/c/..g"), ("./../g", "http://a/b/g"), ("./g/.", "http://a/b/c/g/"), ("g/./h", "http://a/b/c/g/h"), ("g/../h", "http://a/b/c/h"), ("g;x=1/./y", "http://a/b/c/g;x=1/y"), ("g;x=1/../y", "http://a/b/c/y"), ("g?y/./x", "http://a/b/c/g?y/./x"), ("g?y/../x", "http://a/b/c/g?y/../x"), ("g#s/./x", "http://a/b/c/g#s/./x"), ("g#s/../x", "http://a/b/c/g#s/../x"), ] @pytest.mark.parametrize("url,expected", ABNORMAL) def test_join_from_rfc_3986_abnormal(url, expected): # test case from https://tools.ietf.org/html/rfc3986.html#section-5.4.2 base = URL("http://a/b/c/d;p?q") url = URL(url) expected = URL(expected) assert base.join(url) == expected EMPTY_SEGMENTS = [ ( "https://web.archive.org/web/", "./https://github.com/aio-libs/yarl", "https://web.archive.org/web/https://github.com/aio-libs/yarl", ), ( "https://web.archive.org/web/https://github.com/", "aio-libs/yarl", "https://web.archive.org/web/https://github.com/aio-libs/yarl", ), ] @pytest.mark.parametrize("base,url,expected", EMPTY_SEGMENTS) def test_join_empty_segments(base, url, expected): base = URL(base) url = URL(url) expected = URL(expected) joined = base.join(url) assert joined == expected SIMPLE_BASE = "http://a/b/c/d" URLLIB_URLJOIN = [ ("", "http://a/b/c/g?y/./x", "http://a/b/c/g?y/./x"), ("", "http://a/./g", "http://a/./g"), ("svn://pathtorepo/dir1", "dir2", "svn://pathtorepo/dir2"), ("svn+ssh://pathtorepo/dir1", "dir2", "svn+ssh://pathtorepo/dir2"), ("ws://a/b", "g", "ws://a/g"), ("wss://a/b", "g", "wss://a/g"), # test for issue22118 duplicate slashes (SIMPLE_BASE + "/", "foo", SIMPLE_BASE + "/foo"), # Non-RFC-defined tests, covering variations of base and trailing # slashes ("http://a/b/c/d/e/", "../../f/g/", "http://a/b/c/f/g/"), ("http://a/b/c/d/e", "../../f/g/", "http://a/b/f/g/"), ("http://a/b/c/d/e/", "/../../f/g/", "http://a/f/g/"), ("http://a/b/c/d/e", "/../../f/g/", "http://a/f/g/"), ("http://a/b/c/d/e/", "../../f/g", "http://a/b/c/f/g"), ("http://a/b/", "../../f/g/", "http://a/f/g/"), ("a", "b", "b"), ("http:///", "..", "http:///"), ("a/", "b", "a/b"), ("a/b", "c", "a/c"), ("a/b/", "c", "a/b/c"), ( "https://x.org/", "/?text=Hello+G%C3%BCnter", "https://x.org/?text=Hello+G%C3%BCnter", ), ( "https://x.org/", "?text=Hello+G%C3%BCnter", "https://x.org/?text=Hello+G%C3%BCnter", ), ("http://example.com", "http://example.com", "http://example.com"), ("http://x.org", "https://x.org#fragment", "https://x.org#fragment"), ] @pytest.mark.parametrize("base,url,expected", URLLIB_URLJOIN) def test_join_cpython_urljoin(base, url, expected): # tests from cpython urljoin base = URL(base) url = URL(url) expected = URL(expected) joined = base.join(url) assert joined == expected def test_join_preserves_leading_slash(): """Test that join preserves leading slash in path.""" base = URL.build(scheme="https", host="localhost", port=443) new = base.join(URL("") / "_msearch") assert str(new) == "https://localhost/_msearch" assert new.path == "/_msearch" def test_empty_authority(): assert URL("http:///").authority == "" def test_split_result_non_decoded(): with pytest.raises(ValueError): URL(SplitResult("http", "example.com", "path", "qs", "frag")) def test_human_repr(): url = URL("http://бажан:пароль@хост.домен:8080/шлях/сюди?арг=вал#фраг") s = url.human_repr() assert URL(s) == url assert s == "http://бажан:пароль@хост.домен:8080/шлях/сюди?арг=вал#фраг" def test_human_repr_defaults(): url = URL("шлях") s = url.human_repr() assert s == "шлях" def test_human_repr_default_port(): url = URL("http://бажан:пароль@хост.домен/шлях/сюди?арг=вал#фраг") s = url.human_repr() assert URL(s) == url assert s == "http://бажан:пароль@хост.домен/шлях/сюди?арг=вал#фраг" def test_human_repr_ipv6(): url = URL("http://[::1]:8080/path") s = url.human_repr() url2 = URL(s) assert url2 == url assert url2.host == "::1" assert s == "http://[::1]:8080/path" def test_human_repr_delimiters(): url = URL.build( scheme="http", user=" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", password=" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", host="хост.домен", port=8080, path="/ !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", query={ " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" }, fragment=" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", ) s = url.human_repr() assert URL(s) == url assert ( s == "http:// !\"%23$%25&'()*+,-.%2F%3A;<=>%3F%40%5B\\%5D^_`{|}~" ": !\"%23$%25&'()*+,-.%2F%3A;<=>%3F%40%5B\\%5D^_`{|}~" "@хост.домен:8080" "/ !\"%23$%25&'()*+,-./:;<=>%3F@[\\]^_`{|}~" "? !\"%23$%25%26'()*%2B,-./:%3B<%3D>?@[\\]^_`{|}~" "= !\"%23$%25%26'()*%2B,-./:%3B<%3D>?@[\\]^_`{|}~" "# !\"#$%25&'()*+,-./:;<=>?@[\\]^_`{|}~" ) def test_human_repr_non_printable(): url = URL.build( scheme="http", user="бажан\n\xad\u200b", password="пароль\n\xad\u200b", host="хост.домен", port=8080, path="/шлях\n\xad\u200b", query={"арг\n\xad\u200b": "вал\n\xad\u200b"}, fragment="фраг\n\xad\u200b", ) s = url.human_repr() assert URL(s) == url assert ( s == "http://бажан%0A%C2%AD%E2%80%8B:пароль%0A%C2%AD%E2%80%8B" "@хост.домен:8080" "/шлях%0A%C2%AD%E2%80%8B" "?арг%0A%C2%AD%E2%80%8B=вал%0A%C2%AD%E2%80%8B" "#фраг%0A%C2%AD%E2%80%8B" ) # relative def test_relative(): url = URL("http://user:pass@example.com:8080/path?a=b#frag") rel = url.relative() assert str(rel) == "/path?a=b#frag" def test_relative_is_relative(): url = URL("http://user:pass@example.com:8080/path?a=b#frag") rel = url.relative() assert not rel.is_absolute() assert not rel.absolute def test_relative_abs_parts_are_removed(): url = URL("http://user:pass@example.com:8080/path?a=b#frag") rel = url.relative() assert not rel.scheme assert not rel.user assert not rel.password assert not rel.host assert not rel.port def test_relative_fails_on_rel_url(): with pytest.raises(ValueError): URL("/path?a=b#frag").relative() def test_slash_and_question_in_query(): u = URL("http://example.com/path?http://example.com/p?a#b") assert u.query_string == "http://example.com/p?a" def test_slash_and_question_in_fragment(): u = URL("http://example.com/path#http://example.com/p?a") assert u.fragment == "http://example.com/p?a" def test_requoting(): u = URL("http://127.0.0.1/?next=http%3A//example.com/") assert u.raw_query_string == "next=http://example.com/" assert str(u) == "http://127.0.0.1/?next=http://example.com/" def test_join_query_string(): """Test that query strings are correctly joined.""" original = URL("http://127.0.0.1:62869") path_url = URL( "/api?start=2022-03-27T14:05:00%2B03:00&end=2022-03-27T16:05:00%2B03:00" ) assert path_url.query.get("start") == "2022-03-27T14:05:00+03:00" assert path_url.query.get("end") == "2022-03-27T16:05:00+03:00" new = original.join(path_url) assert new.query.get("start") == "2022-03-27T14:05:00+03:00" assert new.query.get("end") == "2022-03-27T16:05:00+03:00" def test_join_query_string_with_special_chars(): """Test url joining when the query string has non-ascii params.""" original = URL("http://127.0.0.1") path_url = URL("/api?text=%D1%82%D0%B5%D0%BA%D1%81%D1%82") assert path_url.query.get("text") == "текст" new = original.join(path_url) assert new.query.get("text") == "текст" def test_join_encoded_url(): """Test that url encoded urls are correctly joined.""" original = URL("http://127.0.0.1:62869") path_url = URL("/api/%34") assert original.path == "/" assert path_url.path == "/api/4" new = original.join(path_url) assert new.path == "/api/4" # cache def test_parsing_populates_cache(): """Test that parsing a URL populates the cache.""" url = URL("http://user:password@example.com:80/path?a=b#frag") assert url._cache["raw_user"] == "user" assert url._cache["raw_password"] == "password" assert url._cache["raw_host"] == "example.com" assert url._cache["explicit_port"] == 80 assert url._cache["raw_query_string"] == "a=b" assert url._cache["raw_fragment"] == "frag" assert url._cache["scheme"] == "http" assert url.raw_user == "user" assert url.raw_password == "password" assert url.raw_host == "example.com" assert url.explicit_port == 80 assert url.raw_query_string == "a=b" assert url.raw_fragment == "frag" assert url.scheme == "http" url._cache.clear() assert url.raw_user == "user" assert url.raw_password == "password" assert url.raw_host == "example.com" assert url.explicit_port == 80 assert url.raw_query_string == "a=b" assert url.raw_fragment == "frag" assert url.scheme == "http" assert url._cache["raw_user"] == "user" assert url._cache["raw_password"] == "password" assert url._cache["raw_host"] == "example.com" assert url._cache["explicit_port"] == 80 assert url._cache["raw_query_string"] == "a=b" assert url._cache["raw_fragment"] == "frag" assert url._cache["scheme"] == "http" @pytest.mark.parametrize( ("host", "is_authority"), [ *(("other_gen_delim_" + c, False) for c in "[]"), ], ) def test_build_with_invalid_ipv6_host(host: str, is_authority: bool): with pytest.raises(ValueError, match="Invalid IPv6 URL"): URL(f"http://{host}/")