Skip to content

Commit 880f7c7

Browse files
authored
Merge pull request #705 from pyinat/close-sqlite
Clean up SQLite connections when session is closed
2 parents b36dad0 + 1b16a64 commit 880f7c7

4 files changed

Lines changed: 78 additions & 0 deletions

File tree

HISTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ In addition, checking attributes on nested objects will not raise `AttributeErro
113113
* Fix corner case resulting in `observed_on` not being converted to `datetime`
114114
* Fix list slicing on custom collection types (`TaxonCounts`, etc.)
115115
* Fix pprint with empty results
116+
* Close any open cache or ratelimit SQLite connections when closing or garbage-collecting `ClientSession`
116117

117118
### Other Changes
118119
* Add support for python 3.15

pyinaturalist/client/session.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,21 @@ def _validate_json(
438438
response.json = lambda **kwargs: response_json # type: ignore
439439
return response
440440

441+
def close(self):
442+
"""Close cache and rate limit backends to avoid unclosed SQLite connections."""
443+
self.cache.close()
444+
for bucket in self.limiter.bucket_factory.get_buckets() or []:
445+
bucket.close()
446+
self.limiter.close()
447+
448+
super().close()
449+
450+
def __del__(self):
451+
try:
452+
self.close()
453+
except Exception:
454+
pass
455+
441456

442457
class RequestTimeout(Timeout):
443458
"""Timeout class that adjusts timeouts for write operations"""

test/client/test_session.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,63 @@ def test_get_refresh_params():
385385
assert session.get_refresh_params('test') == {'refresh': True, 'v': 2}
386386
sleep(2)
387387
assert session.get_refresh_params('test') == {'refresh': True}
388+
389+
390+
def test_session_close():
391+
from unittest.mock import MagicMock, patch
392+
393+
from requests_cache import CacheMixin
394+
395+
session = ClientSession()
396+
mock_bucket_1 = MagicMock()
397+
mock_bucket_2 = MagicMock()
398+
399+
# Patch CacheMixin.close to stop the super() chain and isolate ClientSession.close() logic
400+
with (
401+
patch.object(CacheMixin, 'close'),
402+
patch.object(session.cache, 'close') as mock_cache_close,
403+
patch.object(session.limiter, 'close') as mock_limiter_close,
404+
patch.object(
405+
session.limiter.bucket_factory,
406+
'get_buckets',
407+
return_value=[mock_bucket_1, mock_bucket_2],
408+
),
409+
):
410+
session.close()
411+
412+
mock_cache_close.assert_called_once()
413+
mock_limiter_close.assert_called_once()
414+
mock_bucket_1.close.assert_called_once()
415+
mock_bucket_2.close.assert_called_once()
416+
417+
418+
def test_session_close__no_buckets():
419+
from unittest.mock import patch
420+
421+
from requests_cache import CacheMixin
422+
423+
session = ClientSession()
424+
# Patch CacheMixin.close to stop the super() chain; patch get_buckets() to return None
425+
with (
426+
patch.object(CacheMixin, 'close'),
427+
patch.object(session.limiter.bucket_factory, 'get_buckets', return_value=None),
428+
patch.object(session.limiter, 'close'),
429+
):
430+
session.close() # should not raise
431+
432+
433+
def test_session_del__suppresses_exceptions():
434+
from unittest.mock import patch
435+
436+
session = ClientSession()
437+
with patch.object(session, 'close', side_effect=Exception('boom')):
438+
session.__del__() # should not raise
439+
440+
441+
def test_session_del__calls_close():
442+
from unittest.mock import MagicMock, patch
443+
444+
session = ClientSession()
445+
with patch.object(session, 'close', MagicMock()) as mock_close:
446+
session.__del__()
447+
mock_close.assert_called_once()

test/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import pytest
1616
from requests import HTTPError, Response
1717
from requests_cache import DO_NOT_CACHE, BaseCache
18+
from requests_ratelimiter import InMemoryBucket
1819

1920
from pyinaturalist import enable_logging
2021
from pyinaturalist.client import ClientSession
@@ -52,6 +53,7 @@ class TestSession(ClientSession):
5253
"""Session class to use for tests, which disables rate-limiting and caching"""
5354

5455
def __init__(self, *args, **kwargs):
56+
kwargs.setdefault('bucket_class', InMemoryBucket)
5557
super().__init__(*args, **kwargs)
5658
self.limiter = MagicMock()
5759
self.cache = BaseCache(expire_after=DO_NOT_CACHE)

0 commit comments

Comments
 (0)