Skip to content

Commit 89f962a

Browse files
HrachShahgaborbernat
authored andcommitted
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 c76dee6 commit 89f962a

2 files changed

Lines changed: 38 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,34 @@ 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+
lock = FileLock(tmp_path / "test.lock")
79+
with pytest.raises(ValueError, match="non-negative"):
80+
lock.lifetime = bad_value
81+
82+
83+
@pytest.mark.parametrize("bad_value", ["5", b"5", object(), [1], {1: 2}, complex(1, 0)])
84+
def test_lifetime_setter_rejects_non_numeric(bad_value: object, tmp_path: Path) -> None:
85+
lock = FileLock(tmp_path / "test.lock")
86+
with pytest.raises(TypeError, match="lifetime must be"):
87+
lock.lifetime = bad_value # ty: ignore[invalid-assignment]
88+
89+
90+
@pytest.mark.parametrize("bad_value", [True, False]) # bool is an int subclass; reject it so it can't read as 1s/0s
91+
def test_lifetime_setter_rejects_bool(bad_value: bool, tmp_path: Path) -> None:
92+
lock = FileLock(tmp_path / "test.lock")
93+
with pytest.raises(TypeError, match="lifetime must be"):
94+
lock.lifetime = bad_value
95+
96+
97+
@pytest.mark.parametrize("value", [0, 0.0])
98+
def test_lifetime_setter_accepts_zero(value: float, tmp_path: Path) -> None:
99+
lock = FileLock(tmp_path / "test.lock")
100+
lock.lifetime = value
101+
assert lock.lifetime == 0
102+
103+
76104
def test_lifetime_singleton_mismatch(tmp_path: Path) -> None:
77105
lock_path = tmp_path / "test.lock"
78106
lock1 = FileLock(lock_path, is_singleton=True, lifetime=10.0)

0 commit comments

Comments
 (0)