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 \