Skip to content

Commit 656e5f9

Browse files
committed
Fix optional base_input on can_parse.
Add URLSearchParams wrappers. Update typing stubs.
1 parent ef59c91 commit 656e5f9

File tree

4 files changed

+235
-116
lines changed

4 files changed

+235
-116
lines changed

can_ada.pyi

Lines changed: 54 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
from __future__ import annotations
2-
import can_ada
3-
import typing
1+
from typing import Iterator, overload
42

5-
__all__ = [
6-
"URL",
7-
"can_parse",
8-
"parse",
9-
"idna_encode",
10-
"idna_decode"
11-
]
3+
__version__: str
124

13-
14-
class URL():
15-
def __str__(self) -> str: ...
5+
class URL:
6+
hash: str
7+
host: str
8+
hostname: str
9+
href: str
10+
password: str
11+
pathname: str
12+
port: str
13+
protocol: str
14+
search: str
15+
username: str
16+
def __init__(self, *args, **kwargs) -> None: ...
1617
def has_credentials(self) -> bool: ...
1718
def has_empty_hostname(self) -> bool: ...
1819
def has_hash(self) -> bool: ...
@@ -25,104 +26,45 @@ class URL():
2526
def has_valid_domain(self) -> bool: ...
2627
def to_diagram(self) -> str: ...
2728
def validate(self) -> bool: ...
28-
def __add__(self, other) -> "URL": ...
29-
@property
30-
def hash(self) -> str:
31-
"""
32-
:type: str
33-
"""
34-
@hash.setter
35-
def hash(self, arg1: str) -> None:
36-
pass
37-
@property
38-
def host(self) -> str:
39-
"""
40-
:type: str
41-
"""
42-
@host.setter
43-
def host(self, arg1: str) -> None:
44-
pass
45-
@property
46-
def hostname(self) -> str:
47-
"""
48-
:type: str
49-
"""
50-
@hostname.setter
51-
def hostname(self, arg1: str) -> None:
52-
pass
53-
@property
54-
def href(self) -> str:
55-
"""
56-
:type: str
57-
"""
58-
@href.setter
59-
def href(self, arg1: str) -> None:
60-
pass
61-
@property
62-
def origin(self) -> str:
63-
"""
64-
:type: str
65-
"""
66-
@property
67-
def password(self) -> str:
68-
"""
69-
:type: str
70-
"""
71-
@password.setter
72-
def password(self, arg1: str) -> None:
73-
pass
74-
@property
75-
def pathname(self) -> str:
76-
"""
77-
:type: str
78-
"""
79-
@pathname.setter
80-
def pathname(self, arg1: str) -> None:
81-
pass
29+
def __add__(self, arg0: str) -> URL: ...
8230
@property
83-
def pathname_length(self) -> int:
84-
"""
85-
:type: int
86-
"""
31+
def origin(self) -> str: ...
8732
@property
88-
def port(self) -> str:
89-
"""
90-
:type: str
91-
"""
92-
@port.setter
93-
def port(self, arg1: str) -> None:
94-
pass
95-
@property
96-
def protocol(self) -> str:
97-
"""
98-
:type: str
99-
"""
100-
@protocol.setter
101-
def protocol(self, arg1: str) -> None:
102-
pass
103-
@property
104-
def search(self) -> str:
105-
"""
106-
:type: str
107-
"""
108-
@search.setter
109-
def search(self, arg1: str) -> None:
110-
pass
111-
@property
112-
def username(self) -> str:
113-
"""
114-
:type: str
115-
"""
116-
@username.setter
117-
def username(self, arg1: str) -> None:
118-
pass
119-
pass
120-
def can_parse(input: str, base_input: str = None) -> bool:
121-
pass
122-
def parse(arg0: str) -> URL:
123-
pass
124-
def idna_encode(arg0: str) -> bytes:
125-
pass
126-
def idna_decode(arg0: bytes) -> str:
127-
pass
128-
__version__ = '1.0.0'
33+
def pathname_length(self) -> int: ...
34+
35+
class URLSearchParams:
36+
@overload
37+
def __init__(self) -> None: ...
38+
@overload
39+
def __init__(self, arg0: str) -> None: ...
40+
def append(self, arg0: str, arg1: str) -> None: ...
41+
def copy(self) -> URLSearchParams: ...
42+
def get(self, arg0: str) -> str | None: ...
43+
def get_all(self, arg0: str) -> list[str]: ...
44+
def has(self, key: str, value: str | None = ...) -> bool: ...
45+
def keys(self) -> URLSearchParamsKeysIter: ...
46+
def remove(self, key: str, value: str | None = ...) -> None: ...
47+
def size(self) -> int: ...
48+
def sort(self) -> None: ...
49+
def values(self) -> URLSearchParamsValuesIter: ...
50+
def __contains__(self, arg0: str) -> bool: ...
51+
def __delitem__(self, arg0: str) -> None: ...
52+
def __getitem__(self, arg0: str) -> str: ...
53+
def __iter__(self) -> Iterator[tuple[str, str]]: ...
54+
def __len__(self) -> int: ...
55+
def __setitem__(self, arg0: str, arg1: str) -> None: ...
56+
57+
class URLSearchParamsKeysIter:
58+
def __init__(self, *args, **kwargs) -> None: ...
59+
def __iter__(self) -> URLSearchParamsKeysIter: ...
60+
def __next__(self) -> str | None: ...
61+
62+
class URLSearchParamsValuesIter:
63+
def __init__(self, *args, **kwargs) -> None: ...
64+
def __iter__(self) -> URLSearchParamsValuesIter: ...
65+
def __next__(self) -> str | None: ...
66+
67+
def can_parse(input: str, base_input: str | None = ...) -> bool: ...
68+
def idna_decode(arg0: str) -> str: ...
69+
def idna_encode(arg0: str) -> bytes: ...
70+
def parse(arg0: str) -> URL: ...

src/binding.cpp

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <pybind11/pybind11.h>
2+
#include <pybind11/stl.h>
23
#include "ada.h"
34

45
namespace py = pybind11;
@@ -10,10 +11,9 @@ PYBIND11_MODULE(can_ada, m) {
1011
m.attr("__version__") = "dev";
1112
#endif
1213

13-
m.def("can_parse",
14-
&ada::can_parse,
15-
py::arg("input"),
16-
py::arg("base_input") = static_cast<const std::string_view*>(nullptr));
14+
m.def("can_parse", [](std::string_view input, std::optional<std::string_view*> base_input) {
15+
return ada::can_parse(input, base_input.value_or(nullptr));
16+
}, py::arg("input"), py::arg("base_input") = py::none());
1717

1818
py::class_<ada::url_aggregator>(m, "URL")
1919
.def_property("hash", &ada::url_aggregator::get_hash, &ada::url_aggregator::set_hash)
@@ -52,6 +52,80 @@ PYBIND11_MODULE(can_ada, m) {
5252
return url.value();
5353
});
5454

55+
py::class_<ada::url_search_params_keys_iter>(m, "URLSearchParamsKeysIter")
56+
.def("__iter__", [](ada::url_search_params_keys_iter &self) {
57+
return &self;
58+
})
59+
.def("__next__", [](ada::url_search_params_keys_iter &self) {
60+
if (self.has_next()) {
61+
return self.next();
62+
} else {
63+
throw pybind11::stop_iteration();
64+
}
65+
});
66+
67+
py::class_<ada::url_search_params_values_iter>(m, "URLSearchParamsValuesIter")
68+
.def("__iter__", [](ada::url_search_params_values_iter &self) {
69+
return &self;
70+
})
71+
.def("__next__", [](ada::url_search_params_values_iter &self) {
72+
if (self.has_next()) {
73+
return self.next();
74+
} else {
75+
throw pybind11::stop_iteration();
76+
}
77+
});
78+
79+
py::class_<ada::url_search_params>(m, "URLSearchParams")
80+
.def(py::init<>())
81+
.def(py::init<const std::string_view>())
82+
.def("get", &ada::url_search_params::get)
83+
.def("get_all", &ada::url_search_params::get_all)
84+
.def("has", [](ada::url_search_params &self, std::string_view key, std::optional<std::string_view> value) {
85+
if (value) {
86+
return self.has(key, value.value());
87+
}
88+
return self.has(key);
89+
}, py::arg("key"), py::arg("value") = py::none())
90+
.def("append", &ada::url_search_params::append)
91+
.def("remove", [](ada::url_search_params &self, std::string_view key, std::optional<std::string_view> value) {
92+
if (value) {
93+
self.remove(key, value.value());
94+
return;
95+
}
96+
self.remove(key);
97+
}, py::arg("key"), py::arg("value") = py::none())
98+
.def("copy", [](ada::url_search_params &self) {
99+
return ada::url_search_params(self);
100+
})
101+
.def("sort", &ada::url_search_params::sort)
102+
.def("size", &ada::url_search_params::size)
103+
.def("keys", [](ada::url_search_params &self) {
104+
return self.get_keys();
105+
}, py::keep_alive<0, 1>())
106+
.def("values", [](ada::url_search_params &self) {
107+
return self.get_values();
108+
}, py::keep_alive<0, 1>())
109+
.def("__str__", &ada::url_search_params::to_string)
110+
.def("__getitem__", [](ada::url_search_params &self, std::string_view key) {
111+
auto v = self.get(key);
112+
if (v) {
113+
return v.value();
114+
}
115+
throw pybind11::key_error("Key not found.");
116+
})
117+
.def("__setitem__", &ada::url_search_params::set)
118+
.def("__delitem__", [](ada::url_search_params &self, std::string_view key) {
119+
self.remove(key);
120+
})
121+
.def("__len__", &ada::url_search_params::size)
122+
.def("__contains__", [](ada::url_search_params &self, std::string_view key) {
123+
return self.has(key);
124+
})
125+
.def("__iter__", [](ada::url_search_params &self) {
126+
return py::make_iterator(self.begin(), self.end());
127+
}, py::keep_alive<0, 1>());
128+
55129
m.def("idna_decode", &ada::idna::to_unicode);
56130
m.def("idna_encode", [](std::string input) -> py::bytes {
57131
return py::bytes(ada::idna::to_ascii(input));

tests/test_parsing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ def test_can_parse():
55
assert can_ada.can_parse("/", "https://tkte.ch") is True
66
assert can_ada.can_parse("/", "/tkte.ch") is False
77

8+
assert can_ada.can_parse("https://tkte.ch") is True
9+
assert can_ada.can_parse("//tkte.ch") is False
10+
811

912
def test_parse():
1013
"""

tests/test_search.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import pytest
2+
3+
import can_ada
4+
5+
6+
def test_get_and_set():
7+
"""
8+
Ensure we can get & ste the various parts of the URLSearchParams.
9+
"""
10+
params = can_ada.URLSearchParams("?a=1&b=2&c=3")
11+
assert params["a"] == "1"
12+
assert params["b"] == "2"
13+
assert params["c"] == "3"
14+
15+
with pytest.raises(KeyError):
16+
params["d"]
17+
18+
assert params.get("a") == "1"
19+
assert params.get("d") is None
20+
21+
assert "a" in params
22+
assert "d" not in params
23+
24+
assert params.has("a") is True
25+
assert params.has("d") is False
26+
27+
params.remove("a")
28+
assert params.has("a") is False
29+
assert params.get("a") is None
30+
31+
32+
def test_get_all_and_set():
33+
"""
34+
Ensure we can get & set multiple values for a single key.
35+
"""
36+
params = can_ada.URLSearchParams("?a=1&b=2&c=3&c=4")
37+
assert params.get_all("a") == ["1"]
38+
assert params.get_all("b") == ["2"]
39+
assert params.get_all("c") == ["3", "4"]
40+
41+
params["a"] = "2"
42+
assert params.get_all("a") == ["2"]
43+
44+
params["c"] = "5"
45+
assert params.get_all("c") == ["5"]
46+
params.append("c", "6")
47+
assert params.get_all("c") == ["5", "6"]
48+
49+
params.remove("c", "6")
50+
assert params.get_all("c") == ["5"]
51+
52+
params.remove("c", "5")
53+
assert params.get_all("c") == []
54+
55+
params.append("c", "5")
56+
params.append("c", "6")
57+
assert params.has("c", "5") is True
58+
assert params.has("c", "6") is True
59+
assert params.has("c", "7") is False
60+
61+
62+
def test_size():
63+
"""
64+
Ensure we can get the size of the URLSearchParams.
65+
"""
66+
params = can_ada.URLSearchParams("?a=1&b=2&c=3")
67+
assert len(params) == 3
68+
69+
70+
def test_copy():
71+
"""
72+
Ensure we can copy a URLSearchParams object.
73+
"""
74+
params_1 = can_ada.URLSearchParams("?a=1&b=2&c=3")
75+
params_2 = params_1.copy()
76+
77+
# Ensure the copy isn't sharing memory
78+
assert params_2["a"] == "1"
79+
params_1["a"] = "2"
80+
assert params_2["a"] == "1"
81+
82+
83+
def test_sort():
84+
"""
85+
Ensure we can sort the URLSearchParams.
86+
"""
87+
params = can_ada.URLSearchParams("?c=3&b=2&a=1")
88+
assert list(params.keys()) == ["c", "b", "a"]
89+
params.sort()
90+
assert list(params.keys()) == ["a", "b", "c"]
91+
92+
93+
def test_iterators():
94+
"""
95+
Ensure we can iterate over the URLSearchParams.
96+
"""
97+
params = can_ada.URLSearchParams("?a=1&b=2&c=3")
98+
assert list(params) == [("a", "1"), ("b", "2"), ("c", "3")]
99+
assert list(params.keys()) == ["a", "b", "c"]
100+
assert list(params.values()) == ["1", "2", "3"]

0 commit comments

Comments
 (0)