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 =