You're right, this is not yet documented.
As an aside, when playing on the command line with the datefmt
utilities, I found some surprising differences between our FixedOffset
tzinfo class and the tzinfo classes from pytz: I expected the
"Europe/Paris" timezone from pytz to be the same as our "GMT +2:00"
(when DST is active that is, otherwise GMT +1), but they are not...
>>> from trac.util.datefmt import *
>>> paris = timezone("Europe/Paris")
>>> gmt02 = timezone("GMT +2:00")
>>> (paris, gmt02)
(<DstTzInfo 'Europe/Paris' PMT+0:09:00 STD>, <FixedOffset "GMT +2:00"
2:00:00>)
I have no idea why that DstTzInfo instance shows "PMT+0:09:00", but OK.
First we start by using "naive" datetime objects.
>>> from datetime import datetime
>>> d_naive = datetime.now()
>>> d_paris = to_datetime(d_naive, paris)
>>> d_gmt02 = to_datetime(d_naive, gmt02)
Now if we try to use to_utimestamp() on these datetime objects, we'll
get different values!
>>> to_utimestamp(d_paris)
1347999789289000L
>>> to_utimestamp(d_gmt02)
1347993129289000L
??
But even when comparing directly those datetime objects, we see they're
not the same:
>>> d_paris - d_gmt02
datetime.timedelta(0, 6660)
!? 111 minutes... At first I had no idea what this corresponded to.
The Python docs
(
http://docs.python.org/library/datetime.html#datetime-objects) helped a
bit here: "Subtraction of a datetime from a datetime ... If both are
aware and have different tzinfo attributes, a-b acts as if a and b were
first converted to naive UTC datetimes first. The result is ..." (in our
example:)
>>> (d_paris.replace(tzinfo=None) - d_paris.utcoffset()) -
(d_gmt02.replace(tzinfo=None) - d_gmt02.utcoffset())
datetime.timedelta(0, 6660)
And this helps to pinpoint the cause:
>>> d_gmt02.utcoffset()
datetime.timedelta(0, 7200)
(correct, 2 hours)
>>> d_paris.utcoffset()
datetime.timedelta(0, 540)
Ahem, 9 minutes... (remember the PMT+0:09:00 above?)
Googling a bit reveals a similar "surprise" for someone else, a few
years ago:
http://marc.info/?l=zope3-dev&m=115384670611141&w=2
The answer was: "Read pytz/README.txt. You're not supposed to pass
tzinfo to datetime."
Ok, so it really look like we should find an alternative to the
t.replace(tzinfo=tzinfo) we do in to_datetime() when t is a "naive"
datetime, because if we specify there a timezone from pytz, we'll get
non-sensical datetime objects.
If we start with "aware" datetime objects (bootstrapping the tzinfo with
one of our own FixedOffset instance), it indeed works as expected.
>>> d_utc = d_naive.replace(tzinfo=utc)
>>> du_paris = to_datetime(d_utc, paris)
>>> du_gmt02 = to_datetime(d_utc, gmt02)
Then:
>>> du_paris - du_gmt02
datetime.timedelta(0)
Better!
Looks like in Trac we're mostly using "aware" datetime objects anyway,
like when retrieving dates from the database (from_utimestamp(usecs)) or
from the user (user_time(...)), or the current date (datetime.now(utc)),
so that this problem was not apparent. Still, the docs need to be
clarified about the "naive"/"aware" situation and to_datetime() should
be fixed.
-- Christian