Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom "now" in naturaldelta and naturaltime #144

Merged
merged 3 commits into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repos:
args: ["--py36-plus"]

- repo: https://github.com/psf/black
rev: 19.10b0
rev: 20.8b1
hooks:
- id: black
args: ["--target-version", "py36"]
Expand Down
26 changes: 21 additions & 5 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def date_and_delta(value, *, now=None):
return date, abs_timedelta(delta)


def naturaldelta(value, months=True, minimum_unit="seconds"):
def naturaldelta(value, months=True, minimum_unit="seconds", when=None):
"""Return a natural representation of a timedelta or number of seconds.

This is similar to `naturaltime`, but does not add tense to the result.
Expand All @@ -91,16 +91,30 @@ def naturaldelta(value, months=True, minimum_unit="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.
when (datetime.timedelta): 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 amount of time elapsed.

Examples
Compare two timestamps in a custom local timezone::

from datetime import datetime, timedelta, timezone
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can you change this example to use import datetime as dt and dt.datetime etc?

Rationale: https://adamj.eu/tech/2019/09/12/how-i-import-pythons-datetime-module/

from dateutil.tz import gettz

berlin = gettz("Europe/Berlin")
now = datetime.now(tz=berlin)
later = now + timedelta(minutes=30)

assert naturaldelta(later, when=now) == "30 minutes"
"""
tmp = Unit[minimum_unit.upper()]
if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
raise ValueError(f"Minimum unit '{minimum_unit}' not supported")
minimum_unit = tmp

date, delta = date_and_delta(value)
date, delta = date_and_delta(value, now=when)
if date is None:
return value

Expand Down Expand Up @@ -173,7 +187,7 @@ def naturaldelta(value, months=True, minimum_unit="seconds"):
return ngettext("%d year", "%d years", years) % years


def naturaltime(value, future=False, months=True, minimum_unit="seconds"):
def naturaltime(value, future=False, months=True, minimum_unit="seconds", when=None):
"""Return a natural representation of a time in a resolution that makes sense.

This is more or less compatible with Django's `naturaltime` filter.
Expand All @@ -186,11 +200,13 @@ def naturaltime(value, future=False, months=True, minimum_unit="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.
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.
"""
now = _now()
now = when or _now()
date, delta = date_and_delta(value, now=now)
if date is None:
return value
Expand All @@ -199,7 +215,7 @@ def naturaltime(value, future=False, months=True, minimum_unit="seconds"):
future = date > now

ago = _("%s from now") if future else _("%s ago")
delta = naturaldelta(delta, months, minimum_unit)
delta = naturaldelta(delta, months, minimum_unit, when=when)

if delta == _("a moment"):
return _("now")
Expand Down
33 changes: 33 additions & 0 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

with freeze_time("2020-02-02"):
NOW = dt.datetime.now()
NOW_UTC = dt.datetime.now(tz=dt.timezone.utc)
NOW_UTC_PLUS_01_00 = dt.datetime.now(tz=dt.timezone(offset=dt.timedelta(hours=1)))
TODAY = dt.date.today()
TOMORROW = TODAY + ONE_DAY_DELTA
YESTERDAY = TODAY - ONE_DAY_DELTA
Expand Down Expand Up @@ -331,6 +333,37 @@ def test_naturaldelta_minimum_unit_explicit(minimum_unit, seconds, expected):
assert humanize.naturaldelta(delta, minimum_unit=minimum_unit) == expected


@pytest.mark.parametrize(
"test_input, when, expected",
[
(NOW, NOW, "a moment"),
(NOW_UTC, NOW_UTC, "a moment"),
],
)
def test_naturaldelta_when_explicit(test_input, when, expected):
# Act / Assert
assert humanize.naturaldelta(test_input, when=when) == expected


@pytest.mark.parametrize(
"value, when",
[
(NOW_UTC, None),
(NOW_UTC, NOW),
(NOW_UTC_PLUS_01_00, None),
(NOW_UTC_PLUS_01_00, NOW),
],
)
def test_naturaldelta_when_missing_tzinfo(value, when):
"""Subtraction `when - value` is not defined by the `datetime` module when
either operand has not timezone-info (`tz=None`) and raises a TypeError.
"""

# Act / Assert
with pytest.raises(TypeError):
humanize.naturaldelta(value, when=when)


@pytest.mark.parametrize(
"seconds, expected",
[
Expand Down