humanize-4.16.0/.pre-commit-config.yaml 0000644 0000000 0000000 00000003540 15220765405 014641 0 ustar 00 repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 26.3.1
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: forbid-submodules
- id: requirements-txt-fixer
- id: trailing-whitespace
exclude: \.github/ISSUE_TEMPLATE\.md|\.github/PULL_REQUEST_TEMPLATE\.md
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.37.1
hooks:
- id: check-github-workflows
- id: check-renovate
- repo: https://github.com/rhysd/actionlint
rev: v1.7.12
hooks:
- id: actionlint
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.23.1
hooks:
- id: zizmor
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.21.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.25
hooks:
- id: validate-pyproject
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.7.1
hooks:
- id: tox-ini-fmt
- repo: https://github.com/google/yamlfmt
rev: v0.21.0
hooks:
- id: yamlfmt
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.8.1
hooks:
- id: prettier
args: [--prose-wrap=always, --print-width=88]
exclude: \.github/ISSUE_TEMPLATE\.md|\.github/PULL_REQUEST_TEMPLATE\.md
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
ci:
autoupdate_schedule: quarterly
humanize-4.16.0/.readthedocs.yml 0000644 0000000 0000000 00000000607 15220765405 013447 0 ustar 00 # Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Project page: https://readthedocs.org/projects/python-humanize/
version: 2
mkdocs:
configuration: mkdocs.yml
build:
os: ubuntu-22.04
tools:
python: "3"
commands:
- pip install -U tox
- tox -e docs
- mkdir _readthedocs
- mv site _readthedocs/html
humanize-4.16.0/.yamlfmt.yaml 0000644 0000000 0000000 00000000055 15220765405 012771 0 ustar 00 formatter:
retain_line_breaks_single: true
humanize-4.16.0/RELEASING.md 0000644 0000000 0000000 00000001612 15220765405 012211 0 ustar 00 # Release checklist
- [ ] Get `main` to the appropriate code release state.
[GitHub Actions](https://github.com/python-humanize/humanize/actions) should be
running cleanly for all merges to `main`.
[](https://github.com/python-humanize/humanize/actions)
- [ ] Edit release draft, adjust text if needed:
https://github.com/python-humanize/humanize/releases
- [ ] Check next tag is correct, amend if needed
- [ ] Publish release
- [ ] Check the tagged
[GitHub Actions build](https://github.com/python-humanize/humanize/actions/workflows/release.yml)
has released to [PyPI](https://pypi.org/project/humanize/#history)
- [ ] Check installation:
```bash
pip3 uninstall -y humanize && pip3 install -U humanize && python3 -c "import humanize; print(humanize.__version__)"
```
humanize-4.16.0/mkdocs.yml 0000644 0000000 0000000 00000001533 15220765405 012363 0 ustar 00 site_name: humanize
site_url: https://humanize.readthedocs.io
repo_url: https://github.com/python-humanize/humanize
theme:
name: material
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/toggle-switch
name: Switch to light mode
nav:
- Home: index.md
- Number: number.md
- Time: time.md
- Filesize: filesize.md
- Lists: lists.md
- Internationalisation: i18n.md
plugins:
- search
- mkdocstrings:
- include-markdown
markdown_extensions:
- pymdownx.highlight:
use_pygments: true
pygments_lang_class: true
- pymdownx.superfences
extra_css:
- css/code_select.css
watch:
- src/humanize
humanize-4.16.0/requirements-mypy.txt 0000644 0000000 0000000 00000000064 15220765405 014636 0 ustar 00 mypy==2.1.0
pytest
types-freezegun
types-setuptools
humanize-4.16.0/tox.ini 0000644 0000000 0000000 00000001237 15220765405 011674 0 ustar 00 [tox]
requires =
tox>=4.32
env_list =
docs
lint
mypy
py{py3}
py{310-315}
py{314-315}t
[testenv]
extras =
tests
pass_env =
FORCE_COLOR
commands =
{envpython} -m pytest \
--cov humanize \
--cov tests \
--cov-report html \
--cov-report term \
--cov-report xml \
{posargs}
[testenv:docs]
deps =
-r docs/requirements.txt
commands =
mkdocs build
[testenv:lint]
skip_install = true
deps =
prek
commands =
prek run --all-files --show-diff-on-failure
[testenv:mypy]
deps =
-r requirements-mypy.txt
commands =
mypy . {posargs}
[pytest]
addopts = --color=yes --doctest-modules
humanize-4.16.0/.github/CONTRIBUTING.md 0000644 0000000 0000000 00000000752 15220765405 014153 0 ustar 00 # Contributing
## Linting
Linting is run on the CI using [prek](https://prek.j178.dev//), and can be run locally:
```sh
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
Follow
[Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
for docstrings.
## Localization
See [README](https://github.com/python-humanize/humanize#localization).
humanize-4.16.0/.github/FUNDING.yml 0000644 0000000 0000000 00000000051 15220765405 013527 0 ustar 00 github: hugovk
tidelift: "pypi/humanize"
humanize-4.16.0/.github/ISSUE_TEMPLATE.md 0000644 0000000 0000000 00000000672 15220765405 014430 0 ustar 00 ### What did you do?
### What did you expect to happen?
### What actually happened?
### What versions are you using?
* OS:
* Python:
* Humanize:
Please include **code** that reproduces the issue.
The [best reproductions](https://stackoverflow.com/help/minimal-reproducible-example)
are
[self-contained scripts](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/)
with minimal dependencies.
```python
code goes here
```
humanize-4.16.0/.github/PULL_REQUEST_TEMPLATE.md 0000644 0000000 0000000 00000000072 15220765405 015516 0 ustar 00 Fixes #
Changes proposed in this pull request:
*
*
*
humanize-4.16.0/.github/SECURITY.md 0000644 0000000 0000000 00000000220 15220765405 013501 0 ustar 00 # Security policy
Security reports can be made via [Tidelift](https://tidelift.com/security). Tidelift
will coordinate the fix and disclosure.
humanize-4.16.0/.github/labels.yml 0000644 0000000 0000000 00000003636 15220765405 013713 0 ustar 00 # Default GitHub labels
- color: d73a4a
description: "Something isn't working"
name: bug
- color: cfd3d7
description: "This issue or pull request already exists"
name: duplicate
- color: a2eeef
description: "New feature or request"
name: enhancement
- color: 7057ff
description: "Good for newcomers"
name: good first issue
- color: 008672
description: "Extra attention is needed"
name: help wanted
- color: e4e669
description: "This doesn't seem right"
name: invalid
- color: d876e3
description: "Further information is requested"
name: question
- color: ffffff
description: "This will not be worked on"
name: wontfix
# Keep a Changelog labels
# https://keepachangelog.com/en/1.0.0/
- color: 0e8a16
description: "For new features"
name: "changelog: Added"
- color: af99e5
description: "For changes in existing functionality"
name: "changelog: Changed"
- color: FFA500
description: "For soon-to-be removed features"
name: "changelog: Deprecated"
- color: 00A800
description: "For any bug fixes"
name: "changelog: Fixed"
- color: ff0000
description: "For now removed features"
name: "changelog: Removed"
- color: 045aa0
description: "In case of vulnerabilities"
name: "changelog: Security"
- color: fbca04
description: "Exclude PR from release draft"
name: "changelog: skip"
# Other labels
- color: 0366d6
description: "For dependencies"
name: dependencies
- color: 0075ca
description: "Improvements or additions to documentation"
name: documentation
- color: d0c1ff
description: "Translations need updating"
name: "needs localisation"
- color: eb6123
description: ""
name: Hacktoberfest
- color: eb6123
description: "To credit accepted Hacktoberfest PRs"
name: hacktoberfest-accepted
- color: e29673
name: "needs tests"
- color: d65e88
description: "Deploy and release"
name: release
- color: fbca04
description: "Unit tests, linting, CI, etc."
name: testing
humanize-4.16.0/.github/release-drafter.yml 0000644 0000000 0000000 00000001574 15220765405 015515 0 ustar 00 name-template: "$RESOLVED_VERSION"
tag-template: "$RESOLVED_VERSION"
categories:
- title: "Added"
labels:
- "changelog: Added"
- "enhancement"
- title: "Changed"
label: "changelog: Changed"
- title: "Deprecated"
label: "changelog: Deprecated"
- title: "Removed"
label: "changelog: Removed"
- title: "Fixed"
labels:
- "changelog: Fixed"
- "bug"
- title: "Security"
label: "changelog: Security"
exclude-labels:
- "changelog: skip"
autolabeler:
- label: "changelog: skip"
branch:
- "/pre-commit-ci-update-config/"
template: |
$CHANGES
version-resolver:
major:
labels:
- "changelog: Removed"
minor:
labels:
- "changelog: Added"
- "changelog: Changed"
- "changelog: Deprecated"
- "enhancement"
patch:
labels:
- "changelog: Fixed"
- "bug"
default: minor
humanize-4.16.0/.github/renovate.json 0000644 0000000 0000000 00000001056 15220765405 014436 0 ustar 00 {
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":semanticCommitsDisabled"],
"labels": ["changelog: skip", "dependencies"],
"minimumReleaseAge": "7 days",
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": ["github-actions"],
"separateMajorMinor": false
},
{
"groupName": "docs/requirements.txt",
"matchFileNames": ["docs/requirements.txt"],
"separateMajorMinor": false
}
],
"schedule": ["on the first day of the month"]
}
humanize-4.16.0/.github/workflows/benchmark.yml 0000644 0000000 0000000 00000001756 15220765405 016441 0 ustar 00 name: Benchmark
on: [push, pull_request, workflow_dispatch]
permissions: {}
env:
FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
jobs:
benchmarks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python 3.14
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.14"
allow-prereleases: true
- name: Install gettext
run: |
sudo apt install gettext
- name: Generate translation binaries
run: |
scripts/generate-translation-binaries.sh
- name: Install dependencies
run: pip install -e ".[tests]"
- name: Run benchmarks
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
with:
mode: simulation
run: pytest tests/test_benchmarks.py --codspeed
humanize-4.16.0/.github/workflows/docs.yml 0000644 0000000 0000000 00000001163 15220765405 015427 0 ustar 00 name: Docs
on: [push, pull_request, workflow_dispatch]
permissions: {}
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Docs
run: |
uvx --with tox-uv tox -e docs
humanize-4.16.0/.github/workflows/labels.yml 0000644 0000000 0000000 00000001033 15220765405 015735 0 ustar 00 name: Sync labels
on:
push:
branches:
- main
paths:
- .github/labels.yml
workflow_dispatch:
jobs:
sync:
permissions:
pull-requests: write
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # v1.3.0
with:
prune: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
humanize-4.16.0/.github/workflows/lint.yml 0000644 0000000 0000000 00000001551 15220765405 015446 0 ustar 00 name: Lint
on: [push, pull_request, workflow_dispatch]
permissions: {}
env:
FORCE_COLOR: 1
RUFF_OUTPUT_FORMAT: github
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Mypy
run: uvx --with tox-uv tox -e mypy
humanize-4.16.0/.github/workflows/release-drafter.yml 0000644 0000000 0000000 00000002434 15220765405 017546 0 ustar 00 name: Release drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [opened, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
# pull_request_target:
# types: [opened, reopened, synchronize]
workflow_dispatch:
jobs:
update_release_draft:
if: |
github.event.repository.fork == false
&& github.event_name != 'pull_request'
permissions:
# write permission is required to create a GitHub Release
contents: write
runs-on: ubuntu-slim
steps:
# Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
autolabeler:
if: |
github.event.repository.fork == false
&& github.event_name == 'pull_request'
permissions:
# write permission is required for autolabeler
pull-requests: write
runs-on: ubuntu-slim
steps:
- uses: release-drafter/release-drafter/autolabeler@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
humanize-4.16.0/.github/workflows/release.yml 0000644 0000000 0000000 00000004275 15220765405 016126 0 ustar 00 name: Release
on:
push:
branches: [main]
tags: ["*"]
pull_request:
branches: [main]
release:
types:
- published
workflow_dispatch:
permissions: {}
env:
FORCE_COLOR: 1
jobs:
# Always build & lint package.
build-package:
name: Build & verify package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Install gettext
run: |
sudo apt install gettext
- name: Generate translation binaries
run: |
scripts/generate-translation-binaries.sh
- uses: hynek/build-and-inspect-python-package@d44ca7d91762de7a7d5436ddae667c6da6d1c3df # v2.18.0
# Upload to Test PyPI on every commit on main.
release-test-pypi:
name: Publish in-dev package to test.pypi.org
if: |
github.event.repository.fork == false
&& github.event_name == 'push'
&& github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: build-package
permissions:
id-token: write
steps:
- name: Download packages built by build-and-inspect-python-package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: Packages
path: dist
- name: Upload package to Test PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
repository-url: https://test.pypi.org/legacy/
# Upload to real PyPI on GitHub Releases.
release-pypi:
name: Publish released package to pypi.org
if: |
github.event.repository.fork == false
&& github.event.action == 'published'
runs-on: ubuntu-latest
needs: build-package
permissions:
id-token: write
steps:
- name: Download packages built by build-and-inspect-python-package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: Packages
path: dist
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
humanize-4.16.0/.github/workflows/require-pr-label.yml 0000644 0000000 0000000 00000001150 15220765405 017643 0 ustar 00 name: Require PR label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-slim
permissions:
issues: write
pull-requests: write
steps:
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
with:
mode: minimum
count: 1
labels: |
changelog: Added
changelog: Changed
changelog: Deprecated
changelog: Fixed
changelog: Removed
changelog: Security
changelog: skip
humanize-4.16.0/.github/workflows/test.yml 0000644 0000000 0000000 00000003723 15220765405 015462 0 ustar 00 name: Test
on: [push, pull_request, workflow_dispatch]
permissions: {}
env:
FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version:
- "pypy3.11"
- "3.15t"
- "3.15"
- "3.14t"
- "3.14"
- "3.13"
- "3.12"
- "3.11"
- "3.10"
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
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: |
sudo apt install gettext
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macos')
run: |
brew install gettext
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Generate translation binaries
run: |
scripts/generate-translation-binaries.sh
- name: Tox tests
run: |
uvx --python ${{ matrix.python-version }} --with tox-uv tox -e py
- name: Upload coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
success:
needs: test
runs-on: ubuntu-latest
name: Test successful
steps:
- name: Success
run: echo Test successful
humanize-4.16.0/docs/filesize.md 0000644 0000000 0000000 00000000042 15220765405 013436 0 ustar 00 # Filesize
::: humanize.filesize
humanize-4.16.0/docs/i18n.md 0000644 0000000 0000000 00000000052 15220765405 012404 0 ustar 00 # Internationalisation
::: humanize.i18n
humanize-4.16.0/docs/index.md 0000644 0000000 0000000 00000000424 15220765405 012737 0 ustar 00 # humanize
Welcome to the humanize API reference.
- [Number](number.md)
- [Time](time.md)
- [Filesize](filesize.md)
- [Lists](lists.md)
- [I18n](i18n.md)
{%
include-markdown "../README.md"
start=""
end=""
comments=false
%}
humanize-4.16.0/docs/lists.md 0000644 0000000 0000000 00000000034 15220765405 012763 0 ustar 00 # Lists
::: humanize.lists
humanize-4.16.0/docs/number.md 0000644 0000000 0000000 00000000036 15220765405 013117 0 ustar 00 # Number
::: humanize.number
humanize-4.16.0/docs/requirements.txt 0000644 0000000 0000000 00000000176 15220765405 014576 0 ustar 00 mkdocs==1.6.1
mkdocs-include-markdown-plugin
mkdocs-material
mkdocstrings[python]==1.0.4
pygments
pymdown-extensions==10.21.3
humanize-4.16.0/docs/time.md 0000644 0000000 0000000 00000000032 15220765405 012561 0 ustar 00 # Time
::: humanize.time
humanize-4.16.0/docs/css/code_select.css 0000644 0000000 0000000 00000000375 15220765405 015066 0 ustar 00 /* Don't allow to select >>> in ```pycon``` code blocks */
.language-pycon .gp,
.language-pycon .go {
user-select: none;
}
/* Hide "Copy to clipboard" buttons in ```pycon``` code blocks */
.highlight.language-pycon .md-clipboard {
display: none;
}
humanize-4.16.0/scripts/generate-translation-binaries.sh 0000755 0000000 0000000 00000000314 15220765405 020322 0 ustar 00 set -e
for d in src/humanize/locale/*/; do
locale="$(basename $d)"
echo "$locale"
# compile to binary .mo
msgfmt --check -o src/humanize/locale/$locale/LC_MESSAGES/humanize{.mo,.po}
done
humanize-4.16.0/scripts/update-translations.sh 0000755 0000000 0000000 00000000721 15220765405 016405 0 ustar 00 set -e
# extract new phrases
xgettext --from-code=UTF-8 -o humanize.pot -k'_' -k'N_' -k'P_:1c,2' -k'NS_:1,2' -k'_ngettext:1,2' -l python src/humanize/*.py
for d in src/humanize/locale/*/; do
locale="$(basename $d)"
echo "$locale"
# add them to locale files
msgmerge -U src/humanize/locale/$locale/LC_MESSAGES/humanize.po humanize.pot
# compile to binary .mo
msgfmt --check -o src/humanize/locale/$locale/LC_MESSAGES/humanize{.mo,.po}
done
humanize-4.16.0/src/humanize/__init__.py 0000644 0000000 0000000 00000002060 15220765405 015074 0 ustar 00 """Main package for humanize."""
from __future__ import annotations
__lazy_modules__ = {
"humanize._version",
"humanize.filesize",
"humanize.i18n",
"humanize.lists",
"humanize.number",
"humanize.time",
}
from humanize.filesize import naturalsize
from humanize.i18n import activate, deactivate, decimal_separator, thousands_separator
from humanize.lists import natural_list
from humanize.number import (
apnumber,
clamp,
fractional,
intcomma,
intword,
metric,
ordinal,
scientific,
)
from humanize.time import (
naturaldate,
naturalday,
naturaldelta,
naturaltime,
precisedelta,
)
from ._version import __version__
__all__ = [
"__version__",
"activate",
"apnumber",
"clamp",
"deactivate",
"decimal_separator",
"fractional",
"intcomma",
"intword",
"metric",
"natural_list",
"naturaldate",
"naturalday",
"naturaldelta",
"naturalsize",
"naturaltime",
"ordinal",
"precisedelta",
"scientific",
"thousands_separator",
]
humanize-4.16.0/src/humanize/_version.py 0000644 0000000 0000000 00000001012 15220765405 015155 0 ustar 00 # file generated by vcs-versioning
# don't change, don't track in version control
from __future__ import annotations
__all__ = [
"__version__",
"__version_tuple__",
"version",
"version_tuple",
"__commit_id__",
"commit_id",
]
version: str
__version__: str
__version_tuple__: tuple[int | str, ...]
version_tuple: tuple[int | str, ...]
commit_id: str | None
__commit_id__: str | None
__version__ = version = '4.16.0'
__version_tuple__ = version_tuple = (4, 16, 0)
__commit_id__ = commit_id = None
humanize-4.16.0/src/humanize/filesize.py 0000644 0000000 0000000 00000005476 15220765405 015165 0 ustar 00 """Bits and bytes related humanization."""
from __future__ import annotations
__lazy_modules__ = {"humanize.i18n", "math"}
from math import log
from humanize.i18n import _gettext as _
suffixes = {
"decimal": (
"kB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB",
"RB",
"QB",
),
"binary": (
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
"RiB",
"QiB",
),
"gnu": "KMGTPEZYRQ",
}
def naturalsize(
value: float | str,
binary: bool = False,
gnu: bool = False,
format: str = "%.1f",
) -> str:
"""Format a number of bytes like a human-readable filesize (e.g. 10 kB).
By default, decimal suffixes (kB, MB) are used.
Non-GNU modes are compatible with jinja2's `filesizeformat` filter.
Examples:
```pycon
>>> naturalsize(3000000)
'3.0 MB'
>>> naturalsize(300, False, True)
'300B'
>>> naturalsize(3000, False, True)
'2.9K'
>>> naturalsize(3000, False, True, "%.3f")
'2.930K'
>>> naturalsize(3000, True)
'2.9 KiB'
>>> naturalsize(10**28)
'10.0 RB'
>>> naturalsize(10**34 * 3)
'30000.0 QB'
>>> naturalsize(-4096, True)
'-4.0 KiB'
```
Args:
value (int, float, str): Integer to convert.
binary (bool): If `True`, uses binary suffixes (KiB, MiB) with base
210 instead of 103.
gnu (bool): If `True`, the binary argument is ignored and GNU-style
(`ls -sh` style) prefixes are used (K, M) with the 2**10 definition.
format (str): Custom formatter.
Returns:
str: Human readable representation of a filesize.
"""
if gnu:
suffix = suffixes["gnu"]
elif binary:
suffix = suffixes["binary"]
else:
suffix = suffixes["decimal"]
base = 1024 if (gnu or binary) else 1000
bytes_ = float(value)
abs_bytes = abs(bytes_)
if abs_bytes == 1 and not gnu:
return _("%d Byte") % int(bytes_)
if abs_bytes < base:
return f"{int(bytes_)}B" if gnu else _("%d Bytes") % int(bytes_)
exp = int(min(log(abs_bytes, base), len(suffix)))
# The suffix is chosen from the unrounded byte count, but `format` rounds the
# mantissa afterward; rounding can push it up to `base` (e.g. 999999 is
# 999.999 kB, which formats to "1000.0 kB"). When that happens and a larger
# suffix is available, step up one suffix so the result reads "1.0 MB".
if exp < len(suffix) and abs(float(format % (abs_bytes / (base**exp)))) >= base:
exp += 1
space = "" if gnu else " "
ret: str = format % (bytes_ / (base**exp)) + space + _(suffix[exp - 1])
return ret
humanize-4.16.0/src/humanize/i18n.py 0000644 0000000 0000000 00000011744 15220765405 014125 0 ustar 00 """Activate, get and deactivate translations."""
from __future__ import annotations
import gettext as gettext_module
from threading import local
TYPE_CHECKING = False
if TYPE_CHECKING:
import os
import pathlib
__all__ = ["activate", "deactivate", "decimal_separator", "thousands_separator"]
_TRANSLATIONS: dict[str | None, gettext_module.NullTranslations] = {
None: gettext_module.NullTranslations()
}
_CURRENT = local()
# Mapping of locale to thousands separator
_THOUSANDS_SEPARATOR: dict[str | None, str] = {
"de_DE": ".",
"fr_FR": " ",
"it_IT": ".",
"pt_BR": ".",
"hu_HU": " ",
"lv": " ",
}
# Mapping of locale to decimal separator
_DECIMAL_SEPARATOR: dict[str | None, str] = {
"de_DE": ",",
"fr_FR": ".",
"it_IT": ",",
"pt_BR": ",",
"hu_HU": ",",
"lv": ",",
}
def _get_default_locale_path() -> pathlib.Path | None:
package = __spec__ and __spec__.parent
if not package:
return None
import importlib.resources
with importlib.resources.as_file(importlib.resources.files(package)) as pkg:
return pkg / "locale"
def get_translation() -> gettext_module.NullTranslations:
return _TRANSLATIONS.get(getattr(_CURRENT, "locale", None), _TRANSLATIONS[None])
def activate(
locale: str | None, path: str | os.PathLike[str] | None = None
) -> gettext_module.NullTranslations:
"""Activate internationalisation.
Set `locale` as current locale. Search for locale in directory `path`.
Args:
locale (str | None): Language name, e.g. `en_GB`. If `None`, defaults to no
translation. Similar to calling ``deactivate()``.
path (str | pathlib.Path): Path to search for locales.
Returns:
dict: Translations.
Raises:
FileNotFoundError: If humanize cannot find the locale folder.
"""
if locale is None or locale.startswith("en"):
_CURRENT.locale = None
return _TRANSLATIONS[None]
if path is None:
path = _get_default_locale_path()
if path is None:
msg = (
"Humanize cannot determinate the default location of the 'locale' folder. "
"You need to pass the path explicitly."
)
raise FileNotFoundError(msg)
if locale not in _TRANSLATIONS:
translation = gettext_module.translation("humanize", path, [locale])
_TRANSLATIONS[locale] = translation
_CURRENT.locale = locale
return _TRANSLATIONS[locale]
def deactivate() -> None:
"""Deactivate internationalisation."""
_CURRENT.locale = None
def _gettext(message: str) -> str:
"""Get translation.
Args:
message (str): Text to translate.
Returns:
str: Translated text.
"""
return get_translation().gettext(message)
def _pgettext(msgctxt: str, message: str) -> str:
"""Fetches a particular translation.
It works with `msgctxt` .po modifiers and allows duplicate keys with different
translations.
Args:
msgctxt (str): Context of the translation.
message (str): Text to translate.
Returns:
str: Translated text.
"""
return get_translation().pgettext(msgctxt, message)
def _ngettext(message: str, plural: str, num: int) -> str:
"""Plural version of _gettext.
Args:
message (str): Singular text to translate.
plural (str): Plural text to translate.
num (int): The number (e.g. item count) to determine translation for the
respective grammatical number.
Returns:
str: Translated text.
"""
return get_translation().ngettext(message, plural, num)
def _gettext_noop(message: str) -> str:
"""Mark a string as a translation string without translating it.
Example usage:
```python
CONSTANTS = [_gettext_noop('first'), _gettext_noop('second')]
def num_name(n):
return _gettext(CONSTANTS[n])
```
Args:
message (str): Text to translate in the future.
Returns:
str: Original text, unchanged.
"""
return message
def _ngettext_noop(singular: str, plural: str) -> tuple[str, str]:
"""Mark two strings as pluralized translations without translating them.
Example usage:
```python
CONSTANTS = [ngettext_noop('first', 'firsts'), ngettext_noop('second', 'seconds')]
def num_name(n):
return _ngettext(*CONSTANTS[n])
```
Args:
singular (str): Singular text to translate in the future.
plural (str): Plural text to translate in the future.
Returns:
tuple: Original text, unchanged.
"""
return singular, plural
def thousands_separator() -> str:
"""Return the thousands separator for a locale, default to comma.
Returns:
str: Thousands separator.
"""
return _THOUSANDS_SEPARATOR.get(getattr(_CURRENT, "locale", None), ",")
def decimal_separator() -> str:
"""Return the decimal separator for a locale, default to dot.
Returns:
str: Decimal separator.
"""
return _DECIMAL_SEPARATOR.get(getattr(_CURRENT, "locale", None), ".")
humanize-4.16.0/src/humanize/lists.py 0000644 0000000 0000000 00000001637 15220765405 014504 0 ustar 00 """Lists related humanization."""
from __future__ import annotations
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
__all__ = ["natural_list"]
def natural_list(items: list[Any]) -> str:
"""Natural list.
Convert a list of items into a human-readable string with commas and 'and'.
Examples:
>>> natural_list(["one", "two", "three"])
'one, two and three'
>>> natural_list(["one", "two"])
'one and two'
>>> natural_list(["one"])
'one'
Args:
items (list): An iterable of items.
Returns:
str: A string with commas and 'and' in the right places.
"""
if not items:
return ""
if len(items) == 1:
return str(items[0])
elif len(items) == 2:
return f"{str(items[0])} and {str(items[1])}"
else:
return ", ".join([str(item) for item in items[:-1]]) + f" and {str(items[-1])}"
humanize-4.16.0/src/humanize/number.py 0000644 0000000 0000000 00000037053 15220765405 014637 0 ustar 00 """Humanizing functions for numbers."""
from __future__ import annotations
__lazy_modules__ = {"bisect"}
import bisect
from .i18n import _gettext as _
from .i18n import _ngettext, decimal_separator, thousands_separator
from .i18n import _ngettext_noop as NS_
from .i18n import _pgettext as P_
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import TypeAlias
# This type can be better defined by typing.SupportsFloat
# but that's a Python 3.8 only typing option.
NumberOrString: TypeAlias = float | str
_SUPERSCRIPT_MAP = {
"0": "⁰",
"1": "¹",
"2": "²",
"3": "³",
"4": "⁴",
"5": "⁵",
"6": "⁶",
"7": "⁷",
"8": "⁸",
"9": "⁹",
"-": "⁻",
}
_SUPERSCRIPT_TRANS = str.maketrans(_SUPERSCRIPT_MAP)
_ORDINAL_SUFFIXES = ("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")
_APNUMBER_WORDS = (
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
def _format_not_finite(value: float) -> str:
"""Utility function to handle infinite and nan cases."""
import math
if math.isnan(value):
return "NaN"
if math.isinf(value) and value < 0:
return "-Inf"
if math.isinf(value) and value > 0:
return "+Inf"
return ""
def ordinal(value: NumberOrString, gender: str = "male") -> str:
"""Converts an integer to its ordinal as a string.
For example, 1 is "1st", 2 is "2nd", 3 is "3rd", etc. Works for any integer or
anything `int()` will turn into an integer. Anything else will return the output
of str(value).
Examples:
```pycon
>>> ordinal(1)
'1st'
>>> ordinal(1002)
'1002nd'
>>> ordinal(103)
'103rd'
>>> ordinal(4)
'4th'
>>> ordinal(12)
'12th'
>>> ordinal(101)
'101st'
>>> ordinal(111)
'111th'
>>> ordinal("something else")
'something else'
>>> ordinal([1, 2, 3]) == "[1, 2, 3]"
True
```
Args:
value (int, str, float): Integer to convert.
gender (str): Gender for translations. Accepts either "male" or "female".
Returns:
str: Ordinal string.
"""
import math
try:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
value = int(value)
except (TypeError, ValueError):
return str(value)
gender = "male" if gender == "male" else "female"
digit = 0 if value % 100 in (11, 12, 13) else value % 10
return f"{value}{P_(f'{digit} ({gender})', _ORDINAL_SUFFIXES[digit])}"
def intcomma(value: NumberOrString, ndigits: int | None = None) -> str:
"""Converts an integer to a string containing commas every three digits.
For example, 3000 becomes "3,000" and 45000 becomes "45,000". To maintain some
compatibility with Django's `intcomma`, this function also accepts floats.
Examples:
```pycon
>>> intcomma(100)
'100'
>>> intcomma("1000")
'1,000'
>>> intcomma(1_000_000)
'1,000,000'
>>> intcomma(1_234_567.25)
'1,234,567.25'
>>> intcomma(1234.5454545, 2)
'1,234.55'
>>> intcomma(14308.40, 1)
'14,308.4'
>>> intcomma("14308.40", 1)
'14,308.4'
>>> intcomma(None)
'None'
```
Args:
value (int, float, str): Integer or float to convert.
ndigits (int, None): Digits of precision for rounding after the decimal point.
Returns:
str: String containing commas every three digits.
"""
import math
thousands_sep = thousands_separator()
decimal_sep = decimal_separator()
try:
if isinstance(value, str):
value = value.replace(thousands_sep, "").replace(decimal_sep, ".")
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
if "." in value:
value = float(value)
else:
value = int(value)
else:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
float(value)
except (TypeError, ValueError):
return str(value)
if ndigits is not None:
result = f"{value:,.{ndigits}f}"
else:
result = f"{value:,}"
if thousands_sep != "," or decimal_sep != ".":
result = result.translate(str.maketrans(",.", thousands_sep + decimal_sep))
return result
powers = [10**x for x in (3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)]
human_powers = (
NS_("thousand", "thousand"),
NS_("million", "million"),
NS_("billion", "billion"),
NS_("trillion", "trillion"),
NS_("quadrillion", "quadrillion"),
NS_("quintillion", "quintillion"),
NS_("sextillion", "sextillion"),
NS_("septillion", "septillion"),
NS_("octillion", "octillion"),
NS_("nonillion", "nonillion"),
NS_("decillion", "decillion"),
NS_("googol", "googol"),
)
def intword(value: NumberOrString, format: str = "%.1f") -> str:
"""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",
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
>>> intword("100")
'100'
>>> intword("12400")
'12.4 thousand'
>>> intword("1000000")
'1.0 million'
>>> intword(1_200_000_000)
'1.2 billion'
>>> intword(8100000000000000000000000000000000)
'8.1 decillion'
>>> intword(None)
'None'
>>> intword("1234000", "%0.3f")
'1.234 million'
```
Args:
value (int, float, str): Integer to convert.
format (str): To change the number of decimal or general format of the number
portion.
Returns:
str: Friendly text representation as a string, unless the value passed could not
be coaxed into an `int`.
"""
import math
try:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
value = int(value)
except (TypeError, ValueError):
return str(value)
if value < 0:
value *= -1
negative_prefix = "-"
else:
negative_prefix = ""
if value < powers[0]:
return f"{negative_prefix}{value}"
ordinal = bisect.bisect_right(powers, value)
largest_ordinal = ordinal == len(powers)
# 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
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:
"""Converts an integer to Associated Press style.
Examples:
```pycon
>>> apnumber(0)
'zero'
>>> apnumber(5)
'five'
>>> apnumber(10)
'10'
>>> apnumber("7")
'seven'
>>> apnumber("foo")
'foo'
>>> apnumber(None)
'None'
```
Args:
value (int, float, str): Integer to convert.
Returns:
str: For numbers 0-9, the number spelled out. Otherwise, the number. This always
returns a string unless the value was not `int`-able, then `str(value)`
is returned.
"""
import math
try:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
value = int(value)
except (TypeError, ValueError):
return str(value)
if not 0 <= value < 10:
return str(value)
return _(_APNUMBER_WORDS[value])
def fractional(value: NumberOrString) -> str:
"""Convert to fractional number.
There will be some cases where one might not want to show ugly decimal places for
floats and decimals.
This function returns a human-readable fractional number in form of fractions and
mixed fractions.
Pass in a string, or a number or a float, and this function returns:
* a string representation of a fraction
* or a whole number
* or a mixed fraction
* or the str output of the value, if it could not be converted
Examples:
```pycon
>>> fractional(0.3)
'3/10'
>>> fractional(1.3)
'1 3/10'
>>> fractional(float(1/3))
'1/3'
>>> fractional(1)
'1'
>>> fractional("ten")
'ten'
>>> fractional(None)
'None'
```
Args:
value (int, float, str): Integer to convert.
Returns:
str: Fractional number as a string.
"""
import math
try:
number = float(value)
if not math.isfinite(number):
return _format_not_finite(number)
except (TypeError, ValueError):
return str(value)
from fractions import Fraction
whole_number = int(number)
frac = Fraction(number - whole_number).limit_denominator(1000)
numerator = frac.numerator
denominator = frac.denominator
if whole_number and not numerator and denominator == 1:
# this means that an integer was passed in
# (or variants of that integer like 1.0000)
return f"{whole_number:.0f}"
if not whole_number:
return f"{numerator:.0f}/{denominator:.0f}"
# int() truncates toward zero, so for a negative number both
# whole_number and numerator carry the minus sign, which prints as
# "-1 -3/10". The sign already rides on the whole part; absorb it
# from the fractional part so the result reads as a normal mixed
# fraction.
return f"{whole_number:.0f} {abs(numerator):.0f}/{denominator:.0f}"
def scientific(value: NumberOrString, precision: int = 2) -> str:
"""Return number in string scientific notation z.wq x 10ⁿ.
Examples:
```pycon
>>> scientific(float(0.3))
'3.00 x 10⁻¹'
>>> scientific(int(500))
'5.00 x 10²'
>>> scientific(-1000)
'-1.00 x 10³'
>>> scientific(1000, 1)
'1.0 x 10³'
>>> scientific(1000, 3)
'1.000 x 10³'
>>> scientific("99")
'9.90 x 10¹'
>>> scientific("foo")
'foo'
>>> scientific(None)
'None'
```
Args:
value (int, float, str): Input number.
precision (int): Number of decimal for first part of the number.
Returns:
str: Number in scientific notation z.wq x 10ⁿ.
"""
import math
try:
value = float(value)
if not math.isfinite(value):
return _format_not_finite(value)
except (ValueError, TypeError):
return str(value)
fmt = f"{{:.{int(precision)}e}}"
n = fmt.format(value)
part1, part2 = n.split("e")
# Normalise exponent: int() strips the "+" sign and leading zeros,
# while preserving "-" and a single "0" for 10⁰.
return part1 + " x 10" + str(int(part2)).translate(_SUPERSCRIPT_TRANS)
def clamp(
value: float,
format: str = "{:}",
floor: float | None = None,
ceil: float | None = None,
floor_token: str = "<",
ceil_token: str = ">",
) -> str:
"""Returns number with the specified format, clamped between floor and ceil.
If the number is larger than ceil or smaller than floor, then the respective limit
will be returned, formatted and prepended with a token specifying as such.
Examples:
```pycon
>>> clamp(123.456)
'123.456'
>>> clamp(0.0001, floor=0.01)
'<0.01'
>>> clamp(0.99, format="{:.0%}", ceil=0.99)
'99%'
>>> clamp(0.999, format="{:.0%}", ceil=0.99)
'>99%'
>>> clamp(1, format=intword, floor=1e6, floor_token="under ")
'under 1.0 million'
>>> clamp(None) is None
True
```
Args:
value (int, float): Input number.
format (str OR callable): Can either be a formatting string, or a callable
function that receives value and returns a string.
floor (int, float): Smallest value before clamping.
ceil (int, float): Largest value before clamping.
floor_token (str): If value is smaller than floor, token will be prepended
to output.
ceil_token (str): If value is larger than ceil, token will be prepended
to output.
Returns:
str: Formatted number. The output is clamped between the indicated floor and
ceil. If the number is larger than ceil or smaller than floor, the output
will be prepended with a token indicating as such.
"""
import math
if value is None:
return None
if not math.isfinite(value):
return _format_not_finite(value)
if floor is not None and value < floor:
value = floor
token = floor_token
elif ceil is not None and value > ceil:
value = ceil
token = ceil_token
else:
token = ""
if isinstance(format, str):
return token + format.format(value)
if callable(format):
return token + format(value)
msg = (
"Invalid format. Must be either a valid formatting string, or a function "
"that accepts value and returns a string."
)
raise ValueError(msg)
def metric(value: float, unit: str = "", precision: int = 3) -> str:
"""Return a value with a metric SI unit-prefix appended.
Examples:
```pycon
>>> metric(1500, "V")
'1.50 kV'
>>> metric(2e8, "W")
'200 MW'
>>> metric(220e-6, "F")
'220 μF'
>>> metric(1e-14, precision=4)
'10.00 f'
```
The unit prefix is always chosen so that non-significant zero digits are required.
i.e. `123,000` will become `123k` instead of `0.123M` and `1,230,000` will become
`1.23M` instead of `1230K`. For numbers that are either too huge or too tiny to
represent without resorting to either leading or trailing zeroes, it falls back to
`scientific()`.
```pycon
>>> metric(1e40)
'1.00 x 10⁴⁰'
```
Args:
value (int, float): Input number.
unit (str): Optional base unit.
precision (int): The number of digits the output should contain.
Returns:
str:
"""
import math
if not math.isfinite(value):
return _format_not_finite(value)
exponent = int(math.floor(math.log10(abs(value)))) if value != 0 else 0
if exponent >= 33 or exponent < -30:
return scientific(value, precision - 1) + unit
old_bucket = exponent // 3 * 3
value /= 10**old_bucket
digits = int(max(0, precision - exponent % 3 - 1))
if exponent < 30 and round(abs(value), digits) >= 1000:
exponent += 3 - exponent % 3
new_bucket = exponent // 3 * 3
value /= 10 ** (new_bucket - old_bucket)
digits = int(max(0, precision - exponent % 3 - 1))
if exponent >= 3:
ordinal_ = "kMGTPEZYRQ"[exponent // 3 - 1]
elif exponent < 0:
ordinal_ = "mμnpfazyrq"[(-exponent - 1) // 3]
else:
ordinal_ = ""
value_ = format(value, f".{digits}f")
if not (unit or ordinal_) or unit in ("°", "′", "″"):
space = ""
else:
space = " "
return f"{value_}{space}{ordinal_}{unit}"
humanize-4.16.0/src/humanize/py.typed 0000644 0000000 0000000 00000000000 15220765405 014452 0 ustar 00 humanize-4.16.0/src/humanize/time.py 0000644 0000000 0000000 00000051306 15220765405 014302 0 ustar 00 """Time humanizing functions.
These are largely borrowed from Django's `contrib.humanize`.
"""
from __future__ import annotations
__lazy_modules__ = {"humanize.i18n", "humanize.number"}
from enum import Enum
from functools import total_ordering
from .i18n import _gettext as _
from .i18n import _ngettext
from .number import intcomma
TYPE_CHECKING = False
if TYPE_CHECKING:
import datetime as dt
from collections.abc import Iterable
from typing import Any
__all__ = [
"naturaldate",
"naturalday",
"naturaldelta",
"naturaltime",
"precisedelta",
]
@total_ordering
class Unit(Enum):
MICROSECONDS = 0
MILLISECONDS = 1
SECONDS = 2
MINUTES = 3
HOURS = 4
DAYS = 5
MONTHS = 6
YEARS = 7
def __lt__(self, other: Any) -> Any:
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
def _now() -> dt.datetime:
import datetime as dt
return dt.datetime.now()
def _abs_timedelta(delta: dt.timedelta) -> dt.timedelta:
"""Return an "absolute" value for a timedelta, always representing a time distance.
Args:
delta (datetime.timedelta): Input timedelta.
Returns:
datetime.timedelta: Absolute timedelta.
"""
if delta.days < 0:
now = _now()
return now - (now + delta)
return delta
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)`.
"""
import datetime as dt
if not now:
now = _now()
if isinstance(value, dt.datetime):
date = value
delta = now - value
elif isinstance(value, dt.timedelta):
date = now - value
delta = value
else:
try:
value = value if precise else round(value)
delta = dt.timedelta(seconds=value)
date = now - delta
except (ValueError, TypeError):
return None, value
return date, _abs_timedelta(delta)
def naturaldelta(
value: dt.timedelta | float,
months: bool = True,
minimum_unit: str = "seconds",
) -> str:
"""Return a natural representation of a timedelta or number of seconds.
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
used for fuzziness between years.
minimum_unit (str): The lowest unit that can be used.
Returns:
str (str or `value`): A natural representation of the amount of time
elapsed unless `value` is not datetime.timedelta or cannot be
converted to int (cannot be float due to 'inf' or 'nan').
In that case, a `value` is returned unchanged.
Raises:
OverflowError: If `value` is too large to convert to datetime.timedelta.
Examples:
Compare two timestamps in a custom local timezone::
```pycon
>>> import datetime as dt
>>> from dateutil.tz import gettz
>>> berlin = gettz("Europe/Berlin")
>>> now = dt.datetime.now(tz=berlin)
>>> later = now + dt.timedelta(minutes=30)
>>> assert naturaldelta(later - now) == "30 minutes"
True
```
"""
import datetime as dt
tmp = Unit[minimum_unit.upper()]
if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
msg = f"Minimum unit '{minimum_unit}' not supported"
raise ValueError(msg)
min_unit = tmp
if isinstance(value, dt.timedelta):
delta = value
else:
try:
int(value) # Explicitly don't support string such as "NaN" or "inf"
value = float(value)
delta = dt.timedelta(seconds=value)
except (ValueError, TypeError):
return str(value)
use_months = months
delta = abs(delta)
years = delta.days // 365
days = delta.days % 365
num_months = round(days / 30.5)
if years == 0 and days < 1:
if delta.seconds == 0:
if min_unit == Unit.MICROSECONDS and delta.microseconds < 1000:
return (
_ngettext("%d microsecond", "%d microseconds", delta.microseconds)
% delta.microseconds
)
if min_unit == Unit.MILLISECONDS or (
min_unit == Unit.MICROSECONDS and 1000 <= delta.microseconds < 1_000_000
):
milliseconds = delta.microseconds / 1000
return (
_ngettext("%d millisecond", "%d milliseconds", int(milliseconds))
% milliseconds
)
return _("a moment")
if delta.seconds == 1:
return _("a second")
if delta.seconds < 60:
return _ngettext("%d second", "%d seconds", delta.seconds) % delta.seconds
if 60 <= delta.seconds < 3600:
minutes = round(delta.seconds / 60)
if minutes == 1:
return _("a minute")
if minutes == 60:
return _("an hour")
return _ngettext("%d minute", "%d minutes", minutes) % minutes
if 3600 <= delta.seconds:
hours = round(delta.seconds / 3600)
if hours == 1:
return _("an hour")
if hours == 24:
return _("a day")
return _ngettext("%d hour", "%d hours", hours) % hours
elif years == 0:
if days == 1:
return _("a day")
if not use_months:
return _ngettext("%d day", "%d days", days) % days
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 num_months == 0 and days == 0:
return _("a year")
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
)
return _ngettext("1 year, %d day", "1 year, %d days", days) % days
return _ngettext("%d year", "%d years", years).replace("%d", "%s") % intcomma(years)
def naturaltime(
value: dt.datetime | dt.timedelta | float,
future: bool = False,
months: bool = True,
minimum_unit: str = "seconds",
when: dt.datetime | None = None,
) -> str:
"""Return a natural representation of a time in a resolution that makes sense.
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.
future (bool): Ignored for `datetime`s and `timedelta`s, where the tense is
always figured out based on the current time. For integers and floats, the
return value will be past tense by default, unless future is `True`.
months (bool): If `True`, then a number of months (based on 30.5 days) will be
used for fuzziness between years.
minimum_unit (str): The lowest unit that can be used.
when (datetime.datetime): Point in time relative to which _value_ is
interpreted. Defaults to the current time in the local timezone.
Returns:
str: A natural representation of the input in a resolution that makes sense.
"""
import datetime as dt
value = _convert_aware_datetime(value)
when = _convert_aware_datetime(when)
now = when or _now()
date, delta = _date_and_delta(value, now=now)
if date is None:
return str(value)
# determine tense by value only if datetime/timedelta were passed
if isinstance(value, (dt.datetime, dt.timedelta)):
future = date > now
ago = _("%s from now") if future else _("%s ago")
delta = naturaldelta(delta, months, minimum_unit)
if delta == _("a moment"):
return _("now")
return str(ago % delta)
def _convert_aware_datetime(
value: dt.datetime | dt.timedelta | float | None,
) -> Any:
"""Convert aware datetime to naive datetime and pass through any other type."""
if value is None:
return None
import datetime as dt
if isinstance(value, dt.datetime) and value.tzinfo is not None:
value = dt.datetime.fromtimestamp(value.timestamp())
return value
def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str:
"""Return a natural day.
For date values that are tomorrow, today or yesterday compared to
present day return representing string. Otherwise, return a string
formatted according to `format`.
"""
import datetime as dt
try:
# When value is a tz-aware datetime, compute "today" in that timezone
# so the comparison uses the correct local date.
if isinstance(value, dt.datetime) and value.tzinfo is not None:
today = dt.datetime.now(value.tzinfo).date()
else:
today = dt.date.today()
value = dt.date(value.year, value.month, value.day)
except AttributeError:
# Passed value wasn't date-ish
return str(value)
except (OverflowError, ValueError):
# Date arguments out of range
return str(value)
delta = value - today
if delta.days == 0:
return _("today")
if delta.days == 1:
return _("tomorrow")
if delta.days == -1:
return _("yesterday")
return value.strftime(format)
def naturaldate(value: dt.date | dt.datetime) -> str:
"""Like `naturalday`, but append a year for dates more than ~five months away."""
import datetime as dt
original_value = value
try:
if isinstance(value, dt.datetime) and value.tzinfo is not None:
today = dt.datetime.now(value.tzinfo).date()
else:
today = dt.date.today()
value = dt.date(value.year, value.month, value.day)
except AttributeError:
# Passed value wasn't date-ish
return str(value)
except (OverflowError, ValueError):
# Date arguments out of range
return str(value)
delta = _abs_timedelta(value - today)
if delta.days >= 5 * 365 / 12:
return naturalday(original_value, "%b %d %Y")
return naturalday(original_value)
def _quotient_and_remainder(
value: float,
divisor: float,
unit: Unit,
minimum_unit: Unit,
suppress: Iterable[Unit],
format: str,
) -> tuple[float, float]:
"""Divide `value` by `divisor`, returning the quotient and remainder.
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, [], "%0.2f")
(1.5, 0)
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.
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS], "%0.2f")
(0, 36)
In other cases, return the quotient and remainder as `divmod` would do it.
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [], "%0.2f")
(1, 12)
"""
if unit == minimum_unit:
return _rounding_by_fmt(format, value / divisor), 0
if unit in suppress:
return 0, value
# 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:
"""Return a minimum unit suitable that is not suppressed.
If not suppressed, return the same unit:
>>> from humanize.time import _suitable_minimum_unit, Unit
>>> _suitable_minimum_unit(Unit.HOURS, []).name
'HOURS'
But if suppressed, find a unit greater than the original one that is not
suppressed:
>>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS]).name
'DAYS'
>>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS, Unit.DAYS]).name
'MONTHS'
"""
if min_unit in suppress:
for unit in Unit:
if unit > min_unit and unit not in suppress:
return unit
msg = "Minimum unit is suppressed and no suitable replacement was found"
raise ValueError(msg)
return min_unit
def _suppress_lower_units(min_unit: Unit, suppress: Iterable[Unit]) -> set[Unit]:
"""Extend suppressed units (if any) with all units lower than the minimum unit.
>>> from humanize.time import _suppress_lower_units, Unit
>>> [x.name for x in sorted(_suppress_lower_units(Unit.SECONDS, [Unit.DAYS]))]
['MICROSECONDS', 'MILLISECONDS', 'DAYS']
"""
suppress = set(suppress)
for unit in Unit:
if unit == min_unit:
break
suppress.add(unit)
return suppress
def precisedelta(
value: dt.timedelta | float | None,
minimum_unit: str = "seconds",
suppress: Iterable[str] = (),
format: str = "%0.2f",
) -> str:
"""Return a precise representation of a timedelta or number of seconds.
```pycon
>>> import datetime as dt
>>> from humanize.time import precisedelta
>>> delta = dt.timedelta(seconds=3633, days=2, microseconds=123000)
>>> precisedelta(delta)
'2 days, 1 hour and 33.12 seconds'
```
A custom `format` can be specified to control how the fractional part
is represented:
```pycon
>>> precisedelta(delta, format="%0.4f")
'2 days, 1 hour and 33.1230 seconds'
```
Instead, the `minimum_unit` can be changed to have a better resolution;
the function will still readjust the unit to use the greatest of the
units that does not lose precision.
For example setting microseconds but still representing the date with milliseconds:
```pycon
>>> precisedelta(delta, minimum_unit="microseconds")
'2 days, 1 hour, 33 seconds and 123 milliseconds'
```
If desired, some units can be suppressed: you will not see them represented and the
time of the other units will be adjusted to keep representing the same timedelta:
```pycon
>>> precisedelta(delta, suppress=['days'])
'49 hours and 33.12 seconds'
```
Note that microseconds precision is lost if the seconds and all
the units below are suppressed:
```pycon
>>> delta = dt.timedelta(seconds=90, microseconds=100)
>>> precisedelta(delta, suppress=['seconds', 'milliseconds', 'microseconds'])
'1.50 minutes'
```
If the delta is too small to be represented with the minimum unit,
a value of zero will be returned:
```pycon
>>> delta = dt.timedelta(seconds=1)
>>> precisedelta(delta, minimum_unit="minutes")
'0.02 minutes'
>>> delta = dt.timedelta(seconds=0.1)
>>> precisedelta(delta, minimum_unit="minutes")
'0 minutes'
```
"""
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 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
# Expand the suppressed units list/set to include all the units
# that are below the minimum unit
suppress_set = _suppress_lower_units(min_unit, suppress_set)
# handy aliases
days = delta.days
secs = delta.seconds
usecs = delta.microseconds
MILLISECONDS = Unit.MILLISECONDS
SECONDS = Unit.SECONDS
MINUTES = Unit.MINUTES
HOURS = Unit.HOURS
DAYS = Unit.DAYS
MONTHS = Unit.MONTHS
YEARS = Unit.YEARS
# Given DAYS compute YEARS and the remainder of DAYS as follows:
# if YEARS is the minimum unit, we cannot use DAYS so
# we will use a float for YEARS and 0 for DAYS:
# years, days = years/days, 0
#
# if YEARS is suppressed, use DAYS:
# years, days = 0, days
#
# otherwise:
# 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, format
)
months, days = _quotient_and_remainder(
days, 30.5, MONTHS, min_unit, suppress_set, format
)
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, format
)
minutes, secs = _quotient_and_remainder(
secs, 60, MINUTES, min_unit, suppress_set, format
)
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, format
)
# 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),
("%d month", "%d months", months),
("%d day", "%d days", days),
("%d hour", "%d hours", hours),
("%d minute", "%d minutes", minutes),
("%d second", "%d seconds", secs),
("%d millisecond", "%d milliseconds", msecs),
("%d microsecond", "%d microseconds", usecs),
]
import math
texts: list[str] = []
for unit, fmt in zip(reversed(Unit), fmts):
singular_txt, plural_txt, fmt_value = fmt
if fmt_value > 0 or (not texts and unit == min_unit):
_fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value)
fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value)
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
texts.append(fmt_txt % fmt_value)
if unit == min_unit:
break
if len(texts) == 1:
return texts[0]
head = ", ".join(texts[:-1])
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
humanize-4.16.0/src/humanize/locale/ar/LC_MESSAGES/humanize.mo 0000644 0000000 0000000 00000012562 15220765405 020576 0 ustar 00 Þ• D < a \ à á ð ? T g | � ” ž
ª ¸
Ä Ò Þ " ý
0 >
J X
d r
~ Œ
˜ ¦
² À
Ì Ú
æ ô ) 0 8 H \ b g
l z Š � £ § » ¿ × ï ! % 7 = C L ^ b l ¹ q E + M q “ ¿ � S W á A 9
W {
C Ó
! * ; C K U � ] } ß ] m y ƒ � ™ ¥ ¯ » Å Ñ Û ç ñ ý
$ / 8
? J Q I Z m ¤
( = 3 G q ¹ m  0 y 9 ³ … ¼ y B m ¼ * m 3 ¡ / ¨
Ø
ã î a ÷
Y d k 5 <