]> granicus.if.org Git - python/commitdiff
Issue #9527: datetime.astimezone() method will now supply a class
authorAlexander Belopolsky <alexander.belopolsky@gmail.com>
Fri, 22 Jun 2012 16:23:23 +0000 (12:23 -0400)
committerAlexander Belopolsky <alexander.belopolsky@gmail.com>
Fri, 22 Jun 2012 16:23:23 +0000 (12:23 -0400)
timezone instance corresponding to the system local timezone when
called with no arguments.

Doc/library/datetime.rst
Lib/datetime.py
Lib/test/datetimetester.py
Misc/NEWS
Modules/_datetimemodule.c

index 5c2d3d9d818ca97af3c6e635a8a6f0bb49e536ea..85bbb899789d4967957290cd780f0c96660995c8 100644 (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
index 6ab2499f4b1ec201d978f2b2f7ef8598c37c1535..ce88d852092d5043129f850af27d84660ade9083 100644 (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
index 048d63cdd13f624116c4b0e55e872ba9412c4b61..e0454472035790253fd95cd7d3aae8c84b5f1154 100644 (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
 
index 8abe981ea096640a6d285f2bca3257bec10feef5..8b7e3becd7406b797b6e9c640822061c782f1955 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -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.
 
index d3a502db91fe9b7a6224baac4cc657649db65b50..f28d03c38da168ea0f0b0f2c41d478d766ce4773 100644 (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;
+    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;