Issue #9527: datetime.astimezone() method will now supply a class

timezone instance corresponding to the system local timezone when
called with no arguments.
This commit is contained in:
Alexander Belopolsky 2012-06-22 12:23:23 -04:00
parent 8f904daee9
commit fdc860f310
5 changed files with 138 additions and 12 deletions

View File

@ -958,17 +958,22 @@ Instance methods:
datetime with no conversion of date and time data.
.. method:: datetime.astimezone(tz)
.. method:: datetime.astimezone(tz=None)
Return a :class:`.datetime` object with new :attr:`tzinfo` attribute *tz*,
Return a :class:`datetime` object with new :attr:`tzinfo` attribute *tz*,
adjusting the date and time data so the result is the same UTC time as
*self*, but in *tz*'s local time.
*tz* must be an instance of a :class:`tzinfo` subclass, and its
If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its
:meth:`utcoffset` and :meth:`dst` methods must not return ``None``. *self* must
be aware (``self.tzinfo`` must not be ``None``, and ``self.utcoffset()`` must
not return ``None``).
If called without arguments (or with ``tz=None``) the system local
timezone is assumed. The ``tzinfo`` attribute of the converted
datetime instance will be set to an instance of :class:`timezone`
with the zone name and offset obtained from the OS.
If ``self.tzinfo`` is *tz*, ``self.astimezone(tz)`` is equal to *self*: no
adjustment of date or time data is performed. Else the result is local
time in time zone *tz*, representing the same UTC time as *self*: after

View File

@ -1493,8 +1493,32 @@ class datetime(date):
return datetime(year, month, day, hour, minute, second,
microsecond, tzinfo)
def astimezone(self, tz):
if not isinstance(tz, tzinfo):
def astimezone(self, tz=None):
if tz is None:
if self.tzinfo is None:
raise ValueError("astimezone() requires an aware datetime")
ts = (self - _EPOCH) // timedelta(seconds=1)
localtm = _time.localtime(ts)
local = datetime(*localtm[:6])
try:
# Extract TZ data if available
gmtoff = localtm.tm_gmtoff
zone = localtm.tm_zone
except AttributeError:
# Compute UTC offset and compare with the value implied
# by tm_isdst. If the values match, use the zone name
# implied by tm_isdst.
delta = local - datetime(*_time.gmtime(ts)[:6])
dst = _time.daylight and localtm.tm_isdst > 0
gmtoff = _time.altzone if dst else _time.timezone
if delta == timedelta(seconds=-gmtoff):
tz = timezone(delta, _time.tzname[dst])
else:
tz = timezone(delta)
else:
tz = timezone(timedelta(seconds=-gmtoff), zone)
elif not isinstance(tz, tzinfo):
raise TypeError("tz argument must be an instance of tzinfo")
mytz = self.tzinfo

View File

@ -1972,7 +1972,7 @@ class TestDateTime(TestDate):
# simply can't be applied to a naive object.
dt = self.theclass.now()
f = FixedOffset(44, "")
self.assertRaises(TypeError, dt.astimezone) # not enough args
self.assertRaises(ValueError, dt.astimezone) # naive
self.assertRaises(TypeError, dt.astimezone, f, f) # too many args
self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
self.assertRaises(ValueError, dt.astimezone, f) # naive
@ -3253,8 +3253,6 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase):
self.assertTrue(dt.tzinfo is f44m)
# Replacing with degenerate tzinfo raises an exception.
self.assertRaises(ValueError, dt.astimezone, fnone)
# Ditto with None tz.
self.assertRaises(TypeError, dt.astimezone, None)
# Replacing with same tzinfo makes no change.
x = dt.astimezone(dt.tzinfo)
self.assertTrue(x.tzinfo is f44m)
@ -3274,6 +3272,23 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase):
self.assertTrue(got.tzinfo is expected.tzinfo)
self.assertEqual(got, expected)
@support.run_with_tz('UTC')
def test_astimezone_default_utc(self):
dt = self.theclass.now(timezone.utc)
self.assertEqual(dt.astimezone(None), dt)
self.assertEqual(dt.astimezone(), dt)
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
def test_astimezone_default_eastern(self):
dt = self.theclass(2012, 11, 4, 6, 30, tzinfo=timezone.utc)
local = dt.astimezone()
self.assertEqual(dt, local)
self.assertEqual(local.strftime("%z %Z"), "+0500 EST")
dt = self.theclass(2012, 11, 4, 5, 30, tzinfo=timezone.utc)
local = dt.astimezone()
self.assertEqual(dt, local)
self.assertEqual(local.strftime("%z %Z"), "+0400 EDT")
def test_aware_subtract(self):
cls = self.theclass

View File

@ -40,6 +40,10 @@ Core and Builtins
Library
-------
- Issue #9527: datetime.astimezone() method will now supply a class
timezone instance corresponding to the system local timezone when
called with no arguments.
- Issue #14653: email.utils.mktime_tz() no longer relies on system
mktime() when timezone offest is supplied.

View File

@ -4685,18 +4685,88 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
return clone;
}
static PyObject *
local_timezone(PyObject *utc_time)
{
PyObject *result = NULL;
struct tm *timep;
time_t timestamp;
long offset;
PyObject *delta;
PyObject *one_second;
PyObject *seconds;
PyObject *nameo = NULL;
const char *zone = NULL;
delta = datetime_subtract((PyObject *)utc_time, PyDateTime_Epoch);
if (delta == NULL)
return NULL;
one_second = new_delta(0, 1, 0, 0);
if (one_second == NULL)
goto error;
seconds = divide_timedelta_timedelta((PyDateTime_Delta *)delta,
(PyDateTime_Delta *)one_second);
Py_DECREF(one_second);
if (seconds == NULL)
goto error;
Py_DECREF(delta);
timestamp = PyLong_AsLong(seconds);
Py_DECREF(seconds);
if (timestamp == -1 && PyErr_Occurred())
return NULL;
timep = localtime(&timestamp);
#ifdef HAVE_STRUCT_TM_TM_ZONE
offset = timep->tm_gmtoff;
zone = timep->tm_zone;
delta = new_delta(0, -offset, 0, 0);
#else /* HAVE_STRUCT_TM_TM_ZONE */
{
PyObject *local_time;
Py_INCREF(utc_time->tzinfo);
local_time = new_datetime(timep->tm_year + 1900, timep->tm_mon + 1,
timep->tm_mday, timep->tm_hour, timep->tm_min,
timep->tm_sec, utc_time->tzinfo);
if (local_time == NULL) {
Py_DECREF(utc_time->tzinfo);
goto error;
}
delta = datetime_subtract(local_time, utc_time);
/* XXX: before relying on tzname, we should compare delta
to the offset implied by timezone/altzone */
if (daylight && timep->tm_isdst >= 0)
zone = tzname[timep->tm_isdst % 2];
else
zone = tzname[0];
Py_DECREF(local_time);
}
#endif /* HAVE_STRUCT_TM_TM_ZONE */
if (zone != NULL) {
nameo = PyUnicode_DecodeLocale(zone, "surrogateescape");
if (nameo == NULL)
goto error;
}
result = new_timezone(delta, nameo);
Py_DECREF(nameo);
error:
Py_DECREF(delta);
return result;
}
static PyObject *
datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
{
PyObject *result;
PyObject *offset;
PyObject *temp;
PyObject *tzinfo;
PyObject *tzinfo = Py_None;
_Py_IDENTIFIER(fromutc);
static char *keywords[] = {"tz", NULL};
if (! PyArg_ParseTupleAndKeywords(args, kw, "O!:astimezone", keywords,
&PyDateTime_TZInfoType, &tzinfo))
if (! PyArg_ParseTupleAndKeywords(args, kw, "|O:astimezone", keywords,
&tzinfo))
return NULL;
if (check_tzinfo_subclass(tzinfo) == -1)
return NULL;
if (!HASTZINFO(self) || self->tzinfo == Py_None)
@ -4729,8 +4799,16 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
/* Attach new tzinfo and let fromutc() do the rest. */
temp = ((PyDateTime_DateTime *)result)->tzinfo;
((PyDateTime_DateTime *)result)->tzinfo = tzinfo;
if (tzinfo == Py_None) {
tzinfo = local_timezone(result);
if (tzinfo == NULL) {
Py_DECREF(result);
return NULL;
}
}
else
Py_INCREF(tzinfo);
((PyDateTime_DateTime *)result)->tzinfo = tzinfo;
Py_DECREF(temp);
temp = result;