Skip to content

Commit

Permalink
Base URL improvements (#1130)
Browse files Browse the repository at this point in the history
* URL.join(url=...), not URL.join(relative_url=...)

* Fix URL.join()

* Support no argument 'httpx.URL()' usage

* Support client.base_url as a property

* Resolve base_url joining behaviour

* Fix coverage

* Update _client.py
  • Loading branch information
tomchristie authored Aug 5, 2020
1 parent 7279ed4 commit a3392c6
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 13 deletions.
35 changes: 27 additions & 8 deletions httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = self._enforce_trailing_slash(URL(base_url))

self.auth = auth
self._params = QueryParams(params)
Expand All @@ -87,6 +84,11 @@ 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
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]]:
Expand All @@ -107,6 +109,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 = self._enforce_trailing_slash(URL(url))

@property
def headers(self) -> Headers:
"""
Expand Down Expand Up @@ -208,7 +221,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
Expand Down Expand Up @@ -441,7 +460,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,
Expand Down Expand Up @@ -972,7 +991,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,
Expand Down
2 changes: 1 addition & 1 deletion httpx/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
24 changes: 21 additions & 3 deletions tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,31 @@ 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"
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():
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():
limits = httpx.Limits()

Expand Down
23 changes: 22 additions & 1 deletion tests/client/test_properties.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
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_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():
Expand Down

0 comments on commit a3392c6

Please sign in to comment.