diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -958,17 +958,22 @@ 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 diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1493,8 +1493,32 @@ 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 diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1972,7 +1972,7 @@ # 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 @@ 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 @@ 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 diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -4686,17 +4686,87 @@ } 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(×tamp); +#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 @@ /* Attach new tzinfo and let fromutc() do the rest. */ temp = ((PyDateTime_DateTime *)result)->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_INCREF(tzinfo); Py_DECREF(temp); temp = result;