Skip to content

Commit ec4097f

Browse files
author
Zo Bot
committed
lifetime: reject negative, non-numeric, and bool values at the setter
lifetime.setter stored whatever it received and trusted the call site to pass a non-negative number. A negative value, a non-numeric string, or a bool (bool is a subclass of int, so it slipped past the int check) would be remembered as the expiration window and only blow up later inside _try_break_expired_lock, or quietly compare against st_mtime and behave in surprising ways. Validate at the boundary the same way timeout.setter validates (it calls float(value) and lets ValueError propagate). The new behavior: - None is accepted (disables expiration, the documented contract). - int and float that are >= 0 are accepted. - bool is rejected with TypeError so True/False cannot be misread as a 1-second or 0-second lifetime. - non-numeric objects raise TypeError with the offending type name instead of a confusing downstream TypeError in arithmetic. - negative numbers raise ValueError with the offending value inlined, matching the docstring promise that lifetime is a maximum time in seconds.
1 parent 0b707fe commit ec4097f

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

src/filelock/_api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,17 @@ def lifetime(self, value: float | None) -> None:
346346
347347
:param value: the new value in seconds, or ``None`` to disable expiration
348348
349+
:raises ValueError: if *value* is a negative number
350+
:raises TypeError: if *value* is not ``None`` and not a real number
351+
349352
"""
353+
if value is not None:
354+
if isinstance(value, bool) or not isinstance(value, (int, float)):
355+
msg = f"lifetime must be a non-negative number or None, not {type(value).__name__}"
356+
raise TypeError(msg)
357+
if value < 0:
358+
msg = f"lifetime must be non-negative, not {value!r}"
359+
raise ValueError(msg)
350360
self._context.lifetime = value
351361

352362
@property

tests/test_lock_expiry.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,40 @@ def test_lifetime_default_none(tmp_path: Path) -> None:
7373
assert lock.lifetime is None
7474

7575

76+
@pytest.mark.parametrize("bad_value", [-1, -0.5, -1e9])
77+
def test_lifetime_setter_rejects_negative_number(bad_value: float, tmp_path: Path) -> None:
78+
"""Negative lifetime would always be considered expired and is rejected at the setter."""
79+
lock = FileLock(tmp_path / "test.lock")
80+
with pytest.raises(ValueError, match="non-negative"):
81+
lock.lifetime = bad_value
82+
83+
84+
@pytest.mark.parametrize("bad_value", ["5", b"5", object(), [1], {1: 2}, complex(1, 0)])
85+
def test_lifetime_setter_rejects_non_numeric(bad_value: object, tmp_path: Path) -> None:
86+
"""Only None, int, and float are accepted by the lifetime setter."""
87+
lock = FileLock(tmp_path / "test.lock")
88+
with pytest.raises(TypeError, match="lifetime must be"):
89+
lock.lifetime = bad_value # type: ignore[assignment]
90+
91+
92+
def test_lifetime_setter_rejects_bool(tmp_path: Path) -> None:
93+
"""``bool`` is an ``int`` subclass in Python, so it would silently pass an int check; reject it explicitly."""
94+
lock = FileLock(tmp_path / "test.lock")
95+
with pytest.raises(TypeError, match="lifetime must be"):
96+
lock.lifetime = True # type: ignore[assignment]
97+
with pytest.raises(TypeError, match="lifetime must be"):
98+
lock.lifetime = False # type: ignore[assignment]
99+
100+
101+
def test_lifetime_setter_accepts_zero(tmp_path: Path) -> None:
102+
"""Zero is the smallest legal value: an existing lock is considered expired immediately on the next acquire."""
103+
lock = FileLock(tmp_path / "test.lock")
104+
lock.lifetime = 0
105+
assert lock.lifetime == 0
106+
lock.lifetime = 0.0
107+
assert lock.lifetime == 0.0
108+
109+
76110
def test_lifetime_singleton_mismatch(tmp_path: Path) -> None:
77111
lock_path = tmp_path / "test.lock"
78112
lock1 = FileLock(lock_path, is_singleton=True, lifetime=10.0)

0 commit comments

Comments
 (0)