diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 038355c49fc..fefb28493ea 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -113,13 +113,15 @@ The following utility functions are defined: .. function:: Time2Internaldate(date_time) - Convert *date_time* to an IMAP4 ``INTERNALDATE`` representation. The - return value is a string in the form: ``"DD-Mmm-YYYY HH:MM:SS - +HHMM"`` (including double-quotes). The *date_time* argument can be a - number (int or float) representing seconds since epoch (as returned - by :func:`time.time`), a 9-tuple representing local time (as returned by - :func:`time.localtime`), or a double-quoted string. In the last case, it - is assumed to already be in the correct format. + Convert *date_time* to an IMAP4 ``INTERNALDATE`` representation. + The return value is a string in the form: ``"DD-Mmm-YYYY HH:MM:SS + +HHMM"`` (including double-quotes). The *date_time* argument can + be a number (int or float) representing seconds since epoch (as + returned by :func:`time.time`), a 9-tuple representing local time + an instance of :class:`time.struct_time` (as returned by + :func:`time.localtime`), an aware instance of + :class:`datetime.datetime`, or a double-quoted string. In the last + case, it is assumed to already be in the correct format. Note that IMAP4 message numbers change as the mailbox changes; in particular, after an ``EXPUNGE`` command performs deletions the remaining messages are diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 681c7cfd60c..e16fb957b0a 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -23,7 +23,7 @@ Public functions: Internaldate2tuple __version__ = "2.58" import binascii, errno, random, re, socket, subprocess, sys, time, calendar - +from datetime import datetime, timezone, timedelta try: import ssl HAVE_SSL = True @@ -1313,10 +1313,8 @@ class _Authenticator: return '' return binascii.a2b_base64(inp) - - -Mon2num = {b'Jan': 1, b'Feb': 2, b'Mar': 3, b'Apr': 4, b'May': 5, b'Jun': 6, - b'Jul': 7, b'Aug': 8, b'Sep': 9, b'Oct': 10, b'Nov': 11, b'Dec': 12} +Months = ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ') +Mon2num = {s.encode():n+1 for n, s in enumerate(Months[1:])} def Internaldate2tuple(resp): """Parse an IMAP4 INTERNALDATE string. @@ -1384,28 +1382,37 @@ def Time2Internaldate(date_time): Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The date_time argument can be a number (int or float) representing seconds since epoch (as returned by time.time()), a 9-tuple - representing local time (as returned by time.localtime()), or a + representing local time, an instance of time.struct_time (as + returned by time.localtime()), an aware datetime instance or a double-quoted string. In the last case, it is assumed to already be in the correct format. """ - if isinstance(date_time, (int, float)): - tt = time.localtime(date_time) - elif isinstance(date_time, (tuple, time.struct_time)): - tt = date_time + dt = datetime.fromtimestamp(date_time, + timezone.utc).astimezone() + elif isinstance(date_time, tuple): + try: + gmtoff = date_time.tm_gmtoff + except AttributeError: + if time.daylight: + dst = date_time[8] + if dst == -1: + dst = time.localtime(time.mktime(date_time))[8] + gmtoff = -(time.timezone, time.altzone)[dst] + else: + gmtoff = -time.timezone + delta = timedelta(seconds=gmtoff) + dt = datetime(*date_time[:6], tzinfo=timezone(delta)) + elif isinstance(date_time, datetime): + if date_time.tzinfo is None: + raise ValueError("date_time must be aware") + dt = date_time elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): return date_time # Assume in correct format else: raise ValueError("date_time not of a known type") - - dt = time.strftime("%d-%b-%Y %H:%M:%S", tt) - if dt[0] == '0': - dt = ' ' + dt[1:] - if time.daylight and tt[-1]: - zone = -time.altzone - else: - zone = -time.timezone - return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"' + fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(Months[dt.month]) + return dt.strftime(fmt) diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index e629cb1f66b..bfb90063194 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -11,9 +11,9 @@ import socketserver import time import calendar -from test.support import reap_threads, verbose, transient_internet, run_with_tz +from test.support import reap_threads, verbose, transient_internet, run_with_tz, run_with_locale import unittest - +from datetime import datetime, timezone, timedelta try: import ssl except ImportError: @@ -43,14 +43,30 @@ class TestImaplib(unittest.TestCase): imaplib.Internaldate2tuple( b'25 (INTERNALDATE "02-Apr-2000 03:30:00 +0000")')) - def test_that_Time2Internaldate_returns_a_result(self): - # We can check only that it successfully produces a result, - # not the correctness of the result itself, since the result - # depends on the timezone the machine is in. - timevalues = [2000000000, 2000000000.0, time.localtime(2000000000), - '"18-May-2033 05:33:20 +0200"'] - for t in timevalues: + + def timevalues(self): + return [2000000000, 2000000000.0, time.localtime(2000000000), + (2033, 5, 18, 5, 33, 20, -1, -1, -1), + (2033, 5, 18, 5, 33, 20, -1, -1, 1), + datetime.fromtimestamp(2000000000, + timezone(timedelta(0, 2*60*60))), + '"18-May-2033 05:33:20 +0200"'] + + @run_with_locale('LC_ALL', 'de_DE', 'fr_FR') + @run_with_tz('STD-1DST') + def test_Time2Internaldate(self): + expected = '"18-May-2033 05:33:20 +0200"' + + for t in self.timevalues(): + internal = imaplib.Time2Internaldate(t) + self.assertEqual(internal, expected) + + def test_that_Time2Internaldate_returns_a_result(self): + # Without tzset, we can check only that it successfully + # produces a result, not the correctness of the result itself, + # since the result depends on the timezone the machine is in. + for t in self.timevalues(): imaplib.Time2Internaldate(t) diff --git a/Misc/NEWS b/Misc/NEWS index 373b04bd6a7..b6f735fc7a3 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -40,6 +40,8 @@ Core and Builtins Library ------- +- Issues #11024: Fixes and additional tests for Time2Internaldate. + - Issue #14626: Large refactoring of functions / parameters in the os module. Many functions now support "dir_fd" and "follow_symlinks" parameters; some also support accepting an open file descriptor in place of of a path