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 2026-01-06 17:46:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-humanize (Old)
 and      /work/SRC/openSUSE:Factory/.python-humanize.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-humanize"

Tue Jan  6 17:46:16 2026 rev:18 rq:1325569 version:4.15.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-humanize/python-humanize.changes  
2025-11-11 19:23:34.733107467 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-humanize.new.1928/python-humanize.changes    
    2026-01-06 17:47:33.676974562 +0100
@@ -1,0 +2,11 @@
+Mon Jan  5 13:40:10 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to 4.15.0
+  * Add locale support for decimal separator in intword (#287)
+  * Add support for Python 3.15 (#275)
+  * Replace pre-commit with prek (#276)
+  * naturaldelta: round the value to nearest unit that makes sense (#272)
+  * Fix plural form for intword and improve performance (#273)
+  * Replace Exception with more specific FileNotFoundError (#286)
+
+-------------------------------------------------------------------

Old:
----
  humanize-4.14.0.tar.gz

New:
----
  humanize-4.15.0.tar.gz

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

Other differences:
------------------
++++++ python-humanize.spec ++++++
--- /var/tmp/diff_new_pack.ifUTmN/_old  2026-01-06 17:47:34.144993816 +0100
+++ /var/tmp/diff_new_pack.ifUTmN/_new  2026-01-06 17:47:34.144993816 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-humanize
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 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.14.0
+Version:        4.15.0
 Release:        0
 Summary:        Python humanize utilities
 License:        MIT

++++++ humanize-4.14.0.tar.gz -> humanize-4.15.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/CONTRIBUTING.md 
new/humanize-4.15.0/.github/CONTRIBUTING.md
--- old/humanize-4.14.0/.github/CONTRIBUTING.md 2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/.github/CONTRIBUTING.md 2025-12-20 21:03:38.000000000 
+0100
@@ -2,13 +2,12 @@
 
 ## Linting
 
-Linting is run on the CI using [pre-commit](https://pre-commit.com/), and can 
be run
-locally:
+Linting is run on the CI using [prek](https://prek.j178.dev//), and can be run 
locally:
 
 ```sh
-pip install pre-commit
-pre-commit install  # optional: to run when you commit, on just the staged 
changes
-pre-commit run --all-files  # to run on all files now
+pip install prek
+prek install  # optional: to run when you commit, on just the staged changes
+prek run --all-files  # to run on all files now
 ```
 
 ## Docstrings
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/renovate.json 
new/humanize-4.15.0/.github/renovate.json
--- old/humanize-4.14.0/.github/renovate.json   2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/.github/renovate.json   2025-12-20 21:03:38.000000000 
+0100
@@ -2,6 +2,7 @@
   "$schema": "https://docs.renovatebot.com/renovate-schema.json";,
   "extends": ["config:base", ":semanticCommitsDisabled"],
   "labels": ["changelog: skip", "dependencies"],
+  "minimumReleaseAge": "7 days",
   "packageRules": [
     {
       "groupName": "github-actions",
@@ -10,7 +11,8 @@
     },
     {
       "groupName": "docs/requirements.txt",
-      "matchPaths": ["docs/requirements.txt"]
+      "matchPaths": ["docs/requirements.txt"],
+      "separateMajorMinor": "false"
     }
   ],
   "schedule": ["on the first day of the month"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/workflows/docs.yml 
new/humanize-4.15.0/.github/workflows/docs.yml
--- old/humanize-4.14.0/.github/workflows/docs.yml      2025-10-13 
15:00:06.000000000 +0200
+++ new/humanize-4.15.0/.github/workflows/docs.yml      2025-12-20 
21:03:38.000000000 +0100
@@ -12,7 +12,7 @@
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           persist-credentials: false
 
@@ -22,7 +22,7 @@
           python-version: "3.x"
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v6
+        uses: astral-sh/setup-uv@v7
 
       - name: Docs
         run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/workflows/labels.yml 
new/humanize-4.15.0/.github/workflows/labels.yml
--- old/humanize-4.14.0/.github/workflows/labels.yml    2025-10-13 
15:00:06.000000000 +0200
+++ new/humanize-4.15.0/.github/workflows/labels.yml    2025-12-20 
21:03:38.000000000 +0100
@@ -14,7 +14,7 @@
       pull-requests: write
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           persist-credentials: false
       - uses: micnncim/action-label-syncer@v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/workflows/lint.yml 
new/humanize-4.15.0/.github/workflows/lint.yml
--- old/humanize-4.14.0/.github/workflows/lint.yml      2025-10-13 
15:00:06.000000000 +0200
+++ new/humanize-4.15.0/.github/workflows/lint.yml      2025-12-20 
21:03:38.000000000 +0100
@@ -13,25 +13,22 @@
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           persist-credentials: false
-      - uses: actions/setup-python@v6
-        with:
-          python-version: "3.x"
-      - uses: tox-dev/action-pre-commit-uv@v1
+      - uses: j178/prek-action@v1
 
   mypy:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           persist-credentials: false
       - uses: actions/setup-python@v6
         with:
           python-version: "3.x"
       - name: Install uv
-        uses: astral-sh/setup-uv@v6
+        uses: astral-sh/setup-uv@v7
       - name: Mypy
         run: uvx --with tox-uv tox -e mypy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/workflows/release.yml 
new/humanize-4.15.0/.github/workflows/release.yml
--- old/humanize-4.14.0/.github/workflows/release.yml   2025-10-13 
15:00:06.000000000 +0200
+++ new/humanize-4.15.0/.github/workflows/release.yml   2025-12-20 
21:03:38.000000000 +0100
@@ -23,7 +23,7 @@
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           fetch-depth: 0
           persist-credentials: false
@@ -53,7 +53,7 @@
 
     steps:
       - name: Download packages built by build-and-inspect-python-package
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v6
         with:
           name: Packages
           path: dist
@@ -77,7 +77,7 @@
 
     steps:
       - name: Download packages built by build-and-inspect-python-package
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v6
         with:
           name: Packages
           path: dist
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/.github/workflows/test.yml 
new/humanize-4.15.0/.github/workflows/test.yml
--- old/humanize-4.14.0/.github/workflows/test.yml      2025-10-13 
15:00:06.000000000 +0200
+++ new/humanize-4.15.0/.github/workflows/test.yml      2025-12-20 
21:03:38.000000000 +0100
@@ -16,6 +16,8 @@
       matrix:
         python-version:
           - "pypy3.11"
+          - "3.15t"
+          - "3.15"
           - "3.14t"
           - "3.14"
           - "3.13t"
@@ -26,7 +28,7 @@
         os: [windows-latest, macos-latest, ubuntu-latest]
 
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           persist-credentials: false
 
@@ -52,7 +54,7 @@
           brew install gettext
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v6
+        uses: astral-sh/setup-uv@v7
 
       - name: Generate translation binaries
         run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/PKG-INFO new/humanize-4.15.0/PKG-INFO
--- old/humanize-4.14.0/PKG-INFO        2025-10-13 15:00:06.000000000 +0200
+++ new/humanize-4.15.0/PKG-INFO        2025-12-20 21:03:38.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: humanize
-Version: 4.14.0
+Version: 4.15.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
@@ -23,6 +23,7 @@
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Topic :: Text Processing
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/docs/requirements.txt 
new/humanize-4.15.0/docs/requirements.txt
--- old/humanize-4.14.0/docs/requirements.txt   2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/docs/requirements.txt   2025-12-20 21:03:38.000000000 
+0100
@@ -1,6 +1,6 @@
 mkdocs==1.6.1
 mkdocs-include-markdown-plugin
 mkdocs-material
-mkdocstrings[python]==0.30.1
+mkdocstrings[python]==1.0.0
 pygments
-pymdown-extensions==10.16.1
+pymdown-extensions==10.17.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/pyproject.toml 
new/humanize-4.15.0/pyproject.toml
--- old/humanize-4.14.0/pyproject.toml  2025-10-13 15:00:06.000000000 +0200
+++ new/humanize-4.15.0/pyproject.toml  2025-12-20 21:03:38.000000000 +0100
@@ -28,6 +28,7 @@
   "Programming Language :: Python :: 3.12",
   "Programming Language :: Python :: 3.13",
   "Programming Language :: Python :: 3.14",
+  "Programming Language :: Python :: 3.15",
   "Programming Language :: Python :: Implementation :: CPython",
   "Programming Language :: Python :: Implementation :: PyPy",
   "Topic :: Text Processing",
@@ -87,7 +88,6 @@
   "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",
@@ -98,9 +98,10 @@
 lint.isort.known-first-party = [ "humanize" ]
 lint.isort.required-imports = [ "from __future__ import annotations" ]
 lint.pydocstyle.convention = "google"
+lint.future-annotations = true
 
 [tool.pyproject-fmt]
-max_supported_python = "3.14"
+max_supported_python = "3.15"
 
 [tool.pytest.ini_options]
 addopts = "--color=yes"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/requirements-mypy.txt 
new/humanize-4.15.0/requirements-mypy.txt
--- old/humanize-4.14.0/requirements-mypy.txt   2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/requirements-mypy.txt   2025-12-20 21:03:38.000000000 
+0100
@@ -1,4 +1,4 @@
-mypy==1.18.2
+mypy==1.19.0
 pytest
 types-freezegun
 types-setuptools
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/src/humanize/_version.py 
new/humanize-4.15.0/src/humanize/_version.py
--- old/humanize-4.14.0/src/humanize/_version.py        2025-10-13 
15:00:06.000000000 +0200
+++ new/humanize-4.15.0/src/humanize/_version.py        2025-12-20 
21:03:38.000000000 +0100
@@ -28,7 +28,7 @@
 commit_id: COMMIT_ID
 __commit_id__: COMMIT_ID
 
-__version__ = version = '4.14.0'
-__version_tuple__ = version_tuple = (4, 14, 0)
+__version__ = version = '4.15.0'
+__version_tuple__ = version_tuple = (4, 15, 0)
 
 __commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/src/humanize/i18n.py 
new/humanize-4.15.0/src/humanize/i18n.py
--- old/humanize-4.14.0/src/humanize/i18n.py    2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/src/humanize/i18n.py    2025-12-20 21:03:38.000000000 
+0100
@@ -71,7 +71,7 @@
         dict: Translations.
 
     Raises:
-        Exception: If humanize cannot find the locale folder.
+        FileNotFoundError: If humanize cannot find the locale folder.
     """
     if locale is None or locale.startswith("en"):
         _CURRENT.locale = None
@@ -85,7 +85,7 @@
             "Humanize cannot determinate the default location of the 'locale' 
folder. "
             "You need to pass the path explicitly."
         )
-        raise Exception(msg)
+        raise FileNotFoundError(msg)
     if locale not in _TRANSLATIONS:
         translation = gettext_module.translation("humanize", path, [locale])
         _TRANSLATIONS[locale] = translation
Binary files 
old/humanize-4.14.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.mo and 
new/humanize-4.15.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.mo differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/humanize-4.14.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po 
new/humanize-4.15.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po
--- old/humanize-4.14.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po       
2025-10-13 15:00:06.000000000 +0200
+++ new/humanize-4.15.0/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po       
2025-12-20 21:03:38.000000000 +0100
@@ -140,7 +140,7 @@
 #: src/humanize/number.py:186
 msgid "trillion"
 msgid_plural "trillion"
-msgstr[0] "billions"
+msgstr[0] "billion"
 msgstr[1] "billions"
 
 #: src/humanize/number.py:187
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/src/humanize/number.py 
new/humanize-4.15.0/src/humanize/number.py
--- old/humanize-4.14.0/src/humanize/number.py  2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/src/humanize/number.py  2025-12-20 21:03:38.000000000 
+0100
@@ -2,6 +2,8 @@
 
 from __future__ import annotations
 
+import bisect
+
 from .i18n import _gettext as _
 from .i18n import _ngettext, decimal_separator, thousands_separator
 from .i18n import _ngettext_noop as NS_
@@ -194,8 +196,8 @@
     """Converts a large integer to a friendly text representation.
 
     Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 
million",
-    1200000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". 
Supports up
-    to decillion (33 digits) and googol (100 digits).
+    1_200_000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". 
Supports
+    up to decillion (33 digits) and googol (100 digits).
 
     Examples:
         ```pycon
@@ -241,29 +243,27 @@
         negative_prefix = ""
 
     if value < powers[0]:
-        return negative_prefix + str(value)
+        return f"{negative_prefix}{value}"
+
+    ordinal = bisect.bisect_right(powers, value)
+    largest_ordinal = ordinal == len(powers)
 
-    for ordinal_, power in enumerate(powers[1:], 1):
-        if value < power:
-            chopped = value / float(powers[ordinal_ - 1])
-            powers_difference = powers[ordinal_] / powers[ordinal_ - 1]
-            if float(format % chopped) == powers_difference:
-                chopped = value / float(powers[ordinal_])
-                singular, plural = human_powers[ordinal_]
-                return (
-                    negative_prefix
-                    + " ".join(
-                        [format, _ngettext(singular, plural, 
math.ceil(chopped))]
-                    )
-                ) % chopped
-
-            singular, plural = human_powers[ordinal_ - 1]
-            return (
-                negative_prefix
-                + " ".join([format, _ngettext(singular, plural, 
math.ceil(chopped))])
-            ) % chopped
+    # Consider the biggest power of 10 that is smaller than value
+    ordinal -= 1
+    power = powers[ordinal]
+    chopped = value / power
+    rounded_value = float(format % chopped)
+
+    if not largest_ordinal and rounded_value * power == powers[ordinal + 1]:
+        # After rounding, we end up just at the next power
+        ordinal += 1
+        rounded_value = 1.0
 
-    return negative_prefix + str(value)
+    singular, plural = human_powers[ordinal]
+    unit = _ngettext(singular, plural, math.ceil(rounded_value))
+    decimal_sep = decimal_separator()
+    number = (format % rounded_value).replace(".", decimal_sep)
+    return f"{negative_prefix}{number} {unit}"
 
 
 def apnumber(value: NumberOrString) -> str:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/src/humanize/time.py 
new/humanize-4.15.0/src/humanize/time.py
--- old/humanize-4.14.0/src/humanize/time.py    2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/src/humanize/time.py    2025-12-20 21:03:38.000000000 
+0100
@@ -84,7 +84,7 @@
         delta = value
     else:
         try:
-            value = value if precise else int(value)
+            value = value if precise else round(value)
             delta = dt.timedelta(seconds=value)
             date = now - delta
         except (ValueError, TypeError):
@@ -101,6 +101,8 @@
 
     This is similar to `naturaltime`, but does not add tense to the result.
 
+    The timedelta will be rounded to the nearest unit that makes sense.
+
     Args:
         value (datetime.timedelta, int or float): A timedelta or a number of 
seconds.
         months (bool): If `True`, then a number of months (based on 30.5 days) 
will be
@@ -155,9 +157,9 @@
     delta = abs(delta)
     years = delta.days // 365
     days = delta.days % 365
-    num_months = int(days // 30.5)
+    num_months = round(days / 30.5)
 
-    if not years and days < 1:
+    if years == 0 and days < 1:
         if delta.seconds == 0:
             if min_unit == Unit.MICROSECONDS and delta.microseconds < 1000:
                 return (
@@ -181,18 +183,24 @@
         if delta.seconds < 60:
             return _ngettext("%d second", "%d seconds", delta.seconds) % 
delta.seconds
 
-        if 60 <= delta.seconds < 120:
-            return _("a minute")
+        if 60 <= delta.seconds < 3600:
+            minutes = round(delta.seconds / 60)
+            if minutes == 1:
+                return _("a minute")
+
+            if minutes == 60:
+                return _("an hour")
 
-        if 120 <= delta.seconds < 3600:
-            minutes = delta.seconds // 60
             return _ngettext("%d minute", "%d minutes", minutes) % minutes
 
-        if 3600 <= delta.seconds < 3600 * 2:
-            return _("an hour")
+        if 3600 <= delta.seconds:
+            hours = round(delta.seconds / 3600)
+            if hours == 1:
+                return _("an hour")
+
+            if hours == 24:
+                return _("a day")
 
-        if 3600 < delta.seconds:
-            hours = delta.seconds // 3600
             return _ngettext("%d hour", "%d hours", hours) % hours
 
     elif years == 0:
@@ -202,25 +210,32 @@
         if not use_months:
             return _ngettext("%d day", "%d days", days) % days
 
-        if not num_months:
+        if num_months == 0:
             return _ngettext("%d day", "%d days", days) % days
 
         if num_months == 1:
             return _("a month")
 
+        if num_months == 12:
+            return _("a year")
+
         return _ngettext("%d month", "%d months", num_months) % num_months
 
     elif years == 1:
-        if not num_months and not days:
+        if num_months == 0 and days == 0:
             return _("a year")
 
-        if not num_months:
+        if num_months == 0:
             return _ngettext("1 year, %d day", "1 year, %d days", days) % days
 
         if use_months:
             if num_months == 1:
                 return _("1 year, 1 month")
 
+            if num_months == 12:
+                years += 1
+                return _ngettext("%d year", "%d years", years) % years
+
             return (
                 _ngettext("1 year, %d month", "1 year, %d months", num_months)
                 % num_months
@@ -242,6 +257,8 @@
 
     This is more or less compatible with Django's `naturaltime` filter.
 
+    The time will be rounded to the nearest unit that makes sense.
+
     Args:
         value (datetime.datetime, datetime.timedelta, int or float): A 
`datetime`, a
             `timedelta`, or a number of seconds.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/tests/test_i18n.py 
new/humanize-4.15.0/tests/test_i18n.py
--- old/humanize-4.14.0/tests/test_i18n.py      2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/tests/test_i18n.py      2025-12-20 21:03:38.000000000 
+0100
@@ -91,15 +91,62 @@
 @pytest.mark.parametrize(
     "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"),
+        # Italian uses comma as decimal separator
+        ("it_IT", 1_000_000, "1,0 milione"),
+        ("it_IT", 1_200_000, "1,2 milioni"),
+        ("it_IT", 1_000_000_000, "1,0 miliardo"),
+        ("it_IT", 3_500_000_000, "3,5 miliardi"),
+        # Spanish uses dot as decimal separator
+        ("es_ES", 1_000_000, "1.0 millón"),
+        ("es_ES", 3_500_000, "3.5 millones"),
+        ("es_ES", 1_000_000_000, "1.0 billón"),
+        ("es_ES", 1_200_000_000, "1.2 billones"),
+        ("es_ES", 1_000_000_000_000, "1.0 trillón"),
+        ("es_ES", 6_700_000_000_000, "6.7 trillones"),
+        ("fr_FR", "1_000", "1.0 mille"),
+        ("fr_FR", "12_400", "12.4 milles"),
+        ("fr_FR", "12_490", "12.5 milles"),
+        ("fr_FR", "1_000_000", "1.0 million"),
+        ("fr_FR", "-1_000_000", "-1.0 million"),
+        ("fr_FR", "1_200_000", "1.2 millions"),
+        ("fr_FR", "1_290_000", "1.3 millions"),
+        ("fr_FR", "999_999_999", "1.0 milliard"),
+        ("fr_FR", "1_000_000_000", "1.0 milliard"),
+        ("fr_FR", "-1_000_000_000", "-1.0 milliard"),
+        ("fr_FR", "2_000_000_000", "2.0 milliards"),
+        ("fr_FR", "999_999_999_999", "1.0 billion"),
+        ("fr_FR", "1_000_000_000_000", "1.0 billion"),
+        ("fr_FR", "6_000_000_000_000", "6.0 billions"),
+        ("fr_FR", "-6_000_000_000_000", "-6.0 billions"),
+        ("fr_FR", "999_999_999_999_999", "1.0 billiard"),
+        ("fr_FR", "1_000_000_000_000_000", "1.0 billiard"),
+        ("fr_FR", "1_300_000_000_000_000", "1.3 billiards"),
+        ("fr_FR", "-1_300_000_000_000_000", "-1.3 billiards"),
+        ("fr_FR", "3_500_000_000_000_000_000_000", "3.5 trilliards"),
+        ("fr_FR", "8_100_000_000_000_000_000_000_000_000_000_000", "8.1 
quintilliards"),
+        (
+            "fr_FR",
+            "-8_100_000_000_000_000_000_000_000_000_000_000",
+            "-8.1 quintilliards",
+        ),
+        (
+            "fr_FR",
+            1_000_000_000_000_000_000_000_000_000_000_000_000,
+            "1000.0 quintilliards",
+        ),
+        (
+            "fr_FR",
+            1_100_000_000_000_000_000_000_000_000_000_000_000,
+            "1100.0 quintilliards",
+        ),
+        (
+            "fr_FR",
+            2_100_000_000_000_000_000_000_000_000_000_000_000,
+            "2100.0 quintilliards",
+        ),
     ],
 )
-def test_intword_plurals(locale: str, number: int, expected_result: str) -> 
None:
+def test_intword_i18n(locale: str, number: int, expected_result: str) -> None:
     try:
         humanize.i18n.activate(locale)
     except FileNotFoundError:
@@ -187,7 +234,7 @@
         i18n = importlib.import_module("humanize.i18n")
         monkeypatch.setattr(i18n, "__spec__", None)
 
-        with pytest.raises(Exception, match=self.expected_msg):
+        with pytest.raises(FileNotFoundError, match=self.expected_msg):
             i18n.activate("ru_RU")
 
     def test_default_locale_path_undefined__spec__(
@@ -196,7 +243,7 @@
         i18n = importlib.import_module("humanize.i18n")
         monkeypatch.delattr(i18n, "__spec__")
 
-        with pytest.raises(Exception, match=self.expected_msg):
+        with pytest.raises(FileNotFoundError, match=self.expected_msg):
             i18n.activate("ru_RU")
 
     @freeze_time("2020-02-02")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/tests/test_number.py 
new/humanize-4.15.0/tests/test_number.py
--- old/humanize-4.14.0/tests/test_number.py    2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/tests/test_number.py    2025-12-20 21:03:38.000000000 
+0100
@@ -119,14 +119,21 @@
         ([1_000_000_000_000_000_000_000_000_000_000_000_000], "1000.0 
decillion"),
         ([1_100_000_000_000_000_000_000_000_000_000_000_000], "1100.0 
decillion"),
         ([2_100_000_000_000_000_000_000_000_000_000_000_000], "2100.0 
decillion"),
+        ([2e100], "2.0 googol"),
         ([None], "None"),
         (["1230000", "%0.2f"], "1.23 million"),
-        ([10**101], "1" + "0" * 101),
+        ([10**101], "10.0 googol"),
         ([math.nan], "NaN"),
         ([math.inf], "+Inf"),
         ([-math.inf], "-Inf"),
         (["nan"], "NaN"),
         (["-inf"], "-Inf"),
+        (["1234567", "%.0f"], "1 million"),
+        (["1234567", "%.1f"], "1.2 million"),
+        (["1234567", "%.2f"], "1.23 million"),
+        (["1234567", "%.3f"], "1.235 million"),
+        (["999500", "%.0f"], "1 million"),
+        (["999499", "%.0f"], "999 thousand"),
     ],
 )
 def test_intword(test_args: list[str], expected: str) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/tests/test_time.py 
new/humanize-4.15.0/tests/test_time.py
--- old/humanize-4.14.0/tests/test_time.py      2025-10-13 15:00:06.000000000 
+0200
+++ new/humanize-4.15.0/tests/test_time.py      2025-12-20 21:03:38.000000000 
+0100
@@ -97,15 +97,26 @@
         (23.5, "23 seconds"),
         (30, "30 seconds"),
         (dt.timedelta(microseconds=13), "a moment"),
-        (dt.timedelta(minutes=1, seconds=30), "a minute"),
+        (dt.timedelta(minutes=1, seconds=29), "a minute"),
+        (dt.timedelta(minutes=1, seconds=30), "2 minutes"),
+        (dt.timedelta(minutes=1, seconds=59), "2 minutes"),
         (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(minutes=59), "59 minutes"),
+        (dt.timedelta(minutes=59, seconds=30), "an hour"),
+        (dt.timedelta(hours=1, minutes=29), "an hour"),
+        # Round to nearest, ties to even.
+        # See https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
+        (dt.timedelta(hours=1, minutes=30), "2 hours"),
+        (dt.timedelta(hours=2, minutes=30), "2 hours"),
+        (dt.timedelta(hours=3, minutes=30), "4 hours"),
+        (dt.timedelta(hours=23, minutes=50, seconds=50), "a day"),
         (dt.timedelta(days=1), "a day"),
         (dt.timedelta(days=500), "1 year, 4 months"),
         (dt.timedelta(days=365 * 2 + 35), "2 years"),
         (dt.timedelta(seconds=1), "a 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
         (dt.timedelta(days=10000), "27 years"),
         (dt.timedelta(days=365 + 35), "1 year, 1 month"),
@@ -134,19 +145,25 @@
         (NOW, "now"),
         (NOW - dt.timedelta(seconds=1), "a second ago"),
         (NOW - dt.timedelta(seconds=30), "30 seconds ago"),
-        (NOW - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
+        (NOW - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
+        (NOW - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
         (NOW - dt.timedelta(minutes=2), "2 minutes ago"),
-        (NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
-        (NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
+        (NOW - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
+        (NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
+        (NOW - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
+        (NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
         (NOW - dt.timedelta(days=1), "a day ago"),
         (NOW - dt.timedelta(days=500), "1 year, 4 months ago"),
         (NOW - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
         (NOW + dt.timedelta(seconds=1), "a second from now"),
         (NOW + dt.timedelta(seconds=30), "30 seconds from now"),
-        (NOW + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
+        (NOW + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
+        (NOW + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
         (NOW + dt.timedelta(minutes=2), "2 minutes from now"),
-        (NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from 
now"),
-        (NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from 
now"),
+        (NOW + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from 
now"),
+        (NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from 
now"),
+        (NOW + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from 
now"),
+        (NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from 
now"),
         (NOW + dt.timedelta(days=1), "a day from now"),
         (NOW + dt.timedelta(days=500), "1 year, 4 months from now"),
         (NOW + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
@@ -155,7 +172,9 @@
         (NOW - dt.timedelta(days=365 + 35), "1 year, 1 month ago"),
         (dt.timedelta(days=-10000), "27 years from now"),
         (dt.timedelta(days=365 + 35), "1 year, 1 month ago"),
-        (23.5, "23 seconds ago"),
+        (22.5, "22 seconds ago"),
+        (23.5, "24 seconds ago"),
+        (23.9, "24 seconds ago"),
         (30, "30 seconds ago"),
         (NOW - dt.timedelta(days=365 * 2 + 65), "2 years ago"),
         (NOW - dt.timedelta(days=365 + 4), "1 year, 4 days ago"),
@@ -175,10 +194,12 @@
         (NOW, "now"),
         (NOW - dt.timedelta(seconds=1), "a second ago"),
         (NOW - dt.timedelta(seconds=30), "30 seconds ago"),
-        (NOW - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
+        (NOW - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
+        (NOW - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
         (NOW - dt.timedelta(minutes=2), "2 minutes ago"),
-        (NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
-        (NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
+        (NOW - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
+        (NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
+        (NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
         (NOW - dt.timedelta(days=1), "a day ago"),
         (NOW - dt.timedelta(days=17), "17 days ago"),
         (NOW - dt.timedelta(days=47), "47 days ago"),
@@ -186,10 +207,13 @@
         (NOW - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
         (NOW + dt.timedelta(seconds=1), "a second from now"),
         (NOW + dt.timedelta(seconds=30), "30 seconds from now"),
-        (NOW + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
+        (NOW + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
+        (NOW + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
         (NOW + dt.timedelta(minutes=2), "2 minutes from now"),
-        (NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from 
now"),
-        (NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from 
now"),
+        (NOW + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from 
now"),
+        (NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from 
now"),
+        (NOW + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from 
now"),
+        (NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from 
now"),
         (NOW + dt.timedelta(days=1), "a day from now"),
         (NOW + dt.timedelta(days=500), "1 year, 135 days from now"),
         (NOW + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
@@ -198,7 +222,8 @@
         (NOW - dt.timedelta(days=365 + 35), "1 year, 35 days ago"),
         (dt.timedelta(days=-10000), "27 years from now"),
         (dt.timedelta(days=365 + 35), "1 year, 35 days ago"),
-        (23.5, "23 seconds ago"),
+        (22.5, "22 seconds ago"),
+        (23.5, "24 seconds ago"),
         (30, "30 seconds ago"),
         (NOW - dt.timedelta(days=365 * 2 + 65), "2 years ago"),
         (NOW - dt.timedelta(days=365 + 4), "1 year, 4 days ago"),
@@ -419,19 +444,25 @@
         (NOW_UTC, "now"),
         (NOW_UTC - dt.timedelta(seconds=1), "a second ago"),
         (NOW_UTC - dt.timedelta(seconds=30), "30 seconds ago"),
-        (NOW_UTC - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
+        (NOW_UTC - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
+        (NOW_UTC - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
         (NOW_UTC - dt.timedelta(minutes=2), "2 minutes ago"),
-        (NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour 
ago"),
-        (NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours 
ago"),
+        (NOW_UTC - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour 
ago"),
+        (NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours 
ago"),
+        (NOW_UTC - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours 
ago"),
+        (NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "a day 
ago"),
         (NOW_UTC - dt.timedelta(days=1), "a day ago"),
         (NOW_UTC - dt.timedelta(days=500), "1 year, 4 months ago"),
         (NOW_UTC - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
         (NOW_UTC + dt.timedelta(seconds=1), "a second from now"),
         (NOW_UTC + dt.timedelta(seconds=30), "30 seconds from now"),
-        (NOW_UTC + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
+        (NOW_UTC + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
+        (NOW_UTC + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
         (NOW_UTC + dt.timedelta(minutes=2), "2 minutes from now"),
-        (NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour 
from now"),
-        (NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours 
from now"),
+        (NOW_UTC + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour 
from now"),
+        (NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours 
from now"),
+        (NOW_UTC + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours 
from now"),
+        (NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from 
now"),
         (NOW_UTC + dt.timedelta(days=1), "a day from now"),
         (NOW_UTC + dt.timedelta(days=500), "1 year, 4 months from now"),
         (NOW_UTC + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
@@ -453,19 +484,25 @@
         (NOW_UTC, "now"),
         (NOW_UTC - dt.timedelta(seconds=1), "a second ago"),
         (NOW_UTC - dt.timedelta(seconds=30), "30 seconds ago"),
-        (NOW_UTC - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
+        (NOW_UTC - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
+        (NOW_UTC - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
         (NOW_UTC - dt.timedelta(minutes=2), "2 minutes ago"),
-        (NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour 
ago"),
-        (NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours 
ago"),
+        (NOW_UTC - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour 
ago"),
+        (NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours 
ago"),
+        (NOW_UTC - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours 
ago"),
+        (NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "a day 
ago"),
         (NOW_UTC - dt.timedelta(days=1), "a day ago"),
         (NOW_UTC - dt.timedelta(days=500), "1 year, 4 months ago"),
         (NOW_UTC - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
         (NOW_UTC + dt.timedelta(seconds=1), "a second from now"),
         (NOW_UTC + dt.timedelta(seconds=30), "30 seconds from now"),
-        (NOW_UTC + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
+        (NOW_UTC + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
+        (NOW_UTC + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
         (NOW_UTC + dt.timedelta(minutes=2), "2 minutes from now"),
-        (NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour 
from now"),
-        (NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours 
from now"),
+        (NOW_UTC + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour 
from now"),
+        (NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours 
from now"),
+        (NOW_UTC + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours 
from now"),
+        (NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from 
now"),
         (NOW_UTC + dt.timedelta(days=1), "a day from now"),
         (NOW_UTC + dt.timedelta(days=500), "1 year, 4 months from now"),
         (NOW_UTC + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanize-4.14.0/tox.ini new/humanize-4.15.0/tox.ini
--- old/humanize-4.14.0/tox.ini 2025-10-13 15:00:06.000000000 +0200
+++ new/humanize-4.15.0/tox.ini 2025-12-20 21:03:38.000000000 +0100
@@ -5,7 +5,7 @@
     docs
     lint
     mypy
-    py{py3, 314, 313, 312, 311, 310}
+    py{py3, 315, 314, 313, 312, 311, 310}
 
 [testenv]
 extras =
@@ -30,11 +30,9 @@
 [testenv:lint]
 skip_install = true
 deps =
-    pre-commit-uv
-pass_env =
-    PRE_COMMIT_COLOR
+    prek
 commands =
-    pre-commit run --all-files --show-diff-on-failure
+    prek run --all-files --show-diff-on-failure
 
 [testenv:mypy]
 deps =

Reply via email to