From 527b1b9b6f60b0becae3e63550c1b76de1b189e7 Mon Sep 17 00:00:00 2001 From: Dmitry Erlikh Date: Sun, 31 Oct 2021 19:09:29 +0100 Subject: [PATCH] Add a `base_url` option to `ClientSession` (#6129) Co-authored-by: Andrew Svetlov Co-authored-by: Sviatoslav Sydorenko Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGES/6013.feature | 1 + aiohttp/client.py | 20 +++++++++++++++++++- docs/client_quickstart.rst | 12 ++++++++++++ docs/client_reference.rst | 8 +++++++- tests/test_client_session.py | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 CHANGES/6013.feature diff --git a/CHANGES/6013.feature b/CHANGES/6013.feature new file mode 100644 index 0000000000..82d1be03a1 --- /dev/null +++ b/CHANGES/6013.feature @@ -0,0 +1 @@ +Added ``base_url`` parameter to the initializer of :class:`~aiohttp.ClientSession`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 685e8d9c22..6e5125cfe5 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -169,6 +169,7 @@ class ClientSession: """First-class interface for making HTTP requests.""" __slots__ = ( + "_base_url", "_source_traceback", "_connector", "_loop", @@ -193,6 +194,7 @@ class ClientSession: def __init__( self, + base_url: Optional[StrOrURL] = None, *, connector: Optional[BaseConnector] = None, cookies: Optional[LooseCookies] = None, @@ -216,6 +218,14 @@ def __init__( trace_configs: Optional[List[TraceConfig]] = None, read_bufsize: int = 2 ** 16, ) -> None: + if base_url is None or isinstance(base_url, URL): + self._base_url: Optional[URL] = base_url + else: + self._base_url = URL(base_url) + assert ( + self._base_url.origin() == self._base_url + ), "Only absolute URLs without path part are supported" + loop = asyncio.get_running_loop() if connector is None: @@ -304,6 +314,14 @@ def request( """Perform HTTP request.""" return _RequestContextManager(self._request(method, url, **kwargs)) + def _build_url(self, str_or_url: StrOrURL) -> URL: + url = URL(str_or_url) + if self._base_url is None: + return url + else: + assert not url.is_absolute() and url.path.startswith("/") + return self._base_url.join(url) + async def _request( self, method: str, @@ -363,7 +381,7 @@ async def _request( proxy_headers = self._prepare_headers(proxy_headers) try: - url = URL(str_or_url) + url = self._build_url(str_or_url) except ValueError as e: raise InvalidURL(str_or_url) from e diff --git a/docs/client_quickstart.rst b/docs/client_quickstart.rst index 7ba0bb9e4f..2aedf15f2e 100644 --- a/docs/client_quickstart.rst +++ b/docs/client_quickstart.rst @@ -55,6 +55,18 @@ Other HTTP methods are available as well:: session.options('http://httpbin.org/get') session.patch('http://httpbin.org/patch', data=b'data') +To make several requests to the same site more simple, the parameter ``base_url`` +of :class:`ClientSession` constructor can be used. For example to request different +endpoints of ``http://httpbin.org`` can be used the following code:: + + async with aiohttp.ClientSession('http://httpbin.org') as session: + async with session.get('/get'): + pass + async with session.post('/post', data=b'data'): + pass + async with session.put('/put', data=b'data'): + pass + .. note:: Don't create a session per request. Most likely you need a session diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 23fd050e0d..5df691e128 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -38,7 +38,8 @@ Usage example:: The client session supports the context manager protocol for self closing. -.. class:: ClientSession(*, connector=None, loop=None, cookies=None, \ +.. class:: ClientSession(base_url=None, *, \ + connector=None, cookies=None, \ headers=None, skip_auto_headers=None, \ auth=None, json_serialize=json.dumps, \ version=aiohttp.HttpVersion11, \ @@ -56,6 +57,11 @@ The client session supports the context manager protocol for self closing. The class for creating client sessions and making requests. + :param base_url: Base part of the URL (optional) + If set it allows to skip the base part in request calls. + + .. versionadded:: 3.8 + :param aiohttp.BaseConnector connector: BaseConnector sub-class instance to support connection pooling. diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 63deef311a..ff90a52656 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -816,3 +816,39 @@ async def test_requote_redirect_url_default_disable() -> None: session = ClientSession(requote_redirect_url=False) assert not session.requote_redirect_url await session.close() + + +@pytest.mark.parametrize( + ("base_url", "url", "expected_url"), + [ + pytest.param( + None, + "http://example.com/test", + URL("http://example.com/test"), + id="base_url=None url='http://example.com/test'", + ), + pytest.param( + None, + URL("http://example.com/test"), + URL("http://example.com/test"), + id="base_url=None url=URL('http://example.com/test')", + ), + pytest.param( + "http://example.com", + "/test", + URL("http://example.com/test"), + id="base_url='http://example.com' url='/test'", + ), + pytest.param( + URL("http://example.com"), + "/test", + URL("http://example.com/test"), + id="base_url=URL('http://example.com') url='/test'", + ), + ], +) +async def test_build_url_returns_expected_url( + create_session, base_url, url, expected_url +) -> None: + session = await create_session(base_url) + assert session._build_url(url) == expected_url