Skip to content

Commit 75fcdfa

Browse files
authored
chore(google-auth): fix aiohttp 3.10+ test compatibility (#17643)
1. **Unpinned `aiohttp` in `setup.py`**: - Removed `"aiohttp < 3.10.0"` from `testing_extra_require` and resolved TODO comment for #1722. 2. **Added Shared `_get_local_addr` Helper**: - Added `_get_local_addr` in `google.auth.aio._helpers` to extract local address from `TCPConnector._local_addr` (on `aiohttp < 3.10`) or `TCPConnector._local_addr_infos` (on `aiohttp >= 3.10`). - Updated `google.auth.aio.transport.aiohttp` and `google.auth.transport._aiohttp_requests` to reuse this helper when cloning sessions. 3. **`_CompatClientResponse` Test Adapter in `conftest.py`**: - Registered a `_CompatClientResponse` class in `tests/transport/aio/conftest.py` and `tests_async/transport/conftest.py` that inspects `ClientResponse.__init__` and safely maps `writer` $\rightarrow$ `stream_writer` for `aioresponses` on `aiohttp` 3.10+. 4. **Updated Test Mocks**: - Refactored private `_auto_decompress` $\rightarrow$ public `auto_decompress` property in `tests_async/transport/test_aiohttp_requests.py` for `aiohttp` 3.10+ compatibility. Fixes #1722🦕
1 parent 4cfb931 commit 75fcdfa

8 files changed

Lines changed: 133 additions & 28 deletions

File tree

packages/google-auth/google/auth/aio/_helpers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
"""Helper functions for commonly used utilities."""
1616

17-
1817
import logging
1918
from typing import Any
2019

@@ -60,3 +59,13 @@ async def response_log_async(logger: logging.Logger, response: Any) -> None:
6059
# json_response = await _parse_response_async(response)
6160
json_response = None
6261
_helpers._response_log_base(logger, json_response)
62+
63+
64+
def _get_local_addr(connector: Any) -> Any:
65+
local_addr = getattr(connector, "_local_addr", None)
66+
if local_addr is not None:
67+
return local_addr
68+
local_addr_infos = getattr(connector, "_local_addr_infos", None)
69+
if local_addr_infos:
70+
return local_addr_infos[0][4]
71+
return None

packages/google-auth/google/auth/aio/transport/aiohttp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def _clone(self) -> "Request":
248248
limit=getattr(orig_connector, "_limit", 100),
249249
limit_per_host=getattr(orig_connector, "_limit_per_host", 0),
250250
force_close=getattr(orig_connector, "_force_close", False),
251-
local_addr=getattr(orig_connector, "_local_addr", None),
251+
local_addr=_helpers_async._get_local_addr(orig_connector),
252252
)
253253
elif getattr(aiohttp, "UnixConnector", None) and isinstance(
254254
orig_connector, getattr(aiohttp, "UnixConnector")

packages/google-auth/google/auth/transport/_aiohttp_requests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
_LOGGER = logging.getLogger(__name__)
3838

39+
3940
# Timeout can be re-defined depending on async requirement. Currently made 60s more than
4041
# sync timeout.
4142
_DEFAULT_TIMEOUT = 180 # in seconds
@@ -250,7 +251,7 @@ def _clone(self):
250251
limit=getattr(orig_connector, "_limit", 100),
251252
limit_per_host=getattr(orig_connector, "_limit_per_host", 0),
252253
force_close=getattr(orig_connector, "_force_close", False),
253-
local_addr=getattr(orig_connector, "_local_addr", None),
254+
local_addr=_helpers_async._get_local_addr(orig_connector),
254255
)
255256
elif getattr(aiohttp, "UnixConnector", None) and isinstance(
256257
orig_connector, getattr(aiohttp, "UnixConnector")

packages/google-auth/setup.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@
5959
*aiohttp_extra_require,
6060
"aioresponses",
6161
"pytest-asyncio",
62-
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1722): `test_aiohttp_requests` depend on
63-
# aiohttp < 3.10.0 which is a bug. Investigate and remove the pinned aiohttp version.
64-
"aiohttp < 3.10.0",
6562
]
6663

6764
extras = {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import inspect
16+
from unittest.mock import Mock
17+
18+
import aiohttp
19+
from aioresponses.core import RequestMatch # type: ignore
20+
21+
22+
class _CompatClientResponse(aiohttp.ClientResponse):
23+
"""ClientResponse subclass for aioresponses compatibility across all aiohttp versions."""
24+
25+
def __init__(self, *args, **kwargs):
26+
writer = kwargs.pop("writer", None)
27+
stream_writer = kwargs.pop("stream_writer", None)
28+
writer_obj = stream_writer or writer or Mock()
29+
sig = inspect.signature(super().__init__)
30+
if "stream_writer" in sig.parameters:
31+
kwargs["stream_writer"] = writer_obj
32+
if "writer" in sig.parameters:
33+
kwargs["writer"] = writer_obj
34+
super().__init__(*args, **kwargs)
35+
36+
37+
_orig_request_match_init = RequestMatch.__init__
38+
39+
40+
def _request_match_init(self, *args, **kwargs):
41+
if kwargs.get("response_class") is None:
42+
kwargs["response_class"] = _CompatClientResponse
43+
_orig_request_match_init(self, *args, **kwargs)
44+
45+
46+
RequestMatch.__init__ = _request_match_init

packages/google-auth/tests/transport/aio/test_aiohttp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import pytest_asyncio # type: ignore
2121

2222
from google.auth import exceptions
23+
from google.auth.aio import _helpers as _helpers_async
2324
import google.auth.aio.transport.aiohttp as auth_aiohttp
2425

2526
try:
@@ -234,7 +235,10 @@ async def test_request_clone_with_active_session(self):
234235
assert cloned_connector._limit == 42
235236
assert cloned_connector._limit_per_host == 12
236237
assert cloned_connector._force_close is True
237-
assert cloned_connector._local_addr == ("127.0.0.2", 0)
238+
assert _helpers_async._get_local_addr(cloned_connector) == (
239+
"127.0.0.2",
240+
0,
241+
)
238242

239243
# Verify session-level configuration
240244
assert cloned._session._trust_env is True
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import inspect
16+
from unittest.mock import Mock
17+
18+
import aiohttp
19+
from aioresponses.core import RequestMatch # type: ignore
20+
21+
22+
class _CompatClientResponse(aiohttp.ClientResponse):
23+
"""ClientResponse subclass for aioresponses compatibility across all aiohttp versions."""
24+
25+
def __init__(self, *args, **kwargs):
26+
writer = kwargs.pop("writer", None)
27+
stream_writer = kwargs.pop("stream_writer", None)
28+
writer_obj = stream_writer or writer or Mock()
29+
sig = inspect.signature(super().__init__)
30+
if "stream_writer" in sig.parameters:
31+
kwargs["stream_writer"] = writer_obj
32+
if "writer" in sig.parameters:
33+
kwargs["writer"] = writer_obj
34+
super().__init__(*args, **kwargs)
35+
36+
37+
_orig_request_match_init = RequestMatch.__init__
38+
39+
40+
def _request_match_init(self, *args, **kwargs):
41+
if kwargs.get("response_class") is None:
42+
kwargs["response_class"] = _CompatClientResponse
43+
_orig_request_match_init(self, *args, **kwargs)
44+
45+
46+
RequestMatch.__init__ = _request_match_init

packages/google-auth/tests_async/transport/test_aiohttp_requests.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_timeout(self):
138138
@pytest.mark.asyncio
139139
async def test__clone(self):
140140
http = mock.create_autospec(
141-
aiohttp.ClientSession, instance=True, _auto_decompress=False
141+
aiohttp.ClientSession, instance=True, auto_decompress=False
142142
)
143143
http._connector = mock.Mock(spec=aiohttp.TCPConnector)
144144
http._connector.closed = False
@@ -158,12 +158,13 @@ async def test__clone(self):
158158
http._json_serialize = mock.sentinel.json_serialize
159159

160160
request = aiohttp_requests.Request(http)
161-
with mock.patch(
162-
"aiohttp.ClientSession", autospec=True
163-
) as session_mock, mock.patch.object(
164-
aiohttp.TCPConnector, "__init__", autospec=True, return_value=None
165-
) as connector_init_mock:
166-
session_mock.return_value._auto_decompress = False
161+
with (
162+
mock.patch("aiohttp.ClientSession", autospec=True) as session_mock,
163+
mock.patch.object(
164+
aiohttp.TCPConnector, "__init__", autospec=True, return_value=None
165+
) as connector_init_mock,
166+
):
167+
session_mock.return_value.auto_decompress = False
167168
cloned = request._clone()
168169

169170
assert isinstance(cloned, aiohttp_requests.Request)
@@ -204,7 +205,7 @@ async def test__clone_closed(self):
204205
@pytest.mark.asyncio
205206
async def test__clone_custom_connector(self):
206207
http = mock.create_autospec(
207-
aiohttp.ClientSession, instance=True, _auto_decompress=False
208+
aiohttp.ClientSession, instance=True, auto_decompress=False
208209
)
209210
http._connector = mock.Mock()
210211
http._connector.closed = False
@@ -218,7 +219,7 @@ async def test__clone_custom_connector(self):
218219
@pytest.mark.asyncio
219220
async def test_close(self):
220221
http = mock.create_autospec(
221-
aiohttp.ClientSession, instance=True, _auto_decompress=False
222+
aiohttp.ClientSession, instance=True, auto_decompress=False
222223
)
223224
http.close = mock.AsyncMock()
224225
request = aiohttp_requests.Request(http)
@@ -234,7 +235,7 @@ async def test_close(self):
234235
@pytest.mark.asyncio
235236
async def test_request_call_closed(self):
236237
http = mock.create_autospec(
237-
aiohttp.ClientSession, instance=True, _auto_decompress=False
238+
aiohttp.ClientSession, instance=True, auto_decompress=False
238239
)
239240
request = aiohttp_requests.Request(http)
240241
await request.close()
@@ -255,7 +256,7 @@ async def test__clone_no_session(self):
255256
@pytest.mark.asyncio
256257
async def test__clone_closed_connector(self):
257258
http = mock.create_autospec(
258-
aiohttp.ClientSession, instance=True, _auto_decompress=False
259+
aiohttp.ClientSession, instance=True, auto_decompress=False
259260
)
260261
http._connector = mock.Mock()
261262
http._connector.closed = True
@@ -269,7 +270,7 @@ async def test__clone_closed_connector(self):
269270

270271
request = aiohttp_requests.Request(http)
271272
with mock.patch("aiohttp.ClientSession", autospec=True) as session_mock:
272-
session_mock.return_value._auto_decompress = False
273+
session_mock.return_value.auto_decompress = False
273274
cloned = request._clone()
274275

275276
assert isinstance(cloned, aiohttp_requests.Request)
@@ -283,7 +284,7 @@ async def test__clone_unix_socket_no_path(self):
283284
return
284285

285286
http = mock.create_autospec(
286-
aiohttp.ClientSession, instance=True, _auto_decompress=False
287+
aiohttp.ClientSession, instance=True, auto_decompress=False
287288
)
288289
http._connector = mock.Mock(spec=UnixConnector)
289290
http._connector.closed = False
@@ -298,7 +299,7 @@ async def test__clone_unix_socket_no_path(self):
298299

299300
request = aiohttp_requests.Request(http)
300301
with mock.patch("aiohttp.ClientSession", autospec=True) as session_mock:
301-
session_mock.return_value._auto_decompress = False
302+
session_mock.return_value.auto_decompress = False
302303
cloned = request._clone()
303304

304305
assert isinstance(cloned, aiohttp_requests.Request)
@@ -312,7 +313,7 @@ async def test__clone_unix_socket_with_path(self):
312313
return
313314

314315
http = mock.create_autospec(
315-
aiohttp.ClientSession, instance=True, _auto_decompress=False
316+
aiohttp.ClientSession, instance=True, auto_decompress=False
316317
)
317318
http._connector = mock.Mock(spec=UnixConnector)
318319
http._connector.closed = False
@@ -328,12 +329,13 @@ async def test__clone_unix_socket_with_path(self):
328329
http._json_serialize = None
329330

330331
request = aiohttp_requests.Request(http)
331-
with mock.patch(
332-
"aiohttp.ClientSession", autospec=True
333-
) as session_mock, mock.patch.object(
334-
UnixConnector, "__init__", autospec=True, return_value=None
335-
) as connector_init_mock:
336-
session_mock.return_value._auto_decompress = False
332+
with (
333+
mock.patch("aiohttp.ClientSession", autospec=True) as session_mock,
334+
mock.patch.object(
335+
UnixConnector, "__init__", autospec=True, return_value=None
336+
) as connector_init_mock,
337+
):
338+
session_mock.return_value.auto_decompress = False
337339
cloned = request._clone()
338340

339341
assert isinstance(cloned, aiohttp_requests.Request)

0 commit comments

Comments
 (0)