From dd0c0b0a8405b4342cf4ed92f0129e0612d7bd40 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:04:30 +0300 Subject: [PATCH 1/9] refactor: `get_test_response` rename to `fetch_cached_response` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function sets up a mock aiohttp test application, performs a GET request through a test client, and wraps the result in a `CachedResponse`. The name reflects the primary action (fetching over HTTP) and the return type, while leaving the mock infrastructure as an implementation detail. Alternatives considered: - `get_cached_response` — easily confused with HTTP GET - `make_cached_response` — downplays the network round-trip - `mock_request_cached_response` — accurate but overly verbose --- test/unit/test_response.py | 42 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/unit/test_response.py b/test/unit/test_response.py index 062d691..41b30d6 100644 --- a/test/unit/test_response.py +++ b/test/unit/test_response.py @@ -12,7 +12,7 @@ from aiohttp_client_cache.response import CachedResponse, RequestInfo, UnsupportedExpiresError -async def get_test_response(client_factory, url='/', headers=None, **kwargs): +async def fetch_test_response(client_factory, url='/', headers=None, **kwargs): app = web.Application() app.router.add_route('GET', '/valid_url', mock_handler) app.router.add_route('GET', '/json', json_mock_handler) @@ -50,7 +50,7 @@ async def null_mock_handler(request): async def test_basic_attrs(aiohttp_client): - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) assert response.method == 'GET' assert response.reason == 'Not Found' @@ -68,7 +68,7 @@ async def test_is_expired(mock_utcnow, aiohttp_client): mock_utcnow.return_value = utcnow() expires = utcnow() + timedelta(seconds=0.02) - response = await get_test_response(aiohttp_client, expires=expires) + response = await fetch_test_response(aiohttp_client, expires=expires) assert response.expires == expires assert response.is_expired is False @@ -79,25 +79,25 @@ async def test_is_expired(mock_utcnow, aiohttp_client): async def test_is_expired__invalid(aiohttp_client): with pytest.raises(AttributeError, match="'str' object has no attribute 'tzinfo'"): - await get_test_response(aiohttp_client, expires='asdf') + await fetch_test_response(aiohttp_client, expires='asdf') with pytest.raises(UnsupportedExpiresError, match='Expected a naive datetime'): - await get_test_response(aiohttp_client, expires=datetime.now(timezone.utc)) + await fetch_test_response(aiohttp_client, expires=datetime.now(timezone.utc)) async def test_content_disposition(aiohttp_client): - response = await get_test_response(aiohttp_client, '/valid_url') + response = await fetch_test_response(aiohttp_client, '/valid_url') assert response.content_disposition.type == 'attachment' assert response.content_disposition.filename == 'img.jpg' assert response.content_disposition.parameters.get('name') == 'test-param' async def test_encoding(aiohttp_client): - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) assert response.encoding == response.get_encoding() == 'utf-8' async def test_request_info(aiohttp_client): - response = await get_test_response( + response = await fetch_test_response( aiohttp_client, '/valid_url', headers={'Custom-Header': 'value'} ) request_info = response.request_info @@ -110,7 +110,7 @@ async def test_request_info(aiohttp_client): async def test_headers(aiohttp_client): - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) raw_headers = dict(response.raw_headers) assert b'Content-Type' in raw_headers and b'Content-Length' in raw_headers @@ -121,7 +121,7 @@ async def test_headers(aiohttp_client): async def test_headers__mixin_attributes(aiohttp_client): - response = await get_test_response(aiohttp_client, '/valid_url') + response = await fetch_test_response(aiohttp_client, '/valid_url') assert response.charset == 'utf-8' assert response.content_length == 12 assert response.content_type == 'text/plain' @@ -129,7 +129,7 @@ async def test_headers__mixin_attributes(aiohttp_client): async def test_headers__case_insensitive_multidict(aiohttp_client): """Headers should be case-insensitive and allow multiple values""" - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) response.raw_headers += ((b'Cache-Control', b'public'),) response.raw_headers += ((b'Cache-Control', b'max-age=360'),) @@ -140,7 +140,7 @@ async def test_headers__case_insensitive_multidict(aiohttp_client): async def test_links(aiohttp_client): - response = await get_test_response(aiohttp_client, '/valid_url') + response = await fetch_test_response(aiohttp_client, '/valid_url') expected_links = [('preconnect', [('rel', 'preconnect'), ('url', 'https://example.com')])] assert response._links == expected_links assert isinstance(response.links, MultiDictProxy) @@ -153,52 +153,52 @@ async def test_history(aiohttp_client): async def test_json(aiohttp_client): - response = await get_test_response(aiohttp_client, '/json') + response = await fetch_test_response(aiohttp_client, '/json') assert await response.json() == {'key': 'value'} async def test_json__empty_content(aiohttp_client): - response = await get_test_response(aiohttp_client, '/empty_content') + response = await fetch_test_response(aiohttp_client, '/empty_content') assert await response.json() is None async def test_json__null_content(aiohttp_client): - response = await get_test_response(aiohttp_client, '/null_content') + response = await fetch_test_response(aiohttp_client, '/null_content') assert await response.json() is None async def test_json__non_json_content(aiohttp_client): - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) with pytest.raises(ValueError): await response.json() async def test_raise_for_status__200(aiohttp_client): - response = await get_test_response(aiohttp_client, '/valid_url') + response = await fetch_test_response(aiohttp_client, '/valid_url') assert not response.raise_for_status() assert response.ok is True async def test_raise_for_status__404(aiohttp_client): - response = await get_test_response(aiohttp_client, '/invalid_url') + response = await fetch_test_response(aiohttp_client, '/invalid_url') with pytest.raises(ClientResponseError): response.raise_for_status() assert response.ok is False async def test_text(aiohttp_client): - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) assert await response.text() == '404: Not Found' async def test_read(aiohttp_client): - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) assert await response.read() == b'404: Not Found' async def test_no_ops(aiohttp_client): # Just make sure CachedResponse doesn't explode if extra ClientResponse methods are called - response = await get_test_response(aiohttp_client) + response = await fetch_test_response(aiohttp_client) await response.start() response.release() From bc5d50ac82c547bce51d46b2fc0cfc4b6868c18b Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:29:07 +0300 Subject: [PATCH 2/9] refactor: rename `get_cached_response` to `make_mock_cached_response` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function is a test factory that returns a `MagicMock` with `spec=CachedResponse` and sensible defaults (`status=200`, `is_expired=False`, etc.). The name follows Python testing conventions (`make_*` for lightweight test factories) and clearly describes both the action and the return type. Alternatives considered: - `mock_cached_response` — reads like a fixture, not a callable - `create_mock_cached_response` — implies heavier construction than exists here - `build_cached_response_mock` — overly verbose and awkward word order --- test/unit/test_base_backend.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/test/unit/test_base_backend.py b/test/unit/test_base_backend.py index 6f954a8..dd1965a 100644 --- a/test/unit/test_base_backend.py +++ b/test/unit/test_base_backend.py @@ -11,7 +11,7 @@ TEST_URL = 'https://test.com' -def get_mock_response(**kwargs): +def make_mock_cached_response(**kwargs): response_kwargs = { 'url': TEST_URL, 'method': 'GET', @@ -40,7 +40,7 @@ def __init__(self): async def test_get_response__cache_response_hit(): cache = CacheBackend() - mock_response = get_mock_response() + mock_response = make_mock_cached_response() await cache.responses.write('request-key', mock_response) response = await cache.get_response('request-key') @@ -50,7 +50,7 @@ async def test_get_response__cache_response_hit(): async def test_get_response__cache_redirect_hit(): # Set up a cache with a couple cached items and a redirect cache = CacheBackend() - mock_response = get_mock_response() + mock_response = make_mock_cached_response() await cache.responses.write('request-key', mock_response) await cache.redirects.write('redirect-key', 'request-key') @@ -71,7 +71,7 @@ async def test_get_response__cache_miss(mock_delete): @patch.object(CacheBackend, 'is_cacheable', return_value=False) async def test_get_response__cache_expired(mock_is_cacheable, mock_delete): cache = CacheBackend() - mock_response = get_mock_response(is_expired=True) + mock_response = make_mock_cached_response(is_expired=True) await cache.responses.write('request-key', mock_response) response = await cache.get_response('request-key') @@ -85,7 +85,7 @@ async def test_get_response__cache_expired(mock_is_cacheable, mock_delete): async def test_get_response__cache_invalid(mock_read, mock_delete, error_type): cache = CacheBackend() mock_read.side_effect = error_type - mock_response = get_mock_response() + mock_response = make_mock_cached_response() await cache.responses.write('request-key', mock_response) response = await cache.get_response('request-key') @@ -99,7 +99,7 @@ async def test_get_response__quiet_serde_error(mock_read): missing """ cache = CacheBackend() - mock_response = get_mock_response() + mock_response = make_mock_cached_response() await cache.responses.write('request-key', mock_response) response = await cache.get_response('request-key') @@ -108,7 +108,7 @@ async def test_get_response__quiet_serde_error(mock_read): async def test_save_response(): cache = CacheBackend() - mock_response = get_mock_response() + mock_response = make_mock_cached_response() mock_response.history = [MagicMock(method='GET', url='test')] redirect_key = cache.create_key('GET', 'test') @@ -121,7 +121,7 @@ async def test_save_response(): async def test_save_response__manual_save(): """Manually save a response with no cache key provided""" cache = CacheBackend() - mock_response = get_mock_response() + mock_response = make_mock_cached_response() await cache.save_response(mock_response) cached_response = [r async for r in cache.responses.values()][0] @@ -140,7 +140,7 @@ async def test_clear(): async def test_delete(): cache = CacheBackend() - mock_response = get_mock_response() + mock_response = make_mock_cached_response() mock_response.history = [MagicMock(method='GET', url='test')] redirect_key = cache.create_key('GET', 'test') @@ -155,8 +155,8 @@ async def test_delete(): async def test_delete_expired_responses(): cache = CacheBackend() - await cache.responses.write('request-key-1', get_mock_response(is_expired=False)) - await cache.responses.write('request-key-2', get_mock_response(is_expired=True)) + await cache.responses.write('request-key-1', make_mock_cached_response(is_expired=False)) + await cache.responses.write('request-key-2', make_mock_cached_response(is_expired=True)) assert await cache.responses.size() == 2 await cache.delete_expired_responses() @@ -165,7 +165,7 @@ async def test_delete_expired_responses(): async def test_delete_url(): cache = CacheBackend() - mock_response = await CachedResponse.from_client_response(get_mock_response()) + mock_response = await CachedResponse.from_client_response(make_mock_cached_response()) cache_key = cache.create_key('GET', TEST_URL, params={'param': 'value'}) await cache.responses.write(cache_key, mock_response) @@ -176,7 +176,7 @@ async def test_delete_url(): async def test_has_url(): cache = CacheBackend() - mock_response = await CachedResponse.from_client_response(get_mock_response()) + mock_response = await CachedResponse.from_client_response(make_mock_cached_response()) cache_key = cache.create_key('GET', TEST_URL, params={'param': 'value'}) await cache.responses.write(cache_key, mock_response) @@ -204,7 +204,7 @@ async def test_create_key(mock_create_key): async def test_get_urls(): cache = CacheBackend() for i in range(7): - mock_response = get_mock_response(url=f'https://test.com/{i}') + mock_response = make_mock_cached_response(url=f'https://test.com/{i}') await cache.responses.write(f'request-key-{i}', mock_response) urls = {url async for url in cache.get_urls()} @@ -223,7 +223,7 @@ async def test_get_urls(): ], ) async def test_is_cacheable(method, status, disabled, expired, filter_return, expected_result): - mock_response = get_mock_response( + mock_response = make_mock_cached_response( method=method, status=status, is_expired=expired, @@ -251,7 +251,9 @@ async def filter(resp): return json_resp['success'] - mock_response = get_mock_response(method=method, status=status, is_expired=expired, _body=body) + mock_response = make_mock_cached_response( + method=method, status=status, is_expired=expired, _body=body + ) cache = CacheBackend() cache.filter_fn = filter From d165aa823f12486089eaacc372be96c358006d25 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:40:46 +0300 Subject: [PATCH 3/9] refactor: rename `get_combined_revision` to `merge_signatures` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function extracts parameter definitions from multiple input Callables, deduplicates overlapping kwargs, and returns a unified forged signature. The name reflects the core operation (merging multiple signatures) and the return type. Alternatives considered: - `combine_signatures` — equally valid, but `merge` is more idiomatic for unifying collections - `compose_signatures` — implies functional composition, which doesn't apply here - `collect_signature_params` — too narrow; returns a full signature, not just a dict --- aiohttp_client_cache/signatures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiohttp_client_cache/signatures.py b/aiohttp_client_cache/signatures.py index ffb6b93..ba2b1d6 100644 --- a/aiohttp_client_cache/signatures.py +++ b/aiohttp_client_cache/signatures.py @@ -37,7 +37,7 @@ def extend_signature(super_function: Callable, *extra_functions: Callable) -> Ca def wrapper(target_function: Callable): try: target_function = copy_docstrings(target_function, super_function, *extra_functions) - revision = get_combined_revision(target_function, super_function, *extra_functions) + revision = merge_signatures(target_function, super_function, *extra_functions) return revision(target_function) except Exception as e: logger.debug(e) @@ -46,7 +46,7 @@ def wrapper(target_function: Callable): return wrapper -def get_combined_revision(*functions: Callable): +def merge_signatures(*functions: Callable): """Combine the parameters of all revisions into a single revision""" import forge From 96bfa94da6d974a1a660b5e50bd35a358b315988 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:39:35 +0300 Subject: [PATCH 4/9] fix: delete dead code --- test/conftest.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 65ed38c..0eb8090 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,13 +1,8 @@ import logging -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager from datetime import datetime from os import getenv -from tempfile import NamedTemporaryFile -import pytest - -from aiohttp_client_cache import CachedResponse, CachedSession, SQLiteBackend +from aiohttp_client_cache import CachedResponse ALL_METHODS = ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] CACHE_NAME = 'pytest_cache' @@ -53,22 +48,6 @@ def httpbin_custom(path: str = ''): return base_url + path -@pytest.fixture(scope='function') -async def tempfile_session(): - """:py:func:`.get_tempfile_session` as a pytest fixture""" - async with get_tempfile_session() as session: - yield session - - -@asynccontextmanager -async def get_tempfile_session(**kwargs) -> AsyncIterator[CachedSession]: - """Get a CachedSession using a temporary SQLite db""" - with NamedTemporaryFile(suffix='.db') as temp: - cache = SQLiteBackend(cache_name=temp.name, allowed_methods=ALL_METHODS, **kwargs) - async with CachedSession(cache=cache) as session: - yield session - - def assert_delta_approx_equal(dt1: datetime, dt2: datetime, target_delta, threshold_seconds=2): """Assert that the given datetimes are approximately ``target_delta`` seconds apart""" diff_in_seconds = (dt2 - dt1).total_seconds() From 8311c75e171bdd3140dbaf478d6114b7a3a8ab45 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:54:49 +0300 Subject: [PATCH 5/9] fix: rename misleading inner function `get_db_info` to `probe_redis` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inner async function connects to a Redis instance, calls `info()` to confirm the server is reachable, then closes the connection. The name uses `probe` to convey a lightweight connectivity check. Alternatives considered: - `_ping_redis` — common idiom but technically inaccurate; calls `info()`, not `PING` - `_check_redis_connection` — descriptive but overly long for a nested closure - `_redis_info` — sounds like a getter, not a connectivity probe - `_connect_and_verify` — generic; doesn't name the target service --- test/integration/test_redis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/test_redis.py b/test/integration/test_redis.py index bffd023..ffd1dc8 100644 --- a/test/integration/test_redis.py +++ b/test/integration/test_redis.py @@ -12,13 +12,13 @@ def is_db_running(): """Test if a Redis server is running locally on the default port""" - async def get_db_info(): + async def probe_redis(): client = await from_url(DEFAULT_ADDRESS) await client.info() await client.aclose() # type: ignore[attr-defined] try: - asyncio.run(get_db_info()) + asyncio.run(probe_redis()) return True except OSError as e: print(e) From 80ece9b66ea0bf4b43c253dd49d47d951424ec00 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:57:03 +0300 Subject: [PATCH 6/9] refactor: remove redundant assignment --- test/unit/test_base_backend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/test_base_backend.py b/test/unit/test_base_backend.py index dd1965a..0be7c18 100644 --- a/test/unit/test_base_backend.py +++ b/test/unit/test_base_backend.py @@ -60,9 +60,8 @@ async def test_get_response__cache_redirect_hit(): @patch.object(CacheBackend, 'delete') async def test_get_response__cache_miss(mock_delete): - cache = CacheBackend() - response_1 = await cache.get_response('nonexistent-key') + response_1 = await CacheBackend().get_response('nonexistent-key') assert response_1 is None mock_delete.assert_not_called() From aeed9df7fab3eaf9dab12492e94fdb26f518f749 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:59:50 +0300 Subject: [PATCH 7/9] refactor: remove redundant assignment The code adds a name that does not improve the reader's understanding. That is usually a poor tradeoff: it adds a statement, a local variable, and a name that the reader must mentally associate with statement. Do not introduce a local variable solely to satisfy a line-length limit. --- aiohttp_client_cache/backends/mongodb.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aiohttp_client_cache/backends/mongodb.py b/aiohttp_client_cache/backends/mongodb.py index 6645a01..1c5800b 100644 --- a/aiohttp_client_cache/backends/mongodb.py +++ b/aiohttp_client_cache/backends/mongodb.py @@ -72,8 +72,7 @@ async def bulk_delete(self, keys: set): await self.collection.delete_many(spec) async def delete(self, key: str): - spec = {'_id': key} - await self.collection.delete_one(spec) + await self.collection.delete_one({'_id': key}) async def keys(self) -> AsyncIterable[str]: async for doc in self.collection.find({}, {'_id': True}): @@ -96,8 +95,7 @@ async def values(self) -> AsyncIterable[ResponseOrKey]: yield doc['data'] async def write(self, key: str, item: ResponseOrKey): - update = {'$set': {'data': item}} - await self.collection.update_one({'_id': key}, update, upsert=True) + await self.collection.update_one({'_id': key}, {'$set': {'data': item}}, upsert=True) class MongoDBPickleCache(MongoDBCache): From b56d0cf4c9250f73d240a8e4275007fcd51eef47 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:05:53 +0300 Subject: [PATCH 8/9] fix: delete redundant `str()` call The argument is already `str`. --- aiohttp_client_cache/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_client_cache/backends/base.py b/aiohttp_client_cache/backends/base.py index b1a0d5c..bbcca61 100644 --- a/aiohttp_client_cache/backends/base.py +++ b/aiohttp_client_cache/backends/base.py @@ -164,7 +164,7 @@ async def get_response(self, key: str) -> CachedResponse | None: # Attempt to fetch the cached response logger.debug(f'Attempting to get cached response for key: {key}') try: - response = await self.responses.read(key) or await self._get_redirect_response(str(key)) + response = await self.responses.read(key) or await self._get_redirect_response(key) # Catch "quiet" deserialization errors due to upgrading attrs if response is not None: assert response.method # type: ignore From 4b5ed118c86ef303f7fda5272f801ec075d14516 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+alessio-locatelli@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:28:02 +0300 Subject: [PATCH 9/9] chore: add a note regarding the type annotation and conversion --- aiohttp_client_cache/backends/base.py | 2 ++ aiohttp_client_cache/cache_keys.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/aiohttp_client_cache/backends/base.py b/aiohttp_client_cache/backends/base.py index bbcca61..bdfaa94 100644 --- a/aiohttp_client_cache/backends/base.py +++ b/aiohttp_client_cache/backends/base.py @@ -83,6 +83,8 @@ def __init__( self.responses: BaseCache = DictCache() self.include_headers = include_headers + + # Converted to `set` for fast `in` checks inside `filter_ignored_params`. self.ignored_params = set(ignored_params or []) @property diff --git a/aiohttp_client_cache/cache_keys.py b/aiohttp_client_cache/cache_keys.py index fbffe44..628e144 100644 --- a/aiohttp_client_cache/cache_keys.py +++ b/aiohttp_client_cache/cache_keys.py @@ -46,7 +46,11 @@ def create_key( return key.hexdigest() -def filter_ignored_params(data, ignored_params: Iterable[str]): +def filter_ignored_params( + data, + # Always a set internally, but keep the public utility parameter as-is to avoid breaking changes. + ignored_params: Iterable[str], +): """Remove any ignored params from an object, if it's dict-like""" if not isinstance(data, Mapping) or not ignored_params: return data