diff options
author | rekby <rekby@ydb.tech> | 2023-12-14 16:56:50 +0300 |
---|---|---|
committer | rekby <rekby@ydb.tech> | 2023-12-14 18:09:44 +0300 |
commit | b2b2bb5997507072ca64548efe64447dd6395426 (patch) | |
tree | bbfbf77d11f1972c93ae4101fe561fd440d6ad6a /contrib/python/yarl/tests | |
parent | 8b8678a6a4f57c62e348cdad8afd3849011a5f11 (diff) | |
download | ydb-b2b2bb5997507072ca64548efe64447dd6395426.tar.gz |
KIKIMR-19900 switch arcadia to python ydb sdk from contrib
Этот PR создан скриптом - для переключения зависимостей на python ydb sdk с версии внутри ydb на код, приезжающий через контриб.
Код в обеих версиях одинаковый, так что поломок/изменения функционала на ожидается.
На всякий случай посмотрите свои проекты и если будут возражения пишите сюда в issues или в тикет KIKIMR-19900.
Если всё ок - шипните, для определённости.
При отсутствии блокеров PR будет перегенерирован и влит с force-мёрджем в четверг, 14 декабря.
Diffstat (limited to 'contrib/python/yarl/tests')
-rw-r--r-- | contrib/python/yarl/tests/test_cache.py | 28 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_cached_property.py | 45 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_normalize_path.py | 34 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_pickle.py | 23 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_quoting.py | 450 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_update_query.py | 366 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_url.py | 1732 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_url_build.py | 259 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_url_cmp_and_hash.py | 88 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_url_parsing.py | 582 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_url_query.py | 173 | ||||
-rw-r--r-- | contrib/python/yarl/tests/test_url_update_netloc.py | 228 | ||||
-rw-r--r-- | contrib/python/yarl/tests/ya.make | 24 |
13 files changed, 4032 insertions, 0 deletions
diff --git a/contrib/python/yarl/tests/test_cache.py b/contrib/python/yarl/tests/test_cache.py new file mode 100644 index 0000000000..22141dd085 --- /dev/null +++ b/contrib/python/yarl/tests/test_cache.py @@ -0,0 +1,28 @@ +import yarl + +# Don't check the actual behavior but make sure that calls are allowed + + +def teardown_module(): + yarl.cache_configure() + + +def test_cache_clear() -> None: + yarl.cache_clear() + + +def test_cache_info() -> None: + info = yarl.cache_info() + assert info.keys() == {"idna_encode", "idna_decode"} + + +def test_cache_configure_default() -> None: + yarl.cache_configure() + + +def test_cache_configure_None() -> None: + yarl.cache_configure(idna_encode_size=None, idna_decode_size=None) + + +def test_cache_configure_explicit() -> None: + yarl.cache_configure(idna_encode_size=128, idna_decode_size=128) diff --git a/contrib/python/yarl/tests/test_cached_property.py b/contrib/python/yarl/tests/test_cached_property.py new file mode 100644 index 0000000000..5dcb5ece23 --- /dev/null +++ b/contrib/python/yarl/tests/test_cached_property.py @@ -0,0 +1,45 @@ +import pytest + +from yarl._url import cached_property + + +def test_reify(): + class A: + def __init__(self): + self._cache = {} + + @cached_property + def prop(self): + return 1 + + a = A() + assert 1 == a.prop + + +def test_reify_class(): + class A: + def __init__(self): + self._cache = {} + + @cached_property + def prop(self): + """Docstring.""" + return 1 + + assert isinstance(A.prop, cached_property) + assert "Docstring." == A.prop.__doc__ + + +def test_reify_assignment(): + class A: + def __init__(self): + self._cache = {} + + @cached_property + def prop(self): + return 1 + + a = A() + + with pytest.raises(AttributeError): + a.prop = 123 diff --git a/contrib/python/yarl/tests/test_normalize_path.py b/contrib/python/yarl/tests/test_normalize_path.py new file mode 100644 index 0000000000..defc4d8dd7 --- /dev/null +++ b/contrib/python/yarl/tests/test_normalize_path.py @@ -0,0 +1,34 @@ +import pytest + +from yarl import URL + +PATHS = [ + # No dots + ("", ""), + ("/", "/"), + ("//", "//"), + ("///", "///"), + # 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"), + # absolute path root / is maintained; tests based on two + # tests from web-platform-tests project's urltestdata.json + ("/foo/../../../ton", "/ton"), + ("/foo/../../../..bar", "/..bar"), + # Non-ASCII characters + ("μονοπάτι/../../να/ᴜɴɪ/ᴄᴏᴅᴇ", "να/ᴜɴɪ/ᴄᴏᴅᴇ"), + ("μονοπάτι/../../να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/.", "να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/"), +] + + +@pytest.mark.parametrize("original,expected", PATHS) +def test__normalize_path(original, expected): + assert URL._normalize_path(original) == expected diff --git a/contrib/python/yarl/tests/test_pickle.py b/contrib/python/yarl/tests/test_pickle.py new file mode 100644 index 0000000000..a1f29ab68c --- /dev/null +++ b/contrib/python/yarl/tests/test_pickle.py @@ -0,0 +1,23 @@ +import pickle + +from yarl import URL + +# serialize + + +def test_pickle(): + u1 = URL("test") + hash(u1) + v = pickle.dumps(u1) + u2 = pickle.loads(v) + assert u1._cache + assert not u2._cache + assert hash(u1) == hash(u2) + + +def test_default_style_state(): + u = URL("test") + hash(u) + u.__setstate__((None, {"_val": "test", "_strict": False, "_cache": {"hash": 1}})) + assert not u._cache + assert u._val == "test" diff --git a/contrib/python/yarl/tests/test_quoting.py b/contrib/python/yarl/tests/test_quoting.py new file mode 100644 index 0000000000..7ebc0f9b04 --- /dev/null +++ b/contrib/python/yarl/tests/test_quoting.py @@ -0,0 +1,450 @@ +import pytest + +from yarl._quoting import NO_EXTENSIONS +from yarl._quoting_py import _Quoter as _PyQuoter +from yarl._quoting_py import _Unquoter as _PyUnquoter + +if not NO_EXTENSIONS: + from yarl._quoting_c import _Quoter as _CQuoter + from yarl._quoting_c import _Unquoter as _CUnquoter + + @pytest.fixture(params=[_PyQuoter, _CQuoter], ids=["py_quoter", "c_quoter"]) + def quoter(request): + return request.param + + @pytest.fixture(params=[_PyUnquoter, _CUnquoter], ids=["py_unquoter", "c_unquoter"]) + def unquoter(request): + return request.param + +else: + + @pytest.fixture(params=[_PyQuoter], ids=["py_quoter"]) + def quoter(request): + return request.param + + @pytest.fixture(params=[_PyUnquoter], ids=["py_unquoter"]) + def unquoter(request): + return request.param + + +def hexescape(char): + """Escape char as RFC 2396 specifies""" + hex_repr = hex(ord(char))[2:].upper() + if len(hex_repr) == 1: + hex_repr = "0%s" % hex_repr + return "%" + hex_repr + + +def test_quote_not_allowed_non_strict(quoter): + assert quoter()("%HH") == "%25HH" + + +def test_quote_unfinished_tail_percent_non_strict(quoter): + assert quoter()("%") == "%25" + + +def test_quote_unfinished_tail_digit_non_strict(quoter): + assert quoter()("%2") == "%252" + + +def test_quote_unfinished_tail_safe_non_strict(quoter): + assert quoter()("%x") == "%25x" + + +def test_quote_unfinished_tail_unsafe_non_strict(quoter): + assert quoter()("%#") == "%25%23" + + +def test_quote_unfinished_tail_non_ascii_non_strict(quoter): + assert quoter()("%ß") == "%25%C3%9F" + + +def test_quote_unfinished_tail_non_ascii2_non_strict(quoter): + assert quoter()("%€") == "%25%E2%82%AC" + + +def test_quote_unfinished_tail_non_ascii3_non_strict(quoter): + assert quoter()("%🐍") == "%25%F0%9F%90%8D" + + +def test_quote_from_bytes(quoter): + assert quoter()("archaeological arcana") == "archaeological%20arcana" + assert quoter()("") == "" + + +def test_quote_ignore_broken_unicode(quoter): + s = quoter()( + "j\u001a\udcf4q\udcda/\udc97g\udcee\udccb\u000ch\udccb" + "\u0018\udce4v\u001b\udce2\udcce\udccecom/y\udccepj\u0016" + ) + + assert s == "j%1Aq%2Fg%0Ch%18v%1Bcom%2Fypj%16" + assert quoter()(s) == s + + +def test_unquote_to_bytes(unquoter): + assert unquoter()("abc%20def") == "abc def" + assert unquoter()("") == "" + + +def test_never_quote(quoter): + # Make sure quote() does not quote letters, digits, and "_,.-~" + do_not_quote = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789" "_.-~" + ) + assert quoter()(do_not_quote) == do_not_quote + assert quoter(qs=True)(do_not_quote) == do_not_quote + + +def test_safe(quoter): + # Test setting 'safe' parameter does what it should do + quote_by_default = "<>" + assert quoter(safe=quote_by_default)(quote_by_default) == quote_by_default + + ret = quoter(safe=quote_by_default, qs=True)(quote_by_default) + assert ret == quote_by_default + + +_SHOULD_QUOTE = [chr(num) for num in range(32)] +_SHOULD_QUOTE.append(r'<>#"{}|\^[]`') +_SHOULD_QUOTE.append(chr(127)) # For 0x7F +SHOULD_QUOTE = "".join(_SHOULD_QUOTE) + + +@pytest.mark.parametrize("char", SHOULD_QUOTE) +def test_default_quoting(char, quoter): + # Make sure all characters that should be quoted are by default sans + # space (separate test for that). + result = quoter()(char) + assert hexescape(char) == result + result = quoter(qs=True)(char) + assert hexescape(char) == result + + +# TODO: should it encode percent? +def test_default_quoting_percent(quoter): + result = quoter()("%25") + assert "%25" == result + result = quoter(qs=True)("%25") + assert "%25" == result + result = quoter(requote=False)("%25") + assert "%2525" == result + + +def test_default_quoting_partial(quoter): + partial_quote = "ab[]cd" + expected = "ab%5B%5Dcd" + result = quoter()(partial_quote) + assert expected == result + result = quoter(qs=True)(partial_quote) + assert expected == result + + +def test_quoting_space(quoter): + # Make sure quote() and quote_plus() handle spaces as specified in + # their unique way + result = quoter()(" ") + assert result == hexescape(" ") + result = quoter(qs=True)(" ") + assert result == "+" + + given = "a b cd e f" + expect = given.replace(" ", hexescape(" ")) + result = quoter()(given) + assert expect == result + expect = given.replace(" ", "+") + result = quoter(qs=True)(given) + assert expect == result + + +def test_quoting_plus(quoter): + assert quoter(qs=False)("alpha+beta gamma") == "alpha+beta%20gamma" + assert quoter(qs=True)("alpha+beta gamma") == "alpha%2Bbeta+gamma" + assert quoter(safe="+", qs=True)("alpha+beta gamma") == "alpha+beta+gamma" + + +def test_quote_with_unicode(quoter): + # Characters in Latin-1 range, encoded by default in UTF-8 + given = "\u00a2\u00d8ab\u00ff" + expect = "%C2%A2%C3%98ab%C3%BF" + result = quoter()(given) + assert expect == result + # Characters in BMP, encoded by default in UTF-8 + given = "\u6f22\u5b57" # "Kanji" + expect = "%E6%BC%A2%E5%AD%97" + result = quoter()(given) + assert expect == result + + +def test_quote_plus_with_unicode(quoter): + # Characters in Latin-1 range, encoded by default in UTF-8 + given = "\u00a2\u00d8ab\u00ff" + expect = "%C2%A2%C3%98ab%C3%BF" + result = quoter(qs=True)(given) + assert expect == result + # Characters in BMP, encoded by default in UTF-8 + given = "\u6f22\u5b57" # "Kanji" + expect = "%E6%BC%A2%E5%AD%97" + result = quoter(qs=True)(given) + assert expect == result + + +@pytest.mark.parametrize("num", list(range(128))) +def test_unquoting(num, unquoter): + # Make sure unquoting of all ASCII values works + given = hexescape(chr(num)) + expect = chr(num) + result = unquoter()(given) + assert expect == result + if expect not in "+=&;": + result = unquoter(qs=True)(given) + assert expect == result + + +# Expected value should be the same as given. +# See https://url.spec.whatwg.org/#percent-encoded-bytes +@pytest.mark.parametrize( + ("input", "expected"), + [ + ("%", "%"), + ("%2", "%2"), + ("%x", "%x"), + ("%€", "%€"), + ("%2x", "%2x"), + ("%2 ", "%2 "), + ("% 2", "% 2"), + ("%xa", "%xa"), + ("%%", "%%"), + ("%%3f", "%?"), + ("%2%", "%2%"), + ("%2%3f", "%2?"), + ("%x%3f", "%x?"), + ("%€%3f", "%€?"), + ], +) +def test_unquoting_bad_percent_escapes(unquoter, input, expected): + assert unquoter()(input) == expected + + +@pytest.mark.xfail +# FIXME: After conversion to bytes, should not cause UTF-8 decode fail. +# See https://url.spec.whatwg.org/#percent-encoded-bytes +def test_unquoting_invalid_utf8_sequence(unquoter): + with pytest.raises(ValueError): + unquoter()("%AB") + with pytest.raises(ValueError): + unquoter()("%AB%AB") + + +def test_unquoting_mixed_case_percent_escapes(unquoter): + expected = "𝕦" + assert expected == unquoter()("%F0%9D%95%A6") + assert expected == unquoter()("%F0%9d%95%a6") + assert expected == unquoter()("%f0%9D%95%a6") + assert expected == unquoter()("%f0%9d%95%a6") + + +def test_unquoting_parts(unquoter): + # Make sure unquoting works when have non-quoted characters + # interspersed + given = "ab" + hexescape("c") + "d" + expect = "abcd" + result = unquoter()(given) + assert expect == result + result = unquoter(qs=True)(given) + assert expect == result + + +def test_quote_None(quoter): + assert quoter()(None) is None + + +def test_unquote_None(unquoter): + assert unquoter()(None) is None + + +def test_quote_empty_string(quoter): + assert quoter()("") == "" + + +def test_unquote_empty_string(unquoter): + assert unquoter()("") == "" + + +def test_quote_bad_types(quoter): + with pytest.raises(TypeError): + quoter()(123) + + +def test_unquote_bad_types(unquoter): + with pytest.raises(TypeError): + unquoter()(123) + + +def test_quote_lowercase(quoter): + assert quoter()("%d1%84") == "%D1%84" + + +def test_quote_unquoted(quoter): + assert quoter()("%41") == "A" + + +def test_quote_space(quoter): + assert quoter()(" ") == "%20" # NULL + + +# test to see if this would work to fix +# coverage on this file. +def test_quote_percent_last_character(quoter): + # % is last character in this case. + assert quoter()("%") == "%25" + + +def test_unquote_unsafe(unquoter): + assert unquoter(unsafe="@")("%40") == "%40" + + +def test_unquote_unsafe2(unquoter): + assert unquoter(unsafe="@")("%40abc") == "%40abc" + + +def test_unquote_unsafe3(unquoter): + assert unquoter(qs=True)("a%2Bb=?%3D%2B%26") == "a%2Bb=?%3D%2B%26" + + +def test_unquote_unsafe4(unquoter): + assert unquoter(unsafe="@")("a@b") == "a%40b" + + +@pytest.mark.parametrize( + ("input", "expected"), + [ + ("%e2%82", "%e2%82"), + ("%e2%82ac", "%e2%82ac"), + ("%e2%82%f8", "%e2%82%f8"), + ("%e2%82%2b", "%e2%82+"), + ("%e2%82%e2%82%ac", "%e2%82€"), + ("%e2%82%e2%82", "%e2%82%e2%82"), + ], +) +def test_unquote_non_utf8(unquoter, input, expected): + assert unquoter()(input) == expected + + +def test_unquote_unsafe_non_utf8(unquoter): + assert unquoter(unsafe="\n")("%e2%82%0a") == "%e2%82%0A" + + +def test_unquote_plus_non_utf8(unquoter): + assert unquoter(qs=True)("%e2%82%2b") == "%e2%82%2B" + + +def test_quote_non_ascii(quoter): + assert quoter()("%F8") == "%F8" + + +def test_quote_non_ascii2(quoter): + assert quoter()("a%F8b") == "a%F8b" + + +def test_quote_percent_percent_encoded(quoter): + assert quoter()("%%3f") == "%25%3F" + + +def test_quote_percent_digit_percent_encoded(quoter): + assert quoter()("%2%3f") == "%252%3F" + + +def test_quote_percent_safe_percent_encoded(quoter): + assert quoter()("%x%3f") == "%25x%3F" + + +def test_quote_percent_unsafe_percent_encoded(quoter): + assert quoter()("%#%3f") == "%25%23%3F" + + +def test_quote_percent_non_ascii_percent_encoded(quoter): + assert quoter()("%ß%3f") == "%25%C3%9F%3F" + + +def test_quote_percent_non_ascii2_percent_encoded(quoter): + assert quoter()("%€%3f") == "%25%E2%82%AC%3F" + + +def test_quote_percent_non_ascii3_percent_encoded(quoter): + assert quoter()("%🐍%3f") == "%25%F0%9F%90%8D%3F" + + +class StrLike(str): + pass + + +def test_quote_str_like(quoter): + assert quoter()(StrLike("abc")) == "abc" + + +def test_unquote_str_like(unquoter): + assert unquoter()(StrLike("abc")) == "abc" + + +def test_quote_sub_delims(quoter): + assert quoter()("!$&'()*+,;=") == "!$&'()*+,;=" + + +def test_requote_sub_delims(quoter): + assert quoter()("%21%24%26%27%28%29%2A%2B%2C%3B%3D") == "!$&'()*+,;=" + + +def test_unquoting_plus(unquoter): + assert unquoter(qs=False)("a+b") == "a+b" + + +def test_unquote_plus_to_space(unquoter): + assert unquoter(qs=True)("a+b") == "a b" + + +def test_unquote_plus_to_space_unsafe(unquoter): + assert unquoter(unsafe="+", qs=True)("a+b") == "a+b" + + +def test_quote_qs_with_colon(quoter): + s = quoter(safe="=+&?/:@", qs=True)("next=http%3A//example.com/") + assert s == "next=http://example.com/" + + +def test_quote_protected(quoter): + s = quoter(protected="/")("/path%2fto/three") + assert s == "/path%2Fto/three" + + +def test_quote_fastpath_safe(quoter): + s1 = "/path/to" + s2 = quoter(safe="/")(s1) + assert s1 is s2 + + +def test_quote_fastpath_pct(quoter): + s1 = "abc%A0" + s2 = quoter()(s1) + assert s1 is s2 + + +def test_quote_very_large_string(quoter): + # more than 8 KiB + s = "abcфух%30%0a" * 1024 + assert quoter()(s) == "abc%D1%84%D1%83%D1%850%0A" * 1024 + + +def test_space(quoter): + s = "% A" + assert quoter()(s) == "%25%20A" + + +def test_quoter_path_with_plus(quoter): + s = "/test/x+y%2Bz/:+%2B/" + assert "/test/x+y%2Bz/:+%2B/" == quoter(safe="@:", protected="/+")(s) + + +def test_unquoter_path_with_plus(unquoter): + s = "/test/x+y%2Bz/:+%2B/" + assert "/test/x+y+z/:++/" == unquoter(unsafe="+")(s) diff --git a/contrib/python/yarl/tests/test_update_query.py b/contrib/python/yarl/tests/test_update_query.py new file mode 100644 index 0000000000..e47c468341 --- /dev/null +++ b/contrib/python/yarl/tests/test_update_query.py @@ -0,0 +1,366 @@ +import enum + +import pytest +from multidict import MultiDict + +from yarl import URL + +# with_query + + +def test_with_query(): + url = URL("http://example.com") + assert str(url.with_query({"a": "1"})) == "http://example.com/?a=1" + + +def test_update_query(): + url = URL("http://example.com/") + assert str(url.update_query({"a": "1"})) == "http://example.com/?a=1" + assert str(URL("test").update_query(a=1)) == "test?a=1" + + url = URL("http://example.com/?foo=bar") + expected_url = URL("http://example.com/?foo=bar&baz=foo") + + assert url.update_query({"baz": "foo"}) == expected_url + assert url.update_query(baz="foo") == expected_url + assert url.update_query("baz=foo") == expected_url + + +def test_update_query_with_args_and_kwargs(): + url = URL("http://example.com/") + + with pytest.raises(ValueError): + url.update_query("a", foo="bar") + + +def test_update_query_with_multiple_args(): + url = URL("http://example.com/") + + with pytest.raises(ValueError): + url.update_query("a", "b") + + +def test_update_query_with_none_arg(): + url = URL("http://example.com/?foo=bar&baz=foo") + expected_url = URL("http://example.com/") + assert url.update_query(None) == expected_url + + +def test_update_query_with_empty_dict(): + url = URL("http://example.com/?foo=bar&baz=foo") + assert url.update_query({}) == url + + +def test_with_query_list_of_pairs(): + url = URL("http://example.com") + assert str(url.with_query([("a", "1")])) == "http://example.com/?a=1" + + +def test_with_query_list_non_pairs(): + url = URL("http://example.com") + with pytest.raises(ValueError): + url.with_query(["a=1", "b=2", "c=3"]) + + +def test_with_query_kwargs(): + url = URL("http://example.com") + q = url.with_query(query="1", query2="1").query + assert q == dict(query="1", query2="1") + + +def test_with_query_kwargs_and_args_are_mutually_exclusive(): + url = URL("http://example.com") + with pytest.raises(ValueError): + url.with_query({"a": "2", "b": "4"}, a="1") + + +def test_with_query_only_single_arg_is_supported(): + url = URL("http://example.com") + u1 = url.with_query(b=3) + u2 = URL("http://example.com/?b=3") + assert u1 == u2 + with pytest.raises(ValueError): + url.with_query("a=1", "a=b") + + +def test_with_query_empty_dict(): + url = URL("http://example.com/?a=b") + new_url = url.with_query({}) + assert new_url.query_string == "" + assert str(new_url) == "http://example.com/" + + +def test_with_query_empty_str(): + url = URL("http://example.com/?a=b") + assert str(url.with_query("")) == "http://example.com/" + + +def test_with_query_empty_value(): + url = URL("http://example.com/") + assert str(url.with_query({"a": ""})) == "http://example.com/?a=" + + +def test_with_query_str(): + url = URL("http://example.com") + assert str(url.with_query("a=1&b=2")) == "http://example.com/?a=1&b=2" + + +def test_with_query_str_non_ascii_and_spaces(): + url = URL("http://example.com") + url2 = url.with_query("a=1 2&b=знач") + assert url2.raw_query_string == "a=1+2&b=%D0%B7%D0%BD%D0%B0%D1%87" + assert url2.query_string == "a=1 2&b=знач" + + +def test_with_query_int(): + url = URL("http://example.com") + assert url.with_query({"a": 1}) == URL("http://example.com/?a=1") + + +def test_with_query_kwargs_int(): + url = URL("http://example.com") + assert url.with_query(b=2) == URL("http://example.com/?b=2") + + +def test_with_query_list_int(): + url = URL("http://example.com") + assert str(url.with_query([("a", 1)])) == "http://example.com/?a=1" + + +@pytest.mark.parametrize( + ("query", "expected"), + [ + pytest.param({"a": []}, "", id="empty list"), + pytest.param({"a": ()}, "", id="empty tuple"), + pytest.param({"a": [1]}, "/?a=1", id="single list"), + pytest.param({"a": (1,)}, "/?a=1", id="single tuple"), + pytest.param({"a": [1, 2]}, "/?a=1&a=2", id="list"), + pytest.param({"a": (1, 2)}, "/?a=1&a=2", id="tuple"), + pytest.param({"a[]": [1, 2]}, "/?a%5B%5D=1&a%5B%5D=2", id="key with braces"), + pytest.param({"&": [1, 2]}, "/?%26=1&%26=2", id="quote key"), + pytest.param({"a": ["1", 2]}, "/?a=1&a=2", id="mixed types"), + pytest.param({"&": ["=", 2]}, "/?%26=%3D&%26=2", id="quote key and value"), + pytest.param({"a": 1, "b": [2, 3]}, "/?a=1&b=2&b=3", id="single then list"), + pytest.param({"a": [1, 2], "b": 3}, "/?a=1&a=2&b=3", id="list then single"), + pytest.param({"a": ["1&a=2", 3]}, "/?a=1%26a%3D2&a=3", id="ampersand then int"), + pytest.param({"a": [1, "2&a=3"]}, "/?a=1&a=2%26a%3D3", id="int then ampersand"), + ], +) +def test_with_query_sequence(query, expected): + url = URL("http://example.com") + expected = "http://example.com{expected}".format_map(locals()) + assert str(url.with_query(query)) == expected + + +@pytest.mark.parametrize( + "query", + [ + pytest.param({"a": [[1]]}, id="nested"), + pytest.param([("a", [1, 2])], id="tuple list"), + ], +) +def test_with_query_sequence_invalid_use(query): + url = URL("http://example.com") + with pytest.raises(TypeError, match="Invalid variable type"): + url.with_query(query) + + +class _CStr(str): + pass + + +class _EmptyStrEr: + def __str__(self): + return "" + + +class _CInt(int, _EmptyStrEr): + pass + + +class _CFloat(float, _EmptyStrEr): + pass + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + pytest.param("1", "1", id="str"), + pytest.param(_CStr("1"), "1", id="custom str"), + pytest.param(1, "1", id="int"), + pytest.param(_CInt(1), "1", id="custom int"), + pytest.param(1.1, "1.1", id="float"), + pytest.param(_CFloat(1.1), "1.1", id="custom float"), + ], +) +def test_with_query_valid_type(value, expected): + url = URL("http://example.com") + expected = "http://example.com/?a={expected}".format_map(locals()) + assert str(url.with_query({"a": value})) == expected + + +@pytest.mark.parametrize( + ("value", "exc_type"), + [ + pytest.param(True, TypeError, id="bool"), + pytest.param(None, TypeError, id="none"), + pytest.param(float("inf"), ValueError, id="non-finite float"), + pytest.param(float("nan"), ValueError, id="NaN float"), + ], +) +def test_with_query_invalid_type(value, exc_type): + url = URL("http://example.com") + with pytest.raises(exc_type): + url.with_query({"a": value}) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + pytest.param("1", "1", id="str"), + pytest.param(_CStr("1"), "1", id="custom str"), + pytest.param(1, "1", id="int"), + pytest.param(_CInt(1), "1", id="custom int"), + pytest.param(1.1, "1.1", id="float"), + pytest.param(_CFloat(1.1), "1.1", id="custom float"), + ], +) +def test_with_query_list_valid_type(value, expected): + url = URL("http://example.com") + expected = "http://example.com/?a={expected}".format_map(locals()) + assert str(url.with_query([("a", value)])) == expected + + +@pytest.mark.parametrize( + ("value"), [pytest.param(True, id="bool"), pytest.param(None, id="none")] +) +def test_with_query_list_invalid_type(value): + url = URL("http://example.com") + with pytest.raises(TypeError): + url.with_query([("a", value)]) + + +def test_with_int_enum(): + class IntEnum(int, enum.Enum): + A = 1 + + url = URL("http://example.com/path") + url2 = url.with_query(a=IntEnum.A) + assert str(url2) == "http://example.com/path?a=1" + + +def test_with_float_enum(): + class FloatEnum(float, enum.Enum): + A = 1.1 + + url = URL("http://example.com/path") + url2 = url.with_query(a=FloatEnum.A) + assert str(url2) == "http://example.com/path?a=1.1" + + +def test_with_query_multidict(): + url = URL("http://example.com/path") + q = MultiDict([("a", "b"), ("c", "d")]) + assert str(url.with_query(q)) == "http://example.com/path?a=b&c=d" + + +def test_with_multidict_with_spaces_and_non_ascii(): + url = URL("http://example.com") + url2 = url.with_query({"a b": "ю б"}) + assert url2.raw_query_string == "a+b=%D1%8E+%D0%B1" + + +def test_with_query_multidict_with_unsafe(): + url = URL("http://example.com/path") + url2 = url.with_query({"a+b": "?=+&;"}) + assert url2.raw_query_string == "a%2Bb=?%3D%2B%26%3B" + assert url2.query_string == "a%2Bb=?%3D%2B%26%3B" + assert url2.query == {"a+b": "?=+&;"} + + +def test_with_query_None(): + url = URL("http://example.com/path?a=b") + assert url.with_query(None).query_string == "" + + +def test_with_query_bad_type(): + url = URL("http://example.com") + with pytest.raises(TypeError): + url.with_query(123) + + +def test_with_query_bytes(): + url = URL("http://example.com") + with pytest.raises(TypeError): + url.with_query(b"123") + + +def test_with_query_bytearray(): + url = URL("http://example.com") + with pytest.raises(TypeError): + url.with_query(bytearray(b"123")) + + +def test_with_query_memoryview(): + url = URL("http://example.com") + with pytest.raises(TypeError): + url.with_query(memoryview(b"123")) + + +@pytest.mark.parametrize( + ("query", "expected"), + [ + pytest.param([("key", "1;2;3")], "?key=1%3B2%3B3", id="tuple list semicolon"), + pytest.param({"key": "1;2;3"}, "?key=1%3B2%3B3", id="mapping semicolon"), + pytest.param([("key", "1&a=2")], "?key=1%26a%3D2", id="tuple list ampersand"), + pytest.param({"key": "1&a=2"}, "?key=1%26a%3D2", id="mapping ampersand"), + pytest.param([("&", "=")], "?%26=%3D", id="tuple list quote key"), + pytest.param({"&": "="}, "?%26=%3D", id="mapping quote key"), + pytest.param( + [("a[]", "3")], + "?a%5B%5D=3", + id="quote one key braces", + ), + pytest.param( + [("a[]", "3"), ("a[]", "4")], + "?a%5B%5D=3&a%5B%5D=4", + id="quote many key braces", + ), + ], +) +def test_with_query_params(query, expected): + url = URL("http://example.com/get") + url2 = url.with_query(query) + assert str(url2) == ("http://example.com/get" + expected) + + +def test_with_query_only(): + url = URL() + url2 = url.with_query(key="value") + assert str(url2) == "?key=value" + + +def test_with_query_complex_url(): + target_url = "http://example.com/?game=bulls+%26+cows" + url = URL("/redir").with_query({"t": target_url}) + assert url.query["t"] == target_url + + +def test_update_query_multiple_keys(): + url = URL("http://example.com/path?a=1&a=2") + u2 = url.update_query([("a", "3"), ("a", "4")]) + + assert str(u2) == "http://example.com/path?a=3&a=4" + + +# mod operator + + +def test_update_query_with_mod_operator(): + url = URL("http://example.com/") + assert str(url % {"a": "1"}) == "http://example.com/?a=1" + assert str(url % [("a", "1")]) == "http://example.com/?a=1" + assert str(url % "a=1&b=2") == "http://example.com/?a=1&b=2" + assert str(url % {"a": "1"} % {"b": "2"}) == "http://example.com/?a=1&b=2" + assert str(url % {"a": "1"} % {"a": "3", "b": "2"}) == "http://example.com/?a=3&b=2" + assert str(url / "foo" % {"a": "1"}) == "http://example.com/foo?a=1" diff --git a/contrib/python/yarl/tests/test_url.py b/contrib/python/yarl/tests/test_url.py new file mode 100644 index 0000000000..af13d0b5d5 --- /dev/null +++ b/contrib/python/yarl/tests/test_url.py @@ -0,0 +1,1732 @@ +from enum import Enum +from urllib.parse import SplitResult + +import pytest + +from yarl import URL + + +def test_inheritance(): + with pytest.raises(TypeError) as ctx: + + class MyURL(URL): # type: ignore[misc] + pass + + assert ( + "Inheriting a class " + "<class '__tests__.test_url.test_inheritance.<locals>.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_nonascii(): + url = URL("http://user:password@историк.рф:8888/path/to?a=1&b=2") + assert str(url.origin()) == "http://xn--h1aagokeh.xn--p1ai: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 + + +def test_raw_user_non_ascii(): + url = URL("http://вася@example.com") + assert "%D0%B2%D0%B0%D1%81%D1%8F" == url.raw_user + + +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 + + +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 + + +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_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 + + +def test_raw_host_non_ascii(): + url = URL("http://историк.рф") + assert "xn--h1aagokeh.xn--p1ai" == url.raw_host + + +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 + + +def test_raw_host_from_str_with_ipv4(): + url = URL("http://127.0.0.1:80") + assert url.raw_host == "127.0.0.1" + + +def test_raw_host_from_str_with_ipv6(): + url = URL("http://[::1]:80") + assert url.raw_host == "::1" + + +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 == ( + "%D0%B2%D0%B0%D0%BD%D1%8F:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@" + "xn--80aidohy.xn--p1ai:8080" + ) + assert url.authority == "ваня:пароль@айдеко.рф:8080" + + +def test_lowercase(): + url = URL("http://gitHUB.com") + assert url.raw_host == "github.com" + assert url.host == url.raw_host + + +def test_lowercase_nonascii(): + url = URL("http://Айдеко.Рф") + assert url.raw_host == "xn--80aidohy.xn--p1ai" + 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 + + +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 + + +def test_port_for_explicit_port(): + url = URL("http://example.com:8888") + assert 8888 == url.port + + +def test_port_for_implicit_port(): + url = URL("http://example.com") + assert 80 == url.port + + +def test_port_for_relative_url(): + url = URL("/path/to") + assert url.port is None + + +def test_port_for_unknown_scheme(): + url = URL("unknown://example.com") + assert url.port is None + + +def test_explicit_port_for_explicit_port(): + url = URL("http://example.com:8888") + assert 8888 == url.explicit_port + + +def test_explicit_port_for_implicit_port(): + url = URL("http://example.com") + assert url.explicit_port is None + + +def test_explicit_port_for_relative_url(): + url = URL("/path/to") + assert url.explicit_port is None + + +def test_explicit_port_for_unknown_scheme(): + url = URL("unknown://example.com") + assert url.explicit_port is None + + +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 "/%D0%BF%D1%83%D1%82%D1%8C/%D1%81%D1%8E%D0%B4%D0%B0" == 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_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 == "/%D0%BF%D1%83%D1%82%D1%8C?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%D0%BE%D1%80%D1%8C" == 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 ( + "/", + "%D0%BF%D1%83%D1%82%D1%8C", + "%D1%81%D1%8E%D0%B4%D0%B0", + ) == 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%B0/%D1%82%D1%83%D0%B4%D0%B0" + assert url2.parts == ("/", "сюда", "туда") + assert url2.raw_parts == ( + "/", + "%D1%81%D1%8E%D0%B4%D0%B0", + "%D1%82%D1%83%D0%B4%D0%B0", + ) + + +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?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/", "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( + "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%B0/%D1%82%D1%83%D0%B4%D0%B0", + ("/", "сюда", "туда"), + ("/", "%D1%81%D1%8E%D0%B4%D0%B0", "%D1%82%D1%83%D0%B4%D0%B0"), + 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") + + +# 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 == "/%D0%BF%D1%83%D1%82%D1%8C" + assert url.parts == ("/", "путь") + assert url.raw_parts == ("/", "%D0%BF%D1%83%D1%82%D1%8C") + + +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.%D0%BF%D1%83%D1%82%D1%8C" + assert url.parts == ("/", "path.путь") + assert url.raw_parts == ("/", "path.%D0%BF%D1%83%D1%82%D1%8C") + + +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() + + +def test_is_absolute_for_absolute_url(): + url = URL("http://example.com") + assert url.is_absolute() + + +def test_is_non_absolute_for_empty_url(): + url = URL() + assert not url.is_absolute() + + +def test_is_non_absolute_for_empty_url2(): + url = URL("") + assert not url.is_absolute() + + +def test_is_absolute_path_starting_from_double_slash(): + url = URL("//www.python.org") + assert url.is_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() + + +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_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%B2%D0%B0%D1%81%D1%8F" "@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%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_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/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0" + ) == str(url) + + +def test_bytes(): + url = URL("http://example.com/путь/туда") + assert ( + b"http://example.com/%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0" + == 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 + + +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() + + +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/" diff --git a/contrib/python/yarl/tests/test_url_build.py b/contrib/python/yarl/tests/test_url_build.py new file mode 100644 index 0000000000..51969fa849 --- /dev/null +++ b/contrib/python/yarl/tests/test_url_build.py @@ -0,0 +1,259 @@ +import pytest + +from yarl import URL + +# build classmethod + + +def test_build_without_arguments(): + u = URL.build() + assert str(u) == "" + + +def test_build_simple(): + u = URL.build(scheme="http", host="127.0.0.1") + assert str(u) == "http://127.0.0.1" + + +def test_build_with_scheme(): + u = URL.build(scheme="blob", path="path") + assert str(u) == "blob:path" + + +def test_build_with_host(): + u = URL.build(host="127.0.0.1") + assert str(u) == "//127.0.0.1" + assert u == URL("//127.0.0.1") + + +def test_build_with_scheme_and_host(): + u = URL.build(scheme="http", host="127.0.0.1") + assert str(u) == "http://127.0.0.1" + assert u == URL("http://127.0.0.1") + + +def test_build_with_port(): + with pytest.raises(ValueError): + URL.build(port=8000) + + u = URL.build(scheme="http", host="127.0.0.1", port=8000) + assert str(u) == "http://127.0.0.1:8000" + + +def test_build_with_user(): + u = URL.build(scheme="http", host="127.0.0.1", user="foo") + assert str(u) == "http://foo@127.0.0.1" + + +def test_build_with_user_password(): + u = URL.build(scheme="http", host="127.0.0.1", user="foo", password="bar") + assert str(u) == "http://foo:bar@127.0.0.1" + + +def test_build_with_query_and_query_string(): + with pytest.raises(ValueError): + URL.build( + scheme="http", + host="127.0.0.1", + user="foo", + password="bar", + port=8000, + path="/index.html", + query=dict(arg="value1"), + query_string="arg=value1", + fragment="top", + ) + + +def test_build_with_all(): + u = URL.build( + scheme="http", + host="127.0.0.1", + user="foo", + password="bar", + port=8000, + path="/index.html", + query_string="arg=value1", + fragment="top", + ) + assert str(u) == "http://foo:bar@127.0.0.1:8000/index.html?arg=value1#top" + + +def test_build_with_authority_and_host(): + with pytest.raises(ValueError): + URL.build(authority="host.com", host="example.com") + + +def test_build_with_authority(): + url = URL.build(scheme="http", authority="ваня:bar@host.com:8000", path="path") + assert str(url) == "http://%D0%B2%D0%B0%D0%BD%D1%8F:bar@host.com:8000/path" + + +def test_build_with_authority_without_encoding(): + url = URL.build( + scheme="http", authority="foo:bar@host.com:8000", path="path", encoded=True + ) + assert str(url) == "http://foo:bar@host.com:8000/path" + + +def test_query_str(): + u = URL.build(scheme="http", host="127.0.0.1", path="/", query_string="arg=value1") + assert str(u) == "http://127.0.0.1/?arg=value1" + + +def test_query_dict(): + u = URL.build(scheme="http", host="127.0.0.1", path="/", query=dict(arg="value1")) + + assert str(u) == "http://127.0.0.1/?arg=value1" + + +def test_build_path_quoting(): + u = URL.build( + scheme="http", host="127.0.0.1", path="/файл.jpg", query=dict(arg="Привет") + ) + + assert u == URL("http://127.0.0.1/файл.jpg?arg=Привет") + assert str(u) == ( + "http://127.0.0.1/%D1%84%D0%B0%D0%B9%D0%BB.jpg?" + "arg=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82" + ) + + +def test_build_query_quoting(): + u = URL.build(scheme="http", host="127.0.0.1", path="/файл.jpg", query="arg=Привет") + + assert u == URL("http://127.0.0.1/файл.jpg?arg=Привет") + assert str(u) == ( + "http://127.0.0.1/%D1%84%D0%B0%D0%B9%D0%BB.jpg?" + "arg=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82" + ) + + +def test_build_query_only(): + u = URL.build(query={"key": "value"}) + + assert str(u) == "?key=value" + + +def test_build_drop_dots(): + u = URL.build(scheme="http", host="example.com", path="/path/../to") + assert str(u) == "http://example.com/to" + + +def test_build_encode(): + u = URL.build( + scheme="http", + host="историк.рф", + path="/путь/файл", + query_string="ключ=знач", + fragment="фраг", + ) + expected = ( + "http://xn--h1aagokeh.xn--p1ai" + "/%D0%BF%D1%83%D1%82%D1%8C/%D1%84%D0%B0%D0%B9%D0%BB" + "?%D0%BA%D0%BB%D1%8E%D1%87=%D0%B7%D0%BD%D0%B0%D1%87" + "#%D1%84%D1%80%D0%B0%D0%B3" + ) + assert str(u) == expected + + +def test_build_already_encoded(): + # resulting URL is invalid but not encoded + u = URL.build( + scheme="http", + host="историк.рф", + path="/путь/файл", + query_string="ключ=знач", + fragment="фраг", + encoded=True, + ) + assert str(u) == "http://историк.рф/путь/файл?ключ=знач#фраг" + + +def test_build_percent_encoded(): + u = URL.build( + scheme="http", + host="%2d.org", + user="u%2d", + password="p%2d", + path="/%2d", + query_string="k%2d=v%2d", + fragment="f%2d", + ) + assert str(u) == "http://u%252d:p%252d@%2d.org/%252d?k%252d=v%252d#f%252d" + assert u.raw_host == "%2d.org" + assert u.host == "%2d.org" + assert u.raw_user == "u%252d" + assert u.user == "u%2d" + assert u.raw_password == "p%252d" + assert u.password == "p%2d" + assert u.raw_authority == "u%252d:p%252d@%2d.org" + assert u.authority == "u%2d:p%2d@%2d.org:80" + assert u.raw_path == "/%252d" + assert u.path == "/%2d" + assert u.query == {"k%2d": "v%2d"} + assert u.raw_query_string == "k%252d=v%252d" + assert u.query_string == "k%2d=v%2d" + assert u.raw_fragment == "f%252d" + assert u.fragment == "f%2d" + + +def test_build_with_authority_percent_encoded(): + u = URL.build(scheme="http", authority="u%2d:p%2d@%2d.org") + assert str(u) == "http://u%252d:p%252d@%2d.org" + assert u.raw_host == "%2d.org" + assert u.host == "%2d.org" + assert u.raw_user == "u%252d" + assert u.user == "u%2d" + assert u.raw_password == "p%252d" + assert u.password == "p%2d" + assert u.raw_authority == "u%252d:p%252d@%2d.org" + assert u.authority == "u%2d:p%2d@%2d.org:80" + + +def test_build_with_authority_percent_encoded_already_encoded(): + u = URL.build(scheme="http", authority="u%2d:p%2d@%2d.org", encoded=True) + assert str(u) == "http://u%2d:p%2d@%2d.org" + assert u.raw_host == "%2d.org" + assert u.host == "%2d.org" + assert u.user == "u-" + assert u.raw_user == "u%2d" + assert u.password == "p-" + assert u.raw_password == "p%2d" + assert u.authority == "u-:p-@%2d.org:80" + assert u.raw_authority == "u%2d:p%2d@%2d.org" + + +def test_build_with_authority_with_path_with_leading_slash(): + u = URL.build(scheme="http", host="example.com", path="/path_with_leading_slash") + assert str(u) == "http://example.com/path_with_leading_slash" + + +def test_build_with_authority_with_empty_path(): + u = URL.build(scheme="http", host="example.com", path="") + assert str(u) == "http://example.com" + + +def test_build_with_authority_with_path_without_leading_slash(): + with pytest.raises(ValueError): + URL.build(scheme="http", host="example.com", path="path_without_leading_slash") + + +def test_build_with_none_host(): + with pytest.raises(TypeError, match="NoneType is illegal for.*host"): + URL.build(scheme="http", host=None) + + +def test_build_with_none_path(): + with pytest.raises(TypeError): + URL.build(scheme="http", host="example.com", path=None) + + +def test_build_with_none_query_string(): + with pytest.raises(TypeError): + URL.build(scheme="http", host="example.com", query_string=None) + + +def test_build_with_none_fragment(): + with pytest.raises(TypeError): + URL.build(scheme="http", host="example.com", fragment=None) diff --git a/contrib/python/yarl/tests/test_url_cmp_and_hash.py b/contrib/python/yarl/tests/test_url_cmp_and_hash.py new file mode 100644 index 0000000000..17c42e3566 --- /dev/null +++ b/contrib/python/yarl/tests/test_url_cmp_and_hash.py @@ -0,0 +1,88 @@ +from yarl import URL + +# comparison and hashing + + +def test_ne_str(): + url = URL("http://example.com/") + assert url != "http://example.com/" + + +def test_eq(): + url = URL("http://example.com/") + assert url == URL("http://example.com/") + + +def test_hash(): + assert hash(URL("http://example.com/")) == hash(URL("http://example.com/")) + + +def test_hash_double_call(): + url = URL("http://example.com/") + assert hash(url) == hash(url) + + +def test_le_less(): + url1 = URL("http://example1.com/") + url2 = URL("http://example2.com/") + + assert url1 <= url2 + + +def test_le_eq(): + url1 = URL("http://example.com/") + url2 = URL("http://example.com/") + + assert url1 <= url2 + + +def test_le_not_implemented(): + url = URL("http://example1.com/") + + assert url.__le__(123) is NotImplemented + + +def test_lt(): + url1 = URL("http://example1.com/") + url2 = URL("http://example2.com/") + + assert url1 < url2 + + +def test_lt_not_implemented(): + url = URL("http://example1.com/") + + assert url.__lt__(123) is NotImplemented + + +def test_ge_more(): + url1 = URL("http://example1.com/") + url2 = URL("http://example2.com/") + + assert url2 >= url1 + + +def test_ge_eq(): + url1 = URL("http://example.com/") + url2 = URL("http://example.com/") + + assert url2 >= url1 + + +def test_ge_not_implemented(): + url = URL("http://example1.com/") + + assert url.__ge__(123) is NotImplemented + + +def test_gt(): + url1 = URL("http://example1.com/") + url2 = URL("http://example2.com/") + + assert url2 > url1 + + +def test_gt_not_implemented(): + url = URL("http://example1.com/") + + assert url.__gt__(123) is NotImplemented diff --git a/contrib/python/yarl/tests/test_url_parsing.py b/contrib/python/yarl/tests/test_url_parsing.py new file mode 100644 index 0000000000..cc753fcd0c --- /dev/null +++ b/contrib/python/yarl/tests/test_url_parsing.py @@ -0,0 +1,582 @@ +import sys + +import pytest + +from yarl import URL + + +class TestScheme: + def test_scheme_path(self): + u = URL("scheme:path") + assert u.scheme == "scheme" + assert u.host is None + assert u.path == "path" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_path_other(self): + u = URL("scheme:path:other") + assert u.scheme == "scheme" + assert u.host is None + assert u.path == "path:other" + assert u.query_string == "" + assert u.fragment == "" + + def test_complex_scheme(self): + u = URL("allow+chars-33.:path") + assert u.scheme == "allow+chars-33." + assert u.host is None + assert u.path == "path" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_only(self): + u = URL("simple:") + assert u.scheme == "simple" + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "" + + def test_no_scheme1(self): + u = URL("google.com:80") + # See: https://bugs.python.org/issue27657 + if ( + sys.version_info[:3] == (3, 7, 6) + or sys.version_info[:3] == (3, 8, 1) + or sys.version_info >= (3, 9, 0) + ): + assert u.scheme == "google.com" + assert u.host is None + assert u.path == "80" + else: + assert u.scheme == "" + assert u.host is None + assert u.path == "google.com:80" + assert u.query_string == "" + assert u.fragment == "" + + def test_no_scheme2(self): + u = URL("google.com:80/root") + assert u.scheme == "google.com" + assert u.host is None + assert u.path == "80/root" + assert u.query_string == "" + assert u.fragment == "" + + def test_not_a_scheme1(self): + u = URL("not_cheme:path") + assert u.scheme == "" + assert u.host is None + assert u.path == "not_cheme:path" + assert u.query_string == "" + assert u.fragment == "" + + def test_not_a_scheme2(self): + u = URL("signals37:book") + assert u.scheme == "signals37" + assert u.host is None + assert u.path == "book" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_rel_path1(self): + u = URL(":relative-path") + assert u.scheme == "" + assert u.host is None + assert u.path == ":relative-path" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_rel_path2(self): + u = URL(":relative/path") + assert u.scheme == "" + assert u.host is None + assert u.path == ":relative/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_weird(self): + u = URL("://and-this") + assert u.scheme == "" + assert u.host is None + assert u.path == "://and-this" + assert u.query_string == "" + assert u.fragment == "" + + +class TestHost: + def test_canonical(self): + u = URL("scheme://host/path") + assert u.scheme == "scheme" + assert u.host == "host" + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_absolute_no_scheme(self): + u = URL("//host/path") + assert u.scheme == "" + assert u.host == "host" + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_absolute_no_scheme_complex_host(self): + u = URL("//host+path") + assert u.scheme == "" + assert u.host == "host+path" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_absolute_no_scheme_simple_host(self): + u = URL("//host") + assert u.scheme == "" + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_weird_host(self): + u = URL("//this+is$also&host!") + assert u.scheme == "" + assert u.host == "this+is$also&host!" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_no_host(self): + u = URL("scheme:/host/path") + assert u.scheme == "scheme" + assert u.host is None + assert u.path == "/host/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_scheme_no_host2(self): + u = URL("scheme:///host/path") + assert u.scheme == "scheme" + assert u.host is None + assert u.path == "/host/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_no_scheme_no_host(self): + u = URL("scheme//host/path") + assert u.scheme == "" + assert u.host is None + assert u.path == "scheme//host/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_ipv4(self): + u = URL("//127.0.0.1/") + assert u.scheme == "" + assert u.host == "127.0.0.1" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_ipv6(self): + u = URL("//[::1]/") + assert u.scheme == "" + assert u.host == "::1" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_ipvfuture_address(self): + u = URL("//[v1.-1]/") + assert u.scheme == "" + assert u.host == "v1.-1" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + +class TestPort: + def test_canonical(self): + u = URL("//host:80/path") + assert u.scheme == "" + assert u.host == "host" + assert u.port == 80 + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_no_path(self): + u = URL("//host:80") + assert u.scheme == "" + assert u.host == "host" + assert u.port == 80 + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + @pytest.mark.xfail(reason="https://github.com/aio-libs/yarl/issues/821") + def test_no_host(self): + u = URL("//:80") + assert u.scheme == "" + assert u.host == "" + assert u.port == 80 + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_double_port(self): + with pytest.raises(ValueError): + URL("//h:22:80/") + + def test_bad_port(self): + with pytest.raises(ValueError): + URL("//h:no/path") + + def test_another_bad_port(self): + with pytest.raises(ValueError): + URL("//h:22:no/path") + + def test_bad_port_again(self): + with pytest.raises(ValueError): + URL("//h:-80/path") + + +class TestUserInfo: + def test_canonical(self): + u = URL("sch://user@host/") + assert u.scheme == "sch" + assert u.user == "user" + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_user_pass(self): + u = URL("//user:pass@host") + assert u.scheme == "" + assert u.user == "user" + assert u.password == "pass" + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_complex_userinfo(self): + u = URL("//user:pas:and:more@host") + assert u.scheme == "" + assert u.user == "user" + assert u.password == "pas:and:more" + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_no_user(self): + u = URL("//:pas:@host") + assert u.scheme == "" + assert u.user is None + assert u.password == "pas:" + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_weird_user(self): + u = URL("//!($&')*+,;=@host") + assert u.scheme == "" + assert u.user == "!($&')*+,;=" + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_weird_user2(self): + u = URL("//user@info@ya.ru") + assert u.scheme == "" + assert u.user == "user@info" + assert u.password is None + assert u.host == "ya.ru" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_weird_user3(self): + u = URL("//%5Bsome%5D@host") + assert u.scheme == "" + assert u.user == "[some]" + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + +class TestQuery_String: + def test_simple(self): + u = URL("?query") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "query" + assert u.fragment == "" + + def test_scheme_query(self): + u = URL("http:?query") + assert u.scheme == "http" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "query" + assert u.fragment == "" + + def test_abs_url_query(self): + u = URL("//host?query") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "query" + assert u.fragment == "" + + def test_abs_url_path_query(self): + u = URL("//host/path?query") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/path" + assert u.query_string == "query" + assert u.fragment == "" + + def test_double_question_mark(self): + u = URL("//ho?st/path?query") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "ho" + assert u.path == "/" + assert u.query_string == "st/path?query" + assert u.fragment == "" + + def test_complex_query(self): + u = URL("?a://b:c@d.e/f?g#h") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "a://b:c@d.e/f?g" + assert u.fragment == "h" + + def test_query_in_fragment(self): + u = URL("#?query") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "?query" + + +class TestFragment: + def test_simple(self): + u = URL("#frag") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "frag" + + def test_scheme_frag(self): + u = URL("http:#frag") + assert u.scheme == "http" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "frag" + + def test_host_frag(self): + u = URL("//host#frag") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "frag" + + def test_scheme_path_frag(self): + u = URL("//host/path#frag") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "frag" + + def test_scheme_query_frag(self): + u = URL("//host?query#frag") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "query" + assert u.fragment == "frag" + + def test_host_frag_query(self): + u = URL("//ho#st/path?query") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "ho" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "st/path?query" + + def test_complex_frag(self): + u = URL("#a://b:c@d.e/f?g#h") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "a://b:c@d.e/f?g#h" + + +class TestStripEmptyParts: + def test_all_empty(self): + with pytest.raises(ValueError): + URL("//@:?#") + + def test_path_only(self): + u = URL("///path") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_user(self): + u = URL("//@host") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_port(self): + u = URL("//host:") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_port_and_path(self): + u = URL("//host:/") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host == "host" + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_path_only(self): + u = URL("/") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "/" + assert u.query_string == "" + assert u.fragment == "" + + def test_relative_path_only(self): + u = URL("path") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "path" + assert u.query_string == "" + assert u.fragment == "" + + def test_path(self): + u = URL("/path") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_query_with_path(self): + u = URL("/path?") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_query(self): + u = URL("?") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_query_with_frag(self): + u = URL("?#frag") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "frag" + + def test_path_empty_frag(self): + u = URL("/path#") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "/path" + assert u.query_string == "" + assert u.fragment == "" + + def test_empty_path(self): + u = URL("#") + assert u.scheme == "" + assert u.user is None + assert u.password is None + assert u.host is None + assert u.path == "" + assert u.query_string == "" + assert u.fragment == "" diff --git a/contrib/python/yarl/tests/test_url_query.py b/contrib/python/yarl/tests/test_url_query.py new file mode 100644 index 0000000000..bcd2433cbc --- /dev/null +++ b/contrib/python/yarl/tests/test_url_query.py @@ -0,0 +1,173 @@ +from typing import List, Tuple +from urllib.parse import parse_qs, urlencode + +import pytest +from multidict import MultiDict, MultiDictProxy + +from yarl import URL + +# ======================================== +# Basic chars in query values +# ======================================== + +URLS_WITH_BASIC_QUERY_VALUES: List[Tuple[URL, MultiDict]] = [ + # Empty strings, keys and values + ( + URL("http://example.com"), + MultiDict(), + ), + ( + URL("http://example.com?a="), + MultiDict([("a", "")]), + ), + # ASCII chars + ( + URL("http://example.com?a+b=c+d"), + MultiDict({"a b": "c d"}), + ), + ( + URL("http://example.com?a=1&b=2"), + MultiDict([("a", "1"), ("b", "2")]), + ), + ( + URL("http://example.com?a=1&b=2&a=3"), + MultiDict([("a", "1"), ("b", "2"), ("a", "3")]), + ), + # Non-ASCI BMP chars + ( + URL("http://example.com?ключ=знач"), + MultiDict({"ключ": "знач"}), + ), + ( + URL("http://example.com?foo=ᴜɴɪᴄᴏᴅᴇ"), + MultiDict({"foo": "ᴜɴɪᴄᴏᴅᴇ"}), + ), + # Non-BMP chars + ( + URL("http://example.com?bar=𝕦𝕟𝕚𝕔𝕠𝕕𝕖"), + MultiDict({"bar": "𝕦𝕟𝕚𝕔𝕠𝕕𝕖"}), + ), +] + + +@pytest.mark.parametrize( + "original_url, expected_query", + URLS_WITH_BASIC_QUERY_VALUES, +) +def test_query_basic_parsing(original_url, expected_query): + assert isinstance(original_url.query, MultiDictProxy) + assert original_url.query == expected_query + + +@pytest.mark.parametrize( + "original_url, expected_query", + URLS_WITH_BASIC_QUERY_VALUES, +) +def test_query_basic_update_query(original_url, expected_query): + new_url = original_url.update_query({}) + assert new_url == original_url + + +def test_query_dont_unqoute_twice(): + sample_url = "http://base.place?" + urlencode({"a": "/////"}) + query = urlencode({"url": sample_url}) + full_url = "http://test_url.aha?" + query + + url = URL(full_url) + assert url.query["url"] == sample_url + + +# ======================================== +# Reserved chars in query values +# ======================================== + +# See https://github.com/python/cpython#87133, which introduced a new +# `separator` keyword argument to `urllib.parse.parse_qs` (among others). +# If the name doesn't exist as a variable in the function bytecode, the +# test is expected to fail. +_SEMICOLON_XFAIL = pytest.mark.xfail( + condition="separator" not in parse_qs.__code__.co_varnames, + reason=( + "Python versions < 3.7.10, < 3.8.8 and < 3.9.2 lack a fix for " + 'CVE-2021-23336 dropping ";" as a valid query parameter separator, ' + "making this test fail." + ), + strict=True, +) + + +URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES = [ + # Ampersand + (URL("http://127.0.0.1/?a=10&b=20"), 2, "10"), + (URL("http://127.0.0.1/?a=10%26b=20"), 1, "10&b=20"), + (URL("http://127.0.0.1/?a=10%3Bb=20"), 1, "10;b=20"), + # Semicolon, which is *not* a query parameter separator as of RFC3986 + (URL("http://127.0.0.1/?a=10;b=20"), 1, "10;b=20"), + (URL("http://127.0.0.1/?a=10%26b=20"), 1, "10&b=20"), + (URL("http://127.0.0.1/?a=10%3Bb=20"), 1, "10;b=20"), +] +URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES_W_XFAIL = [ + # Ampersand + *URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES[:3], + # Semicolon, which is *not* a query parameter separator as of RFC3986 + # Mark the first of these as expecting to fail on old Python patch releases. + pytest.param(*URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES[3], marks=_SEMICOLON_XFAIL), + *URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES[4:], +] + + +@pytest.mark.parametrize( + "original_url, expected_query_len, expected_value_a", + URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES_W_XFAIL, +) +def test_query_separators_from_parsing( + original_url, + expected_query_len, + expected_value_a, +): + assert len(original_url.query) == expected_query_len + assert original_url.query["a"] == expected_value_a + + +@pytest.mark.parametrize( + "original_url, expected_query_len, expected_value_a", + URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES_W_XFAIL, +) +def test_query_separators_from_update_query( + original_url, + expected_query_len, + expected_value_a, +): + new_url = original_url.update_query({"c": expected_value_a}) + assert new_url.query["a"] == expected_value_a + assert new_url.query["c"] == expected_value_a + + +@pytest.mark.parametrize( + "original_url, expected_query_len, expected_value_a", + URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES, +) +def test_query_separators_from_with_query( + original_url, + expected_query_len, + expected_value_a, +): + new_url = original_url.with_query({"c": expected_value_a}) + assert new_url.query["c"] == expected_value_a + + +@pytest.mark.parametrize( + "original_url, expected_query_len, expected_value_a", + URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES, +) +def test_query_from_empty_update_query( + original_url, + expected_query_len, + expected_value_a, +): + new_url = original_url.update_query({}) + + assert new_url.query["a"] == original_url.query["a"] + + if "b" in original_url.query: + assert new_url.query["b"] == original_url.query["b"] diff --git a/contrib/python/yarl/tests/test_url_update_netloc.py b/contrib/python/yarl/tests/test_url_update_netloc.py new file mode 100644 index 0000000000..cf0cc1c44c --- /dev/null +++ b/contrib/python/yarl/tests/test_url_update_netloc.py @@ -0,0 +1,228 @@ +import pytest + +from yarl import URL + +# with_* + + +def test_with_scheme(): + url = URL("http://example.com") + assert str(url.with_scheme("https")) == "https://example.com" + + +def test_with_scheme_uppercased(): + url = URL("http://example.com") + assert str(url.with_scheme("HTTPS")) == "https://example.com" + + +def test_with_scheme_for_relative_url(): + with pytest.raises(ValueError): + URL("path/to").with_scheme("http") + + +def test_with_scheme_invalid_type(): + url = URL("http://example.com") + with pytest.raises(TypeError): + assert str(url.with_scheme(123)) + + +def test_with_user(): + url = URL("http://example.com") + assert str(url.with_user("john")) == "http://john@example.com" + + +def test_with_user_non_ascii(): + url = URL("http://example.com") + url2 = url.with_user("вася") + assert url2.raw_user == "%D0%B2%D0%B0%D1%81%D1%8F" + assert url2.user == "вася" + assert url2.raw_authority == "%D0%B2%D0%B0%D1%81%D1%8F@example.com" + assert url2.authority == "вася@example.com:80" + + +def test_with_user_percent_encoded(): + url = URL("http://example.com") + url2 = url.with_user("%cf%80") + assert url2.raw_user == "%25cf%2580" + assert url2.user == "%cf%80" + assert url2.raw_authority == "%25cf%2580@example.com" + assert url2.authority == "%cf%80@example.com:80" + + +def test_with_user_for_relative_url(): + with pytest.raises(ValueError): + URL("path/to").with_user("user") + + +def test_with_user_invalid_type(): + url = URL("http://example.com:123") + with pytest.raises(TypeError): + url.with_user(123) + + +def test_with_user_None(): + url = URL("http://john@example.com") + assert str(url.with_user(None)) == "http://example.com" + + +def test_with_user_ipv6(): + url = URL("http://john:pass@[::1]:8080/") + assert str(url.with_user(None)) == "http://[::1]:8080/" + + +def test_with_user_None_when_password_present(): + url = URL("http://john:pass@example.com") + assert str(url.with_user(None)) == "http://example.com" + + +def test_with_password(): + url = URL("http://john@example.com") + assert str(url.with_password("pass")) == "http://john:pass@example.com" + + +def test_with_password_ipv6(): + url = URL("http://john:pass@[::1]:8080/") + assert str(url.with_password(None)) == "http://john@[::1]:8080/" + + +def test_with_password_non_ascii(): + url = URL("http://john@example.com") + url2 = url.with_password("пароль") + assert url2.raw_password == "%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" + assert url2.password == "пароль" + assert url2.raw_authority == "john:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@example.com" + assert url2.authority == "john:пароль@example.com:80" + + +def test_with_password_percent_encoded(): + url = URL("http://john@example.com") + url2 = url.with_password("%cf%80") + assert url2.raw_password == "%25cf%2580" + assert url2.password == "%cf%80" + assert url2.raw_authority == "john:%25cf%2580@example.com" + assert url2.authority == "john:%cf%80@example.com:80" + + +def test_with_password_non_ascii_with_colon(): + url = URL("http://john@example.com") + url2 = url.with_password("п:а") + assert url2.raw_password == "%D0%BF%3A%D0%B0" + assert url2.password == "п:а" + + +def test_with_password_for_relative_url(): + with pytest.raises(ValueError): + URL("path/to").with_password("pass") + + +def test_with_password_None(): + url = URL("http://john:pass@example.com") + assert str(url.with_password(None)) == "http://john@example.com" + + +def test_with_password_invalid_type(): + url = URL("http://example.com:123") + with pytest.raises(TypeError): + url.with_password(123) + + +def test_with_password_and_empty_user(): + url = URL("http://example.com") + url2 = url.with_password("pass") + assert url2.password == "pass" + assert url2.user is None + assert str(url2) == "http://:pass@example.com" + + +def test_from_str_with_host_ipv4(): + url = URL("http://host:80") + url = url.with_host("192.168.1.1") + assert url.raw_host == "192.168.1.1" + + +def test_from_str_with_host_ipv6(): + url = URL("http://host:80") + url = url.with_host("::1") + assert url.raw_host == "::1" + + +def test_with_host(): + url = URL("http://example.com:123") + assert str(url.with_host("example.org")) == "http://example.org:123" + + +def test_with_host_empty(): + url = URL("http://example.com:123") + with pytest.raises(ValueError): + url.with_host("") + + +def test_with_host_non_ascii(): + url = URL("http://example.com:123") + url2 = url.with_host("историк.рф") + assert url2.raw_host == "xn--h1aagokeh.xn--p1ai" + assert url2.host == "историк.рф" + assert url2.raw_authority == "xn--h1aagokeh.xn--p1ai:123" + assert url2.authority == "историк.рф:123" + + +def test_with_host_percent_encoded(): + url = URL("http://%25cf%2580%cf%80:%25cf%2580%cf%80@example.com:123") + url2 = url.with_host("%cf%80.org") + assert url2.raw_host == "%cf%80.org" + assert url2.host == "%cf%80.org" + assert url2.raw_authority == "%25cf%2580%CF%80:%25cf%2580%CF%80@%cf%80.org:123" + assert url2.authority == "%cf%80π:%cf%80π@%cf%80.org:123" + + +def test_with_host_for_relative_url(): + with pytest.raises(ValueError): + URL("path/to").with_host("example.com") + + +def test_with_host_invalid_type(): + url = URL("http://example.com:123") + with pytest.raises(TypeError): + url.with_host(None) + + +def test_with_port(): + url = URL("http://example.com") + assert str(url.with_port(8888)) == "http://example.com:8888" + + +def test_with_port_with_no_port(): + url = URL("http://example.com") + assert str(url.with_port(None)) == "http://example.com" + + +def test_with_port_ipv6(): + url = URL("http://[::1]:8080/") + assert str(url.with_port(80)) == "http://[::1]:80/" + + +def test_with_port_keeps_query_and_fragment(): + url = URL("http://example.com/?a=1#frag") + assert str(url.with_port(8888)) == "http://example.com:8888/?a=1#frag" + + +def test_with_port_percent_encoded(): + url = URL("http://user%name:pass%word@example.com/") + assert str(url.with_port(808)) == "http://user%25name:pass%25word@example.com:808/" + + +def test_with_port_for_relative_url(): + with pytest.raises(ValueError): + URL("path/to").with_port(1234) + + +def test_with_port_invalid_type(): + with pytest.raises(TypeError): + URL("http://example.com").with_port("123") + with pytest.raises(TypeError): + URL("http://example.com").with_port(True) + + +def test_with_port_invalid_range(): + with pytest.raises(ValueError): + URL("http://example.com").with_port(-1) diff --git a/contrib/python/yarl/tests/ya.make b/contrib/python/yarl/tests/ya.make new file mode 100644 index 0000000000..f86b0f6380 --- /dev/null +++ b/contrib/python/yarl/tests/ya.make @@ -0,0 +1,24 @@ +PY3TEST() + +PEERDIR( + contrib/python/yarl +) + +TEST_SRCS( + test_cache.py + test_cached_property.py + test_normalize_path.py + test_pickle.py + test_quoting.py + test_update_query.py + test_url.py + test_url_build.py + test_url_cmp_and_hash.py + test_url_parsing.py + test_url_query.py + test_url_update_netloc.py +) + +NO_LINT() + +END() |