-
Notifications
You must be signed in to change notification settings - Fork 148
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
Comments
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. |
I think what @merwok said is this 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. |
UPDATE: I should have checked PRs first. #22 provides an alternate solution. This should work as a drop-in replacements for 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 >>> 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')) |
Could this issue be reopened? Working with offset-aware datetime still causes issues as was mentioned:
I can provide a PR for this if needed. |
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.
The text was updated successfully, but these errors were encountered: