Skip to content

Commit fdc860f

Browse files
committed
Issue #9527: datetime.astimezone() method will now supply a class
timezone instance corresponding to the system local timezone when called with no arguments.
1 parent 8f904da commit fdc860f

File tree

5 files changed

+138
-12
lines changed

5 files changed

+138
-12
lines changed

‎Doc/library/datetime.rst‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -958,17 +958,22 @@ Instance methods:
958958
datetime with no conversion of date and time data.
959959

960960

961-
.. method:: datetime.astimezone(tz)
961+
.. method:: datetime.astimezone(tz=None)
962962

963-
Return a :class:`.datetime` object with new :attr:`tzinfo` attribute *tz*,
963+
Return a :class:`datetime` object with new :attr:`tzinfo` attribute *tz*,
964964
adjusting the date and time data so the result is the same UTC time as
965965
*self*, but in *tz*'s local time.
966966

967-
*tz* must be an instance of a :class:`tzinfo` subclass, and its
967+
If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its
968968
:meth:`utcoffset` and :meth:`dst` methods must not return ``None``. *self* must
969969
be aware (``self.tzinfo`` must not be ``None``, and ``self.utcoffset()`` must
970970
not return ``None``).
971971

972+
If called without arguments (or with ``tz=None``) the system local
973+
timezone is assumed. The ``tzinfo`` attribute of the converted
974+
datetime instance will be set to an instance of :class:`timezone`
975+
with the zone name and offset obtained from the OS.
976+
972977
If ``self.tzinfo`` is *tz*, ``self.astimezone(tz)`` is equal to *self*: no
973978
adjustment of date or time data is performed. Else the result is local
974979
time in time zone *tz*, representing the same UTC time as *self*: after

‎Lib/datetime.py‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,8 +1493,32 @@ def replace(self, year=None, month=None, day=None, hour=None,
14931493
return datetime(year, month, day, hour, minute, second,
14941494
microsecond, tzinfo)
14951495

1496-
def astimezone(self, tz):
1497-
if not isinstance(tz, tzinfo):
1496+
def astimezone(self, tz=None):
1497+
if tz is None:
1498+
if self.tzinfo is None:
1499+
raise ValueError("astimezone() requires an aware datetime")
1500+
ts = (self - _EPOCH) // timedelta(seconds=1)
1501+
localtm = _time.localtime(ts)
1502+
local = datetime(*localtm[:6])
1503+
try:
1504+
# Extract TZ data if available
1505+
gmtoff = localtm.tm_gmtoff
1506+
zone = localtm.tm_zone
1507+
except AttributeError:
1508+
# Compute UTC offset and compare with the value implied
1509+
# by tm_isdst. If the values match, use the zone name
1510+
# implied by tm_isdst.
1511+
delta = local - datetime(*_time.gmtime(ts)[:6])
1512+
dst = _time.daylight and localtm.tm_isdst > 0
1513+
gmtoff = _time.altzone if dst else _time.timezone
1514+
if delta == timedelta(seconds=-gmtoff):
1515+
tz = timezone(delta, _time.tzname[dst])
1516+
else:
1517+
tz = timezone(delta)
1518+
else:
1519+
tz = timezone(timedelta(seconds=-gmtoff), zone)
1520+
1521+
elif not isinstance(tz, tzinfo):
14981522
raise TypeError("tz argument must be an instance of tzinfo")
14991523

15001524
mytz = self.tzinfo

‎Lib/test/datetimetester.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1972,7 +1972,7 @@ def test_astimezone(self):
19721972
# simply can't be applied to a naive object.
19731973
dt = self.theclass.now()
19741974
f = FixedOffset(44, "")
1975-
self.assertRaises(TypeError, dt.astimezone) # not enough args
1975+
self.assertRaises(ValueError, dt.astimezone) # naive
19761976
self.assertRaises(TypeError, dt.astimezone, f, f) # too many args
19771977
self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
19781978
self.assertRaises(ValueError, dt.astimezone, f) # naive
@@ -3253,8 +3253,6 @@ def test_more_astimezone(self):
32533253
self.assertTrue(dt.tzinfo is f44m)
32543254
# Replacing with degenerate tzinfo raises an exception.
32553255
self.assertRaises(ValueError, dt.astimezone, fnone)
3256-
# Ditto with None tz.
3257-
self.assertRaises(TypeError, dt.astimezone, None)
32583256
# Replacing with same tzinfo makes no change.
32593257
x = dt.astimezone(dt.tzinfo)
32603258
self.assertTrue(x.tzinfo is f44m)
@@ -3274,6 +3272,23 @@ def test_more_astimezone(self):
32743272
self.assertTrue(got.tzinfo is expected.tzinfo)
32753273
self.assertEqual(got, expected)
32763274

3275+
@support.run_with_tz('UTC')
3276+
def test_astimezone_default_utc(self):
3277+
dt = self.theclass.now(timezone.utc)
3278+
self.assertEqual(dt.astimezone(None), dt)
3279+
self.assertEqual(dt.astimezone(), dt)
3280+
3281+
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
3282+
def test_astimezone_default_eastern(self):
3283+
dt = self.theclass(2012, 11, 4, 6, 30, tzinfo=timezone.utc)
3284+
local = dt.astimezone()
3285+
self.assertEqual(dt, local)
3286+
self.assertEqual(local.strftime("%z %Z"), "+0500 EST")
3287+
dt = self.theclass(2012, 11, 4, 5, 30, tzinfo=timezone.utc)
3288+
local = dt.astimezone()
3289+
self.assertEqual(dt, local)
3290+
self.assertEqual(local.strftime("%z %Z"), "+0400 EDT")
3291+
32773292
def test_aware_subtract(self):
32783293
cls = self.theclass
32793294

‎Misc/NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Core and Builtins
4040
Library
4141
-------
4242

43+
- Issue #9527: datetime.astimezone() method will now supply a class
44+
timezone instance corresponding to the system local timezone when
45+
called with no arguments.
46+
4347
- Issue #14653: email.utils.mktime_tz() no longer relies on system
4448
mktime() when timezone offest is supplied.
4549

‎Modules/_datetimemodule.c‎

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4685,18 +4685,88 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
46854685
return clone;
46864686
}
46874687

4688+
static PyObject *
4689+
local_timezone(PyObject *utc_time)
4690+
{
4691+
PyObject *result = NULL;
4692+
struct tm *timep;
4693+
time_t timestamp;
4694+
long offset;
4695+
PyObject *delta;
4696+
PyObject *one_second;
4697+
PyObject *seconds;
4698+
PyObject *nameo = NULL;
4699+
const char *zone = NULL;
4700+
4701+
delta = datetime_subtract((PyObject *)utc_time, PyDateTime_Epoch);
4702+
if (delta == NULL)
4703+
return NULL;
4704+
one_second = new_delta(0, 1, 0, 0);
4705+
if (one_second == NULL)
4706+
goto error;
4707+
seconds = divide_timedelta_timedelta((PyDateTime_Delta *)delta,
4708+
(PyDateTime_Delta *)one_second);
4709+
Py_DECREF(one_second);
4710+
if (seconds == NULL)
4711+
goto error;
4712+
Py_DECREF(delta);
4713+
timestamp = PyLong_AsLong(seconds);
4714+
Py_DECREF(seconds);
4715+
if (timestamp == -1 && PyErr_Occurred())
4716+
return NULL;
4717+
timep = localtime(&timestamp);
4718+
#ifdef HAVE_STRUCT_TM_TM_ZONE
4719+
offset = timep->tm_gmtoff;
4720+
zone = timep->tm_zone;
4721+
delta = new_delta(0, -offset, 0, 0);
4722+
#else /* HAVE_STRUCT_TM_TM_ZONE */
4723+
{
4724+
PyObject *local_time;
4725+
Py_INCREF(utc_time->tzinfo);
4726+
local_time = new_datetime(timep->tm_year + 1900, timep->tm_mon + 1,
4727+
timep->tm_mday, timep->tm_hour, timep->tm_min,
4728+
timep->tm_sec, utc_time->tzinfo);
4729+
if (local_time == NULL) {
4730+
Py_DECREF(utc_time->tzinfo);
4731+
goto error;
4732+
}
4733+
delta = datetime_subtract(local_time, utc_time);
4734+
/* XXX: before relying on tzname, we should compare delta
4735+
to the offset implied by timezone/altzone */
4736+
if (daylight && timep->tm_isdst >= 0)
4737+
zone = tzname[timep->tm_isdst % 2];
4738+
else
4739+
zone = tzname[0];
4740+
Py_DECREF(local_time);
4741+
}
4742+
#endif /* HAVE_STRUCT_TM_TM_ZONE */
4743+
if (zone != NULL) {
4744+
nameo = PyUnicode_DecodeLocale(zone, "surrogateescape");
4745+
if (nameo == NULL)
4746+
goto error;
4747+
}
4748+
result = new_timezone(delta, nameo);
4749+
Py_DECREF(nameo);
4750+
error:
4751+
Py_DECREF(delta);
4752+
return result;
4753+
}
4754+
46884755
static PyObject *
46894756
datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
46904757
{
46914758
PyObject *result;
46924759
PyObject *offset;
46934760
PyObject *temp;
4694-
PyObject *tzinfo;
4761+
PyObject *tzinfo = Py_None;
46954762
_Py_IDENTIFIER(fromutc);
46964763
static char *keywords[] = {"tz", NULL};
46974764

4698-
if (! PyArg_ParseTupleAndKeywords(args, kw, "O!:astimezone", keywords,
4699-
&PyDateTime_TZInfoType, &tzinfo))
4765+
if (! PyArg_ParseTupleAndKeywords(args, kw, "|O:astimezone", keywords,
4766+
&tzinfo))
4767+
return NULL;
4768+
4769+
if (check_tzinfo_subclass(tzinfo) == -1)
47004770
return NULL;
47014771

47024772
if (!HASTZINFO(self) || self->tzinfo == Py_None)
@@ -4729,8 +4799,16 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
47294799

47304800
/* Attach new tzinfo and let fromutc() do the rest. */
47314801
temp = ((PyDateTime_DateTime *)result)->tzinfo;
4802+
if (tzinfo == Py_None) {
4803+
tzinfo = local_timezone(result);
4804+
if (tzinfo == NULL) {
4805+
Py_DECREF(result);
4806+
return NULL;
4807+
}
4808+
}
4809+
else
4810+
Py_INCREF(tzinfo);
47324811
((PyDateTime_DateTime *)result)->tzinfo = tzinfo;
4733-
Py_INCREF(tzinfo);
47344812
Py_DECREF(temp);
47354813

47364814
temp = result;

0 commit comments

Comments
 (0)