From fec14caa2adf05abf4e9ce1bca1fdf80b2cea70a Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:23:43 -0700 Subject: [PATCH 1/3] fix(filesize): don't parse formatted mantissa back to float naturalsize()'s unit rollover check ran the user-supplied format string through float(), which raises ValueError whenever that format contains text around the numeric conversion (e.g. "Size: %.1f"), a regression from 4.15.0. Compare the rendered mantissa against the rendered base instead, so the carry-over still works and arbitrary formats are accepted again. Closes #366 --- src/humanize/filesize.py | 4 +++- tests/test_filesize.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/humanize/filesize.py b/src/humanize/filesize.py index fb675fdc..35fb59cf 100644 --- a/src/humanize/filesize.py +++ b/src/humanize/filesize.py @@ -103,7 +103,9 @@ def naturalsize( # mantissa afterward; rounding can push it up to `base` (e.g. 999999 is # 999.999 kB, which formats to "1000.0 kB"). When that happens and a larger # suffix is available, step up one suffix so the result reads "1.0 MB". - if exp < len(suffix) and abs(float(format % (abs_bytes / (base**exp)))) >= base: + # `format` may contain text around the conversion, so compare the rendered + # mantissa with the rendered base instead of parsing it back to a float. + if exp < len(suffix) and format % (abs_bytes / (base**exp)) == format % base: exp += 1 space = "" if gnu else " " ret: str = format % (bytes_ / (base**exp)) + space + _(suffix[exp - 1]) diff --git a/tests/test_filesize.py b/tests/test_filesize.py index e6956399..fc1ceefa 100644 --- a/tests/test_filesize.py +++ b/tests/test_filesize.py @@ -91,6 +91,10 @@ ([1024**2 - 1, True], "1.0 MiB"), ([1024**3 - 1, True], "1.0 GiB"), ([1024**2 - 1, False, True], "1.0M"), + # A custom format may contain text around the numeric conversion, which + # must not break the rounding carry-over check above. + ([999999, False, True, "%.1f~"], "976.6~K"), + ([999999, False, False, "%.1f~"], "1.0~ MB"), ], ) def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> None: From 2577658946d8cad9e57163e16ea875347d62f995 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam Date: Tue, 8 Sep 2026 23:35:25 -0700 Subject: [PATCH 2/3] Address review feedback from @MohammedAlkindi: compare numeric mantissa instead of rendered strings --- src/humanize/filesize.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/humanize/filesize.py b/src/humanize/filesize.py index 35fb59cf..41101884 100644 --- a/src/humanize/filesize.py +++ b/src/humanize/filesize.py @@ -4,6 +4,7 @@ __lazy_modules__ = {"humanize.i18n", "math"} +import re from math import log from humanize.i18n import _gettext as _ @@ -36,6 +37,10 @@ "gnu": "KMGTPEZYRQ", } +# Matches the numeric part of a rendered mantissa, ignoring any surrounding +# text a custom `format` may add (e.g. "%.1f~" renders "976.6~"). +_MANTISSA_RE = re.compile(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?") + def naturalsize( value: float | str, @@ -103,9 +108,13 @@ def naturalsize( # mantissa afterward; rounding can push it up to `base` (e.g. 999999 is # 999.999 kB, which formats to "1000.0 kB"). When that happens and a larger # suffix is available, step up one suffix so the result reads "1.0 MB". - # `format` may contain text around the conversion, so compare the rendered - # mantissa with the rendered base instead of parsing it back to a float. - if exp < len(suffix) and format % (abs_bytes / (base**exp)) == format % base: + # `format` may contain text around the conversion, so extract the numeric + # mantissa from the rendered string before comparing it with `base`: parsing + # the whole string raises ValueError, and comparing rendered strings + # misjudges formats that do not round-trip through str -> float (e.g. + # "%.0e" % 1024 is "1e+03"). + mantissa = _MANTISSA_RE.search(format % (abs_bytes / (base**exp))) + if exp < len(suffix) and mantissa and abs(float(mantissa.group())) >= base: exp += 1 space = "" if gnu else " " ret: str = format % (bytes_ / (base**exp)) + space + _(suffix[exp - 1]) From eb0b87bb7d6dafeb1dfba4da93af9b474f7cf1ff Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam Date: Tue, 8 Sep 2026 23:35:32 -0700 Subject: [PATCH 3/3] Add regression test for %.0e binary-mode carry-over check --- tests/test_filesize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_filesize.py b/tests/test_filesize.py index fc1ceefa..ef212b42 100644 --- a/tests/test_filesize.py +++ b/tests/test_filesize.py @@ -95,6 +95,10 @@ # must not break the rounding carry-over check above. ([999999, False, True, "%.1f~"], "976.6~K"), ([999999, False, False, "%.1f~"], "1.0~ MB"), + # The carry-over check must compare the numeric mantissa, not rendered + # strings: "%.0e" renders 1024 as "1e+03", which a string comparison + # misreads as already at the base and steps up one unit too early. + ([1048575, True, False, "%.0e"], "1e+03 KiB"), ], ) def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> None: