diff --git a/HISTORY.md b/HISTORY.md index c00aca9..46be2ad 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # History + +## 0.8.3 (2023-07-TBD) +* Add `autoclose` option to `CacheBackend` to close backend connections when the session context exits. + * Enabled by default for SQLite backend, and disabled by default for other backends. + ## 0.8.2 (2023-07-14) * Add some missing type annotations to backend classes * Fix passing connection parameters to MongoDB backend diff --git a/aiohttp_client_cache/backends/base.py b/aiohttp_client_cache/backends/base.py index a73ac28..c97e95f 100644 --- a/aiohttp_client_cache/backends/base.py +++ b/aiohttp_client_cache/backends/base.py @@ -44,6 +44,7 @@ def __init__( allowed_methods: Tuple[str, ...] = ('GET', 'HEAD'), include_headers: bool = False, ignored_params: Optional[Iterable[str]] = None, + autoclose: bool = False, cache_control: bool = False, filter_fn: _FilterFn = lambda r: True, **kwargs: Any, @@ -58,6 +59,7 @@ def __init__( allowed_methods: Only cache requests with these HTTP methods include_headers: Cache requests with different headers separately ignored_params: Request parameters to be excluded from the cache key + autoclose: Close any active backend connections when the session is closed cache_control: Use Cache-Control response headers filter_fn: function that takes a :py:class:`aiohttp.ClientResponse` object and returns a boolean indicating whether or not that response should be cached. Will be @@ -70,6 +72,7 @@ def __init__( self.allowed_methods = allowed_methods self.cache_control = cache_control self.filter_fn = filter_fn + self.autoclose = autoclose self.disabled = False # Allows multiple redirects or other aliased URLs to point to the same cached response @@ -252,6 +255,11 @@ async def close(self): await self.responses.close() await self.redirects.close() + async def close_if_enabled(self): + """Close any active connections, if ``autoclose`` is enabled""" + if self.autoclose: + await self.close() + # TODO: Support yarl.URL like aiohttp does? # TODO: Implement __aiter__? diff --git a/aiohttp_client_cache/backends/redis.py b/aiohttp_client_cache/backends/redis.py index 03722c3..5b8d259 100644 --- a/aiohttp_client_cache/backends/redis.py +++ b/aiohttp_client_cache/backends/redis.py @@ -26,11 +26,6 @@ def __init__( self.responses = RedisCache(cache_name, 'responses', address=address, **kwargs) self.redirects = RedisCache(cache_name, 'redirects', address=address, **kwargs) - async def close(self): - """Close any active connections""" - await self.responses.close() - await self.redirects.close() - class RedisCache(BaseCache): """An async interface for caching objects in Redis. diff --git a/aiohttp_client_cache/backends/sqlite.py b/aiohttp_client_cache/backends/sqlite.py index 7387ed7..0333bea 100644 --- a/aiohttp_client_cache/backends/sqlite.py +++ b/aiohttp_client_cache/backends/sqlite.py @@ -11,12 +11,11 @@ import aiosqlite from aiohttp_client_cache.backends import BaseCache, CacheBackend, ResponseOrKey, get_valid_kwargs -from aiohttp_client_cache.signatures import extend_init_signature, sqlite_template +from aiohttp_client_cache.signatures import sqlite_template bulk_commit_var: ContextVar[bool] = ContextVar('bulk_commit', default=False) -@extend_init_signature(CacheBackend, sqlite_template) class SQLiteBackend(CacheBackend): """Async cache backend for `SQLite `_ (requires `aiosqlite `_) @@ -37,9 +36,10 @@ def __init__( cache_name: str = 'aiohttp-cache', use_temp: bool = False, fast_save: bool = False, + autoclose: bool = True, **kwargs: Any, ): - super().__init__(cache_name=cache_name, **kwargs) + super().__init__(cache_name=cache_name, autoclose=autoclose, **kwargs) self.responses = SQLitePickleCache( cache_name, 'responses', use_temp=use_temp, fast_save=fast_save, **kwargs ) diff --git a/aiohttp_client_cache/session.py b/aiohttp_client_cache/session.py index d21c7d7..498a128 100644 --- a/aiohttp_client_cache/session.py +++ b/aiohttp_client_cache/session.py @@ -65,6 +65,7 @@ async def _request( async def close(self): """Close both aiohttp connector and any backend connection(s) on contextmanager exit""" await super().close() + await self.cache.close_if_enabled() @asynccontextmanager async def disabled(self): diff --git a/aiohttp_client_cache/signatures.py b/aiohttp_client_cache/signatures.py index be08b36..becee9d 100644 --- a/aiohttp_client_cache/signatures.py +++ b/aiohttp_client_cache/signatures.py @@ -54,7 +54,9 @@ def extend_init_signature(super_class: Type, *extra_functions: Callable) -> Call def wrapper(target_class: Type): try: # Modify init signature + docstring - revision = extend_signature(super_class.__init__, *extra_functions) + revision = extend_signature( + super_class.__init__, target_class.__init__, *extra_functions + ) target_class.__init__ = revision(target_class.__init__) # Include init docs in class docs target_class.__doc__ = target_class.__doc__ or '' diff --git a/test/integration/base_backend_test.py b/test/integration/base_backend_test.py index 023e506..41cfd5f 100644 --- a/test/integration/base_backend_test.py +++ b/test/integration/base_backend_test.py @@ -4,6 +4,7 @@ from contextlib import asynccontextmanager from datetime import datetime from typing import Any, AsyncIterator, Dict, Type +from unittest.mock import patch from uuid import uuid4 import pytest @@ -249,6 +250,20 @@ async def test_cookies_with_redirect(self): cookies = session.cookie_jar.filter_cookies(httpbin()) assert cookies['test_cookie'].value == 'value' + @skip_37 + @patch.object(CacheBackend, 'close') + async def test_autoclose(self, mock_close): + async with self.init_session(autoclose=True) as session: + await session.get(httpbin('get')) + mock_close.assert_called_once() + + @skip_37 + @patch.object(CacheBackend, 'close') + async def test_autoclose__disabled(self, mock_close): + async with self.init_session(autoclose=False) as session: + await session.get(httpbin('get')) + mock_close.assert_not_called() + async def test_serializer__pickle(self): """Without a secret key, plain pickle should be used""" async with self.init_session() as session: diff --git a/test/integration/test_sqlite.py b/test/integration/test_sqlite.py index 8e6207b..10fcab5 100644 --- a/test/integration/test_sqlite.py +++ b/test/integration/test_sqlite.py @@ -5,8 +5,9 @@ import pytest +from aiohttp_client_cache.backends import CacheBackend from aiohttp_client_cache.backends.sqlite import SQLiteBackend, SQLiteCache, SQLitePickleCache -from test.conftest import CACHE_NAME, skip_37 +from test.conftest import CACHE_NAME, httpbin, skip_37 from test.integration import BaseBackendTest, BaseStorageTest pytestmark = pytest.mark.asyncio @@ -145,3 +146,12 @@ class TestSQLitePickleCache(BaseStorageTest): class TestSQLiteBackend(BaseBackendTest): backend_class = SQLiteBackend init_kwargs = {'use_temp': True} + + @skip_37 + @patch.object(CacheBackend, 'close') + async def test_autoclose__default(self, mock_close): + """By default, the backend should be closed when the session is closed""" + + async with self.init_session() as session: + await session.get(httpbin('get')) + mock_close.assert_called_once()