From a6bf5060025bf81634cab49a507d56a3419e189b Mon Sep 17 00:00:00 2001 From: Abhiram Mandala Date: Thu, 3 Sep 2026 17:29:23 +0530 Subject: [PATCH 1/5] Return NaN/Inf unchanged from naturalsize() naturalsize() raised ValueError for math.nan and produced malformed output like 'inf QB' for math.inf, unlike other numeric humanizers that consistently return 'NaN', '+Inf', or '-Inf' for non-finite input via number._format_not_finite(). Add the same isfinite() short-circuit to naturalsize(), reusing number._format_not_finite() for consistent formatting, plus docstring examples and a dedicated parametrized test. --- src/humanize/filesize.py | 17 +++++++++++++++-- tests/test_filesize.py | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/humanize/filesize.py b/src/humanize/filesize.py index fb675fdc..e396d9ba 100644 --- a/src/humanize/filesize.py +++ b/src/humanize/filesize.py @@ -2,11 +2,19 @@ from __future__ import annotations -__lazy_modules__ = {"humanize.i18n", "math"} +__lazy_modules__ = {"humanize.i18n", "humanize.number", "math"} -from math import log +from math import isfinite, log +# Import isfinite so we can check whether a number is +# a normal finite number or NaN / positive infinity / negative infinity. + + +# Reuse the existing helper from number.py so that +# naturalsize() behaves consistently with other numeric +# humanization functions in the library. from humanize.i18n import _gettext as _ +from humanize.number import _format_not_finite suffixes = { "decimal": ( @@ -91,6 +99,10 @@ def naturalsize( base = 1024 if (gnu or binary) else 1000 bytes_ = float(value) abs_bytes = abs(bytes_) + + # Handle NaN and infinity before filesize formatting. + if not isfinite(bytes_): + return _format_not_finite(bytes_) if abs_bytes == 1 and not gnu: return _("%d Byte") % int(bytes_) @@ -98,6 +110,7 @@ def naturalsize( if abs_bytes < base: return f"{int(bytes_)}B" if gnu else _("%d Bytes") % int(bytes_) + exp = int(min(log(abs_bytes, base), len(suffix))) # The suffix is chosen from the unrounded byte count, but `format` rounds the # mantissa afterward; rounding can push it up to `base` (e.g. 999999 is diff --git a/tests/test_filesize.py b/tests/test_filesize.py index e6956399..2283cb7b 100644 --- a/tests/test_filesize.py +++ b/tests/test_filesize.py @@ -4,6 +4,8 @@ from __future__ import annotations +import math + import pytest import humanize @@ -95,7 +97,6 @@ ) def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> None: assert humanize.naturalsize(*test_args) == expected - # Retest with negative input if isinstance(test_args[0], int): test_args[0] *= -1 @@ -103,3 +104,16 @@ def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> test_args[0] = f"-{test_args[0]}" assert humanize.naturalsize(*test_args) == "-" + expected + +@pytest.mark.parametrize( + "test_input, expected", + [ + (math.nan, "NaN"), + (math.inf, "+Inf"), + (-math.inf, "-Inf"), + ], +) +def test_naturalsize_not_finite(test_input: float, expected: str) -> None: + assert humanize.naturalsize(test_input) == expected + assert humanize.naturalsize(test_input, binary=True) == expected + assert humanize.naturalsize(test_input, gnu=True) == expected \ No newline at end of file From 6295b55b589089518562000d925542986e273280 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:14:54 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/humanize/filesize.py | 7 ++----- tests/test_filesize.py | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/humanize/filesize.py b/src/humanize/filesize.py index e396d9ba..776be8bf 100644 --- a/src/humanize/filesize.py +++ b/src/humanize/filesize.py @@ -2,14 +2,12 @@ from __future__ import annotations -__lazy_modules__ = {"humanize.i18n", "humanize.number", "math"} +__lazy_modules__ = {"humanize.i18n", "humanize.number", "math"} from math import isfinite, log # Import isfinite so we can check whether a number is # a normal finite number or NaN / positive infinity / negative infinity. - - # Reuse the existing helper from number.py so that # naturalsize() behaves consistently with other numeric # humanization functions in the library. @@ -99,7 +97,7 @@ def naturalsize( base = 1024 if (gnu or binary) else 1000 bytes_ = float(value) abs_bytes = abs(bytes_) - + # Handle NaN and infinity before filesize formatting. if not isfinite(bytes_): return _format_not_finite(bytes_) @@ -110,7 +108,6 @@ def naturalsize( if abs_bytes < base: return f"{int(bytes_)}B" if gnu else _("%d Bytes") % int(bytes_) - exp = int(min(log(abs_bytes, base), len(suffix))) # The suffix is chosen from the unrounded byte count, but `format` rounds the # mantissa afterward; rounding can push it up to `base` (e.g. 999999 is diff --git a/tests/test_filesize.py b/tests/test_filesize.py index 2283cb7b..0c49cc36 100644 --- a/tests/test_filesize.py +++ b/tests/test_filesize.py @@ -105,6 +105,7 @@ def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> assert humanize.naturalsize(*test_args) == "-" + expected + @pytest.mark.parametrize( "test_input, expected", [ @@ -116,4 +117,4 @@ def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> def test_naturalsize_not_finite(test_input: float, expected: str) -> None: assert humanize.naturalsize(test_input) == expected assert humanize.naturalsize(test_input, binary=True) == expected - assert humanize.naturalsize(test_input, gnu=True) == expected \ No newline at end of file + assert humanize.naturalsize(test_input, gnu=True) == expected From 3301957b840da69aa0b5d8cb25b3c5e03df91d23 Mon Sep 17 00:00:00 2001 From: Abhiram Mandala Date: Fri, 4 Sep 2026 15:49:36 +0530 Subject: [PATCH 3/5] Fix negative signs in precisedelta --- src/humanize/time.py | 24 ++++++++++++++++++------ tests/test_time.py | 4 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index 975cc03c..f77e43a6 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -490,6 +490,9 @@ def precisedelta( >>> precisedelta(delta) '2 days, 1 hour and 33.12 seconds' + >>> precisedelta(dt.timedelta(seconds=-90)) + '-1 minute and 30 seconds' + ``` A custom `format` can be specified to control how the fractional part @@ -546,6 +549,13 @@ def precisedelta( ``` """ + import datetime as dt + + is_negative = ( + (isinstance(value, dt.timedelta) and value.total_seconds() < 0) + or (isinstance(value, (int, float)) and value < 0) + ) + date, delta = _date_and_delta(value, precise=True) if date is None: return str(value) @@ -651,11 +661,13 @@ def precisedelta( ] import math + has_nonzero_value = False texts: list[str] = [] for unit, fmt in zip(reversed(Unit), fmts): singular_txt, plural_txt, fmt_value = fmt if fmt_value > 0 or (not texts and unit == min_unit): + has_nonzero_value = has_nonzero_value or fmt_value != 0 _fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value) fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value) if unit == min_unit and math.modf(fmt_value)[0] > 0: @@ -673,13 +685,13 @@ def precisedelta( break if len(texts) == 1: - return texts[0] - - head = ", ".join(texts[:-1]) - tail = texts[-1] - - return _("%s and %s") % (head, tail) + result = texts[0] + else: + head = ", ".join(texts[:-1]) + tail = texts[-1] + result = _("%s and %s") % (head, tail) + return f"-{result}" if is_negative and has_nonzero_value else result def _rounding_by_fmt(format: str, value: float) -> float | int: """Round a number according to the string format provided. diff --git a/tests/test_time.py b/tests/test_time.py index c3743bbc..3fe5a4e9 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -115,6 +115,10 @@ def test_naturaldelta_nomonths(test_input: dt.timedelta, expected: str) -> None: (dt.timedelta(days=365 * 2 + 35), "2 years"), (dt.timedelta(seconds=1), "a second"), (dt.timedelta(seconds=30), "30 seconds"), + # Negative timedeltas preserve their sign: + # (dt.timedelta(seconds=-1), "-1 second"), + # (dt.timedelta(seconds=-30), "-30 seconds"), + (dt.timedelta(days=364), "a year"), (dt.timedelta(days=365 + 364), "2 years"), # regression tests for bugs in post-release humanize From e7288055480b61abf4c9cb38aaeda5a0baa0e1c6 Mon Sep 17 00:00:00 2001 From: Abhiram Mandala Date: Fri, 4 Sep 2026 15:58:26 +0530 Subject: [PATCH 4/5] Add precisedelta negative sign regression test --- tests/test_time.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_time.py b/tests/test_time.py index 3fe5a4e9..429136a7 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -593,6 +593,31 @@ def test_precisedelta_one_unit_enough( assert humanize.precisedelta(val, minimum_unit=min_unit) == expected +@pytest.mark.parametrize( + "val, min_unit, suppress, expected", + [ + (dt.timedelta(seconds=-1), "seconds", [], "-1 second"), + (-1, "seconds", [], "-1 second"), + ( + dt.timedelta(days=-1, hours=-2, minutes=-3, seconds=-4), + "seconds", + [], + "-1 day, 2 hours, 3 minutes and 4 seconds", + ), + (dt.timedelta(hours=-1, minutes=-30), "hours", [], "-1.50 hours"), + (-0.1, "minutes", [], "0 minutes"), + (-0.1, "minutes", ["seconds"], "0 minutes"), + (dt.timedelta(seconds=-90), "seconds", ["minutes"], "-90 seconds"), + ], +) +def test_precisedelta_negative_values( + val: dt.timedelta | float, min_unit: str, suppress: list[str], expected: str +) -> None: + assert ( + humanize.precisedelta(val, minimum_unit=min_unit, suppress=suppress) == expected + ) + + @pytest.mark.parametrize( "val, min_unit, expected", [ From 3282cc3755389375bd5e912515241e5017194582 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:29:41 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/humanize/time.py | 7 ++++--- tests/test_time.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index f77e43a6..984298f8 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -551,9 +551,8 @@ def precisedelta( """ import datetime as dt - is_negative = ( - (isinstance(value, dt.timedelta) and value.total_seconds() < 0) - or (isinstance(value, (int, float)) and value < 0) + is_negative = (isinstance(value, dt.timedelta) and value.total_seconds() < 0) or ( + isinstance(value, (int, float)) and value < 0 ) date, delta = _date_and_delta(value, precise=True) @@ -661,6 +660,7 @@ def precisedelta( ] import math + has_nonzero_value = False texts: list[str] = [] @@ -693,6 +693,7 @@ def precisedelta( return f"-{result}" if is_negative and has_nonzero_value else result + def _rounding_by_fmt(format: str, value: float) -> float | int: """Round a number according to the string format provided. diff --git a/tests/test_time.py b/tests/test_time.py index 429136a7..3bf3fac4 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -118,7 +118,6 @@ def test_naturaldelta_nomonths(test_input: dt.timedelta, expected: str) -> None: # Negative timedeltas preserve their sign: # (dt.timedelta(seconds=-1), "-1 second"), # (dt.timedelta(seconds=-30), "-30 seconds"), - (dt.timedelta(days=364), "a year"), (dt.timedelta(days=365 + 364), "2 years"), # regression tests for bugs in post-release humanize