From b6a1f03f06aaf0c038ff1f0b2b7fb5c619dcdb27 Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch <297513015+Pitchfork-and-Torch@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:45:57 +0000 Subject: [PATCH] Reject bool for Cache timeout and expire bool subclasses int, so timeout=True became a 1s SQLite timeout and expire=True/False became a 1s TTL or immediate expiry (now+0). Raise TypeError for bool so callers pass an explicit numeric value. --- diskcache/core.py | 29 ++++++++++++++++++++ tests/test_bool_expire_timeout.py | 44 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/test_bool_expire_timeout.py diff --git a/diskcache/core.py b/diskcache/core.py index 7a3d23b..51bcef7 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -25,6 +25,17 @@ def full_name(func): return func.__module__ + '.' + func.__qualname__ + +def _reject_bool_numeric(value, name): + """Raise TypeError if value is bool (bool subclasses int/float).""" + if isinstance(value, bool): + raise TypeError( + '%s must be a number, not bool (got %r; bool subclasses int so ' + '%r would silently become %d)' % (name, value, value, int(value)) + ) + + + class Constant(tuple): """Pretty display of immutable constant.""" @@ -587,6 +598,8 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): # Close and re-open database connection with given timeout. self.close() + # bool subclasses int; timeout=True would silently become 1s + _reject_bool_numeric(timeout, 'timeout') self._timeout = timeout self._sql # pylint: disable=pointless-statement @@ -768,6 +781,10 @@ def set(self, key, value, expire=None, read=False, tag=None, retry=False): """ now = time.time() db_key, raw = self._disk.put(key) + # bool subclasses int; expire=True would silently become 1s TTL; + # expire=False would become 0 (immediate expiry) + if expire is not None: + _reject_bool_numeric(expire, 'expire') expire_time = None if expire is None else now + expire size, mode, filename, db_value = self._disk.store(value, read, key=key) columns = (expire_time, tag, size, mode, filename, db_value) @@ -940,6 +957,10 @@ def touch(self, key, expire=None, retry=False): """ now = time.time() db_key, raw = self._disk.put(key) + # bool subclasses int; expire=True would silently become 1s TTL; + # expire=False would become 0 (immediate expiry) + if expire is not None: + _reject_bool_numeric(expire, 'expire') expire_time = None if expire is None else now + expire with self._transact(retry) as (sql, _): @@ -988,6 +1009,10 @@ def add(self, key, value, expire=None, read=False, tag=None, retry=False): """ now = time.time() db_key, raw = self._disk.put(key) + # bool subclasses int; expire=True would silently become 1s TTL; + # expire=False would become 0 (immediate expiry) + if expire is not None: + _reject_bool_numeric(expire, 'expire') expire_time = None if expire is None else now + expire size, mode, filename, db_value = self._disk.store(value, read, key=key) columns = (expire_time, tag, size, mode, filename, db_value) @@ -1445,6 +1470,10 @@ def push( now = time.time() raw = True + # bool subclasses int; expire=True would silently become 1s TTL; + # expire=False would become 0 (immediate expiry) + if expire is not None: + _reject_bool_numeric(expire, 'expire') expire_time = None if expire is None else now + expire size, mode, filename, db_value = self._disk.store(value, read) columns = (expire_time, tag, size, mode, filename, db_value) diff --git a/tests/test_bool_expire_timeout.py b/tests/test_bool_expire_timeout.py new file mode 100644 index 0000000..2045efd --- /dev/null +++ b/tests/test_bool_expire_timeout.py @@ -0,0 +1,44 @@ +"""bool subclasses int; expire=True must not silently become 1s TTL.""" +import shutil + +import pytest + +import diskcache as dc + + +@pytest.fixture +def dcache(): + with dc.Cache() as cache: + yield cache + shutil.rmtree(cache.directory, ignore_errors=True) + + +@pytest.mark.parametrize("value", [True, False]) +def test_cache_timeout_rejects_bool(value, tmpdir): + with pytest.raises(TypeError, match="timeout"): + dc.Cache(str(tmpdir), timeout=value) + + +@pytest.mark.parametrize("value", [True, False]) +def test_set_expire_rejects_bool(value, dcache): + with pytest.raises(TypeError, match="expire"): + dcache.set("k", "v", expire=value) + + +@pytest.mark.parametrize("value", [True, False]) +def test_add_expire_rejects_bool(value, dcache): + with pytest.raises(TypeError, match="expire"): + dcache.add("k", "v", expire=value) + + +@pytest.mark.parametrize("value", [True, False]) +def test_touch_expire_rejects_bool(value, dcache): + dcache.set("k", "v") + with pytest.raises(TypeError, match="expire"): + dcache.touch("k", expire=value) + + +def test_expire_still_accepts_number(dcache): + assert dcache.set("k", "v", expire=5) is True + assert dcache.get("k") == "v" + assert dcache.touch("k", expire=10) is True