humanize-4.12.1/.coveragerc 0000644 0000000 0000000 00000000273 14755134763 012507 0 ustar 00 # .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:
humanize-4.12.1/.pre-commit-config.yaml 0000644 0000000 0000000 00000003525 14755134763 014652 0 ustar 00 repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.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.31.1
hooks:
- id: check-github-workflows
- id: check-renovate
- repo: https://github.com/rhysd/actionlint
rev: v1.7.7
hooks:
- id: actionlint
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.3.1
hooks:
- id: zizmor
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23
hooks:
- id: validate-pyproject
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0
hooks:
- id: tox-ini-fmt
- repo: https://github.com/google/yamlfmt
rev: v0.16.0
hooks:
- id: yamlfmt
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.5.0
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.12.1/.readthedocs.yml 0000644 0000000 0000000 00000000607 14755134763 013455 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.12.1/.yamlfmt.yaml 0000644 0000000 0000000 00000000055 14755134763 012777 0 ustar 00 formatter:
retain_line_breaks_single: true
humanize-4.12.1/LICENCE 0000644 0000000 0000000 00000002066 14755134763 011355 0 ustar 00 Copyright (c) 2010-2020 Jason Moiron and Contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
humanize-4.12.1/RELEASING.md 0000644 0000000 0000000 00000001612 14755134763 012217 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.12.1/mkdocs.yml 0000644 0000000 0000000 00000001533 14755134763 012371 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.12.1/requirements-mypy.txt 0000644 0000000 0000000 00000000065 14755134763 014645 0 ustar 00 mypy==1.15.0
pytest
types-freezegun
types-setuptools
humanize-4.12.1/tox.ini 0000644 0000000 0000000 00000001353 14755134763 011701 0 ustar 00 [tox]
requires =
tox>=4.2
env_list =
docs
lint
mypy
py{py3, 314, 313, 312, 311, 310, 39}
[testenv]
extras =
tests
pass_env =
FORCE_COLOR
set_env =
COVERAGE_CORE = sysmon
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 =
pre-commit
pass_env =
PRE_COMMIT_COLOR
commands =
pre-commit 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.12.1/.github/CONTRIBUTING.md 0000644 0000000 0000000 00000001002 14755134763 014146 0 ustar 00 # Contributing
## Linting
Linting is run on the CI using [pre-commit](https://pre-commit.com/), and can be run
locally:
```sh
pip install pre-commit
pre-commit install # optional: to run when you commit, on just the staged changes
pre-commit run --all-files # to run on all files now
```
## 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.12.1/.github/FUNDING.yml 0000644 0000000 0000000 00000000051 14755134763 013535 0 ustar 00 github: hugovk
tidelift: "pypi/humanize"
humanize-4.12.1/.github/ISSUE_TEMPLATE.md 0000644 0000000 0000000 00000000672 14755134763 014436 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.12.1/.github/PULL_REQUEST_TEMPLATE.md 0000644 0000000 0000000 00000000072 14755134763 015524 0 ustar 00 Fixes #
Changes proposed in this pull request:
*
*
*
humanize-4.12.1/.github/SECURITY.md 0000644 0000000 0000000 00000000220 14755134763 013507 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.12.1/.github/labels.yml 0000644 0000000 0000000 00000003636 14755134763 013721 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.12.1/.github/release-drafter.yml 0000644 0000000 0000000 00000001574 14755134763 015523 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.12.1/.github/renovate.json 0000644 0000000 0000000 00000000741 14755134763 014444 0 ustar 00 {
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":semanticCommitsDisabled"],
"labels": ["changelog: skip", "dependencies"],
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": ["github-actions"],
"separateMajorMinor": "false"
},
{
"groupName": "docs/requirements.txt",
"matchPaths": ["docs/requirements.txt"]
}
],
"schedule": ["on the first day of the month"]
}
humanize-4.12.1/.github/workflows/docs.yml 0000644 0000000 0000000 00000000746 14755134763 015443 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@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Docs
run: |
uvx --with tox-uv tox -e docs
humanize-4.12.1/.github/workflows/labels.yml 0000644 0000000 0000000 00000000677 14755134763 015760 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-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: micnncim/action-label-syncer@v1
with:
prune: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
humanize-4.12.1/.github/workflows/lint.yml 0000644 0000000 0000000 00000001336 14755134763 015455 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@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: tox-dev/action-pre-commit-uv@v1
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Mypy
run: uvx --with tox-uv tox -e mypy
humanize-4.12.1/.github/workflows/release-drafter.yml 0000644 0000000 0000000 00000002046 14755134763 017553 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.repository_owner == 'python-humanize'
permissions:
# write permission is required to create a GitHub Release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
humanize-4.12.1/.github/workflows/release.yml 0000644 0000000 0000000 00000003676 14755134763 016140 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@v4
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@v2
# Upload to Test PyPI on every commit on main.
release-test-pypi:
name: Publish in-dev package to test.pypi.org
if: |
github.repository_owner == 'python-humanize'
&& 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@v4
with:
name: Packages
path: dist
- name: Upload package to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
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.repository_owner == 'python-humanize'
&& 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@v4
with:
name: Packages
path: dist
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
humanize-4.12.1/.github/workflows/require-pr-label.yml 0000644 0000000 0000000 00000001073 14755134763 017655 0 ustar 00 name: Require PR label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: mheap/github-action-required-labels@v5
with:
mode: minimum
count: 1
labels: |
changelog: Added
changelog: Changed
changelog: Deprecated
changelog: Fixed
changelog: Removed
changelog: Security
changelog: skip
humanize-4.12.1/.github/workflows/test.yml 0000644 0000000 0000000 00000002714 14755134763 015467 0 ustar 00 name: Test
on: [push, pull_request, workflow_dispatch]
permissions: {}
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.11", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- 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@v5
- name: Generate translation binaries
run: |
scripts/generate-translation-binaries.sh
- name: Tox tests
run: |
uvx --with tox-uv tox -e py
- name: Upload coverage
uses: codecov/codecov-action@v5
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.12.1/docs/filesize.md 0000644 0000000 0000000 00000000042 14755134763 013444 0 ustar 00 # Filesize
::: humanize.filesize
humanize-4.12.1/docs/i18n.md 0000644 0000000 0000000 00000000052 14755134763 012412 0 ustar 00 # Internationalisation
::: humanize.i18n
humanize-4.12.1/docs/index.md 0000644 0000000 0000000 00000000424 14755134763 012745 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.12.1/docs/lists.md 0000644 0000000 0000000 00000000034 14755134763 012771 0 ustar 00 # Lists
::: humanize.lists
humanize-4.12.1/docs/number.md 0000644 0000000 0000000 00000000036 14755134763 013125 0 ustar 00 # Number
::: humanize.number
humanize-4.12.1/docs/requirements.txt 0000644 0000000 0000000 00000000177 14755134763 014605 0 ustar 00 mkdocs==1.6.1
mkdocs-include-markdown-plugin
mkdocs-material
mkdocstrings[python]==0.27.0
pygments
pymdown-extensions==10.14.3
humanize-4.12.1/docs/time.md 0000644 0000000 0000000 00000000032 14755134763 012567 0 ustar 00 # Time
::: humanize.time
humanize-4.12.1/docs/css/code_select.css 0000644 0000000 0000000 00000000375 14755134763 015074 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.12.1/scripts/generate-translation-binaries.sh 0000755 0000000 0000000 00000000314 14755134763 020330 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.12.1/scripts/update-translations.sh 0000755 0000000 0000000 00000000721 14755134763 016413 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.12.1/src/humanize/__init__.py 0000644 0000000 0000000 00000001617 14755134763 015111 0 ustar 00 """Main package for humanize."""
from __future__ import annotations
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.12.1/src/humanize/_version.py 0000644 0000000 0000000 00000000635 14755134763 015175 0 ustar 00 # file generated by setuptools_scm
# don't change, don't track in version control
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Tuple, Union
VERSION_TUPLE = Tuple[Union[int, str], ...]
else:
VERSION_TUPLE = object
version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
__version__ = version = '4.12.1'
__version_tuple__ = version_tuple = (4, 12, 1)
humanize-4.12.1/src/humanize/filesize.py 0000644 0000000 0000000 00000004574 14755134763 015171 0 ustar 00 """Bits and bytes related humanization."""
from __future__ import annotations
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
if isinstance(value, str):
bytes_ = float(value)
else:
bytes_ = value
abs_bytes = abs(bytes_)
if abs_bytes == 1 and not gnu:
return f"{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
return ret
humanize-4.12.1/src/humanize/i18n.py 0000644 0000000 0000000 00000012024 14755134763 014123 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 = {
"de_DE": ".",
"fr_FR": " ",
"it_IT": ".",
"pt_BR": ".",
"hu_HU": " ",
}
# Mapping of locale to decimal separator
_DECIMAL_SEPARATOR = {
"de_DE": ",",
"it_IT": ",",
"pt_BR": ",",
"hu_HU": ",",
}
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:
try:
return _TRANSLATIONS[_CURRENT.locale]
except (AttributeError, KeyError):
return _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
transaltion. Similar to calling ``deactivate()``.
path (str | pathlib.Path): Path to search for locales.
Returns:
dict: Translations.
Raises:
Exception: 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 Exception(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.
"""
try:
sep = _THOUSANDS_SEPARATOR[_CURRENT.locale]
except (AttributeError, KeyError):
sep = ","
return sep
def decimal_separator() -> str:
"""Return the decimal separator for a locale, default to dot.
Returns:
str: Decimal separator.
"""
try:
sep = _DECIMAL_SEPARATOR[_CURRENT.locale]
except (AttributeError, KeyError):
sep = "."
return sep
humanize-4.12.1/src/humanize/lists.py 0000644 0000000 0000000 00000001571 14755134763 014507 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 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.12.1/src/humanize/number.py 0000644 0000000 0000000 00000040007 14755134763 014636 0 ustar 00 """Humanizing functions for numbers."""
from __future__ import annotations
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:
import sys
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions 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
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)
if gender == "male":
t = (
P_("0 (male)", "th"),
P_("1 (male)", "st"),
P_("2 (male)", "nd"),
P_("3 (male)", "rd"),
P_("4 (male)", "th"),
P_("5 (male)", "th"),
P_("6 (male)", "th"),
P_("7 (male)", "th"),
P_("8 (male)", "th"),
P_("9 (male)", "th"),
)
else:
t = (
P_("0 (female)", "th"),
P_("1 (female)", "st"),
P_("2 (female)", "nd"),
P_("3 (female)", "rd"),
P_("4 (female)", "th"),
P_("5 (female)", "th"),
P_("6 (female)", "th"),
P_("7 (female)", "th"),
P_("8 (female)", "th"),
P_("9 (female)", "th"),
)
if value % 100 in (11, 12, 13): # special case
return f"{value}{t[0]}"
return f"{value}{t[value % 10]}"
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:
orig = "{0:.{1}f}".format(value, ndigits)
else:
orig = str(value)
orig = orig.replace(".", decimal_sep)
import re
while True:
new = re.sub(r"^(-?\d+)(\d{3})", rf"\g<1>{thousands_sep}\g<2>", orig)
if orig == new:
return new
orig = new
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",
1200000 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 negative_prefix + str(value)
for ordinal_, power in enumerate(powers[1:], 1):
if value < power:
chopped = value / float(powers[ordinal_ - 1])
powers_difference = powers[ordinal_] / powers[ordinal_ - 1]
if float(format % chopped) == powers_difference:
chopped = value / float(powers[ordinal_])
singular, plural = human_powers[ordinal_]
return (
negative_prefix
+ " ".join(
[format, _ngettext(singular, plural, math.ceil(chopped))]
)
) % chopped
singular, plural = human_powers[ordinal_ - 1]
return (
negative_prefix
+ " ".join([format, _ngettext(singular, plural, math.ceil(chopped))])
) % chopped
return negative_prefix + str(value)
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 (
_("zero"),
_("one"),
_("two"),
_("three"),
_("four"),
_("five"),
_("six"),
_("seven"),
_("eight"),
_("nine"),
)[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}"
return f"{whole_number:.0f} {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
exponents = {
"0": "⁰",
"1": "¹",
"2": "²",
"3": "³",
"4": "⁴",
"5": "⁵",
"6": "⁶",
"7": "⁷",
"8": "⁸",
"9": "⁹",
"-": "⁻",
}
try:
value = float(value)
if not math.isfinite(value):
return _format_not_finite(value)
except (ValueError, TypeError):
return str(value)
fmt = f"{{:.{str(int(precision))}e}}"
n = fmt.format(value)
part1, part2 = n.split("e")
# Remove redundant leading '+' or '0's (preserving the last '0' for 10⁰).
import re
part2 = re.sub(r"^\+?(\-?)0*(.+)$", r"\1\2", part2)
new_part2 = []
for char in part2:
new_part2.append(exponents[char])
final_str = part1 + " x 10" + "".join(new_part2)
return final_str
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
value /= 10 ** (exponent // 3 * 3)
if exponent >= 3:
ordinal_ = "kMGTPEZYRQ"[exponent // 3 - 1]
elif exponent < 0:
ordinal_ = "mμnpfazyrq"[(-exponent - 1) // 3]
else:
ordinal_ = ""
value_ = format(value, f".{int(max(0, precision - exponent % 3 - 1))}f")
if not (unit or ordinal_) or unit in ("°", "′", "″"):
space = ""
else:
space = " "
return f"{value_}{space}{ordinal_}{unit}"
humanize-4.12.1/src/humanize/py.typed 0000644 0000000 0000000 00000000000 14755134763 014460 0 ustar 00 humanize-4.12.1/src/humanize/time.py 0000644 0000000 0000000 00000045217 14755134763 014314 0 ustar 00 """Time humanizing functions.
These are largely borrowed from Django's `contrib.humanize`.
"""
from __future__ import annotations
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) -> 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 = int(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.
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::
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"
"""
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 = int(days // 30.5)
if not years 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 < 120:
return _("a minute")
if 120 <= delta.seconds < 3600:
minutes = delta.seconds // 60
return _ngettext("%d minute", "%d minutes", minutes) % minutes
if 3600 <= delta.seconds < 3600 * 2:
return _("an hour")
if 3600 < delta.seconds:
hours = delta.seconds // 3600
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 not num_months:
return _ngettext("%d day", "%d days", days) % days
if num_months == 1:
return _("a month")
return _ngettext("%d month", "%d months", num_months) % num_months
elif years == 1:
if not num_months and not days:
return _("a year")
if not num_months:
return _ngettext("1 year, %d day", "1 year, %d days", days) % days
if use_months:
if num_months == 1:
return _("1 year, 1 month")
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.
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."""
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:
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 - dt.date.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
try:
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 - dt.date.today())
if delta.days >= 5 * 365 / 12:
return naturalday(value, "%b %d %Y")
return naturalday(value)
def _quotient_and_remainder(
value: float,
divisor: float,
unit: Unit,
minimum_unit: Unit,
suppress: Iterable[Unit],
) -> tuple[float, float]:
"""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`.
>>> from humanize.time import _quotient_and_remainder, Unit
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [])
(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, 36)
In other case return quotient and remainder as `divmod` would do it.
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [])
(1, 12)
"""
if unit == minimum_unit:
return 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
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 | int | None,
minimum_unit: str = "seconds",
suppress: Iterable[str] = (),
format: str = "%0.2f",
) -> str:
"""Return a precise representation of a timedelta.
```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)
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).
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
MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS, MONTHS, YEARS = list(
Unit
)
# 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)
months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, suppress_set)
# 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)
hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, suppress_set)
minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, suppress_set)
secs, usecs = _carry(secs, usecs, 1e6, SECONDS, min_unit, suppress_set)
msecs, usecs = _quotient_and_remainder(
usecs, 1000, MILLISECONDS, min_unit, suppress_set
)
# if _unused != 0 we had lost some precision
usecs, _unused = _carry(usecs, 0, 1, MICROSECONDS, min_unit, suppress_set)
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),
]
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)
import math
if unit == min_unit and math.modf(fmt_value)[0] > 0:
fmt_txt = fmt_txt.replace("%d", format)
elif unit == YEARS:
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)
humanize-4.12.1/src/humanize/locale/ar/LC_MESSAGES/humanize.mo 0000644 0000000 0000000 00000007622 14755134763 020605 0 ustar 00 D < a \ ? T g |
"
0 >
J X
d r
~
) 0 8 H \ b g
l z ! % 7 = C L ^ b l m q
C ? S
" ) 2 - 9 - g
(
1
<
E
L
W
`
g
r
y
! 4 ) = g - p - % % ! (
8
C N ! W
y 5 <