Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ repos:
- id: pydocstyle
args: ["--convention", "google"]
files: "src/"

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.782
hooks:
- id: mypy
additional_dependencies: [typing-extensions==3.7.4.3]
13 changes: 10 additions & 3 deletions src/humanize/filesize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

"""Bits and bytes related humanization."""

suffixes = {
import typing

suffixes: typing.Dict[str, typing.Tuple[str, ...]] = {
"decimal": ("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"),
"binary": ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"),
"gnu": "KMGTPEZY",
"gnu": ("K", "M", "G", "T", "P", "E", "Z", "Y"),
}


def naturalsize(value, binary=False, gnu=False, format="%.1f"):
def naturalsize(
value: typing.Union[int, 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.
Expand Down
23 changes: 14 additions & 9 deletions src/humanize/i18n.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Activate, get and deactivate translations."""
import gettext as gettext_module
import os.path
import typing
from threading import local

__all__ = ["activate", "deactivate", "gettext", "ngettext"]

_TRANSLATIONS = {None: gettext_module.NullTranslations()}
_TRANSLATIONS: typing.Dict[typing.Optional[str], gettext_module.NullTranslations] = {
None: gettext_module.NullTranslations()
}
_CURRENT = local()


Expand All @@ -18,14 +21,16 @@ def _get_default_locale_path():
return None


def get_translation():
def get_translation() -> gettext_module.NullTranslations:
try:
return _TRANSLATIONS[_CURRENT.locale]
except (AttributeError, KeyError):
return _TRANSLATIONS[None]


def activate(locale, path=None):
def activate(
locale: str, path: typing.Optional[str] = None
) -> gettext_module.NullTranslations:
"""Activate internationalisation.

Set `locale` as current locale. Search for locale in directory `path`.
Expand Down Expand Up @@ -55,12 +60,12 @@ def activate(locale, path=None):
return _TRANSLATIONS[locale]


def deactivate():
def deactivate() -> None:
"""Deactivate internationalisation."""
_CURRENT.locale = None


def gettext(message):
def gettext(message: str) -> str:
"""Get translation.

Args:
Expand All @@ -72,7 +77,7 @@ def gettext(message):
return get_translation().gettext(message)


def pgettext(msgctxt, message):
def pgettext(msgctxt: str, message: str) -> str:
"""Fetches a particular translation.

It works with `msgctxt` .po modifiers and allows duplicate keys with different
Expand All @@ -97,13 +102,13 @@ def pgettext(msgctxt, message):
return message if translation == key else translation


def ngettext(message, plural, num):
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 (str): The number (e.g. item count) to determine translation for the
num (int): The number (e.g. item count) to determine translation for the
respective grammatical number.

Returns:
Expand All @@ -112,7 +117,7 @@ def ngettext(message, plural, num):
return get_translation().ngettext(message, plural, num)


def gettext_noop(message):
def gettext_noop(message: str) -> str:
"""Mark a string as a translation string without translating it.

Example usage:
Expand Down
71 changes: 40 additions & 31 deletions src/humanize/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
"""Humanizing functions for numbers."""

import re
import typing
from fractions import Fraction

from .i18n import gettext as _
from .i18n import gettext_noop as N_
from .i18n import pgettext as P_

# This type can be better defined by typing.SupportsInt, typing.SupportsFloat
# but that's a Python 3.8 only typing option.
NumberOrString = typing.Union[str, float, int]

def ordinal(value):

def ordinal(value: NumberOrString) -> 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 other value will have nothing
done to it.
anything `int()` will turn into an integer. Anything else will just return
the output of str(value). done to it.

Examples:
```pycon
Expand All @@ -35,7 +40,7 @@ def ordinal(value):
'111th'
>>> ordinal("something else")
'something else'
>>> ordinal(None) is None
>>> ordinal([1, 2, 3]) == "[1, 2, 3]"
True

```
Expand All @@ -48,7 +53,7 @@ def ordinal(value):
try:
value = int(value)
except (TypeError, ValueError):
return value
return str(value)
t = (
P_("0", "th"),
P_("1", "st"),
Expand All @@ -66,7 +71,7 @@ def ordinal(value):
return f"{value}{t[value % 10]}"


def intcomma(value, ndigits=None):
def intcomma(value: NumberOrString, ndigits: typing.Optional[int] = 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
Expand All @@ -86,8 +91,8 @@ def intcomma(value, ndigits=None):
'1,234.55'
>>> intcomma(14308.40, 1)
'14,308.4'
>>> intcomma(None) is None
True
>>> intcomma(None)
'None'

```
Args:
Expand All @@ -103,7 +108,7 @@ def intcomma(value, ndigits=None):
else:
float(value)
except (TypeError, ValueError):
return value
return str(value)

if ndigits:
orig = "{0:.{1}f}".format(value, ndigits)
Expand All @@ -117,8 +122,10 @@ def intcomma(value, ndigits=None):
return intcomma(new)


powers = [10 ** x for x in (6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)]
human_powers = (
powers: typing.List[int] = [
10 ** x for x in (6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)
]
human_powers: typing.Tuple[str, ...] = (
N_("million"),
N_("billion"),
N_("trillion"),
Expand All @@ -133,7 +140,7 @@ def intcomma(value, ndigits=None):
)


def intword(value, format="%.1f"):
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",
Expand All @@ -150,8 +157,8 @@ def intword(value, format="%.1f"):
'1.2 billion'
>>> intword(8100000000000000000000000000000000)
'8.1 decillion'
>>> intword(None) is None
True
>>> intword(None)
'None'
>>> intword("1234000", "%0.3f")
'1.234 million'

Expand All @@ -163,12 +170,12 @@ def intword(value, format="%.1f"):

Returns:
str: Friendly text representation as a string, unless the value passed could not
be coaxed into an `int`.
be coaxed into an `int`, in which case, `str(value)` is returned.
"""
try:
value = int(value)
except (TypeError, ValueError):
return value
return str(value)

if value < powers[0]:
return str(value)
Expand All @@ -183,7 +190,7 @@ def intword(value, format="%.1f"):
return str(value)


def apnumber(value):
def apnumber(value: NumberOrString) -> str:
"""Converts an integer to Associated Press style.

Examples:
Expand All @@ -198,21 +205,22 @@ def apnumber(value):
'seven'
>>> apnumber("foo")
'foo'
>>> apnumber(None) is None
True
>>> 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, unlike the Django filter.
returns a string. If the value was not `int`-able, then `str(value)`
is returned.
"""
try:
value = int(value)
except (TypeError, ValueError):
return value
return str(value)
if not 0 <= value < 10:
return str(value)
return (
Expand All @@ -229,7 +237,7 @@ def apnumber(value):
)[value]


def fractional(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
Expand All @@ -243,6 +251,7 @@ def fractional(value):
* 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
Expand All @@ -256,8 +265,8 @@ def fractional(value):
'1'
>>> fractional("ten")
'ten'
>>> fractional(None) is None
True
>>> fractional(None)
'None'

```
Args:
Expand All @@ -269,11 +278,11 @@ def fractional(value):
try:
number = float(value)
except (TypeError, ValueError):
return value
return str(value)
whole_number = int(number)
frac = Fraction(number - whole_number).limit_denominator(1000)
numerator = frac._numerator
denominator = frac._denominator
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)
Expand All @@ -284,7 +293,7 @@ def fractional(value):
return f"{whole_number:.0f} {numerator:.0f}/{denominator:.0f}"


def scientific(value, precision=2):
def scientific(value: NumberOrString, precision: int = 2) -> str:
"""Return number in string scientific notation z.wq x 10ⁿ.

Examples:
Expand All @@ -303,8 +312,8 @@ def scientific(value, precision=2):
'9.90 x 10¹'
>>> scientific("foo")
'foo'
>>> scientific(None) is None
True
>>> scientific(None)
'None'

```

Expand Down Expand Up @@ -342,7 +351,7 @@ def scientific(value, precision=2):
n = fmt.format(value)

except (ValueError, TypeError):
return value
return str(value)

part1, part2 = n.split("e")
if "-0" in part2:
Expand Down
Empty file added src/humanize/py.typed
Empty file.
Loading