Skip to content
Open
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
38 changes: 33 additions & 5 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ def _date_and_delta(
return date, _abs_timedelta(delta)


def _minimum_unit_or_raise(name: str) -> Unit:
"""Resolve *name* to a ``Unit`` or raise a clear ``ValueError``.

A bare ``Unit[name.upper()]`` lookup raises an opaque ``KeyError`` for an
unknown unit name; this helper raises a consistent, helpful ``ValueError``
instead.
"""
try:
return Unit[name.upper()]
except KeyError:
msg = f"Minimum unit '{name}' not supported"
raise ValueError(msg) from None


def naturaldelta(
value: dt.timedelta | float,
months: bool = True,
Expand All @@ -119,6 +133,7 @@ def naturaldelta(

Raises:
OverflowError: If `value` is too large to convert to datetime.timedelta.
ValueError: If `minimum_unit` is not a supported unit.

Examples:
Compare two timestamps in a custom local timezone::
Expand All @@ -138,11 +153,10 @@ def naturaldelta(
"""
import datetime as dt

tmp = Unit[minimum_unit.upper()]
if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
min_unit = _minimum_unit_or_raise(minimum_unit)
if min_unit 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
Expand Down Expand Up @@ -275,6 +289,9 @@ def naturaltime(

Returns:
str: A natural representation of the input in a resolution that makes sense.

Raises:
ValueError: If `minimum_unit` is not a supported unit.
"""
import datetime as dt

Expand Down Expand Up @@ -534,17 +551,28 @@ def precisedelta(
>>> precisedelta(delta, minimum_unit="minutes")
'0 minutes'

```

An unsupported ``minimum_unit`` raises a clear ``ValueError`` rather than
an opaque ``KeyError``:

```pycon
>>> precisedelta(dt.timedelta(seconds=1), minimum_unit="fortnights")
Traceback (most recent call last):
...
ValueError: Minimum unit 'fortnights' not supported

```
"""
date, delta = _date_and_delta(value, precise=True)
if date is None:
return str(value)

suppress_set = {Unit[s.upper()] for s in suppress}
suppress_set = {_minimum_unit_or_raise(s) 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 = _minimum_unit_or_raise(minimum_unit)
min_unit = _suitable_minimum_unit(min_unit, suppress_set)
del minimum_unit

Expand Down
30 changes: 30 additions & 0 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,36 @@ def test_precisedelta_bogus_call() -> None:
humanize.naturaldelta(1, minimum_unit="years")


@pytest.mark.parametrize(
"func",
[humanize.precisedelta, humanize.naturaldelta, humanize.naturaltime],
)
@pytest.mark.parametrize("minimum_unit", ["weeks", "fortnights", "nanoseconds"])
def test_minimum_unit_unknown_raises_valueerror(
func: typing.Callable[..., str], minimum_unit: str
) -> None:
# Units that are not part of ``humanize.time.Unit`` used to raise a raw
# ``KeyError``. They must raise a clear ``ValueError`` instead, consistent
# with the message used for enum members that are not allowed as a minimum
# unit (e.g. ``years`` for ``naturaldelta``).
with pytest.raises(
ValueError, match=rf"^Minimum unit '{minimum_unit}' not supported$"
):
func(dt.timedelta(seconds=1), minimum_unit=minimum_unit)


@pytest.mark.parametrize("suppress_unit", ["weeks", "fortnights"])
def test_suppress_unknown_unit_raises_valueerror(suppress_unit: str) -> None:
# An unknown unit passed to ``suppress`` must also raise a clear
# ``ValueError`` rather than an opaque ``KeyError``.
with pytest.raises(
ValueError, match=rf"^Minimum unit '{suppress_unit}' not supported$"
):
humanize.precisedelta(
dt.timedelta(seconds=1), minimum_unit="seconds", suppress=[suppress_unit]
)


def test_time_unit() -> None:
years, minutes = time.Unit["YEARS"], time.Unit["MINUTES"]
assert minutes < years
Expand Down