diff --git a/CHANGES/6013.feature b/CHANGES/6013.feature new file mode 100644 index 00000000000..82d1be03a1b --- /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 5ad09c7cc2c..67e44cebd0c 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -166,6 +166,7 @@ class ClientSession: ATTRS = frozenset( [ + "_base_url", "_source_traceback", "_connector", "requote_redirect_url", @@ -194,6 +195,7 @@ class ClientSession: def __init__( self, + base_url: Optional[StrOrURL] = None, *, connector: Optional[BaseConnector] = None, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -218,13 +220,20 @@ def __init__( trace_configs: Optional[List[TraceConfig]] = None, read_bufsize: int = 2 ** 16, ) -> None: - if loop is None: if connector is not None: loop = connector._loop loop = get_running_loop(loop) + 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" + if connector is None: connector = TCPConnector(loop=loop) @@ -343,6 +352,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, @@ -402,7 +419,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 95026ac6232..549c455ac9f 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 015b05b60e7..51139bfe28f 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 4b8a5cfbfbd..ce82bad818c 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -862,3 +862,39 @@ async def test_requote_redirect_setter() -> None: session.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