From 446b1e58f5a93d77e11639ad43fcb768788bb83e Mon Sep 17 00:00:00 2001 From: Zo Bot Date: Sat, 4 Jul 2026 23:35:43 +0000 Subject: [PATCH 1/2] return non-finite floats unchanged from naturaltime and precisedelta naturaltime and precisedelta both routed their float input through _date_and_delta, which fed it straight to round() (or skipped round in precise mode) and then dt.timedelta(seconds=...). For a non-finite float like inf or -inf, round(float("inf")) raises OverflowError and dt.timedelta(seconds=float("inf")) raises OverflowError too. NaN happened to work because round(float("nan")) raises ValueError which the except clause caught, but inf/-inf slipped past. The naturaldelta path caught inf/-inf via a different probe (it calls int(value) first, which raises OverflowError for inf) and that case is fixed in the still-open PR #342 -- this commit leaves that probe alone and adds the equivalent isfinite guard inside _date_and_delta, which is what both naturaltime and precisedelta go through. Adding OverflowError to the except tuple covers the last remaining case where a finite but oversized number (> ~3e9 days in seconds) would otherwise crash. Three new parametrized test functions in tests/test_time.py cover naturaltime, precisedelta, and the internal _date_and_delta contract for inf/-inf/nan. The inf and -inf cases fail on the pre-fix code with OverflowError; the nan case already worked and is included as a regression guard. Full suite: 724 passed, 69 skipped. --- src/humanize/time.py | 13 +++++++++- tests/test_time.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index 4a07d528..5d68c547 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -75,6 +75,7 @@ def _date_and_delta( If that's not possible, return `(None, value)`. """ import datetime as dt + import math if not now: now = _now() @@ -85,11 +86,19 @@ def _date_and_delta( date = now - value delta = value else: + # Reject non-finite floats (inf, -inf, NaN) before they reach + # round() or dt.timedelta(): `round(float("inf"))` raises + # OverflowError, `dt.timedelta(seconds=float("inf"))` raises + # OverflowError, and `round(float("nan"))` raises ValueError. The + # downstream callers already treat `(None, value)` as "return + # str(value) unchanged", which matches the docstring contract. + if isinstance(value, float) and not math.isfinite(value): + return None, value try: value = value if precise else round(value) delta = dt.timedelta(seconds=value) date = now - delta - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return None, value return date, _abs_timedelta(delta) @@ -275,6 +284,8 @@ def naturaltime( Returns: str: A natural representation of the input in a resolution that makes sense. + Non-finite floats (`inf`, `-inf`, `nan`) and unparseable inputs are + returned as their `str()` representation unchanged. """ import datetime as dt diff --git a/tests/test_time.py b/tests/test_time.py index 76997704..1f175a15 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -852,3 +852,60 @@ def test_time_unit() -> None: ) def test_rounding_by_fmt(fmt: str, value: float, expected: float) -> None: assert time._rounding_by_fmt(fmt, value) == pytest.approx(expected) + +@pytest.mark.parametrize( + "value, expected", + [ + (float("inf"), "inf"), + (float("-inf"), "-inf"), + (float("nan"), "nan"), + ], +) +def test_naturaltime_non_finite_returns_str(value: float, expected: str) -> None: + """naturaltime(float) must not crash on non-finite floats. + + Pre-fix, naturaltime(float('inf')) and naturaltime(float('-inf')) raised + OverflowError: cannot convert float infinity to integer from inside + _date_and_delta's round() call. NaN was already handled because + round(float('nan')) raises ValueError, which the except clause caught. + """ + assert humanize.naturaltime(value) == expected + assert humanize.naturaltime(value, future=True) == expected + + +@pytest.mark.parametrize( + "value, expected", + [ + (float("inf"), "inf"), + (float("-inf"), "-inf"), + (float("nan"), "nan"), + ], +) +def test_precisedelta_non_finite_returns_str(value: float, expected: str) -> None: + """precisedelta(float) must not crash on non-finite floats. + + Pre-fix, precisedelta(float('inf')) and precisedelta(float('-inf')) + raised OverflowError: cannot convert float infinity to integer from + _date_and_delta(value, precise=True). NaN was already handled. + """ + assert humanize.precisedelta(value) == expected + assert humanize.precisedelta(value, minimum_unit="microseconds") == expected + + +@pytest.mark.parametrize( + "value", + [float("inf"), float("-inf"), float("nan")], +) +def test_date_and_delta_non_finite_returns_none_tuple(value: float) -> None: + """_date_and_delta must return (None, value) for non-finite floats. + + This is the contract that the public-facing naturaltime/precisedelta + functions rely on to fall through to return str(value). + """ + date, delta = time._date_and_delta(value) + assert date is None + assert delta is value + # precise=True exercises the precisedelta code path + date_p, delta_p = time._date_and_delta(value, precise=True) + assert date_p is None + assert delta_p is value From f40f05491c414f4469707d6c65df65e4f3e00f1d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:37:59 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_time.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_time.py b/tests/test_time.py index 1f175a15..eb3549e9 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -853,6 +853,7 @@ def test_time_unit() -> None: def test_rounding_by_fmt(fmt: str, value: float, expected: float) -> None: assert time._rounding_by_fmt(fmt, value) == pytest.approx(expected) + @pytest.mark.parametrize( "value, expected", [