Skip to content

Commit 4a58ec4

Browse files
fix(time): raise ValueError instead of KeyError for unsupported minimum_unit
1 parent ce4147b commit 4a58ec4

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

src/humanize/time.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ def _date_and_delta(
9494
return date, _abs_timedelta(delta)
9595

9696

97+
def _minimum_unit_or_raise(name: str) -> Unit:
98+
"""Resolve *name* to a ``Unit`` or raise a clear ``ValueError``.
99+
100+
A bare ``Unit[name.upper()]`` lookup raises an opaque ``KeyError`` for an
101+
unknown unit name; this helper raises a consistent, helpful ``ValueError``
102+
instead.
103+
"""
104+
try:
105+
return Unit[name.upper()]
106+
except KeyError:
107+
msg = f"Minimum unit '{name}' not supported"
108+
raise ValueError(msg) from None
109+
110+
97111
def naturaldelta(
98112
value: dt.timedelta | float,
99113
months: bool = True,
@@ -119,6 +133,7 @@ def naturaldelta(
119133
120134
Raises:
121135
OverflowError: If `value` is too large to convert to datetime.timedelta.
136+
ValueError: If `minimum_unit` is not a supported unit.
122137
123138
Examples:
124139
Compare two timestamps in a custom local timezone::
@@ -138,11 +153,10 @@ def naturaldelta(
138153
"""
139154
import datetime as dt
140155

141-
tmp = Unit[minimum_unit.upper()]
142-
if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
156+
min_unit = _minimum_unit_or_raise(minimum_unit)
157+
if min_unit not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
143158
msg = f"Minimum unit '{minimum_unit}' not supported"
144159
raise ValueError(msg)
145-
min_unit = tmp
146160

147161
if isinstance(value, dt.timedelta):
148162
delta = value
@@ -275,6 +289,9 @@ def naturaltime(
275289
276290
Returns:
277291
str: A natural representation of the input in a resolution that makes sense.
292+
293+
Raises:
294+
ValueError: If `minimum_unit` is not a supported unit.
278295
"""
279296
import datetime as dt
280297

@@ -534,17 +551,28 @@ def precisedelta(
534551
>>> precisedelta(delta, minimum_unit="minutes")
535552
'0 minutes'
536553
554+
```
555+
556+
An unsupported ``minimum_unit`` raises a clear ``ValueError`` rather than
557+
an opaque ``KeyError``:
558+
559+
```pycon
560+
>>> precisedelta(dt.timedelta(seconds=1), minimum_unit="fortnights")
561+
Traceback (most recent call last):
562+
...
563+
ValueError: Minimum unit 'fortnights' not supported
564+
537565
```
538566
"""
539567
date, delta = _date_and_delta(value, precise=True)
540568
if date is None:
541569
return str(value)
542570

543-
suppress_set = {Unit[s.upper()] for s in suppress}
571+
suppress_set = {_minimum_unit_or_raise(s) for s in suppress}
544572

545573
# Find a suitable minimum unit (it can be greater than the one that the
546574
# user gave us, if that one is suppressed).
547-
min_unit = Unit[minimum_unit.upper()]
575+
min_unit = _minimum_unit_or_raise(minimum_unit)
548576
min_unit = _suitable_minimum_unit(min_unit, suppress_set)
549577
del minimum_unit
550578

tests/test_time.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,36 @@ def test_precisedelta_bogus_call() -> None:
827827
with pytest.raises(ValueError, match="Minimum unit 'years' not supported"):
828828
humanize.naturaldelta(1, minimum_unit="years")
829829

830+
@pytest.mark.parametrize(
831+
"func",
832+
[humanize.precisedelta, humanize.naturaldelta, humanize.naturaltime],
833+
)
834+
@pytest.mark.parametrize("minimum_unit", ["weeks", "fortnights", "nanoseconds"])
835+
def test_minimum_unit_unknown_raises_valueerror(
836+
func: typing.Callable[..., str], minimum_unit: str
837+
) -> None:
838+
# Units that are not part of ``humanize.time.Unit`` used to raise a raw
839+
# ``KeyError``. They must raise a clear ``ValueError`` instead, consistent
840+
# with the message used for enum members that are not allowed as a minimum
841+
# unit (e.g. ``years`` for ``naturaldelta``).
842+
with pytest.raises(
843+
ValueError, match=rf"^Minimum unit '{minimum_unit}' not supported$"
844+
):
845+
func(dt.timedelta(seconds=1), minimum_unit=minimum_unit)
846+
847+
848+
@pytest.mark.parametrize("suppress_unit", ["weeks", "fortnights"])
849+
def test_suppress_unknown_unit_raises_valueerror(suppress_unit: str) -> None:
850+
# An unknown unit passed to ``suppress`` must also raise a clear
851+
# ``ValueError`` rather than an opaque ``KeyError``.
852+
with pytest.raises(
853+
ValueError, match=rf"^Minimum unit '{suppress_unit}' not supported$"
854+
):
855+
humanize.precisedelta(
856+
dt.timedelta(seconds=1), minimum_unit="seconds", suppress=[suppress_unit]
857+
)
858+
859+
830860

831861
def test_time_unit() -> None:
832862
years, minutes = time.Unit["YEARS"], time.Unit["MINUTES"]

0 commit comments

Comments
 (0)