From d8b6c06f61a3792ea405d3ce1d85797041f70a91 Mon Sep 17 00:00:00 2001 From: abebus Date: Fri, 23 May 2025 18:11:29 +0300 Subject: [PATCH 1/7] add support for returning type of urllib.parse.ParseResult --- can_ada-stubs/__init__.pyi | 6 +++- src/binding.cpp | 69 +++++++++++++++++++++++++++++++++++++- tests/test_benchmark.py | 12 +++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/can_ada-stubs/__init__.pyi b/can_ada-stubs/__init__.pyi index 389d4fa..242e40d 100644 --- a/can_ada-stubs/__init__.pyi +++ b/can_ada-stubs/__init__.pyi @@ -1,4 +1,7 @@ -from typing import Iterator, overload +from typing import Iterator, overload, TYPE_CHECKING + +if TYPE_CHECKING: + from urllib.parse import ParseResult __version__: str @@ -68,3 +71,4 @@ def can_parse(input: str, base_input: str | None = ...) -> bool: ... def idna_decode(arg0: str) -> str: ... def idna_encode(arg0: str) -> bytes: ... def parse(arg0: str) -> URL: ... +def parse_compat(arg0: str) -> ParseResult: ... diff --git a/src/binding.cpp b/src/binding.cpp index eac2a68..d33a9be 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -9,7 +9,12 @@ namespace py = nanobind; -NB_MODULE(can_ada, m) { +static py::object get_parse_result_class() { + static py::object cls = py::module_::import("urllib.parse").attr("ParseResult"); + return cls; +} + +PYBIND11_MODULE(can_ada, m) { #ifdef VERSION_INFO m.attr("__version__") = Py_STRINGIFY(VERSION_INFO); #else @@ -153,4 +158,66 @@ NB_MODULE(can_ada, m) { return std::move(*url); }); + m.def("parse_compat", [](std::string_view input) { + ada::result result = ada::parse(input); + if (!result) { + throw py::value_error("URL could not be parsed."); + } + + auto& url = result.value(); + + std::string scheme = [&] { + std::string s = std::string(url.get_protocol()); + return (!s.empty() && s.back() == ':') ? s.substr(0, s.size() - 1) : s; + }(); + + + std::string netloc; + if (url.has_non_empty_username()) { + netloc += std::string(url.get_username()); + if (url.has_password()) { + netloc += ":" + std::string(url.get_password()); + } + netloc += "@"; + } + netloc += std::string(url.get_host()); + if (url.has_port()) { + netloc += ":" + std::string(url.get_port()); + } + + std::string path, params; + // not really correct, but this is urllib.parse.urlparse behaviour + [&] { + std::string raw_path = std::string(url.get_pathname()); + size_t last_slash = raw_path.rfind('/'); + std::string last_segment = (last_slash != std::string::npos) + ? raw_path.substr(last_slash + 1) + : raw_path; + + size_t semi = last_segment.find(';'); + if (semi != std::string::npos) { + path = (last_slash != std::string::npos ? raw_path.substr(0, last_slash + 1) : "") + + last_segment.substr(0, semi); + params = last_segment.substr(semi + 1); + } else { + path = raw_path; + params = ""; + } + }(); + + std::string query = [&] { + std::string s = std::string(url.get_search()); + return (!s.empty() && s.front() == '?') ? s.substr(1) : s; + }(); + + std::string fragment = [&] { + std::string s = std::string(url.get_hash()); + return (!s.empty() && s.front() == '#') ? s.substr(1) : s; + }(); + + + return get_parse_result_class()(scheme, netloc, path, params, query, fragment); + }); + + } diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index 639e0d3..2c35731 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -40,6 +40,14 @@ def can_ada_parse(): # not valid WHATWG URLs. pass +def can_ada_parse_compat(): + for line in data(): + try: + can_ada.parse_compat(line) + except ValueError: + # There are a small number of URLs in the sample data that are + # not valid WHATWG URLs. + pass def yarl_parse(): for line in data(): @@ -69,3 +77,7 @@ def test_can_ada_parse(benchmark): @pytest.mark.slow def test_yarl_parse(benchmark): benchmark(yarl_parse) + +@pytest.mark.slow +def test_can_ada_parse_compat(benchmark): + benchmark(can_ada_parse_compat) From 18cbd9a8496cce1b31ebbee39f3e6ca4b4e783c4 Mon Sep 17 00:00:00 2001 From: abebus Date: Wed, 28 Jan 2026 21:56:05 +0300 Subject: [PATCH 2/7] fix after rebase --- src/binding.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binding.cpp b/src/binding.cpp index d33a9be..ea3ed39 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -14,7 +14,7 @@ static py::object get_parse_result_class() { return cls; } -PYBIND11_MODULE(can_ada, m) { +NB_MODULE(can_ada, m) { #ifdef VERSION_INFO m.attr("__version__") = Py_STRINGIFY(VERSION_INFO); #else From b62362321bbac5eeec3bd19c27ba34f6222c145e Mon Sep 17 00:00:00 2001 From: abebus Date: Wed, 28 Jan 2026 22:10:47 +0300 Subject: [PATCH 3/7] fix after rebase 2 --- src/binding.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binding.cpp b/src/binding.cpp index ea3ed39..02684dd 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -10,7 +10,7 @@ namespace py = nanobind; static py::object get_parse_result_class() { - static py::object cls = py::module_::import("urllib.parse").attr("ParseResult"); + static py::object cls = py::module_::import_("urllib.parse").attr("ParseResult"); return cls; } From 10509a248262f23ce82c07fe4e96b14d38597d08 Mon Sep 17 00:00:00 2001 From: abebus Date: Thu, 29 Jan 2026 02:22:37 +0300 Subject: [PATCH 4/7] add support for ParseResultBytes --- can_ada-stubs/__init__.pyi | 9 +-- src/binding.cpp | 124 ++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 60 deletions(-) diff --git a/can_ada-stubs/__init__.pyi b/can_ada-stubs/__init__.pyi index 242e40d..19ff9aa 100644 --- a/can_ada-stubs/__init__.pyi +++ b/can_ada-stubs/__init__.pyi @@ -1,7 +1,5 @@ -from typing import Iterator, overload, TYPE_CHECKING - -if TYPE_CHECKING: - from urllib.parse import ParseResult +from typing import Iterator, overload +from urllib.parse import ParseResult, ParseResultBytes __version__: str @@ -71,4 +69,7 @@ def can_parse(input: str, base_input: str | None = ...) -> bool: ... def idna_decode(arg0: str) -> str: ... def idna_encode(arg0: str) -> bytes: ... def parse(arg0: str) -> URL: ... +@overload def parse_compat(arg0: str) -> ParseResult: ... +@overload +def parse_compat(arg0: bytes) -> ParseResultBytes: ... diff --git a/src/binding.cpp b/src/binding.cpp index 02684dd..92f4d63 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -9,10 +9,8 @@ namespace py = nanobind; -static py::object get_parse_result_class() { - static py::object cls = py::module_::import_("urllib.parse").attr("ParseResult"); - return cls; -} +static py::object parse_compat_impl(std::string_view input, + py::object ParseResult_type); NB_MODULE(can_ada, m) { #ifdef VERSION_INFO @@ -158,66 +156,80 @@ NB_MODULE(can_ada, m) { return std::move(*url); }); - m.def("parse_compat", [](std::string_view input) { - ada::result result = ada::parse(input); - if (!result) { - throw py::value_error("URL could not be parsed."); - } + auto urllib = py::module_::import_("urllib.parse"); - auto& url = result.value(); + static auto ParseResult = urllib.attr("ParseResult"); + static auto ParseResultBytes = urllib.attr("ParseResultBytes"); - std::string scheme = [&] { - std::string s = std::string(url.get_protocol()); - return (!s.empty() && s.back() == ':') ? s.substr(0, s.size() - 1) : s; - }(); + m.def("parse_compat", [&](py::bytes input) { + return parse_compat_impl(std::string_view(input.c_str(), input.size()), + ParseResultBytes); + }); + m.def("parse_compat", [&](std::string_view input) { + return parse_compat_impl(input, ParseResult); + }); +} - std::string netloc; +static py::object parse_compat_impl(std::string_view input, + py::object ParseResult_type) { + auto result = ada::parse(input); + if (!result) { + throw py::value_error("URL could not be parsed."); + } + + auto url = std::move(*result); + + auto scheme = url.get_protocol(); + if (!scheme.empty() && scheme.back() == ':') { + scheme.remove_suffix(1); + } + + std::string netloc; + { if (url.has_non_empty_username()) { - netloc += std::string(url.get_username()); + netloc.append(url.get_username()); if (url.has_password()) { - netloc += ":" + std::string(url.get_password()); + netloc.push_back(':'); + netloc.append(url.get_password()); } - netloc += "@"; - } - netloc += std::string(url.get_host()); - if (url.has_port()) { - netloc += ":" + std::string(url.get_port()); + netloc.push_back('@'); } - std::string path, params; - // not really correct, but this is urllib.parse.urlparse behaviour - [&] { - std::string raw_path = std::string(url.get_pathname()); - size_t last_slash = raw_path.rfind('/'); - std::string last_segment = (last_slash != std::string::npos) - ? raw_path.substr(last_slash + 1) - : raw_path; - - size_t semi = last_segment.find(';'); - if (semi != std::string::npos) { - path = (last_slash != std::string::npos ? raw_path.substr(0, last_slash + 1) : "") - + last_segment.substr(0, semi); - params = last_segment.substr(semi + 1); - } else { - path = raw_path; - params = ""; - } - }(); - - std::string query = [&] { - std::string s = std::string(url.get_search()); - return (!s.empty() && s.front() == '?') ? s.substr(1) : s; - }(); - - std::string fragment = [&] { - std::string s = std::string(url.get_hash()); - return (!s.empty() && s.front() == '#') ? s.substr(1) : s; - }(); - - - return get_parse_result_class()(scheme, netloc, path, params, query, fragment); - }); - + netloc.append(url.get_host()); + if (url.has_port()) { + netloc.push_back(':'); + netloc.append(url.get_port()); + } + } + + auto raw_path = url.get_pathname(); + auto path = raw_path; + std::string_view params{}; + + auto last_slash = raw_path.rfind('/'); + auto last_segment = (last_slash != std::string_view::npos) + ? raw_path.substr(last_slash + 1) + : raw_path; + + auto semi = last_segment.find(';'); + if (semi != std::string_view::npos) { + path = (last_slash != std::string_view::npos) + ? raw_path.substr(0, last_slash + 1 + semi) + : last_segment.substr(0, semi); + params = last_segment.substr(semi + 1); + } + + auto query = url.get_search(); + if (!query.empty() && query.front() == '?') { + query.remove_prefix(1); + } + + auto fragment = url.get_hash(); + if (!fragment.empty() && fragment.front() == '#') { + fragment.remove_prefix(1); + } + + return ParseResult_type(scheme, netloc, path, params, query, fragment); } From e58986af160fb75d87f1f6c3971cb1f2fdb06222 Mon Sep 17 00:00:00 2001 From: abebus Date: Thu, 29 Jan 2026 22:07:34 +0300 Subject: [PATCH 5/7] add compatibility tests and fix ParseResultBytes --- src/binding.cpp | 31 +++++++--- tests/conftest.py | 15 +++++ tests/test_benchmark.py | 124 +++++++++++++++++++++------------------- tests/test_compat.py | 17 ++++++ 4 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 tests/test_compat.py diff --git a/src/binding.cpp b/src/binding.cpp index 92f4d63..3ff7533 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -9,8 +9,16 @@ namespace py = nanobind; -static py::object parse_compat_impl(std::string_view input, - py::object ParseResult_type); +struct parse_impl_result { + std::string_view scheme; + std::string_view netloc; + std::string_view path; + std::string_view params; + std::string_view query; + std::string_view fragment; +}; + +static parse_impl_result parse_compat_impl(std::string_view input); NB_MODULE(can_ada, m) { #ifdef VERSION_INFO @@ -162,17 +170,24 @@ NB_MODULE(can_ada, m) { static auto ParseResultBytes = urllib.attr("ParseResultBytes"); m.def("parse_compat", [&](py::bytes input) { - return parse_compat_impl(std::string_view(input.c_str(), input.size()), - ParseResultBytes); + auto [scheme, netloc, path, params, query, fragment] = + parse_compat_impl(std::string_view(input.c_str(), input.size())); + return ParseResult(scheme, netloc, path, params, query, fragment); }); m.def("parse_compat", [&](std::string_view input) { - return parse_compat_impl(input, ParseResult); + auto [scheme, netloc, path, params, query, fragment] = + parse_compat_impl(input); + return ParseResultBytes(py::bytes(scheme.data(), scheme.size()), + py::bytes(netloc.data(), netloc.size()), + py::bytes(path.data(), path.size()), + py::bytes(params.data(), params.size()), + py::bytes(query.data(), query.size()), + py::bytes(fragment.data(), fragment.size())); }); } -static py::object parse_compat_impl(std::string_view input, - py::object ParseResult_type) { +static parse_impl_result parse_compat_impl(std::string_view input) { auto result = ada::parse(input); if (!result) { throw py::value_error("URL could not be parsed."); @@ -231,5 +246,5 @@ static py::object parse_compat_impl(std::string_view input, fragment.remove_prefix(1); } - return ParseResult_type(scheme, netloc, path, params, query, fragment); + return {scheme, netloc, path, params, query, fragment}; } diff --git a/tests/conftest.py b/tests/conftest.py index 0769639..f5321ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +from pathlib import Path import pytest @@ -23,3 +24,17 @@ def pytest_collection_modifyitems(config, items): for item in items: if 'slow' in item.keywords: item.add_marker(skip_slow) + + +@pytest.fixture(scope="session") +def top100str() -> list[str]: + current_file_dir = Path(__file__).parent + with open(current_file_dir / "data" / "top100.txt", "r") as f: + return f.readlines() + + +@pytest.fixture(scope="session") +def top100bytes() -> list[bytes]: + current_file_dir = Path(__file__).parent + with open(current_file_dir / "data" / "top100.txt", "rb") as f: + return f.readlines() diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index 2c35731..5a52153 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -1,6 +1,5 @@ -from functools import lru_cache -from pathlib import Path - +from typing import Callable, Any +from collections.abc import Iterable import pytest import urllib.parse @@ -9,75 +8,84 @@ yarl = pytest.importorskip("yarl") -@lru_cache -def data() -> list[str]: - current_file_dir = Path(__file__).parent - with open(current_file_dir / "data" / "top100.txt", "r") as f: - return f.readlines() - - -def urllib_parse(): - for line in data(): - urllib.parse.urlparse(line) - - -def ada_python_parse(): - for line in data(): - try: - ada_url.URL(line) - except ValueError: - # There are a small number of URLs in the sample data that are - # not valid WHATWG URLs. - pass - - -def can_ada_parse(): - for line in data(): - try: - can_ada.parse(line) - except ValueError: - # There are a small number of URLs in the sample data that are - # not valid WHATWG URLs. - pass - -def can_ada_parse_compat(): - for line in data(): - try: - can_ada.parse_compat(line) - except ValueError: - # There are a small number of URLs in the sample data that are - # not valid WHATWG URLs. - pass - -def yarl_parse(): - for line in data(): - try: - yarl.URL(line) - except ValueError: - # There are a small number of URLs in the sample data that are - # not valid WHATWG URLs. - pass - - @pytest.mark.slow -def test_urllib_parse(benchmark): +def test_urllib_parse(benchmark: Callable[[Any], Any], top100str: Iterable[str]): + def urllib_parse(): + for line in top100str: + urllib.parse.urlparse(line) + benchmark(urllib_parse) @pytest.mark.slow -def test_ada_python_parse(benchmark): +def test_ada_python_parse(benchmark: Callable[[Any], Any], top100str: Iterable[str]): + def ada_python_parse(): + for line in top100str: + try: + ada_url.URL(line) + except ValueError: + # There are a small number of URLs in the sample data that are + # not valid WHATWG URLs. + pass + benchmark(ada_python_parse) @pytest.mark.slow -def test_can_ada_parse(benchmark): +def test_can_ada_parse(benchmark: Callable[[Any], Any], top100str: Iterable[str]): + def can_ada_parse(): + for line in top100str: + try: + can_ada.parse(line) + except ValueError: + # There are a small number of URLs in the sample data that are + # not valid WHATWG URLs. + pass + benchmark(can_ada_parse) @pytest.mark.slow -def test_yarl_parse(benchmark): +def test_yarl_parse(benchmark: Callable[[Any], Any], top100str: Iterable[str]): + def yarl_parse(): + for line in top100str: + try: + yarl.URL(line) + except ValueError: + # There are a small number of URLs in the sample data that are + # not valid WHATWG URLs. + pass + benchmark(yarl_parse) + @pytest.mark.slow -def test_can_ada_parse_compat(benchmark): +def test_can_ada_parse_compat_str( + benchmark: Callable[[Any], Any], top100str: Iterable[str] +): + def can_ada_parse_compat(): + for line in top100str: + try: + can_ada.parse_compat(line) + except ValueError: + # There are a small number of URLs in the sample data that are + # not valid WHATWG URLs. + pass + + benchmark(can_ada_parse_compat) + + +@pytest.mark.slow +def test_can_ada_parse_compat_bytes( + benchmark: Callable[[Any], Any], top100bytes: Iterable[str] +): + def can_ada_parse_compat(): + for line in top100bytes: + try: + can_ada.parse_compat(line) + except ValueError: + # There are a small number of URLs in the sample data that are + # not valid WHATWG URLs. + pass + benchmark(can_ada_parse_compat) diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 0000000..69b2629 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,17 @@ +import pytest +import can_ada +import urllib.parse + + +@pytest.mark.xfail(reason="parse_compat is not 100% urllib-compatible yet") +def test_urllib_parse_str_matches(subtests: pytest.Subtests, top100str: list[str]): + for line in top100str: + assert urllib.parse.urlparse(line) == can_ada.parse_compat(line) + + +@pytest.mark.xfail(reason="parse_compat is not 100% urllib-compatible yet") +def test_urllib_parse_bytes_matches( + subtests: pytest.Subtests, top100bytes: list[bytes] +): + for line in top100bytes: + assert urllib.parse.urlparse(line) == can_ada.parse_compat(line) From e645404af34ea617f829ace72dcbd9e63bf65788 Mon Sep 17 00:00:00 2001 From: abebus Date: Thu, 29 Jan 2026 22:07:43 +0300 Subject: [PATCH 6/7] fix typing --- can_ada-stubs/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can_ada-stubs/__init__.pyi b/can_ada-stubs/__init__.pyi index 19ff9aa..e0ed606 100644 --- a/can_ada-stubs/__init__.pyi +++ b/can_ada-stubs/__init__.pyi @@ -66,7 +66,7 @@ class URLSearchParamsValuesIter: def __next__(self) -> str | None: ... def can_parse(input: str, base_input: str | None = ...) -> bool: ... -def idna_decode(arg0: str) -> str: ... +def idna_decode(arg0: bytes) -> str: ... def idna_encode(arg0: str) -> bytes: ... def parse(arg0: str) -> URL: ... @overload From 61fd6b0d9337040f6c0740cfd787521293665c87 Mon Sep 17 00:00:00 2001 From: abebus Date: Thu, 29 Jan 2026 23:23:20 +0300 Subject: [PATCH 7/7] fixes --- src/binding.cpp | 27 ++++++++++++++------------- tests/conftest.py | 4 ++-- tests/test_benchmark.py | 2 +- tests/test_compat.py | 6 ++---- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/binding.cpp b/src/binding.cpp index 3ff7533..43ad3e1 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -10,12 +10,12 @@ namespace py = nanobind; struct parse_impl_result { - std::string_view scheme; - std::string_view netloc; - std::string_view path; - std::string_view params; - std::string_view query; - std::string_view fragment; + std::string scheme; + std::string netloc; + std::string path; + std::string params; + std::string query; + std::string fragment; }; static parse_impl_result parse_compat_impl(std::string_view input); @@ -166,18 +166,18 @@ NB_MODULE(can_ada, m) { auto urllib = py::module_::import_("urllib.parse"); - static auto ParseResult = urllib.attr("ParseResult"); - static auto ParseResultBytes = urllib.attr("ParseResultBytes"); + auto ParseResult = py::object(urllib.attr("ParseResult")); + auto ParseResultBytes = py::object(urllib.attr("ParseResultBytes")); - m.def("parse_compat", [&](py::bytes input) { + m.def("parse_compat", [ParseResult](std::string_view input) { auto [scheme, netloc, path, params, query, fragment] = - parse_compat_impl(std::string_view(input.c_str(), input.size())); + parse_compat_impl(input); return ParseResult(scheme, netloc, path, params, query, fragment); }); - m.def("parse_compat", [&](std::string_view input) { + m.def("parse_compat", [ParseResultBytes](py::bytes input) { auto [scheme, netloc, path, params, query, fragment] = - parse_compat_impl(input); + parse_compat_impl(std::string_view(input.c_str(), input.size())); return ParseResultBytes(py::bytes(scheme.data(), scheme.size()), py::bytes(netloc.data(), netloc.size()), py::bytes(path.data(), path.size()), @@ -246,5 +246,6 @@ static parse_impl_result parse_compat_impl(std::string_view input) { fragment.remove_prefix(1); } - return {scheme, netloc, path, params, query, fragment}; + return {std::string(scheme), std::move(netloc), std::string(path), + std::string(params), std::string(query), std::string(fragment)}; } diff --git a/tests/conftest.py b/tests/conftest.py index f5321ef..d9b4add 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,11 +30,11 @@ def pytest_collection_modifyitems(config, items): def top100str() -> list[str]: current_file_dir = Path(__file__).parent with open(current_file_dir / "data" / "top100.txt", "r") as f: - return f.readlines() + return f.read().splitlines() @pytest.fixture(scope="session") def top100bytes() -> list[bytes]: current_file_dir = Path(__file__).parent with open(current_file_dir / "data" / "top100.txt", "rb") as f: - return f.readlines() + return f.read().splitlines() diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index 5a52153..b2f7bfc 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -83,7 +83,7 @@ def can_ada_parse_compat(): for line in top100bytes: try: can_ada.parse_compat(line) - except ValueError: + except (ValueError, UnicodeDecodeError): # There are a small number of URLs in the sample data that are # not valid WHATWG URLs. pass diff --git a/tests/test_compat.py b/tests/test_compat.py index 69b2629..4fea8a2 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -4,14 +4,12 @@ @pytest.mark.xfail(reason="parse_compat is not 100% urllib-compatible yet") -def test_urllib_parse_str_matches(subtests: pytest.Subtests, top100str: list[str]): +def test_urllib_parse_str_matches(top100str: list[str]): for line in top100str: assert urllib.parse.urlparse(line) == can_ada.parse_compat(line) @pytest.mark.xfail(reason="parse_compat is not 100% urllib-compatible yet") -def test_urllib_parse_bytes_matches( - subtests: pytest.Subtests, top100bytes: list[bytes] -): +def test_urllib_parse_bytes_matches(top100bytes: list[bytes]): for line in top100bytes: assert urllib.parse.urlparse(line) == can_ada.parse_compat(line)