diff --git a/Doc/library/email.util.rst b/Doc/library/email.util.rst --- a/Doc/library/email.util.rst +++ b/Doc/library/email.util.rst @@ -98,8 +98,12 @@ Fri, 09 Nov 2001 01:08:47 -0000 Optional *timeval* if given is a floating point time value as accepted by - :func:`time.gmtime` and :func:`time.localtime`, otherwise the current time is - used. + :func:`time.gmtime` and :func:`time.localtime`, otherwise the current time + is used. *timeval* may also be a :class:`~datetime.datetime` instance. If + the intsance does not have a :class:`~datetime.tzinfo`, the behavior is as + with a floating point time value. If it does have a + :class:`~datetime.tzinfo`, then *localtime* and *usegmt* are ignored and the + output string is in the timezone specified by the ``tzinfo``. Optional *localtime* is a flag that when ``True``, interprets *timeval*, and returns a date relative to the local timezone instead of UTC, properly taking diff --git a/Lib/email/utils.py b/Lib/email/utils.py --- a/Lib/email/utils.py +++ b/Lib/email/utils.py @@ -110,14 +110,17 @@ ''', re.VERBOSE | re.IGNORECASE) - def formatdate(timeval=None, localtime=False, usegmt=False): """Returns a date string as specified by RFC 2822, e.g.: Fri, 09 Nov 2001 01:08:47 -0000 Optional timeval if given is a floating point time value as accepted by - gmtime() and localtime(), otherwise the current time is used. + gmtime() and localtime(), otherwise the current time is used. timeval + may also be a datetime instance. If it does not have a tzinfo, the + behavior is as with a floating point time value. If it does have + a tzinfo, then localtime and usegmt are ignored and the output string + is in the timezone specified by the tzinfo. Optional localtime is a flag that when True, interprets timeval, and returns a date relative to the local timezone instead of UTC, properly @@ -129,13 +132,26 @@ """ # Note: we cannot use strftime() because that honors the locale and RFC # 2822 requires that day and month names be the English abbreviations. + aware = False if timeval is None: - timeval = time.time() - if localtime: + now = time.localtime() + elif hasattr(timeval, 'timetuple'): + now = timeval.timetuple() + aware = timeval.tzinfo is not None + if not aware: + if localtime: + # get the correct dst flag + now = time.localtime(time.mktime(now)) + else: + now = time.gmtime(time.mktime(now)) + elif localtime: now = time.localtime(timeval) + else: + now = time.gmtime(timeval) + if localtime and not aware: # Calculate timezone offset, based on whether the local zone has # daylight savings time, and whether DST is in effect. - if time.daylight and now[-1]: + if time.daylight and now[-1]>0: offset = time.altzone else: offset = time.timezone @@ -147,13 +163,14 @@ else: sign = '+' zone = '%s%02d%02d' % (sign, hours, minutes // 60) - else: - now = time.gmtime(timeval) + elif not localtime and not aware: # Timezone offset is always -0000 if usegmt: zone = 'GMT' else: zone = '-0000' + else: + zone = timeval.strftime("%z") return '%s, %02d %s %04d %02d:%02d:%02d %s' % ( ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]], now[2], diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -8,6 +8,7 @@ import time import base64 import difflib +import datetime import unittest import warnings import textwrap @@ -2549,25 +2550,64 @@ 'quoprimime', 'utils', ]) + fd_input = (2010, 12, 11, 12, 51, 23) + fd_struct = fd_input + (5, 1, 0) + fd_timestamp = time.mktime(fd_struct) + fd_gmstruct = time.gmtime(fd_timestamp) + # XXX: Can't figure out how to do this without essentially replicating + # the algorithm. + fd_localzone = "{}{:02d}{:02d}".format( + '-' if time.timezone > 0 else '+', + time.timezone//3600, + (time.timezone % 3600) // 60) + # Python and its test suite don't set the locale, so we can use + # strftime here even though we can't in the library code itself. + fd_expected_local = time.strftime('%a, %d %b %Y %H:%M:%S ', fd_struct) + fd_expected_gmt = time.strftime('%a, %d %b %Y %H:%M:%S ', + time.gmtime(fd_timestamp)) + + def _test_formatdate(self, timeval): + self.assertEqual(utils.formatdate(timeval), + self.fd_expected_gmt + "-0000") + def test_formatdate(self): - now = time.time() - self.assertEqual(utils.parsedate(utils.formatdate(now))[:6], - time.gmtime(now)[:6]) + self._test_formatdate(self.fd_timestamp) + + def test_formatdate_datetime(self): + self._test_formatdate(datetime.datetime(*self.fd_input)) + + def _test_formatdate_localtime(self, timeval): + self.assertEqual(utils.formatdate(timeval, localtime=True), + self.fd_expected_local + self.fd_localzone) def test_formatdate_localtime(self): - now = time.time() + self._test_formatdate_localtime(self.fd_timestamp) + + def test_formatdate_localtime_datetime(self): + self._test_formatdate_localtime(datetime.datetime(*self.fd_input)) + + def _test_formatdate_usegmt(self, timeval): self.assertEqual( - utils.parsedate(utils.formatdate(now, localtime=True))[:6], - time.localtime(now)[:6]) + utils.formatdate(timeval, localtime=False), + self.fd_expected_gmt + "-0000") + self.assertEqual( + utils.formatdate(timeval, localtime=False, usegmt=True), + self.fd_expected_gmt + "GMT") def test_formatdate_usegmt(self): - now = time.time() - self.assertEqual( - utils.formatdate(now, localtime=False), - time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now))) - self.assertEqual( - utils.formatdate(now, localtime=False, usegmt=True), - time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now))) + self._test_formatdate_usegmt(self.fd_timestamp) + + def test_formatdate_datetime_usegmt(self): + self._test_formatdate_usegmt(datetime.datetime(*self.fd_input)) + + def test_formatdate_aware_datetime(self): + tz = datetime.timezone(-datetime.timedelta(hours=7, minutes=30)) + dt = datetime.datetime(*self.fd_input, tzinfo=tz) + expected = self.fd_expected_local + "-0730" + self.assertEqual(utils.formatdate(dt), expected) + self.assertEqual(utils.formatdate(dt, localtime=True), expected) + self.assertEqual(utils.formatdate(dt, localtime=False, usegmt=True), + expected) def test_parsedate_none(self): self.assertEqual(utils.parsedate(''), None)