diff --git a/buildurl/__init__.py b/buildurl/__init__.py index d022d10..94662b6 100644 --- a/buildurl/__init__.py +++ b/buildurl/__init__.py @@ -1,12 +1,18 @@ from copy import deepcopy -from typing import Any, Dict, Union -from urllib.parse import parse_qs, urlencode, urlparse, urlunparse +from typing import Any, Dict, List, Optional, Tuple, Union +from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit + +# Type aliases +PathList = List[str] +Path = Union[str, PathList] +QueryDict = Dict[str, Any] +Query = Union[str, QueryDict] class BuildURL: """Tool to simplify the creation of URLs with query parameters""" - def __init__(self, base: str): + def __init__(self, base: str = ""): """Start the creation of an URL. Args: @@ -16,39 +22,30 @@ def __init__(self, base: str): # self.base = base - purl = urlparse(base) + purl = urlsplit(base) # scheme://netloc/path;params?query#fragment + # There can be one `params` per `path` element, so it's included as + # part of `path`, and not isolated self.scheme: str = purl.scheme self.netloc: str = purl.netloc - self._path_list: list = list() - self.params: str = purl.params - self._query_dict: dict = dict() + self.path_list: PathList = list() + self.query_dict: QueryDict = dict() self.fragment: str = purl.fragment path_str: str = purl.path - query_str: str = purl.query - if path_str: - self.add_path(path_str.split("/")) + self.path = path_str + + query_str: str = purl.query if query_str: - self.add_query(parse_qs(query_str)) + self.query = query_str def copy(self) -> "BuildURL": """Create a deep copy of itself.""" return deepcopy(self) - def add_query(self, query: Dict[str, Any]) -> None: - """Add a query argument. - - Args: - query: - The query keys and arguments to add. - """ - - self._query_dict.update(query) - - def add_path(self, path: Union[str, list]) -> None: + def add_path(self, path: Path) -> None: """Add to the path. Args: @@ -56,31 +53,67 @@ def add_path(self, path: Union[str, list]) -> None: The path to add. """ + path_list = list() if isinstance(path, str): - self._path_list.append(path) + path_list = path.split("/") elif isinstance(path, list): - self._path_list.extend(path) + path_list = path + else: + raise AttributeError + + path_list = [p for p in path_list if p] # Remove empty strings + + self.path_list.extend(path_list) + + def add_query(self, query: Query) -> None: + """Add a query argument. + + Args: + query: + The query keys and arguments to add. + """ + + query_dict = dict() + if isinstance(query, str): + query_dict = parse_qs(query) + elif isinstance(query, dict): + query_dict = query else: raise AttributeError + self.query_dict.update(query_dict) + @property def path(self) -> str: """Path string.""" - return "/".join(self._path_list) + return "/".join(self.path_list) + + @path.setter + def path(self, path: Optional[Path]): + """Replace current path.""" + self.path_list = list() + if path is not None: + self.add_path(path) @property def query(self) -> str: """Query string.""" - return urlencode(self._query_dict, doseq=True) + return urlencode(self.query_dict, doseq=True) + + @query.setter + def query(self, query: Optional[Query]): + """Replace current query.""" + self.query_dict = dict() + if query is not None: + self.add_query(query) @property - def parts(self) -> tuple: + def parts(self) -> Tuple[str, ...]: """Tuple of necessary parts to construct the URL.""" return ( self.scheme, self.netloc, self.path, - self.params, self.query, self.fragment, ) @@ -88,9 +121,9 @@ def parts(self) -> tuple: @property def get(self) -> str: """Get the generated URL.""" - return urlunparse(self.parts) + return urlunsplit(self.parts) - def __itruediv__(self, path: str) -> "BuildURL": + def __itruediv__(self, path: Path) -> "BuildURL": """Add new path part to the URL inplace. Args: @@ -104,7 +137,7 @@ def __itruediv__(self, path: str) -> "BuildURL": self.add_path(path) return self - def __truediv__(self, path: str) -> "BuildURL": + def __truediv__(self, path: Path) -> "BuildURL": """Generate new URL with added path. Args: @@ -119,7 +152,7 @@ def __truediv__(self, path: str) -> "BuildURL": out /= path return out - def __iadd__(self, query: Dict[str, Any]) -> "BuildURL": + def __iadd__(self, query: Query) -> "BuildURL": """Add query arguments inplace. Args: @@ -133,7 +166,7 @@ def __iadd__(self, query: Dict[str, Any]) -> "BuildURL": self.add_query(query) return self - def __add__(self, query: Dict[str, Any]) -> "BuildURL": + def __add__(self, query: Query) -> "BuildURL": """Generate new URL with added query. Args: diff --git a/tests/test_buildurl.py b/tests/test_buildurl.py index b5fea8b..4f54383 100644 --- a/tests/test_buildurl.py +++ b/tests/test_buildurl.py @@ -10,6 +10,27 @@ def test_base(): assert len(url) == 19 +def test_split(): + url = BuildURL("scheme://netloc/path;params?query=value#fragment") + assert url.get == "scheme://netloc/path;params?query=value#fragment" + assert url.scheme == "scheme" + assert url.netloc == "netloc" + assert url.path == "path;params" + assert url.query == "query=value" + assert url.fragment == "fragment" + + +def test_fresh(): + url = BuildURL() + assert url.get == "" + url.scheme = "scheme" + url.netloc = "netloc" + url.path = "path;params" + url.query = "query=value" + url.fragment = "fragment" + assert url.get == "scheme://netloc/path;params?query=value#fragment" + + def test_path(): url = BuildURL("https://example.com") url /= "test" @@ -20,6 +41,7 @@ def test_path(): assert url.get == "https://example.com/test/more" url /= ["paths", "added"] assert url.get == "https://example.com/test/more/paths/added" + assert url.path == "test/more/paths/added" with raises(AttributeError): url /= 0 @@ -32,6 +54,14 @@ def test_path(): url = BuildURL("https://example.com/why") assert url.get == "https://example.com/why" + url.path = ["still", "testing"] + assert url.get == "https://example.com/still/testing" + url.path = "/once/more/" + assert url.get == "https://example.com/once/more" + url.path_list = ["again", "and", "again"] + assert url.get == "https://example.com/again/and/again" + url.path = None + assert url.get == "https://example.com" def test_query(): @@ -48,6 +78,14 @@ def test_query(): url = BuildURL("https://example.com?testing=true") assert url.get == "https://example.com?testing=true" + url.query = {"still": "testing"} + assert url.get == "https://example.com?still=testing" + url.query = "once=more" + assert url.get == "https://example.com?once=more" + url.query_dict = {"again": "and-again"} + assert url.get == "https://example.com?again=and-again" + url.query = None + assert url.get == "https://example.com" def test_copy():