Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-humanize for openSUSE:Factory 
checked in at 2025-09-29 16:37:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-humanize (Old)
 and      /work/SRC/openSUSE:Factory/.python-humanize.new.11973 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-humanize"

Mon Sep 29 16:37:37 2025 rev:16 rq:1307743 version:4.13.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-humanize/python-humanize.changes  
2025-04-02 17:15:36.322992395 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-humanize.new.11973/python-humanize.changes   
    2025-09-29 16:37:56.036786656 +0200
@@ -1,0 +2,10 @@
+Mon Sep 29 10:56:40 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 4.13.0:
+  * Optimise `naturalsize` algorithm by using `math.log` (#253)
+  * Fix `precisedelta` rounding (#254) @dangillet
+- update to 4.12.3:
+  * Fix regression in `naturalsize` for `float` and `str` (#250)
+  * Improvements for French translation (#248)
+
+-------------------------------------------------------------------

Old:
----
  humanize-4.12.2.tar.gz

New:
----
  humanize-4.13.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-humanize.spec ++++++
--- /var/tmp/diff_new_pack.2hdt5a/_old  2025-09-29 16:37:56.672813305 +0200
+++ /var/tmp/diff_new_pack.2hdt5a/_new  2025-09-29 16:37:56.676813472 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-humanize
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2025 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
 %{?sle15_python_module_pythons}
 %global modname humanize
 Name:           python-humanize
-Version:        4.12.2
+Version:        4.13.0
 Release:        0
 Summary:        Python humanize utilities
 License:        MIT

++++++ humanize-4.12.2.tar.gz -> humanize-4.13.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/.coveragerc 
new/humanize-4.13.0/.coveragerc
--- old/humanize-4.12.2/.coveragerc     2025-03-24 17:42:38.000000000 +0100
+++ new/humanize-4.13.0/.coveragerc     1970-01-01 01:00:00.000000000 +0100
@@ -1,11 +0,0 @@
-# .coveragerc to control coverage.py
-
-[report]
-# Regexes for lines to exclude from consideration
-exclude_also =
-    # Don't complain if non-runnable code isn't run:
-    if TYPE_CHECKING:
-
-[run]
-disable_warnings =
-    no-sysmon
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/.github/workflows/docs.yml 
new/humanize-4.13.0/.github/workflows/docs.yml
--- old/humanize-4.12.2/.github/workflows/docs.yml      2025-03-24 
17:42:38.000000000 +0100
+++ new/humanize-4.13.0/.github/workflows/docs.yml      2025-08-25 
11:33:38.000000000 +0200
@@ -22,7 +22,7 @@
           python-version: "3.x"
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v5
+        uses: astral-sh/setup-uv@v6
 
       - name: Docs
         run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/.github/workflows/lint.yml 
new/humanize-4.13.0/.github/workflows/lint.yml
--- old/humanize-4.12.2/.github/workflows/lint.yml      2025-03-24 
17:42:38.000000000 +0100
+++ new/humanize-4.13.0/.github/workflows/lint.yml      2025-08-25 
11:33:38.000000000 +0200
@@ -32,6 +32,6 @@
         with:
           python-version: "3.x"
       - name: Install uv
-        uses: astral-sh/setup-uv@v5
+        uses: astral-sh/setup-uv@v6
       - name: Mypy
         run: uvx --with tox-uv tox -e mypy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/.github/workflows/test.yml 
new/humanize-4.13.0/.github/workflows/test.yml
--- old/humanize-4.12.2/.github/workflows/test.yml      2025-03-24 
17:42:38.000000000 +0100
+++ new/humanize-4.13.0/.github/workflows/test.yml      2025-08-25 
11:33:38.000000000 +0200
@@ -6,6 +6,7 @@
 
 env:
   FORCE_COLOR: 1
+  PIP_DISABLE_PIP_VERSION_CHECK: 1
 
 jobs:
   test:
@@ -13,11 +14,20 @@
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["pypy3.11", "3.9", "3.10", "3.11", "3.12", "3.13", 
"3.14"]
+        python-version:
+          - "pypy3.11"
+          - "3.14t"
+          - "3.14"
+          - "3.13t"
+          - "3.13"
+          - "3.12"
+          - "3.11"
+          - "3.10"
+          - "3.9"
         os: [windows-latest, macos-latest, ubuntu-latest]
 
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
         with:
           persist-credentials: false
 
@@ -27,6 +37,11 @@
           python-version: ${{ matrix.python-version }}
           allow-prereleases: true
 
+      - name: Set PYTHON_GIL
+        if: endsWith(matrix.python-version, 't')
+        run: |
+          echo "PYTHON_GIL=0" >> "$GITHUB_ENV"
+
       - name: Install Linux dependencies
         if: startsWith(matrix.os, 'ubuntu')
         run: |
@@ -38,7 +53,7 @@
           brew install gettext
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v5
+        uses: astral-sh/setup-uv@v6
 
       - name: Generate translation binaries
         run: |
@@ -46,7 +61,7 @@
 
       - name: Tox tests
         run: |
-          uvx --with tox-uv tox -e py
+          uvx --python ${{ matrix.python-version }} --with tox-uv tox -e py
 
       - name: Upload coverage
         uses: codecov/codecov-action@v5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/.github/zizmor.yml 
new/humanize-4.13.0/.github/zizmor.yml
--- old/humanize-4.12.2/.github/zizmor.yml      1970-01-01 01:00:00.000000000 
+0100
+++ new/humanize-4.13.0/.github/zizmor.yml      2025-08-25 11:33:38.000000000 
+0200
@@ -0,0 +1,7 @@
+# Configuration for the zizmor static analysis tool, run via pre-commit in CI
+# https://woodruffw.github.io/zizmor/configuration/
+rules:
+  unpinned-uses:
+    config:
+      policies:
+        "*": ref-pin
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/.pre-commit-config.yaml 
new/humanize-4.13.0/.pre-commit-config.yaml
--- old/humanize-4.12.2/.pre-commit-config.yaml 2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/.pre-commit-config.yaml 2025-08-25 11:33:38.000000000 
+0200
@@ -1,8 +1,8 @@
 repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.9.6
+    rev: v0.12.10
     hooks:
-      - id: ruff
+      - id: ruff-check
         args: [--exit-non-zero-on-fix]
 
   - repo: https://github.com/psf/black-pre-commit-mirror
@@ -11,7 +11,7 @@
       - id: black
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v5.0.0
+    rev: v6.0.0
     hooks:
       - id: check-added-large-files
       - id: check-case-conflict
@@ -27,7 +27,7 @@
         exclude: \.github/ISSUE_TEMPLATE\.md|\.github/PULL_REQUEST_TEMPLATE\.md
 
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.31.1
+    rev: 0.33.3
     hooks:
       - id: check-github-workflows
       - id: check-renovate
@@ -38,32 +38,32 @@
       - id: actionlint
 
   - repo: https://github.com/woodruffw/zizmor-pre-commit
-    rev: v1.3.1
+    rev: v1.12.1
     hooks:
       - id: zizmor
 
   - repo: https://github.com/tox-dev/pyproject-fmt
-    rev: v2.5.0
+    rev: v2.6.0
     hooks:
       - id: pyproject-fmt
 
   - repo: https://github.com/abravalheri/validate-pyproject
-    rev: v0.23
+    rev: v0.24.1
     hooks:
       - id: validate-pyproject
 
   - repo: https://github.com/tox-dev/tox-ini-fmt
-    rev: 1.5.0
+    rev: 1.6.0
     hooks:
       - id: tox-ini-fmt
 
   - repo: https://github.com/google/yamlfmt
-    rev: v0.16.0
+    rev: v0.17.2
     hooks:
       - id: yamlfmt
 
   - repo: https://github.com/rbubley/mirrors-prettier
-    rev: v3.5.0
+    rev: v3.6.2
     hooks:
       - id: prettier
         args: [--prose-wrap=always, --print-width=88]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/PKG-INFO new/humanize-4.13.0/PKG-INFO
--- old/humanize-4.12.2/PKG-INFO        2025-03-24 17:42:38.000000000 +0100
+++ new/humanize-4.13.0/PKG-INFO        2025-08-25 11:33:38.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: humanize
-Version: 4.12.2
+Version: 4.13.0
 Summary: Python humanize utilities
 Project-URL: Documentation, https://humanize.readthedocs.io/
 Project-URL: Funding, 
https://tidelift.com/subscription/pkg/pypi-humanize?utm_source=pypi-humanize&utm_medium=pypi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/docs/requirements.txt 
new/humanize-4.13.0/docs/requirements.txt
--- old/humanize-4.12.2/docs/requirements.txt   2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/docs/requirements.txt   2025-08-25 11:33:38.000000000 
+0200
@@ -1,6 +1,6 @@
 mkdocs==1.6.1
 mkdocs-include-markdown-plugin
 mkdocs-material
-mkdocstrings[python]==0.28.2
+mkdocstrings[python]==0.30.0
 pygments
-pymdown-extensions==10.14.3
+pymdown-extensions==10.16.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/pyproject.toml 
new/humanize-4.13.0/pyproject.toml
--- old/humanize-4.12.2/pyproject.toml  2025-03-24 17:42:38.000000000 +0100
+++ new/humanize-4.13.0/pyproject.toml  2025-08-25 11:33:38.000000000 +0200
@@ -65,7 +65,7 @@
 lint.select = [
   "C4",     # flake8-comprehensions
   "D",      # pydocstyle
-  "E",      # pycodestyle
+  "E",      # pycodestyle errors
   "EM",     # flake8-errmsg
   "F",      # pyflakes
   "I",      # isort
@@ -73,25 +73,29 @@
   "ISC",    # flake8-implicit-str-concat
   "LOG",    # flake8-logging
   "PGH",    # pygrep-hooks
+  "PIE",    # flake8-pie
+  "PT",     # flake8-pytest-style
   "PYI",    # flake8-pyi
   "RUF022", # unsorted-dunder-all
   "RUF100", # unused noqa (yesqa)
   "UP",     # pyupgrade
-  "W",      # pycodestyle
+  "W",      # pycodestyle warnings
   "YTT",    # flake8-2020
 ]
 lint.ignore = [
-  "E203",  # Whitespace before ':'
-  "E221",  # Multiple spaces before operator
-  "E226",  # Missing whitespace around arithmetic operator
-  "E241",  # Multiple spaces after ','
-  "UP038", # Makes code slower and more verbose
+  "E203",   # Whitespace before ':'
+  "E221",   # Multiple spaces before operator
+  "E226",   # Missing whitespace around arithmetic operator
+  "E241",   # Multiple spaces after ','
+  "PIE790", # flake8-pie: unnecessary-placeholder
+  "UP038",  # Makes code slower and more verbose
 ]
 lint.per-file-ignores."tests/*" = [
   "D",
 ]
 lint.flake8-import-conventions.aliases.datetime = "dt"
 lint.flake8-import-conventions.banned-from = [ "datetime" ]
+lint.flake8-pytest-style.parametrize-names-type = "csv"
 lint.isort.known-first-party = [ "humanize" ]
 lint.isort.required-imports = [ "from __future__ import annotations" ]
 lint.pydocstyle.convention = "google"
@@ -103,11 +107,16 @@
 addopts = "--color=yes"
 filterwarnings = [
   "error",
-  # https://github.com/dateutil/dateutil/issues/1314
-  
"ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz",
 ]
 testpaths = [ "tests" ]
 
+[tool.coverage.report]
+# Regexes for lines to exclude from consideration
+exclude_also = [
+  # Don't complain if non-runnable code isn't run:
+  "if __name__ == .__main__.:",
+]
+
 [tool.mypy]
 pretty = true
 strict = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/requirements-mypy.txt 
new/humanize-4.13.0/requirements-mypy.txt
--- old/humanize-4.12.2/requirements-mypy.txt   2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/requirements-mypy.txt   2025-08-25 11:33:38.000000000 
+0200
@@ -1,4 +1,4 @@
-mypy==1.15.0
+mypy==1.17.1
 pytest
 types-freezegun
 types-setuptools
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/src/humanize/_version.py 
new/humanize-4.13.0/src/humanize/_version.py
--- old/humanize-4.12.2/src/humanize/_version.py        2025-03-24 
17:42:38.000000000 +0100
+++ new/humanize-4.13.0/src/humanize/_version.py        2025-08-25 
11:33:38.000000000 +0200
@@ -1,7 +1,14 @@
 # file generated by setuptools-scm
 # don't change, don't track in version control
 
-__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
+__all__ = [
+    "__version__",
+    "__version_tuple__",
+    "version",
+    "version_tuple",
+    "__commit_id__",
+    "commit_id",
+]
 
 TYPE_CHECKING = False
 if TYPE_CHECKING:
@@ -9,13 +16,19 @@
     from typing import Union
 
     VERSION_TUPLE = Tuple[Union[int, str], ...]
+    COMMIT_ID = Union[str, None]
 else:
     VERSION_TUPLE = object
+    COMMIT_ID = object
 
 version: str
 __version__: str
 __version_tuple__: VERSION_TUPLE
 version_tuple: VERSION_TUPLE
+commit_id: COMMIT_ID
+__commit_id__: COMMIT_ID
 
-__version__ = version = '4.12.2'
-__version_tuple__ = version_tuple = (4, 12, 2)
+__version__ = version = '4.13.0'
+__version_tuple__ = version_tuple = (4, 13, 0)
+
+__commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/src/humanize/filesize.py 
new/humanize-4.13.0/src/humanize/filesize.py
--- old/humanize-4.12.2/src/humanize/filesize.py        2025-03-24 
17:42:38.000000000 +0100
+++ new/humanize-4.13.0/src/humanize/filesize.py        2025-08-25 
11:33:38.000000000 +0200
@@ -2,6 +2,8 @@
 
 from __future__ import annotations
 
+from math import log
+
 suffixes = {
     "decimal": (
         " kB",
@@ -83,23 +85,15 @@
         suffix = suffixes["decimal"]
 
     base = 1024 if (gnu or binary) else 1000
-    if isinstance(value, str):
-        bytes_ = float(value)
-    else:
-        bytes_ = value
-
+    bytes_ = float(value)
     abs_bytes = abs(bytes_)
 
     if abs_bytes == 1 and not gnu:
-        return f"{bytes_} Byte"
+        return f"{int(bytes_)} Byte"
 
     if abs_bytes < base:
         return f"{int(bytes_)}B" if gnu else f"{int(bytes_)} Bytes"
 
-    for i, s in enumerate(suffix, 2):
-        unit = base**i
-        if abs_bytes < unit:
-            break
-
-    ret: str = format % (base * (bytes_ / unit)) + s
+    exp = int(min(log(abs_bytes, base), len(suffix)))
+    ret: str = format % (bytes_ / (base**exp)) + suffix[exp - 1]
     return ret
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/src/humanize/i18n.py 
new/humanize-4.13.0/src/humanize/i18n.py
--- old/humanize-4.12.2/src/humanize/i18n.py    2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/src/humanize/i18n.py    2025-08-25 11:33:38.000000000 
+0200
@@ -30,6 +30,7 @@
 # Mapping of locale to decimal separator
 _DECIMAL_SEPARATOR = {
     "de_DE": ",",
+    "fr_FR": ".",
     "it_IT": ",",
     "pt_BR": ",",
     "hu_HU": ",",
Binary files 
old/humanize-4.12.2/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.mo and 
new/humanize-4.13.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.mo differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/humanize-4.12.2/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po 
new/humanize-4.13.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po
--- old/humanize-4.12.2/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po       
2025-03-24 17:42:38.000000000 +0100
+++ new/humanize-4.13.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po       
2025-08-25 11:33:38.000000000 +0200
@@ -7,7 +7,7 @@
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-01-08 19:22+0200\n"
+"POT-Creation-Date: 2025-04-01 12:17-0400\n"
 "PO-Revision-Date: 2013-06-22 08:52+0100\n"
 "Last-Translator: Olivier Cortès <[email protected]>\n"
 "Language-Team: fr_FR <[email protected]>\n"
@@ -119,257 +119,247 @@
 msgid "th"
 msgstr "e"
 
-#: src/humanize/number.py:178
+#: src/humanize/number.py:183
 msgid "thousand"
 msgid_plural "thousand"
 msgstr[0] "mille"
 msgstr[1] "milles"
 
-#: src/humanize/number.py:179
-#, fuzzy
+#: src/humanize/number.py:184
 msgid "million"
 msgid_plural "million"
-msgstr[0] "%(value)s million"
-msgstr[1] "%(value)s millions"
+msgstr[0] "million"
+msgstr[1] "millions"
 
-#: src/humanize/number.py:180
+#: src/humanize/number.py:185
 msgid "billion"
 msgid_plural "billion"
 msgstr[0] "milliard"
 msgstr[1] "milliards"
 
-#: src/humanize/number.py:181
-#, fuzzy
+#: src/humanize/number.py:186
 msgid "trillion"
 msgid_plural "trillion"
-msgstr[0] "%(value)s billions"
-msgstr[1] "%(value)s billions"
+msgstr[0] "billions"
+msgstr[1] "billions"
 
-#: src/humanize/number.py:182
-#, fuzzy
+#: src/humanize/number.py:187
 msgid "quadrillion"
 msgid_plural "quadrillion"
-msgstr[0] "%(value)s billiard"
-msgstr[1] "%(value)s billiards"
+msgstr[0] "billiard"
+msgstr[1] "billiards"
 
-#: src/humanize/number.py:183
-#, fuzzy
+#: src/humanize/number.py:188
 msgid "quintillion"
 msgid_plural "quintillion"
-msgstr[0] "%(value)s trillion"
-msgstr[1] "%(value)s trillions"
+msgstr[0] "trillion"
+msgstr[1] "trillions"
 
-#: src/humanize/number.py:184
-#, fuzzy
+#: src/humanize/number.py:189
 msgid "sextillion"
 msgid_plural "sextillion"
-msgstr[0] "%(value)s trilliard"
-msgstr[1] "%(value)s trilliards"
+msgstr[0] "trilliard"
+msgstr[1] "trilliards"
 
-#: src/humanize/number.py:185
-#, fuzzy
+#: src/humanize/number.py:190
 msgid "septillion"
 msgid_plural "septillion"
-msgstr[0] "%(value)s quatrillion"
-msgstr[1] "%(value)s quatrillions"
+msgstr[0] "quatrillion"
+msgstr[1] "quatrillions"
 
-#: src/humanize/number.py:186
-#, fuzzy
+#: src/humanize/number.py:191
 msgid "octillion"
 msgid_plural "octillion"
-msgstr[0] "%(value)s quadrilliard"
-msgstr[1] "%(value)s quadrilliards"
+msgstr[0] "quadrilliard"
+msgstr[1] "quadrilliards"
 
-#: src/humanize/number.py:187
-#, fuzzy
+#: src/humanize/number.py:192
 msgid "nonillion"
 msgid_plural "nonillion"
-msgstr[0] "%(value)s quintillion"
-msgstr[1] "%(value)s quintillions"
+msgstr[0] "quintillion"
+msgstr[1] "quintillions"
 
-#: src/humanize/number.py:188
-#, fuzzy
+#: src/humanize/number.py:193
 msgid "decillion"
 msgid_plural "decillion"
-msgstr[0] "%(value)s quintilliard"
-msgstr[1] "%(value)s quintilliards"
+msgstr[0] "quintilliard"
+msgstr[1] "quintilliards"
 
-#: src/humanize/number.py:189
-#, fuzzy
+#: src/humanize/number.py:194
 msgid "googol"
 msgid_plural "googol"
-msgstr[0] "%(value)s gogol"
-msgstr[1] "%(value)s gogols"
+msgstr[0] "gogol"
+msgstr[1] "gogols"
 
-#: src/humanize/number.py:301
+#: src/humanize/number.py:313
 msgid "zero"
 msgstr "zéro"
 
-#: src/humanize/number.py:302
+#: src/humanize/number.py:314
 msgid "one"
 msgstr "un"
 
-#: src/humanize/number.py:303
+#: src/humanize/number.py:315
 msgid "two"
 msgstr "deux"
 
-#: src/humanize/number.py:304
+#: src/humanize/number.py:316
 msgid "three"
 msgstr "trois"
 
-#: src/humanize/number.py:305
+#: src/humanize/number.py:317
 msgid "four"
 msgstr "quatre"
 
-#: src/humanize/number.py:306
+#: src/humanize/number.py:318
 msgid "five"
 msgstr "cinq"
 
-#: src/humanize/number.py:307
+#: src/humanize/number.py:319
 msgid "six"
 msgstr "six"
 
-#: src/humanize/number.py:308
+#: src/humanize/number.py:320
 msgid "seven"
 msgstr "sept"
 
-#: src/humanize/number.py:309
+#: src/humanize/number.py:321
 msgid "eight"
 msgstr "huit"
 
-#: src/humanize/number.py:310
+#: src/humanize/number.py:322
 msgid "nine"
 msgstr "neuf"
 
-#: src/humanize/time.py:152
-#, fuzzy, python-format
+#: src/humanize/time.py:162
+#, python-format
 msgid "%d microsecond"
 msgid_plural "%d microseconds"
 msgstr[0] "%d microseconde"
 msgstr[1] "%d microsecondes"
 
-#: src/humanize/time.py:161
-#, fuzzy, python-format
+#: src/humanize/time.py:171
+#, python-format
 msgid "%d millisecond"
 msgid_plural "%d milliseconds"
 msgstr[0] "%d milliseconde"
 msgstr[1] "%d millisecondes"
 
-#: src/humanize/time.py:164 src/humanize/time.py:259
+#: src/humanize/time.py:174 src/humanize/time.py:275
 msgid "a moment"
 msgstr "un instant"
 
-#: src/humanize/time.py:167
+#: src/humanize/time.py:177
 msgid "a second"
 msgstr "une seconde"
 
-#: src/humanize/time.py:170
+#: src/humanize/time.py:180
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d seconde"
 msgstr[1] "%d secondes"
 
-#: src/humanize/time.py:173
+#: src/humanize/time.py:183
 msgid "a minute"
 msgstr "une minute"
 
-#: src/humanize/time.py:177
+#: src/humanize/time.py:187
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d minute"
 msgstr[1] "%d minutes"
 
-#: src/humanize/time.py:180
+#: src/humanize/time.py:190
 msgid "an hour"
 msgstr "une heure"
 
-#: src/humanize/time.py:184
+#: src/humanize/time.py:194
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d heure"
 msgstr[1] "%d heures"
 
-#: src/humanize/time.py:188
+#: src/humanize/time.py:198
 msgid "a day"
 msgstr "un jour"
 
-#: src/humanize/time.py:191 src/humanize/time.py:194
+#: src/humanize/time.py:201 src/humanize/time.py:204
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d jour"
 msgstr[1] "%d jours"
 
-#: src/humanize/time.py:197
+#: src/humanize/time.py:207
 msgid "a month"
 msgstr "un mois"
 
-#: src/humanize/time.py:199
+#: src/humanize/time.py:209
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d mois"
 msgstr[1] "%d mois"
 
-#: src/humanize/time.py:203
+#: src/humanize/time.py:213
 msgid "a year"
 msgstr "un an"
 
-#: src/humanize/time.py:206 src/humanize/time.py:217
+#: src/humanize/time.py:216 src/humanize/time.py:227
 #, python-format
 msgid "1 year, %d day"
 msgid_plural "1 year, %d days"
 msgstr[0] "un an et %d jour"
 msgstr[1] "un an et %d jours"
 
-#: src/humanize/time.py:210
+#: src/humanize/time.py:220
 msgid "1 year, 1 month"
 msgstr "un an et un mois"
 
-#: src/humanize/time.py:213
+#: src/humanize/time.py:223
 #, python-format
 msgid "1 year, %d month"
 msgid_plural "1 year, %d months"
 msgstr[0] "un an et %d mois"
 msgstr[1] "un an et %d mois"
 
-#: src/humanize/time.py:219
+#: src/humanize/time.py:229
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d an"
 msgstr[1] "%d ans"
 
-#: src/humanize/time.py:256
+#: src/humanize/time.py:272
 #, python-format
 msgid "%s from now"
 msgstr "dans %s"
 
-#: src/humanize/time.py:256
+#: src/humanize/time.py:272
 #, python-format
 msgid "%s ago"
 msgstr "il y a %s"
 
-#: src/humanize/time.py:260
+#: src/humanize/time.py:276
 msgid "now"
 msgstr "maintenant"
 
-#: src/humanize/time.py:284
+#: src/humanize/time.py:313
 msgid "today"
 msgstr "aujourd'hui"
 
-#: src/humanize/time.py:287
+#: src/humanize/time.py:316
 msgid "tomorrow"
 msgstr "demain"
 
-#: src/humanize/time.py:290
+#: src/humanize/time.py:319
 msgid "yesterday"
 msgstr "hier"
 
-#: src/humanize/time.py:600
+#: src/humanize/time.py:634
 #, python-format
 msgid "%s and %s"
 msgstr "%s et %s"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/src/humanize/time.py 
new/humanize-4.13.0/src/humanize/time.py
--- old/humanize-4.12.2/src/humanize/time.py    2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/src/humanize/time.py    2025-08-25 11:33:38.000000000 
+0200
@@ -65,7 +65,9 @@
     return delta
 
 
-def _date_and_delta(value: Any, *, now: dt.datetime | None = None) -> 
tuple[Any, Any]:
+def _date_and_delta(
+    value: Any, *, now: dt.datetime | None = None, precise: bool = False
+) -> tuple[Any, Any]:
     """Turn a value into a date and a timedelta which represents how long ago 
it was.
 
     If that's not possible, return `(None, value)`.
@@ -82,7 +84,7 @@
         delta = value
     else:
         try:
-            value = int(value)
+            value = value if precise else int(value)
             delta = dt.timedelta(seconds=value)
             date = now - delta
         except (ValueError, TypeError):
@@ -345,77 +347,43 @@
     unit: Unit,
     minimum_unit: Unit,
     suppress: Iterable[Unit],
+    format: str,
 ) -> tuple[float, float]:
-    """Divide `value` by `divisor` returning the quotient and remainder.
+    """Divide `value` by `divisor`, returning the quotient and remainder.
 
-    If `unit` is `minimum_unit`, makes the quotient a float number and the 
remainder
-    will be zero. The rational is that if `unit` is the unit of the quotient, 
we cannot
-    represent the remainder because it would require a unit smaller than the
-    `minimum_unit`.
+    If `unit` is `minimum_unit`, the quotient will be the rounding of `value / 
divisor`
+    according to the `format` string and the remainder will be zero. The 
rationale is
+    that if `unit` is the unit of the quotient, we cannot represent the 
remainder
+    because it would require a unit smaller than the `minimum_unit`.
 
     >>> from humanize.time import _quotient_and_remainder, Unit
-    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [])
+    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [], "%0.2f")
     (1.5, 0)
 
-    If unit is in `suppress`, the quotient will be zero and the remainder will 
be the
+    If `unit` is in `suppress`, the quotient will be zero and the remainder 
will be the
     initial value. The idea is that if we cannot use `unit`, we are forced to 
use a
-    lower unit so we cannot do the division.
+    lower unit, so we cannot do the division.
 
-    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS])
+    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS], 
"%0.2f")
     (0, 36)
 
-    In other case return quotient and remainder as `divmod` would do it.
+    In other cases, return the quotient and remainder as `divmod` would do it.
 
-    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [])
+    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [], "%0.2f")
     (1, 12)
 
     """
     if unit == minimum_unit:
-        return value / divisor, 0
+        return _rounding_by_fmt(format, value / divisor), 0
 
     if unit in suppress:
         return 0, value
 
-    return divmod(value, divisor)
-
-
-def _carry(
-    value1: float,
-    value2: float,
-    ratio: float,
-    unit: Unit,
-    min_unit: Unit,
-    suppress: Iterable[Unit],
-) -> tuple[float, float]:
-    """Return a tuple with two values.
-
-    If the unit is in `suppress`, multiply `value1` by `ratio` and add it to 
`value2`
-    (carry to right). The idea is that if we cannot represent `value1` we need 
to
-    represent it in a lower unit.
-
-    >>> from humanize.time import _carry, Unit
-    >>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [Unit.DAYS])
-    (0, 54)
-
-    If the unit is the minimum unit, `value2` is divided by `ratio` and added 
to
-    `value1` (carry to left). We assume that `value2` has a lower unit so we 
need to
-    carry it to `value1`.
-
-    >>> _carry(2, 6, 24, Unit.DAYS, Unit.DAYS, [])
-    (2.25, 0)
-
-    Otherwise, just return the same input:
-
-    >>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [])
-    (2, 6)
-    """
-    if unit == min_unit:
-        return value1 + value2 / ratio, 0
-
-    if unit in suppress:
-        return 0, value2 + value1 * ratio
-
-    return value1, value2
+    # Convert the remainder back to integer is necessary for months. 1 month 
is 30.5
+    # days on average, but if we have 31 days, we want to count is as a whole 
month,
+    # and not as 1 month plus a remainder of 0.5 days.
+    q, r = divmod(value, divisor)
+    return q, int(r)
 
 
 def _suitable_minimum_unit(min_unit: Unit, suppress: Iterable[Unit]) -> Unit:
@@ -464,12 +432,12 @@
 
 
 def precisedelta(
-    value: dt.timedelta | int | None,
+    value: dt.timedelta | float | None,
     minimum_unit: str = "seconds",
     suppress: Iterable[str] = (),
     format: str = "%0.2f",
 ) -> str:
-    """Return a precise representation of a timedelta.
+    """Return a precise representation of a timedelta or number of seconds.
 
     ```pycon
     >>> import datetime as dt
@@ -535,14 +503,14 @@
 
     ```
     """
-    date, delta = _date_and_delta(value)
+    date, delta = _date_and_delta(value, precise=True)
     if date is None:
         return str(value)
 
     suppress_set = {Unit[s.upper()] for s in suppress}
 
-    # Find a suitable minimum unit (it can be greater the one that the
-    # user gave us if it is suppressed).
+    # Find a suitable minimum unit (it can be greater than the one that the
+    # user gave us, if that one is suppressed).
     min_unit = Unit[minimum_unit.upper()]
     min_unit = _suitable_minimum_unit(min_unit, suppress_set)
     del minimum_unit
@@ -572,27 +540,57 @@
     #       years, days = divmod(years, days)
     #
     # The same applies for months, hours, minutes and milliseconds below
-    years, days = _quotient_and_remainder(days, 365, YEARS, min_unit, 
suppress_set)
-    months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, 
suppress_set)
+    years, days = _quotient_and_remainder(
+        days, 365, YEARS, min_unit, suppress_set, format
+    )
+    months, days = _quotient_and_remainder(
+        days, 30.5, MONTHS, min_unit, suppress_set, format
+    )
 
-    # If DAYS is not in suppress, we can represent the days but
-    # if it is a suppressed unit, we need to carry it to a lower unit,
-    # seconds in this case.
-    #
-    # The same applies for secs and usecs below
-    days, secs = _carry(days, secs, 24 * 3600, DAYS, min_unit, suppress_set)
+    secs = days * 24 * 3600 + secs
+    days, secs = _quotient_and_remainder(
+        secs, 24 * 3600, DAYS, min_unit, suppress_set, format
+    )
 
-    hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, 
suppress_set)
-    minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, 
suppress_set)
+    hours, secs = _quotient_and_remainder(
+        secs, 3600, HOURS, min_unit, suppress_set, format
+    )
+    minutes, secs = _quotient_and_remainder(
+        secs, 60, MINUTES, min_unit, suppress_set, format
+    )
 
-    secs, usecs = _carry(secs, usecs, 1e6, SECONDS, min_unit, suppress_set)
+    usecs = secs * 1e6 + usecs
+    secs, usecs = _quotient_and_remainder(
+        usecs, 1e6, SECONDS, min_unit, suppress_set, format
+    )
 
     msecs, usecs = _quotient_and_remainder(
-        usecs, 1000, MILLISECONDS, min_unit, suppress_set
+        usecs, 1000, MILLISECONDS, min_unit, suppress_set, format
     )
 
-    # if _unused != 0 we had lost some precision
-    usecs, _unused = _carry(usecs, 0, 1, MICROSECONDS, min_unit, suppress_set)
+    # Due to rounding, it could be that a unit is high enough to be promoted 
to a higher
+    # unit. Example: 59.9 minutes was rounded to 60 minutes, and thus it 
should become 0
+    # minutes and one hour more.
+    if msecs >= 1_000 and SECONDS not in suppress_set:
+        msecs -= 1_000
+        secs += 1
+    if secs >= 60 and MINUTES not in suppress_set:
+        secs -= 60
+        minutes += 1
+    if minutes >= 60 and HOURS not in suppress_set:
+        minutes -= 60
+        hours += 1
+    if hours >= 24 and DAYS not in suppress_set:
+        hours -= 24
+        days += 1
+    # When adjusting we should not deal anymore with fractional days as all 
rounding has
+    # been already made. We promote 31 days to an extra month.
+    if days >= 31 and MONTHS not in suppress_set:
+        days -= 31
+        months += 1
+    if months >= 12 and YEARS not in suppress_set:
+        months -= 12
+        years += 1
 
     fmts = [
         ("%d year", "%d years", years),
@@ -616,6 +614,8 @@
             if unit == min_unit and math.modf(fmt_value)[0] > 0:
                 fmt_txt = fmt_txt.replace("%d", format)
             elif unit == YEARS:
+                if math.modf(fmt_value)[0] == 0:
+                    fmt_value = int(fmt_value)
                 fmt_txt = fmt_txt.replace("%d", "%s")
                 texts.append(fmt_txt % intcomma(fmt_value))
                 continue
@@ -632,3 +632,24 @@
     tail = texts[-1]
 
     return _("%s and %s") % (head, tail)
+
+
+def _rounding_by_fmt(format: str, value: float) -> float | int:
+    """Round a number according to the string format provided.
+
+    The string format is the old printf-style string formatting.
+
+    If we are using a format which truncates the value, such as "%d" or "%i", 
the
+    returned value will be of type `int`.
+
+    If we are using a format which rounds the value, such as "%.2f" or even 
"%.0f",
+    we will return a float.
+    """
+    result = format % value
+
+    try:
+        value = int(result)
+    except ValueError:
+        value = float(result)
+
+    return value
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/tests/test_filesize.py 
new/humanize-4.13.0/tests/test_filesize.py
--- old/humanize-4.12.2/tests/test_filesize.py  2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/tests/test_filesize.py  2025-08-25 11:33:38.000000000 
+0200
@@ -69,6 +69,8 @@
         ([3000000, False, True], "2.9M"),
         ([1024, False, True], "1.0K"),
         ([1, False, False], "1 Byte"),
+        ([1.0, False, False], "1 Byte"),
+        (["1", False, False], "1 Byte"),
         ([3141592, False, False, "%.2f"], "3.14 MB"),
         ([3000, False, True, "%.3f"], "2.930K"),
         ([3000000000, False, True, "%.0f"], "3G"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/tests/test_i18n.py 
new/humanize-4.13.0/tests/test_i18n.py
--- old/humanize-4.12.2/tests/test_i18n.py      2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/tests/test_i18n.py      2025-08-25 11:33:38.000000000 
+0200
@@ -55,6 +55,8 @@
 
         humanize.i18n.activate("fr_FR")
         assert humanize.intcomma(number) == "10 000 000"
+        assert humanize.intcomma(1_234_567.89) == "1 234 567.89"
+        assert humanize.intcomma("1 234 567.89") == "1 234 567.89"
 
         humanize.i18n.activate("pt_BR")
         assert humanize.intcomma(number) == "10.000.000"
@@ -87,15 +89,15 @@
 
 
 @pytest.mark.parametrize(
-    ("locale", "number", "expected_result"),
-    (
+    "locale, number, expected_result",
+    [
         ("es_ES", 1000000, "1.0 millón"),
         ("es_ES", 3500000, "3.5 millones"),
         ("es_ES", 1000000000, "1.0 billón"),
         ("es_ES", 1200000000, "1.2 billones"),
         ("es_ES", 1000000000000, "1.0 trillón"),
         ("es_ES", 6700000000000, "6.7 trillones"),
-    ),
+    ],
 )
 def test_intword_plurals(locale: str, number: int, expected_result: str) -> 
None:
     try:
@@ -109,8 +111,8 @@
 
 
 @pytest.mark.parametrize(
-    ("locale", "expected_result"),
-    (
+    "locale, expected_result",
+    [
         ("ar", "5خامس"),
         ("ar_SA", "5خامس"),
         ("fr", "5e"),
@@ -118,7 +120,7 @@
         ("pt", "5º"),
         ("pt_BR", "5º"),
         ("pt_PT", "5º"),
-    ),
+    ],
 )
 def test_langauge_codes(locale: str, expected_result: str) -> None:
     try:
@@ -132,8 +134,8 @@
 
 
 @pytest.mark.parametrize(
-    ("locale", "number", "gender", "expected_result"),
-    (
+    "locale, number, gender, expected_result",
+    [
         ("fr_FR", 1, "male", "1er"),
         ("fr_FR", 1, "female", "1ère"),
         ("fr_FR", 2, "male", "2e"),
@@ -141,7 +143,7 @@
         ("es_ES", 5, "female", "5ª"),
         ("it_IT", 3, "male", "3º"),
         ("it_IT", 8, "female", "8ª"),
-    ),
+    ],
 )
 def test_ordinal_genders(
     locale: str, number: int, gender: str, expected_result: str
@@ -185,9 +187,8 @@
         i18n = importlib.import_module("humanize.i18n")
         monkeypatch.setattr(i18n, "__spec__", None)
 
-        with pytest.raises(Exception) as excinfo:
+        with pytest.raises(Exception, match=self.expected_msg):
             i18n.activate("ru_RU")
-        assert str(excinfo.value) == self.expected_msg
 
     def test_default_locale_path_undefined__spec__(
         self, monkeypatch: pytest.MonkeyPatch
@@ -195,9 +196,8 @@
         i18n = importlib.import_module("humanize.i18n")
         monkeypatch.delattr(i18n, "__spec__")
 
-        with pytest.raises(Exception) as excinfo:
+        with pytest.raises(Exception, match=self.expected_msg):
             i18n.activate("ru_RU")
-        assert str(excinfo.value) == self.expected_msg
 
     @freeze_time("2020-02-02")
     def test_en_locale(self) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/tests/test_time.py 
new/humanize-4.13.0/tests/test_time.py
--- old/humanize-4.12.2/tests/test_time.py      2025-03-24 17:42:38.000000000 
+0100
+++ new/humanize-4.13.0/tests/test_time.py      2025-08-25 11:33:38.000000000 
+0200
@@ -106,17 +106,9 @@
         (dt.timedelta(days=365 * 2 + 35), "2 years"),
         (dt.timedelta(seconds=1), "a second"),
         (dt.timedelta(seconds=30), "30 seconds"),
-        (dt.timedelta(minutes=1, seconds=30), "a minute"),
-        (dt.timedelta(minutes=2), "2 minutes"),
-        (dt.timedelta(hours=1, minutes=30, seconds=30), "an hour"),
-        (dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours"),
-        (dt.timedelta(days=1), "a day"),
-        (dt.timedelta(days=500), "1 year, 4 months"),
-        (dt.timedelta(days=365 * 2 + 35), "2 years"),
         # regression tests for bugs in post-release humanize
         (dt.timedelta(days=10000), "27 years"),
         (dt.timedelta(days=365 + 35), "1 year, 1 month"),
-        (30, "30 seconds"),
         (dt.timedelta(days=365 * 2 + 65), "2 years"),
         (dt.timedelta(days=365 + 4), "1 year, 4 days"),
         (dt.timedelta(days=35), "a month"),
@@ -170,7 +162,9 @@
         ("NaN", "NaN"),
     ],
 )
-def test_naturaltime(test_input: dt.datetime, expected: str) -> None:
+def test_naturaltime(
+    test_input: dt.datetime | dt.timedelta | float, expected: str
+) -> None:
     assert humanize.naturaltime(test_input) == expected
 
 
@@ -211,7 +205,9 @@
         ("NaN", "NaN"),
     ],
 )
-def test_naturaltime_nomonths(test_input: dt.datetime, expected: str) -> None:
+def test_naturaltime_nomonths(
+    test_input: dt.datetime | dt.timedelta | float, expected: str
+) -> None:
     assert humanize.naturaltime(test_input, months=False) == expected
 
 
@@ -506,7 +502,7 @@
     ],
 )
 def test_precisedelta_one_unit_enough(
-    val: int | dt.timedelta, min_unit: str, expected: str
+    val: dt.timedelta | float, min_unit: str, expected: str
 ) -> None:
     assert humanize.precisedelta(val, minimum_unit=min_unit) == expected
 
@@ -559,10 +555,18 @@
             "minutes",
             "0 minutes",
         ),
+        (dt.timedelta(days=31), "seconds", "1 month"),
+        (dt.timedelta(days=32), "seconds", "1 month and 1 day"),
+        (dt.timedelta(days=62), "seconds", "2 months and 1 day"),
+        (dt.timedelta(days=92), "seconds", "3 months"),
+        (dt.timedelta(days=31), "days", "1 month"),
+        (dt.timedelta(days=32), "days", "1 month and 1 day"),
+        (dt.timedelta(days=62), "days", "2 months and 1 day"),
+        (dt.timedelta(days=92), "days", "3 months"),
     ],
 )
 def test_precisedelta_multiple_units(
-    val: dt.timedelta, min_unit: str, expected: str
+    val: dt.timedelta | float, min_unit: str, expected: str
 ) -> None:
     assert humanize.precisedelta(val, minimum_unit=min_unit) == expected
 
@@ -582,7 +586,7 @@
             "%0.4f",
             "2.0020 milliseconds",
         ),
-        (dt.timedelta(microseconds=2002), "milliseconds", "%0.2f", "2.00 
milliseconds"),
+        (dt.timedelta(microseconds=2002), "milliseconds", "%0.2f", "2 
milliseconds"),
         (
             dt.timedelta(seconds=1, microseconds=230000),
             "seconds",
@@ -608,12 +612,63 @@
             "5 days and 4.50 hours",
         ),
         (dt.timedelta(days=5, hours=4, seconds=30 * 60), "days", "%0.2f", 
"5.19 days"),
+        # 1 month is 30.5 days but remainder is always rounded down.
+        (dt.timedelta(days=31), "days", "%d", "1 month"),
+        (dt.timedelta(days=31), "days", "%.0f", "1 month"),
+        (dt.timedelta(days=32), "days", "%d", "1 month and 1 day"),
+        (dt.timedelta(days=32), "days", "%.0f", "1 month and 1 day"),
+        (dt.timedelta(days=62), "days", "%d", "2 months and 1 day"),
+        (dt.timedelta(days=92), "days", "%d", "3 months"),
         (dt.timedelta(days=120), "months", "%0.2f", "3.93 months"),
         (dt.timedelta(days=183), "years", "%0.1f", "0.5 years"),
+        (0.01, "seconds", "%0.3f", "0.010 seconds"),
+        # 31 seconds will be truncated to 0 with %d and rounded to the nearest
+        # number with %.0f, ie. 1
+        (31, "minutes", "%d", "0 minutes"),
+        (31, "minutes", "%0.0f", "1 minute"),
+        (60 + 29.99, "minutes", "%d", "1 minute"),
+        (60 + 29.99, "minutes", "%.0f", "1 minute"),
+        (60 + 30, "minutes", "%d", "1 minute"),
+        # 30 sec is 0.5 minutes. Round to nearest, ties away from zero.
+        # See https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
+        (60 + 30, "minutes", "%.0f", "2 minutes"),
+        (60 * 60 + 30.99, "minutes", "%.0f", "1 hour"),
+        (60 * 60 + 31, "minutes", "%.0f", "1 hour and 1 minute"),
+        (
+            ONE_DAY - MILLISECONDS_1_337,
+            "seconds",
+            "%.1f",
+            "23 hours, 59 minutes and 58.7 seconds",
+        ),
+        (
+            ONE_DAY - ONE_MILLISECOND,
+            "seconds",
+            "%.4f",
+            "23 hours, 59 minutes and 59.9990 seconds",
+        ),
+        (91500, "hours", "%0.0f", "1 day and 1 hour"),
+        # Because we use a format to round, we will end up with 9 hours.
+        (9 * 60 * 60 - 1, "minutes", "%0.0f", "9 hours"),
+        (dt.timedelta(days=30.99999), "minutes", "%0.0f", "1 month"),
+        # We round at the hour. We end up with 12.5 hours. It's a tie, so 
round to the
+        # nearest even number which is 12, thus we round down.
+        (
+            dt.timedelta(days=30.5 * 3, minutes=30),
+            "hours",
+            "%0.0f",
+            "2 months, 30 days and 12 hours",
+        ),
+        (dt.timedelta(days=10, hours=6), "days", "%0.2f", "10.25 days"),
+        (dt.timedelta(days=30.55), "days", "%0.1f", "30.6 days"),
+        (dt.timedelta(microseconds=999.5), "microseconds", "%0.0f", "1 
millisecond"),
+        (dt.timedelta(milliseconds=999.5), "milliseconds", "%0.0f", "1 
second"),
+        (dt.timedelta(seconds=59.5), "seconds", "%0.0f", "1 minute"),
+        (dt.timedelta(minutes=59.5), "minutes", "%0.0f", "1 hour"),
+        (dt.timedelta(days=364), "months", "%0.0f", "1 year"),
     ],
 )
 def test_precisedelta_custom_format(
-    val: dt.timedelta, min_unit: str, fmt: str, expected: str
+    val: dt.timedelta | float, min_unit: str, fmt: str, expected: str
 ) -> None:
     assert humanize.precisedelta(val, minimum_unit=min_unit, format=fmt) == 
expected
 
@@ -690,7 +745,7 @@
     ],
 )
 def test_precisedelta_suppress_units(
-    val: dt.timedelta, min_unit: str, suppress: list[str], expected: str
+    val: dt.timedelta | float, min_unit: str, suppress: list[str], expected: 
str
 ) -> None:
     assert (
         humanize.precisedelta(val, minimum_unit=min_unit, suppress=suppress) 
== expected
@@ -700,10 +755,13 @@
 def test_precisedelta_bogus_call() -> None:
     assert humanize.precisedelta(None) == "None"
 
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError,
+        match="Minimum unit is suppressed and no suitable replacement was 
found",
+    ):
         humanize.precisedelta(1, minimum_unit="years", suppress=["years"])
 
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="Minimum unit 'years' not supported"):
         humanize.naturaldelta(1, minimum_unit="years")
 
 
@@ -714,3 +772,20 @@
 
     with pytest.raises(TypeError):
         _ = years < "foo"
+
+
[email protected](
+    "fmt, value, expected",
+    [
+        ("%.2f", 1.011, 1.01),
+        ("%.0f", 1.01, 1.0),
+        ("%.0f", 1.5, 2.0),
+        ("%10.0f", 1.01, 1.0),
+        ("%i", 1.01, 1),
+        # Surprising rounding with %d. It does not truncate for all values...
+        ("%d", 1.999999999999999, 1),
+        ("%d", 1.9999999999999999, 2),
+    ],
+)
+def test_rounding_by_fmt(fmt: str, value: float, expected: float) -> None:
+    assert time._rounding_by_fmt(fmt, value) == pytest.approx(expected)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.12.2/tox.ini new/humanize-4.13.0/tox.ini
--- old/humanize-4.12.2/tox.ini 2025-03-24 17:42:38.000000000 +0100
+++ new/humanize-4.13.0/tox.ini 2025-08-25 11:33:38.000000000 +0200
@@ -12,8 +12,6 @@
     tests
 pass_env =
     FORCE_COLOR
-set_env =
-    COVERAGE_CORE = sysmon
 commands =
     {envpython} -m pytest \
       --cov humanize \

Reply via email to