From 0b9b76325da470d7070b0d2877ff91aa0d5e3190 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Oct 2025 18:30:39 -1000 Subject: [PATCH 01/24] Fix blocking I/O to load netrc when creating requests --- aiohttp/client.py | 27 +++++++++++ aiohttp/client_reqrep.py | 6 --- tests/conftest.py | 20 ++++++++ tests/test_client_functional.py | 66 +++++++++++++++++++++++++ tests/test_client_request.py | 22 +-------- tests/test_client_session.py | 86 +++++++++++++++++++++++++++++++++ 6 files changed, 200 insertions(+), 27 deletions(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index a7da3ff0c57..b5d7b71771f 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -86,8 +86,10 @@ EMPTY_BODY_METHODS, BasicAuth, TimeoutHandle, + basicauth_from_netrc, frozen_dataclass_decorator, get_env_proxy_for_url, + netrc_from_env, sentinel, strip_auth_from_url, ) @@ -586,6 +588,20 @@ async def _request( ) ): auth = self._default_auth + + # Try netrc if auth is still None and trust_env is enabled. + # Only check if NETRC environment variable is set to avoid + # creating an expensive executor job unnecessarily. + if ( + auth is None + and self._trust_env + and url.host is not None + and os.environ.get("NETRC") + ): + auth = await self._loop.run_in_executor( + None, self._get_netrc_auth, url.host + ) + # It would be confusing if we support explicit # Authorization header with auth argument if auth is not None and hdrs.AUTHORIZATION in headers: @@ -1131,6 +1147,17 @@ def _prepare_headers(self, headers: LooseHeaders | None) -> "CIMultiDict[str]": added_names.add(key) return result + def _get_netrc_auth(self, host: str) -> BasicAuth | None: + """Get auth from netrc for the given host. + + This method is designed to be called in an executor to avoid + blocking I/O in the event loop. + """ + netrc_obj = netrc_from_env() + with suppress(LookupError): + return basicauth_from_netrc(netrc_obj, host) + return None + if sys.version_info >= (3, 11) and TYPE_CHECKING: def get( diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 880e1085bab..050d3a259e1 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -40,10 +40,8 @@ BasicAuth, HeadersMixin, TimerNoop, - basicauth_from_netrc, frozen_dataclass_decorator, is_expected_content_type, - netrc_from_env, parse_mimetype, reify, sentinel, @@ -1068,10 +1066,6 @@ def update_auth(self, auth: BasicAuth | None, trust_env: bool = False) -> None: """Set basic auth.""" if auth is None: auth = self.auth - if auth is None and trust_env and self.url.host is not None: - netrc_obj = netrc_from_env() - with contextlib.suppress(LookupError): - auth = basicauth_from_netrc(netrc_obj, self.url.host) if auth is None: return diff --git a/tests/conftest.py b/tests/conftest.py index 5e872dec5c7..ed6c5de520e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -292,6 +292,26 @@ def netrc_contents( return netrc_file_path +@pytest.fixture +def netrc_default_contents(tmp_path: Path) -> Iterator[Path]: + """Create a temporary netrc file with default test credentials and set NETRC env var.""" + netrc_file = tmp_path / ".netrc" + netrc_file.write_text("default login netrc_user password netrc_pass\n") + + with mock.patch.dict(os.environ, {"NETRC": str(netrc_file)}): + yield netrc_file + + +@pytest.fixture +def no_netrc() -> Iterator[None]: + """Ensure NETRC environment variable is not set.""" + env = os.environ.copy() + env.pop("NETRC", None) + + with mock.patch.dict(os.environ, env, clear=True): + yield + + @pytest.fixture def start_connection() -> Iterator[mock.Mock]: with mock.patch( diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 5006a745346..555c4c75c59 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3746,6 +3746,72 @@ async def handler(request: web.Request) -> NoReturn: await client.get("/", headers=headers) +async def test_netrc_auth_from_env( + aiohttp_client: AiohttpClient, netrc_default_contents: pathlib.Path +) -> None: + """Test that netrc authentication works when NETRC env var is set and trust_env=True.""" + + async def handler(request: web.Request) -> web.Response: + return web.json_response({"headers": dict(request.headers)}) + + app = web.Application() + app.router.add_get("/", handler) + + # Create client with trust_env=True + client = await aiohttp_client(app, trust_env=True) + async with client.get("/") as r: + assert r.status == 200 + content = await r.json() + # Base64 encoded "netrc_user:netrc_pass" is "bmV0cmNfdXNlcjpuZXRyY19wYXNz" + assert content["headers"]["Authorization"] == "Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" + + +async def test_netrc_auth_skipped_without_env_var( + aiohttp_client: AiohttpClient, no_netrc: None +) -> None: + """Test that netrc authentication is skipped when NETRC env var is not set.""" + + async def handler(request: web.Request) -> web.Response: + return web.json_response({"headers": dict(request.headers)}) + + app = web.Application() + app.router.add_get("/", handler) + + # Create client with trust_env=True but no NETRC env var + client = await aiohttp_client(app, trust_env=True) + async with client.get("/") as r: + assert r.status == 200 + content = await r.json() + # No Authorization header should be present + assert "Authorization" not in content["headers"] + + +async def test_netrc_auth_overridden_by_explicit_auth( + aiohttp_client: AiohttpClient, netrc_default_contents: pathlib.Path +) -> None: + """Test that explicit auth parameter overrides netrc authentication.""" + + async def handler(request: web.Request) -> web.Response: + return web.json_response({"headers": dict(request.headers)}) + + app = web.Application() + app.router.add_get("/", handler) + + # Create client with trust_env=True + client = await aiohttp_client(app, trust_env=True) + # Make request with explicit auth (should override netrc) + async with client.get( + "/", auth=aiohttp.BasicAuth("explicit_user", "explicit_pass") + ) as r: + assert r.status == 200 + content = await r.json() + # Base64 encoded "explicit_user:explicit_pass" is "ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" + assert ( + content["headers"]["Authorization"] + == "Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" + ) + + async def test_session_headers(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.Response: return web.json_response({"headers": dict(request.headers)}) diff --git a/tests/test_client_request.py b/tests/test_client_request.py index ef444f1008f..e05b3198a79 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -14,7 +14,7 @@ from yarl import URL import aiohttp -from aiohttp import BaseConnector, hdrs, helpers, payload +from aiohttp import BaseConnector, hdrs, payload from aiohttp.abc import AbstractStreamWriter from aiohttp.base_protocol import BaseProtocol from aiohttp.client_exceptions import ClientConnectionError @@ -1574,26 +1574,6 @@ def test_gen_default_accept_encoding( assert _gen_default_accept_encoding() == expected -@pytest.mark.parametrize( - ("netrc_contents", "expected_auth"), - [ - ( - "machine example.com login username password pass\n", - helpers.BasicAuth("username", "pass"), - ) - ], - indirect=("netrc_contents",), -) -@pytest.mark.usefixtures("netrc_contents") -def test_basicauth_from_netrc_present( # type: ignore[misc] - make_request: _RequestMaker, - expected_auth: helpers.BasicAuth, -) -> None: - """Test appropriate Authorization header is sent when netrc is not empty.""" - req = make_request("get", "http://example.com", trust_env=True) - assert req.headers[hdrs.AUTHORIZATION] == expected_auth.encode() - - @pytest.mark.parametrize( "netrc_contents", ("machine example.com login username password pass\n",), diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 11a815a325e..2cb1e4290e5 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -3,6 +3,7 @@ import gc import io import json +import pathlib import sys import warnings from collections import deque @@ -89,6 +90,18 @@ def params() -> _Params: ) +def _make_auth_handler() -> Callable[[web.Request], Awaitable[web.Response]]: + """Create a handler that returns auth header or 'no_auth'.""" + + async def handler(request: web.Request) -> web.Response: + auth_header = request.headers.get(hdrs.AUTHORIZATION) + if auth_header: + return web.Response(text=f"auth:{auth_header}") + return web.Response(text="no_auth") + + return handler + + async def test_close_coro( create_session: Callable[..., Awaitable[ClientSession]], ) -> None: @@ -1326,3 +1339,76 @@ async def test_properties( value = uuid4() setattr(session, inner_name, value) assert value == getattr(session, outer_name) + + +async def test_netrc_auth_with_trust_env( + aiohttp_server: AiohttpServer, netrc_default_contents: pathlib.Path +) -> None: + """Test that netrc authentication works with ClientSession when NETRC env var is set.""" + app = web.Application() + app.router.add_get("/", _make_auth_handler()) + + server = await aiohttp_server(app) + # Create session with trust_env=True to test ClientSession directly + async with ( + ClientSession(trust_env=True) as session, + session.get(server.make_url("/")) as resp, + ): + text = await resp.text() + # Base64 encoded "netrc_user:netrc_pass" is "bmV0cmNfdXNlcjpuZXRyY19wYXNz" + assert text == "auth:Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" + + +async def test_netrc_auth_skipped_without_trust_env( + aiohttp_server: AiohttpServer, netrc_default_contents: pathlib.Path +) -> None: + """Test that netrc authentication is skipped when trust_env=False.""" + app = web.Application() + app.router.add_get("/", _make_auth_handler()) + + server = await aiohttp_server(app) + # Create session with trust_env=False (default) to test ClientSession directly + async with ( + ClientSession(trust_env=False) as session, + session.get(server.make_url("/")) as resp, + ): + text = await resp.text() + assert text == "no_auth" + + +async def test_netrc_auth_skipped_without_netrc_env( + aiohttp_server: AiohttpServer, no_netrc: None +) -> None: + """Test that netrc authentication is skipped when NETRC env var is not set.""" + app = web.Application() + app.router.add_get("/", _make_auth_handler()) + + server = await aiohttp_server(app) + # Create session with trust_env=True but no NETRC env var to test ClientSession directly + async with ( + ClientSession(trust_env=True) as session, + session.get(server.make_url("/")) as resp, + ): + text = await resp.text() + assert text == "no_auth" + + +async def test_netrc_auth_overridden_by_explicit_auth( + aiohttp_server: AiohttpServer, netrc_default_contents: pathlib.Path +) -> None: + """Test that explicit auth parameter overrides netrc authentication.""" + app = web.Application() + app.router.add_get("/", _make_auth_handler()) + + server = await aiohttp_server(app) + # Create session with trust_env=True to test ClientSession directly + async with ( + ClientSession(trust_env=True) as session, + session.get( + server.make_url("/"), + auth=aiohttp.BasicAuth("explicit_user", "explicit_pass"), + ) as resp, + ): + text = await resp.text() + # Base64 encoded "explicit_user:explicit_pass" is "ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" + assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" From 2ff3b70cdacbf26d9f0af2542407c0a4859cd6bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Oct 2025 18:34:15 -1000 Subject: [PATCH 02/24] changes --- CHANGES/10435.bugfix.rst | 1 + CHANGES/11634.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 120000 CHANGES/10435.bugfix.rst create mode 100644 CHANGES/11634.bugfix.rst diff --git a/CHANGES/10435.bugfix.rst b/CHANGES/10435.bugfix.rst new file mode 120000 index 00000000000..ffbf42f2152 --- /dev/null +++ b/CHANGES/10435.bugfix.rst @@ -0,0 +1 @@ +11634.bugfix.rst \ No newline at end of file diff --git a/CHANGES/11634.bugfix.rst b/CHANGES/11634.bugfix.rst new file mode 100644 index 00000000000..649577c50b9 --- /dev/null +++ b/CHANGES/11634.bugfix.rst @@ -0,0 +1 @@ +Fixed blocking I/O in the event loop when using netrc authentication by moving netrc file lookup to an executor -- by :user:`bdraco`. From c78571381a4de9efaeb694ff250d54e0da5093be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Oct 2025 18:35:56 -1000 Subject: [PATCH 03/24] preen --- aiohttp/client.py | 4 ++-- tests/conftest.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index b5d7b71771f..b5e8d80cf32 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -1148,7 +1148,8 @@ def _prepare_headers(self, headers: LooseHeaders | None) -> "CIMultiDict[str]": return result def _get_netrc_auth(self, host: str) -> BasicAuth | None: - """Get auth from netrc for the given host. + """ + Get auth from netrc for the given host. This method is designed to be called in an executor to avoid blocking I/O in the event loop. @@ -1156,7 +1157,6 @@ def _get_netrc_auth(self, host: str) -> BasicAuth | None: netrc_obj = netrc_from_env() with suppress(LookupError): return basicauth_from_netrc(netrc_obj, host) - return None if sys.version_info >= (3, 11) and TYPE_CHECKING: diff --git a/tests/conftest.py b/tests/conftest.py index ed6c5de520e..cd7bfad8cd3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,10 +71,6 @@ def blockbuster(request: pytest.FixtureRequest) -> Iterator[None]: with blockbuster_ctx( "aiohttp", excluded_modules=["aiohttp.pytest_plugin", "aiohttp.test_utils"] ) as bb: - # TODO: Fix blocking call in ClientRequest's constructor. - # https://github.com/aio-libs/aiohttp/issues/10435 - for func in ["io.TextIOWrapper.read", "os.stat"]: - bb.functions[func].can_block_in("aiohttp/client_reqrep.py", "update_auth") for func in [ "os.getcwd", "os.readlink", From 5d50745fb855cbb85328c8ace094eb72924d6c00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Oct 2025 18:38:32 -1000 Subject: [PATCH 04/24] preen --- aiohttp/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiohttp/client.py b/aiohttp/client.py index b5e8d80cf32..4220faa3c4d 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -1157,6 +1157,7 @@ def _get_netrc_auth(self, host: str) -> BasicAuth | None: netrc_obj = netrc_from_env() with suppress(LookupError): return basicauth_from_netrc(netrc_obj, host) + return None if sys.version_info >= (3, 11) and TYPE_CHECKING: From 720d3d53565e451996da45b7fd9340ad0e1eb6a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Oct 2025 18:43:03 -1000 Subject: [PATCH 05/24] cover raise case --- tests/conftest.py | 10 ++++++++++ tests/test_client_session.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index cd7bfad8cd3..340da3d573f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -308,6 +308,16 @@ def no_netrc() -> Iterator[None]: yield +@pytest.fixture +def netrc_other_host(tmp_path: Path) -> Iterator[Path]: + """Create a temporary netrc file with credentials for a different host and set NETRC env var.""" + netrc_file = tmp_path / ".netrc" + netrc_file.write_text("machine other.example.com login user password pass\n") + + with mock.patch.dict(os.environ, {"NETRC": str(netrc_file)}): + yield netrc_file + + @pytest.fixture def start_connection() -> Iterator[mock.Mock]: with mock.patch( diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 2cb1e4290e5..ebdc2510cea 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1412,3 +1412,21 @@ async def test_netrc_auth_overridden_by_explicit_auth( text = await resp.text() # Base64 encoded "explicit_user:explicit_pass" is "ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" + + +async def test_netrc_auth_host_not_in_netrc( + aiohttp_server: AiohttpServer, netrc_other_host: pathlib.Path +) -> None: + """Test that netrc lookup returns None when host is not in netrc file.""" + app = web.Application() + app.router.add_get("/", _make_auth_handler()) + + server = await aiohttp_server(app) + + async with ( + ClientSession(trust_env=True) as session, + session.get(server.make_url("/")) as resp, + ): + text = await resp.text() + # Should not have auth since the host is not in netrc + assert text == "no_auth" From 2cca6edd1588ca7330f2cef6c859fadf5240ec4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:08:44 -1000 Subject: [PATCH 06/24] Update tests/conftest.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/conftest.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 340da3d573f..e35c33df29b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -289,33 +289,31 @@ def netrc_contents( @pytest.fixture -def netrc_default_contents(tmp_path: Path) -> Iterator[Path]: +def netrc_default_contents(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: """Create a temporary netrc file with default test credentials and set NETRC env var.""" netrc_file = tmp_path / ".netrc" netrc_file.write_text("default login netrc_user password netrc_pass\n") - with mock.patch.dict(os.environ, {"NETRC": str(netrc_file)}): - yield netrc_file + monkeypatch.setenv("NETRC", str(netrc_file)) + + return netrc_file @pytest.fixture -def no_netrc() -> Iterator[None]: +def no_netrc(monkeypatch: pytest.MonkeyPatch) -> None: """Ensure NETRC environment variable is not set.""" - env = os.environ.copy() - env.pop("NETRC", None) - - with mock.patch.dict(os.environ, env, clear=True): - yield + monkeypatch.delenv("NETRC", raising=False) @pytest.fixture -def netrc_other_host(tmp_path: Path) -> Iterator[Path]: +def netrc_other_host(tmp_path: Path) -> Path: """Create a temporary netrc file with credentials for a different host and set NETRC env var.""" netrc_file = tmp_path / ".netrc" netrc_file.write_text("machine other.example.com login user password pass\n") - with mock.patch.dict(os.environ, {"NETRC": str(netrc_file)}): - yield netrc_file + monkeypatch.setenv("NETRC", str(netrc_file)) + + return netrc_file @pytest.fixture From 37198481ba79c823aafdd11a4b8b213c6836851f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:08:50 -1000 Subject: [PATCH 07/24] Update tests/test_client_session.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index ebdc2510cea..a54c13c84c8 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1359,9 +1359,8 @@ async def test_netrc_auth_with_trust_env( assert text == "auth:Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" -async def test_netrc_auth_skipped_without_trust_env( - aiohttp_server: AiohttpServer, netrc_default_contents: pathlib.Path -) -> None: +@pytest.mark.usefixtures('netrc_default_contents') +async def test_netrc_auth_skipped_without_trust_env(aiohttp_server: AiohttpServer) -> None: """Test that netrc authentication is skipped when trust_env=False.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) From a134768d71a80f1e8c68b4129c53ee5e7e8915c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:08:57 -1000 Subject: [PATCH 08/24] Update tests/test_client_session.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index a54c13c84c8..7ec08602380 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1392,9 +1392,8 @@ async def test_netrc_auth_skipped_without_netrc_env( assert text == "no_auth" -async def test_netrc_auth_overridden_by_explicit_auth( - aiohttp_server: AiohttpServer, netrc_default_contents: pathlib.Path -) -> None: +@pytest.mark.usefixtures('netrc_default_contents') +async def test_netrc_auth_overridden_by_explicit_auth(aiohttp_server: AiohttpServer) -> None: """Test that explicit auth parameter overrides netrc authentication.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) From 34af199489ad99af3a62ebaec8744c0535c26347 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:09:02 -1000 Subject: [PATCH 09/24] Update tests/test_client_session.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 7ec08602380..27a171932a8 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1412,9 +1412,8 @@ async def test_netrc_auth_overridden_by_explicit_auth(aiohttp_server: AiohttpSer assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" -async def test_netrc_auth_host_not_in_netrc( - aiohttp_server: AiohttpServer, netrc_other_host: pathlib.Path -) -> None: +@pytest.mark.usefixtures('netrc_other_host') +async def test_netrc_auth_host_not_in_netrc(aiohttp_server: AiohttpServer) -> None: """Test that netrc lookup returns None when host is not in netrc file.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) From 71d3a5bf458ec9eb4cafe03fa1dc7a88cb1d61ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:10:02 +0000 Subject: [PATCH 10/24] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_client_session.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 27a171932a8..923852fc20d 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1359,8 +1359,10 @@ async def test_netrc_auth_with_trust_env( assert text == "auth:Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" -@pytest.mark.usefixtures('netrc_default_contents') -async def test_netrc_auth_skipped_without_trust_env(aiohttp_server: AiohttpServer) -> None: +@pytest.mark.usefixtures("netrc_default_contents") +async def test_netrc_auth_skipped_without_trust_env( + aiohttp_server: AiohttpServer, +) -> None: """Test that netrc authentication is skipped when trust_env=False.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) @@ -1392,8 +1394,10 @@ async def test_netrc_auth_skipped_without_netrc_env( assert text == "no_auth" -@pytest.mark.usefixtures('netrc_default_contents') -async def test_netrc_auth_overridden_by_explicit_auth(aiohttp_server: AiohttpServer) -> None: +@pytest.mark.usefixtures("netrc_default_contents") +async def test_netrc_auth_overridden_by_explicit_auth( + aiohttp_server: AiohttpServer, +) -> None: """Test that explicit auth parameter overrides netrc authentication.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) @@ -1412,7 +1416,7 @@ async def test_netrc_auth_overridden_by_explicit_auth(aiohttp_server: AiohttpSer assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" -@pytest.mark.usefixtures('netrc_other_host') +@pytest.mark.usefixtures("netrc_other_host") async def test_netrc_auth_host_not_in_netrc(aiohttp_server: AiohttpServer) -> None: """Test that netrc lookup returns None when host is not in netrc file.""" app = web.Application() From 4efa317a4d742e2a421548a3302c59b858146fac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:10:56 -1000 Subject: [PATCH 11/24] add monkey --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e35c33df29b..6833d2c1653 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -306,7 +306,7 @@ def no_netrc(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture -def netrc_other_host(tmp_path: Path) -> Path: +def netrc_other_host(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: """Create a temporary netrc file with credentials for a different host and set NETRC env var.""" netrc_file = tmp_path / ".netrc" netrc_file.write_text("machine other.example.com login user password pass\n") From 1a1e5794e06045489c67a7745e7f6138720068dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:11:43 -1000 Subject: [PATCH 12/24] Update tests/test_client_functional.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_functional.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 555c4c75c59..1402421d345 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3746,9 +3746,8 @@ async def handler(request: web.Request) -> NoReturn: await client.get("/", headers=headers) -async def test_netrc_auth_from_env( - aiohttp_client: AiohttpClient, netrc_default_contents: pathlib.Path -) -> None: +@pytest.mark.usefixtures('netrc_default_contents') +async def test_netrc_auth_from_env(aiohttp_client: AiohttpClient) -> None: """Test that netrc authentication works when NETRC env var is set and trust_env=True.""" async def handler(request: web.Request) -> web.Response: From 130aef4e9f043b109655b9f2837d1f8cb51b2555 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:11:49 -1000 Subject: [PATCH 13/24] Update tests/test_client_functional.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_functional.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 1402421d345..0ebad28a06b 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3765,9 +3765,8 @@ async def handler(request: web.Request) -> web.Response: assert content["headers"]["Authorization"] == "Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" -async def test_netrc_auth_skipped_without_env_var( - aiohttp_client: AiohttpClient, no_netrc: None -) -> None: +@pytest.mark.usefixtures('no_netrc') +async def test_netrc_auth_skipped_without_env_var(aiohttp_client: AiohttpClient) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" async def handler(request: web.Request) -> web.Response: From 9de96aa3419b5600eb76acebdb885a3643bd589c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:11:55 -1000 Subject: [PATCH 14/24] Update tests/test_client_functional.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_functional.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 0ebad28a06b..cc468002034 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3784,9 +3784,8 @@ async def handler(request: web.Request) -> web.Response: assert "Authorization" not in content["headers"] -async def test_netrc_auth_overridden_by_explicit_auth( - aiohttp_client: AiohttpClient, netrc_default_contents: pathlib.Path -) -> None: +@pytest.mark.usefixtures('netrc_default_contents') +async def test_netrc_auth_overridden_by_explicit_auth(aiohttp_client: AiohttpClient) -> None: """Test that explicit auth parameter overrides netrc authentication.""" async def handler(request: web.Request) -> web.Response: From ddf9d4dcf1eee679614b6ddcbc0fa3bc67310bc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:12:03 -1000 Subject: [PATCH 15/24] Update tests/test_client_session.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 923852fc20d..a95ce621e25 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1341,9 +1341,8 @@ async def test_properties( assert value == getattr(session, outer_name) -async def test_netrc_auth_with_trust_env( - aiohttp_server: AiohttpServer, netrc_default_contents: pathlib.Path -) -> None: +@pytest.mark.usefixtures('netrc_default_contents') +async def test_netrc_auth_with_trust_env(aiohttp_server: AiohttpServer) -> None: """Test that netrc authentication works with ClientSession when NETRC env var is set.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) From 5858b5dec795d4446033768c7e06b1f6e02ceaf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:12:09 -1000 Subject: [PATCH 16/24] Update tests/test_client_session.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_client_session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index a95ce621e25..c068122971c 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1376,9 +1376,8 @@ async def test_netrc_auth_skipped_without_trust_env( assert text == "no_auth" -async def test_netrc_auth_skipped_without_netrc_env( - aiohttp_server: AiohttpServer, no_netrc: None -) -> None: +@pytest.mark.usefixtures('no_netrc') +async def test_netrc_auth_skipped_without_netrc_env(aiohttp_server: AiohttpServer) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) From 22efeb9ea1795ea05c9aa132ee740675af7d4835 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:12:23 +0000 Subject: [PATCH 17/24] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_client_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index cc468002034..b1fd1472ed9 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3746,7 +3746,7 @@ async def handler(request: web.Request) -> NoReturn: await client.get("/", headers=headers) -@pytest.mark.usefixtures('netrc_default_contents') +@pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_from_env(aiohttp_client: AiohttpClient) -> None: """Test that netrc authentication works when NETRC env var is set and trust_env=True.""" From 43463c571e7896c192d1dbd2733144bf004dee4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:16:14 -1000 Subject: [PATCH 18/24] preen --- CHANGES/10435.bugfix.rst | 1 - tests/test_client_session.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 120000 CHANGES/10435.bugfix.rst diff --git a/CHANGES/10435.bugfix.rst b/CHANGES/10435.bugfix.rst deleted file mode 120000 index ffbf42f2152..00000000000 --- a/CHANGES/10435.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -11634.bugfix.rst \ No newline at end of file diff --git a/tests/test_client_session.py b/tests/test_client_session.py index c068122971c..2bf55d011a1 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -3,7 +3,6 @@ import gc import io import json -import pathlib import sys import warnings from collections import deque @@ -1341,7 +1340,7 @@ async def test_properties( assert value == getattr(session, outer_name) -@pytest.mark.usefixtures('netrc_default_contents') +@pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_with_trust_env(aiohttp_server: AiohttpServer) -> None: """Test that netrc authentication works with ClientSession when NETRC env var is set.""" app = web.Application() @@ -1376,8 +1375,10 @@ async def test_netrc_auth_skipped_without_trust_env( assert text == "no_auth" -@pytest.mark.usefixtures('no_netrc') -async def test_netrc_auth_skipped_without_netrc_env(aiohttp_server: AiohttpServer) -> None: +@pytest.mark.usefixtures("no_netrc") +async def test_netrc_auth_skipped_without_netrc_env( + aiohttp_server: AiohttpServer, +) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" app = web.Application() app.router.add_get("/", _make_auth_handler()) From ca0f7cf1f6a4397687cd1ac855c22b6849e7e41d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:16:58 +0000 Subject: [PATCH 19/24] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_client_functional.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index b1fd1472ed9..918c2f5ab8e 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3765,8 +3765,10 @@ async def handler(request: web.Request) -> web.Response: assert content["headers"]["Authorization"] == "Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" -@pytest.mark.usefixtures('no_netrc') -async def test_netrc_auth_skipped_without_env_var(aiohttp_client: AiohttpClient) -> None: +@pytest.mark.usefixtures("no_netrc") +async def test_netrc_auth_skipped_without_env_var( + aiohttp_client: AiohttpClient, +) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" async def handler(request: web.Request) -> web.Response: @@ -3784,8 +3786,10 @@ async def handler(request: web.Request) -> web.Response: assert "Authorization" not in content["headers"] -@pytest.mark.usefixtures('netrc_default_contents') -async def test_netrc_auth_overridden_by_explicit_auth(aiohttp_client: AiohttpClient) -> None: +@pytest.mark.usefixtures("netrc_default_contents") +async def test_netrc_auth_overridden_by_explicit_auth( + aiohttp_client: AiohttpClient, +) -> None: """Test that explicit auth parameter overrides netrc authentication.""" async def handler(request: web.Request) -> web.Response: From dad2c27efee8ab3ae68addef6cd9b6375689b005 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:21:44 -1000 Subject: [PATCH 20/24] dry --- tests/test_client_session.py | 61 ++++++++++-------------------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 2bf55d011a1..84a417f9219 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -26,6 +26,7 @@ from aiohttp.cookiejar import CookieJar from aiohttp.http import RawResponseMessage from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer +from aiohttp.test_utils import TestServer from aiohttp.tracing import Trace @@ -89,8 +90,9 @@ def params() -> _Params: ) -def _make_auth_handler() -> Callable[[web.Request], Awaitable[web.Response]]: - """Create a handler that returns auth header or 'no_auth'.""" +@pytest.fixture +async def auth_server(aiohttp_server: AiohttpServer) -> TestServer: + """Create a server with an auth handler that returns auth header or 'no_auth'.""" async def handler(request: web.Request) -> web.Response: auth_header = request.headers.get(hdrs.AUTHORIZATION) @@ -98,7 +100,9 @@ async def handler(request: web.Request) -> web.Response: return web.Response(text=f"auth:{auth_header}") return web.Response(text="no_auth") - return handler + app = web.Application() + app.router.add_get("/", handler) + return await aiohttp_server(app) async def test_close_coro( @@ -1341,16 +1345,11 @@ async def test_properties( @pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_with_trust_env(aiohttp_server: AiohttpServer) -> None: +async def test_netrc_auth_with_trust_env(auth_server: TestServer) -> None: """Test that netrc authentication works with ClientSession when NETRC env var is set.""" - app = web.Application() - app.router.add_get("/", _make_auth_handler()) - - server = await aiohttp_server(app) - # Create session with trust_env=True to test ClientSession directly async with ( ClientSession(trust_env=True) as session, - session.get(server.make_url("/")) as resp, + session.get(auth_server.make_url("/")) as resp, ): text = await resp.text() # Base64 encoded "netrc_user:netrc_pass" is "bmV0cmNfdXNlcjpuZXRyY19wYXNz" @@ -1358,55 +1357,34 @@ async def test_netrc_auth_with_trust_env(aiohttp_server: AiohttpServer) -> None: @pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_skipped_without_trust_env( - aiohttp_server: AiohttpServer, -) -> None: +async def test_netrc_auth_skipped_without_trust_env(auth_server: TestServer) -> None: """Test that netrc authentication is skipped when trust_env=False.""" - app = web.Application() - app.router.add_get("/", _make_auth_handler()) - - server = await aiohttp_server(app) - # Create session with trust_env=False (default) to test ClientSession directly async with ( ClientSession(trust_env=False) as session, - session.get(server.make_url("/")) as resp, + session.get(auth_server.make_url("/")) as resp, ): text = await resp.text() assert text == "no_auth" @pytest.mark.usefixtures("no_netrc") -async def test_netrc_auth_skipped_without_netrc_env( - aiohttp_server: AiohttpServer, -) -> None: +async def test_netrc_auth_skipped_without_netrc_env(auth_server: TestServer) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" - app = web.Application() - app.router.add_get("/", _make_auth_handler()) - - server = await aiohttp_server(app) - # Create session with trust_env=True but no NETRC env var to test ClientSession directly async with ( ClientSession(trust_env=True) as session, - session.get(server.make_url("/")) as resp, + session.get(auth_server.make_url("/")) as resp, ): text = await resp.text() assert text == "no_auth" @pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_overridden_by_explicit_auth( - aiohttp_server: AiohttpServer, -) -> None: +async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) -> None: """Test that explicit auth parameter overrides netrc authentication.""" - app = web.Application() - app.router.add_get("/", _make_auth_handler()) - - server = await aiohttp_server(app) - # Create session with trust_env=True to test ClientSession directly async with ( ClientSession(trust_env=True) as session, session.get( - server.make_url("/"), + auth_server.make_url("/"), auth=aiohttp.BasicAuth("explicit_user", "explicit_pass"), ) as resp, ): @@ -1416,16 +1394,11 @@ async def test_netrc_auth_overridden_by_explicit_auth( @pytest.mark.usefixtures("netrc_other_host") -async def test_netrc_auth_host_not_in_netrc(aiohttp_server: AiohttpServer) -> None: +async def test_netrc_auth_host_not_in_netrc(auth_server: TestServer) -> None: """Test that netrc lookup returns None when host is not in netrc file.""" - app = web.Application() - app.router.add_get("/", _make_auth_handler()) - - server = await aiohttp_server(app) - async with ( ClientSession(trust_env=True) as session, - session.get(server.make_url("/")) as resp, + session.get(auth_server.make_url("/")) as resp, ): text = await resp.text() # Should not have auth since the host is not in netrc From a66abded3306e08e81413315feebd5ff19cd092b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:25:04 -1000 Subject: [PATCH 21/24] preen --- tests/test_client_functional.py | 105 +++++++++++++------------------- 1 file changed, 42 insertions(+), 63 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 918c2f5ab8e..8ad5a598859 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -70,6 +70,23 @@ def fname(here: pathlib.Path) -> pathlib.Path: return here / "conftest.py" +@pytest.fixture +def headers_echo_client( + aiohttp_client: AiohttpClient, +) -> Callable[..., Awaitable[TestClient]]: + """Create a client with an app that echoes request headers as JSON.""" + + async def factory(**kwargs: Any) -> TestClient: + async def handler(request: web.Request) -> web.Response: + return web.json_response({"headers": dict(request.headers)}) + + app = web.Application() + app.router.add_get("/", handler) + return await aiohttp_client(app, **kwargs) + + return factory + + async def test_keepalive_two_requests_success(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.Response: body = await request.read() @@ -3702,14 +3719,10 @@ async def handler(request: web.Request) -> web.Response: assert resp.status == 200 -async def test_session_auth(aiohttp_client: AiohttpClient) -> None: - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - client = await aiohttp_client(app, auth=aiohttp.BasicAuth("login", "pass")) +async def test_session_auth( + headers_echo_client: Callable[..., Awaitable[TestClient]], +) -> None: + client = await headers_echo_client(auth=aiohttp.BasicAuth("login", "pass")) async with client.get("/") as r: assert r.status == 200 @@ -3717,14 +3730,10 @@ async def handler(request: web.Request) -> web.Response: assert content["headers"]["Authorization"] == "Basic bG9naW46cGFzcw==" -async def test_session_auth_override(aiohttp_client: AiohttpClient) -> None: - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - client = await aiohttp_client(app, auth=aiohttp.BasicAuth("login", "pass")) +async def test_session_auth_override( + headers_echo_client: Callable[..., Awaitable[TestClient]], +) -> None: + client = await headers_echo_client(auth=aiohttp.BasicAuth("login", "pass")) async with client.get("/", auth=aiohttp.BasicAuth("other_login", "pass")) as r: assert r.status == 200 @@ -3747,17 +3756,11 @@ async def handler(request: web.Request) -> NoReturn: @pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_from_env(aiohttp_client: AiohttpClient) -> None: +async def test_netrc_auth_from_env( + headers_echo_client: Callable[..., Awaitable[TestClient]], +) -> None: """Test that netrc authentication works when NETRC env var is set and trust_env=True.""" - - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - # Create client with trust_env=True - client = await aiohttp_client(app, trust_env=True) + client = await headers_echo_client(trust_env=True) async with client.get("/") as r: assert r.status == 200 content = await r.json() @@ -3767,18 +3770,10 @@ async def handler(request: web.Request) -> web.Response: @pytest.mark.usefixtures("no_netrc") async def test_netrc_auth_skipped_without_env_var( - aiohttp_client: AiohttpClient, + headers_echo_client: Callable[..., Awaitable[TestClient]], ) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" - - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - # Create client with trust_env=True but no NETRC env var - client = await aiohttp_client(app, trust_env=True) + client = await headers_echo_client(trust_env=True) async with client.get("/") as r: assert r.status == 200 content = await r.json() @@ -3788,18 +3783,10 @@ async def handler(request: web.Request) -> web.Response: @pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_overridden_by_explicit_auth( - aiohttp_client: AiohttpClient, + headers_echo_client: Callable[..., Awaitable[TestClient]], ) -> None: """Test that explicit auth parameter overrides netrc authentication.""" - - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - # Create client with trust_env=True - client = await aiohttp_client(app, trust_env=True) + client = await headers_echo_client(trust_env=True) # Make request with explicit auth (should override netrc) async with client.get( "/", auth=aiohttp.BasicAuth("explicit_user", "explicit_pass") @@ -3813,14 +3800,10 @@ async def handler(request: web.Request) -> web.Response: ) -async def test_session_headers(aiohttp_client: AiohttpClient) -> None: - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - client = await aiohttp_client(app, headers={"X-Real-IP": "192.168.0.1"}) +async def test_session_headers( + headers_echo_client: Callable[..., Awaitable[TestClient]], +) -> None: + client = await headers_echo_client(headers={"X-Real-IP": "192.168.0.1"}) async with client.get("/") as r: assert r.status == 200 @@ -3828,15 +3811,11 @@ async def handler(request: web.Request) -> web.Response: assert content["headers"]["X-Real-IP"] == "192.168.0.1" -async def test_session_headers_merge(aiohttp_client: AiohttpClient) -> None: - async def handler(request: web.Request) -> web.Response: - return web.json_response({"headers": dict(request.headers)}) - - app = web.Application() - app.router.add_get("/", handler) - - client = await aiohttp_client( - app, headers=[("X-Real-IP", "192.168.0.1"), ("X-Sent-By", "requests")] +async def test_session_headers_merge( + headers_echo_client: Callable[..., Awaitable[TestClient]], +) -> None: + client = await headers_echo_client( + headers=[("X-Real-IP", "192.168.0.1"), ("X-Sent-By", "requests")] ) async with client.get("/", headers={"X-Sent-By": "aiohttp"}) as r: From d274ff7216f09b49ac4b34a962cd4b2ec59b0425 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:45:10 -1000 Subject: [PATCH 22/24] lint --- tests/test_client_functional.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 8ad5a598859..d65ab39fde4 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -73,10 +73,10 @@ def fname(here: pathlib.Path) -> pathlib.Path: @pytest.fixture def headers_echo_client( aiohttp_client: AiohttpClient, -) -> Callable[..., Awaitable[TestClient]]: +) -> Callable[..., Awaitable[TestClient[web.Request, web.Application]]]: """Create a client with an app that echoes request headers as JSON.""" - async def factory(**kwargs: Any) -> TestClient: + async def factory(**kwargs: Any) -> TestClient[web.Request, web.Application]: async def handler(request: web.Request) -> web.Response: return web.json_response({"headers": dict(request.headers)}) @@ -3720,7 +3720,9 @@ async def handler(request: web.Request) -> web.Response: async def test_session_auth( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: client = await headers_echo_client(auth=aiohttp.BasicAuth("login", "pass")) @@ -3731,7 +3733,9 @@ async def test_session_auth( async def test_session_auth_override( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: client = await headers_echo_client(auth=aiohttp.BasicAuth("login", "pass")) @@ -3757,7 +3761,9 @@ async def handler(request: web.Request) -> NoReturn: @pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_from_env( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: """Test that netrc authentication works when NETRC env var is set and trust_env=True.""" client = await headers_echo_client(trust_env=True) @@ -3770,7 +3776,9 @@ async def test_netrc_auth_from_env( @pytest.mark.usefixtures("no_netrc") async def test_netrc_auth_skipped_without_env_var( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: """Test that netrc authentication is skipped when NETRC env var is not set.""" client = await headers_echo_client(trust_env=True) @@ -3783,7 +3791,9 @@ async def test_netrc_auth_skipped_without_env_var( @pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_overridden_by_explicit_auth( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: """Test that explicit auth parameter overrides netrc authentication.""" client = await headers_echo_client(trust_env=True) @@ -3801,7 +3811,9 @@ async def test_netrc_auth_overridden_by_explicit_auth( async def test_session_headers( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: client = await headers_echo_client(headers={"X-Real-IP": "192.168.0.1"}) @@ -3812,7 +3824,9 @@ async def test_session_headers( async def test_session_headers_merge( - headers_echo_client: Callable[..., Awaitable[TestClient]], + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], ) -> None: client = await headers_echo_client( headers=[("X-Real-IP", "192.168.0.1"), ("X-Sent-By", "requests")] From db59ba7bcdec4d21e0c8452ebbf93f45fde33bc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Oct 2025 08:53:38 -1000 Subject: [PATCH 23/24] lint --- tests/test_client_functional.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index d65ab39fde4..731878d7c1b 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3760,7 +3760,7 @@ async def handler(request: web.Request) -> NoReturn: @pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_from_env( +async def test_netrc_auth_from_env( # type: ignore[misc] headers_echo_client: Callable[ ..., Awaitable[TestClient[web.Request, web.Application]] ], @@ -3775,7 +3775,7 @@ async def test_netrc_auth_from_env( @pytest.mark.usefixtures("no_netrc") -async def test_netrc_auth_skipped_without_env_var( +async def test_netrc_auth_skipped_without_env_var( # type: ignore[misc] headers_echo_client: Callable[ ..., Awaitable[TestClient[web.Request, web.Application]] ], @@ -3790,7 +3790,7 @@ async def test_netrc_auth_skipped_without_env_var( @pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_overridden_by_explicit_auth( +async def test_netrc_auth_overridden_by_explicit_auth( # type: ignore[misc] headers_echo_client: Callable[ ..., Awaitable[TestClient[web.Request, web.Application]] ], From f629a1118e4e71d076b29f2f0440f9770bc0f841 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Oct 2025 13:41:30 -1000 Subject: [PATCH 24/24] Update aiohttp/client.py Co-authored-by: Sam Bull --- aiohttp/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index 4220faa3c4d..7a4ad715362 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -1155,9 +1155,10 @@ def _get_netrc_auth(self, host: str) -> BasicAuth | None: blocking I/O in the event loop. """ netrc_obj = netrc_from_env() - with suppress(LookupError): + try: return basicauth_from_netrc(netrc_obj, host) - return None + except LookupError: + return None if sys.version_info >= (3, 11) and TYPE_CHECKING: