From 6456b2b2fb2cfe06d1d23e1d4a08682e9e2f9a64 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 14:39:12 +0100 Subject: [PATCH 1/7] URL.join(url=...), not URL.join(relative_url=...) --- httpx/_client.py | 2 +- httpx/_models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index dad4a42aab..0a2e525a2f 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -208,7 +208,7 @@ def _merge_url(self, url: URLTypes) -> URL: Merge a URL argument together with any 'base_url' on the client, to create the URL used for the outgoing request. """ - return self.base_url.join(relative_url=url) + return self.base_url.join(url) def _merge_cookies( self, cookies: CookieTypes = None diff --git a/httpx/_models.py b/httpx/_models.py index 9c5aa1773a..ee52111d53 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -183,7 +183,7 @@ def copy_with(self, **kwargs: typing.Any) -> "URL": return URL(self._uri_reference.copy_with(**kwargs).unsplit(),) - def join(self, relative_url: URLTypes) -> "URL": + def join(self, url: URLTypes) -> "URL": """ Return an absolute URL, using given this URL as the base. """ From 8c532d4cef0727f0668441744d7d57cdc38a286f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 14:42:23 +0100 Subject: [PATCH 2/7] Fix URL.join() --- httpx/_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index ee52111d53..41c7a274e6 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -188,12 +188,12 @@ def join(self, url: URLTypes) -> "URL": Return an absolute URL, using given this URL as the base. """ if self.is_relative_url: - return URL(relative_url) + return URL(url) # We drop any fragment portion, because RFC 3986 strictly # treats URLs with a fragment portion as not being absolute URLs. base_uri = self._uri_reference.copy_with(fragment=None) - relative_url = URL(relative_url) + relative_url = URL(url) return URL(relative_url._uri_reference.resolve_with(base_uri).unsplit()) def __hash__(self) -> int: From c3c03cb007853bd08d9bd6b1b97e59da4ebdf09d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 14:57:21 +0100 Subject: [PATCH 3/7] Support no argument 'httpx.URL()' usage --- httpx/_client.py | 11 ++++------- httpx/_models.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 0a2e525a2f..69183fc0b8 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -66,13 +66,10 @@ def __init__( cookies: CookieTypes = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, max_redirects: int = DEFAULT_MAX_REDIRECTS, - base_url: URLTypes = None, + base_url: URLTypes = "", trust_env: bool = True, ): - if base_url is None: - self.base_url = URL("") - else: - self.base_url = URL(base_url) + self.base_url = URL(base_url) self.auth = auth self._params = QueryParams(params) @@ -441,7 +438,7 @@ def __init__( limits: Limits = DEFAULT_LIMITS, pool_limits: Limits = None, max_redirects: int = DEFAULT_MAX_REDIRECTS, - base_url: URLTypes = None, + base_url: URLTypes = "", transport: httpcore.SyncHTTPTransport = None, app: typing.Callable = None, trust_env: bool = True, @@ -972,7 +969,7 @@ def __init__( limits: Limits = DEFAULT_LIMITS, pool_limits: Limits = None, max_redirects: int = DEFAULT_MAX_REDIRECTS, - base_url: URLTypes = None, + base_url: URLTypes = "", transport: httpcore.AsyncHTTPTransport = None, app: typing.Callable = None, trust_env: bool = True, diff --git a/httpx/_models.py b/httpx/_models.py index 41c7a274e6..e898515291 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -55,7 +55,7 @@ class URL: - def __init__(self, url: URLTypes, params: QueryParamTypes = None) -> None: + def __init__(self, url: URLTypes = "", params: QueryParamTypes = None) -> None: if isinstance(url, str): self._uri_reference = rfc3986.api.iri_reference(url).encode() else: From d1c88d18402577b7a169969e3c32a908a039144e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 15:05:43 +0100 Subject: [PATCH 4/7] Support client.base_url as a property --- httpx/_client.py | 13 ++++++++++++- tests/client/test_properties.py | 9 ++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 69183fc0b8..4790b64ccf 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -69,7 +69,7 @@ def __init__( base_url: URLTypes = "", trust_env: bool = True, ): - self.base_url = URL(base_url) + self._base_url = URL(base_url) self.auth = auth self._params = QueryParams(params) @@ -104,6 +104,17 @@ def _get_proxy_map( proxy = Proxy(url=proxies) if isinstance(proxies, (str, URL)) else proxies return {"all": proxy} + @property + def base_url(self) -> URL: + """ + Base URL to use when sending requests with relative URLs. + """ + return self._base_url + + @base_url.setter + def base_url(self, url: URLTypes) -> None: + self._base_url = URL(url) + @property def headers(self) -> Headers: """ diff --git a/tests/client/test_properties.py b/tests/client/test_properties.py index 011c593cd3..5655ecd30f 100644 --- a/tests/client/test_properties.py +++ b/tests/client/test_properties.py @@ -1,4 +1,11 @@ -from httpx import AsyncClient, Cookies, Headers +from httpx import URL, AsyncClient, Cookies, Headers + + +def test_client_base_url(): + client = AsyncClient() + client.base_url = "https://www.example.org/" # type: ignore + assert isinstance(client.base_url, URL) + assert client.base_url == URL("https://www.example.org/") def test_client_headers(): From df2304a7fd8684d3ce0fc07eaf25e31e9b980bd0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 15:23:02 +0100 Subject: [PATCH 5/7] Resolve base_url joining behaviour --- httpx/_client.py | 18 +++++++++++++++--- tests/client/test_client.py | 25 +++++++++++++++++++++---- tests/client/test_properties.py | 14 ++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 4790b64ccf..05288d6224 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -69,7 +69,7 @@ def __init__( base_url: URLTypes = "", trust_env: bool = True, ): - self._base_url = URL(base_url) + self.base_url = self._enforce_trailing_slash(URL(base_url)) self.auth = auth self._params = QueryParams(params) @@ -84,6 +84,12 @@ def __init__( def trust_env(self) -> bool: return self._trust_env + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.path.endswith("/"): + return url + else: + return url.copy_with(path=url.path + "/") + def _get_proxy_map( self, proxies: typing.Optional[ProxiesTypes], allow_env_proxies: bool, ) -> typing.Dict[str, typing.Optional[Proxy]]: @@ -113,7 +119,7 @@ def base_url(self) -> URL: @base_url.setter def base_url(self, url: URLTypes) -> None: - self._base_url = URL(url) + self._base_url = self._enforce_trailing_slash(URL(url)) @property def headers(self) -> Headers: @@ -216,7 +222,13 @@ def _merge_url(self, url: URLTypes) -> URL: Merge a URL argument together with any 'base_url' on the client, to create the URL used for the outgoing request. """ - return self.base_url.join(url) + merge_url = URL(url) + if merge_url.is_relative_url: + # We always ensure the base_url paths include the trailing '/', + # and always strip any leading '/' from the merge URL. + merge_url = merge_url.copy_with(path=merge_url.path.lstrip("/")) + return self.base_url.join(merge_url) + return merge_url def _merge_cookies( self, cookies: CookieTypes = None diff --git a/tests/client/test_client.py b/tests/client/test_client.py index ea57c11c35..57260d816a 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -174,11 +174,28 @@ def test_base_url(server): assert response.url == base_url -def test_merge_url(): +def test_merge_absolute_url(): client = httpx.Client(base_url="https://www.example.com/") - request = client.build_request("GET", "http://www.example.com") - assert request.url.scheme == "http" - assert not request.url.is_ssl + request = client.build_request("GET", "http://www.example.com/") + assert request.url == httpx.URL("http://www.example.com/") + + +def test_merge_relative_url(): + client = httpx.Client(base_url="https://www.example.com/") + request = client.build_request("GET", "/testing/123") + assert request.url == httpx.URL("https://www.example.com/testing/123") + + +def test_merge_relative_url_with_path(): + client = httpx.Client(base_url="https://www.example.com/some/path") + request = client.build_request("GET", "/testing/123") + assert request.url == httpx.URL("https://www.example.com/some/path/testing/123") + + +def test_merge_relative_url_with_dotted_path(): + client = httpx.Client(base_url="https://www.example.com/some/path") + request = client.build_request("GET", "../testing/123") + assert request.url == httpx.URL("https://www.example.com/some/testing/123") def test_pool_limits_deprecated(): diff --git a/tests/client/test_properties.py b/tests/client/test_properties.py index 5655ecd30f..3532774727 100644 --- a/tests/client/test_properties.py +++ b/tests/client/test_properties.py @@ -8,6 +8,20 @@ def test_client_base_url(): assert client.base_url == URL("https://www.example.org/") +def test_client_base_url_without_trailing_slash(): + client = AsyncClient() + client.base_url = "https://www.example.org/path" # type: ignore + assert isinstance(client.base_url, URL) + assert client.base_url == URL("https://www.example.org/path/") + + +def test_client_base_url_with_trailing_slash(): + client = AsyncClient() + client.base_url = "https://www.example.org/path/" # type: ignore + assert isinstance(client.base_url, URL) + assert client.base_url == URL("https://www.example.org/path/") + + def test_client_headers(): client = AsyncClient() client.headers = {"a": "b"} # type: ignore From 24a56ff00fe8649c2820beec806f999453468da1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 15:46:18 +0100 Subject: [PATCH 6/7] Fix coverage --- tests/client/test_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 57260d816a..b05735ea5e 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -178,6 +178,7 @@ def test_merge_absolute_url(): client = httpx.Client(base_url="https://www.example.com/") request = client.build_request("GET", "http://www.example.com/") assert request.url == httpx.URL("http://www.example.com/") + assert not request.url.is_ssl def test_merge_relative_url(): From a3b5fc18275e29c49f854b0b04145d5423fb878e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Aug 2020 18:01:53 +0100 Subject: [PATCH 7/7] Update _client.py --- httpx/_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 05288d6224..645c83e0f1 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -69,7 +69,7 @@ def __init__( base_url: URLTypes = "", trust_env: bool = True, ): - self.base_url = self._enforce_trailing_slash(URL(base_url)) + self._base_url = self._enforce_trailing_slash(URL(base_url)) self.auth = auth self._params = QueryParams(params) @@ -87,8 +87,7 @@ def trust_env(self) -> bool: def _enforce_trailing_slash(self, url: URL) -> URL: if url.path.endswith("/"): return url - else: - return url.copy_with(path=url.path + "/") + return url.copy_with(path=url.path + "/") def _get_proxy_map( self, proxies: typing.Optional[ProxiesTypes], allow_env_proxies: bool,