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

Works with naïve datetimes #9

Closed
merwok opened this issue May 21, 2013 · 4 comments
Closed

Works with naïve datetimes #9

merwok opened this issue May 21, 2013 · 4 comments

Comments

@merwok
Copy link

merwok commented May 21, 2013

Hi! I’m not currently a user of the library but it looks interesting. I noticed that functions use datetime.now() as basis for relative times and such, which can easily lead to issues. I tend to try hard to avoid timezone-less dates and datetimes, and always work in UTC (just like one wants to work with unicode objects instead of byte strings). I would suggest that humanize should reject naïve date(time)s, or (less drastic) that functions that work with date use the argument’s timezone in the _now function.

@jmoiron
Copy link
Owner

jmoiron commented May 21, 2013

I'm going to ask some questions which will sound like snark, but to which I either don't know or am not sure of the answers:

Is the delta in error because of timezone issues? Are we comparing a timezone datetime against a naive one and then returning the wrong delta because of it? If so, this is certainly a bug that must be fixed, as this is unexpected output. If I take a timestamp in PDT and am running on a box in EDT, I do not expect a return of "3 hours ago".

What would also be unexpected, I'd argue, is the library refusing to use naive datetimes. This isn't the place to try to force the best practices for your program upon other peoples programs, and there are many instances where there's no need to deal with the mess that is timezones.

@youngrok
Copy link
Contributor

I think what @merwok said is this error:

TypeError: can't subtract offset-naive and offset-aware datetimes

datetime.now() is naive, so when I give timezone-aware datetime to humanize.naturaltime, the error above occurred. It doesn't return wrong delta, but it stops with error.

I'm not sure forcing timezone-aware datetime, but more frameworks use timezone-aware datetime now, for example, django. Either forcing timezone-aware datetime or adding support for that, I also want this issue solved.

@posita
Copy link

posita commented May 12, 2015

UPDATE: I should have checked PRs first. #22 provides an alternate solution. Also, if I recall, dateutil suffers from some problems with daylight savings calculations that pytz doesn't, but I don't remember the specifics. 😞 UPDATE on my UPDATE: I might not be remembering this accurately (see below).

This should work as a drop-in replacements for naturaldelta and naturaltime that are timezone-tolerant (requires dateutil or pytz; or you can provide your own tzinfo implementation for _TZ_UTC in lieu of the ugly _utctzinfo function below):

from datetime import datetime
from functools import wraps
from humanize import (
    naturaldelta as _naturaldelta,
    naturaltime as _naturaltime,
)

__all__ = (
    'naturaldelta',
    'naturaltime',
    'tztolerant',
)

def tztolerant(f):
    @wraps(f)
    def _wrapped(value, *args, **kw):
        if isinstance(value, datetime):
            if value.tzinfo is None:
                now = datetime.now() # assume offset-naive time is local
            else:
                now = datetime.utcnow().replace(tzinfo=_utctzinfo())
            value = now - value
        return f(value, *args, **kw)
    return _wrapped

naturaldelta = tztolerant(_naturaldelta)
naturaltime = tztolerant(_naturaltime)

def monkeypatchhumanize():
    # probably not a good idea, but here goes
    import humanize
    humanize.naturaldelta = humanize.time.naturaldelta = naturaldelta
    humanize.naturaltime = humanize.time.naturaltime = naturaltime

_TZ_UTC = None

def _utctzinfo():
    # forgive the arrow-ish antipattern
    global _TZ_UTC
    if _TZ_UTC is not None:
        return _TZ_UTC
    try:
        from dateutil.tz import tzutc
        _TZ_UTC = tzutc()
    except ImportError:
        try:
            from pytz.tz import UTC
            _TZ_UTC = UTC
        except ImportError as _exc:
            _new_exc = ImportError('either dateutil or pytz is required')
            try:
                from future.utils import raise_from
                raise_from(_new_exc, _exc)
            except ImportError:
                raise _new_exc

Both dateutil and pytz seem to handle DST correctly (at least based on my completely inadequate examination):

>>> from datetime import datetime
>>> # with dateutil
>>> from dateutil.tz import gettz, tzutc
>>> d1 = datetime(2015, 1, 1, 19, 0, 0).replace(tzinfo=tzutc())
>>> assert (d1.astimezone(gettz('EST5EDT')) - d1).total_seconds() == 0
>>> d2 = datetime(2015, 5, 1, 0, 30, 0).replace(tzinfo=tzutc())
>>> assert (d2.astimezone(gettz('EST5EDT')) - d2).total_seconds() == 0
>>> diff_dateutil = d2 - d1 ; diff_dateutil
datetime.timedelta(119, 19800)
>>> assert diff_dateutil == d2.astimezone(gettz('EST5EDT')) - d1.astimezone(gettz('EST5EDT'))
>>> assert diff_dateutil == d2 - d1.astimezone(gettz('EST5EDT'))
>>> assert diff_dateutil == d2.astimezone(gettz('EST5EDT')) - d1
>>> # with pytz
>>> from pytz import UTC, timezone
>>> d1 = datetime(2015, 1, 1, 19, 0, 0).replace(tzinfo=UTC)
>>> assert (d1.astimezone(timezone('EST5EDT')) - d1).total_seconds() == 0
>>> d2 = datetime(2015, 5, 1, 0, 30, 0).replace(tzinfo=UTC)
>>> assert (d1.astimezone(timezone('EST5EDT')) - d1).total_seconds() == 0
>>> diff_pytz = d2 - d1 ; diff_pytz
datetime.timedelta(119, 19800)
>>> assert diff_pytz == d2.astimezone(timezone('EST5EDT')) - d1.astimezone(timezone('EST5EDT'))
>>> assert diff_pytz == d2 - d1.astimezone(timezone('EST5EDT'))
>>> assert diff_pytz == d2.astimezone(timezone('EST5EDT')) - d1
>>> # mix them?
>>> d2 = datetime(2015, 5, 1, 0, 30, 0).replace(tzinfo=tzutc())
>>> d1 # using pytz ...
datetime.datetime(2015, 1, 1, 19, 0, tzinfo=<UTC>)
>>> d1.astimezone(gettz('EST5EDT')) # ... with tzinfo from dateutil
datetime.datetime(2015, 1, 1, 14, 0, tzinfo=tzfile(u'/usr/share/zoneinfo/EST5EDT'))
>>> d2 # using dateutil ...
datetime.datetime(2015, 5, 1, 0, 30, tzinfo=tzutc())
>>> d2.astimezone(timezone('EST5EDT')) # ... with tzinfo from pytz
datetime.datetime(2015, 4, 30, 20, 30, tzinfo=<DstTzInfo 'EST5EDT' EDT-1 day, 20:00:00 DST>)
>>> assert diff_pytz == diff_dateutil
>>> assert diff_pytz == d2.astimezone(timezone('EST5EDT')) - d1
>>> assert diff_dateutil == d2 - d1.astimezone(gettz('EST5EDT'))
>>> assert d2 - d1 == d2.astimezone(gettz('EST5EDT')) - d1.astimezone(timezone('EST5EDT'))

@AndreasBackx
Copy link

Could this issue be reopened? Working with offset-aware datetime still causes issues as was mentioned:

TypeError: can't subtract offset-naive and offset-aware datetimes

I can provide a PR for this if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants