diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1850,6 +1850,9 @@ | ``%M`` | Minute as a zero-padded | 00, 01, ..., 59 | | | | decimal number. | | | +-----------+--------------------------------+------------------------+-------+ +| ``%s`` | Seconds since the Epoch as a | 0, 1400356800, | \(8) | +| | decimal number. | -62135596800, etc | | ++-----------+--------------------------------+------------------------+-------+ | ``%S`` | Second as a zero-padded | 00, 01, ..., 59 | \(4) | | | decimal number. | | | +-----------+--------------------------------+------------------------+-------+ @@ -1964,6 +1967,15 @@ When used with the :meth:`strptime` method, ``%U`` and ``%W`` are only used in calculations when the day of the week and the year are specified. +(8) + :meth:`datetime.strftime` support for ``%s`` depends on platform. + + .. versionchanged:: 3.5 + + ``%s`` is supported by :meth:`datetime.strptime` class method on all + platforms. An aware :class:`.datetime` object in the local timezone is + returned. + .. rubric:: Footnotes .. [#] If, that is, we ignore the effects of Relativity diff --git a/Doc/library/time.rst b/Doc/library/time.rst --- a/Doc/library/time.rst +++ b/Doc/library/time.rst @@ -406,6 +406,9 @@ | ``%S`` | Second as a decimal number [00,61]. | \(2) | | | | | +-----------+------------------------------------------------+-------+ + | ``%s`` | Seconds since the Epoch as a decimal number. | \(4) | + | | | | + +-----------+------------------------------------------------+-------+ | ``%U`` | Week number of the year (Sunday as the first | \(3) | | | day of the week) as a decimal number [00,53]. | | | | All days in a new year preceding the first | | @@ -464,6 +467,12 @@ When used with the :func:`strptime` function, ``%U`` and ``%W`` are only used in calculations when the day of the week and the year are specified. + (4) + :func:`strftime` support for ``%s`` depends on platform. + + .. versionchanged:: 3.5 + ``%s`` is supported by :func:`strptime` function on all platforms. + Here is an example, a format for dates compatible with that specified in the :rfc:`2822` Internet email standard. [#]_ :: @@ -688,4 +697,3 @@ year (%y rather than %Y), but practice moved to 4-digit years long before the year 2000. After that, :rfc:`822` became obsolete and the 4-digit year has been first recommended by :rfc:`1123` and then mandated by :rfc:`2822`. - diff --git a/Lib/_strptime.py b/Lib/_strptime.py --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -199,6 +199,7 @@ 'm': r"(?P1[0-2]|0[1-9]|[1-9])", 'M': r"(?P[0-5]\d|\d)", 'S': r"(?P6[0-1]|[0-5]\d|\d)", + 's': r"(?P0|-?[1-9][0-9]*)", 'U': r"(?P5[0-3]|[0-4]\d|\d)", 'w': r"(?P[0-6])", # W is set below by using 'U' @@ -343,7 +344,7 @@ month = day = 1 hour = minute = second = fraction = 0 tz = -1 - tzoffset = None + tzname = gmtoff = None # Default to -1 to signify that values not known; not critical to have, # though week_of_year = -1 @@ -352,7 +353,7 @@ # values weekday = julian = -1 found_dict = found.groupdict() - for group_key in found_dict.keys(): + for group_key in sorted(found_dict.keys()): # make it deterministic # Directives not explicitly handled below: # c, x, X # handled by making out of other directives @@ -399,6 +400,26 @@ minute = int(found_dict['M']) elif group_key == 'S': second = int(found_dict['S']) + elif group_key == 's': + timestamp = int(found_dict['s']) + #XXX time.localtime is simpler but tzname, gmtoff are not available + # sometimes + ### See also email.utils.localtime() and issues #9527, #1647654 + ##tm = time.localtime(timestamp) + ##year, month, day, hour, minute, second, weekday, julian, tz = tm + ##if time._STRUCT_TM_ITEMS >= 11: # tm_zone support + ## tzname, gmtoff = tm.tm_zone, tm.tm_gmtoff + from datetime import datetime, timezone, timedelta + + dt = datetime.fromtimestamp(timestamp, timezone.utc).astimezone() + (year, month, day, + hour, minute, second, + weekday, julian, tz) = dt.timetuple() + assert tz == -1 # if it fails remove it and the next line + tz = -1 if dt.dst() is None else bool(dt.dst()) + tzname = dt.tzname() + gmtoff = dt.utcoffset() // timedelta(seconds=1) + del timestamp, dt, datetime, timezone, timedelta elif group_key == 'f': s = found_dict['f'] # Pad to always return microseconds. @@ -426,9 +447,9 @@ week_of_year_start = 0 elif group_key == 'z': z = found_dict['z'] - tzoffset = int(z[1:3]) * 60 + int(z[3:5]) + gmtoff = 60 * (int(z[1:3]) * 60 + int(z[3:5])) if z.startswith("-"): - tzoffset = -tzoffset + gmtoff = -gmtoff elif group_key == 'Z': # Since -1 is default value only need to worry about setting tz if # it can be something other than -1. @@ -472,11 +493,8 @@ if weekday == -1: weekday = datetime_date(year, month, day).weekday() # Add timezone info - tzname = found_dict.get("Z") - if tzoffset is not None: - gmtoff = tzoffset * 60 - else: - gmtoff = None + if tzname is None: + tzname = found_dict.get("Z") if leap_year_fix: # the caller didn't supply a year but asked for Feb 29th. We couldn't diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2,7 +2,7 @@ See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases """ - +import os import sys import pickle import random @@ -2914,6 +2914,105 @@ self.assertEqual(dt.time(), time(18, 45, 3, 1234)) self.assertEqual(dt.timetz(), time(18, 45, 3, 1234, tzinfo=met)) + def _test_strptime_timestamp(self, timestamp): + # test %s strptime-format + tm = _time.localtime(timestamp) + strptime = self.theclass.strptime + s = str(timestamp) + + parsed_tm = _time.strptime(s, '%s') + self.assertEqual(-1, parsed_tm.tm_isdst) + self.assertEqual(tm[:8], parsed_tm[:8]) + + dt = strptime(s, '%s') + self.verify_field_equality(tm, dt) + self.assertEqual(tm.tm_isdst > 0, bool(dt.dst())) + + # check group_key order (it seems Python strptime doesn't + # guarantee left-to-right parsing) + L = sorted({'s': s, 'Y': '%04d' % 42}.items()) + for items in [L, reversed(L)]: + format, string = '', '' + for k, v in items: + format += ' %' + k + string += ' ' + v + self.assertEqual(dt.year, strptime(string, format).year) + + # check timezone info + if _time._STRUCT_TM_ITEMS >= 11: # tm_zone support + self.assertEqual(tm.tm_gmtoff, parsed_tm.tm_gmtoff) + utc_offset = dt.utcoffset() // timedelta(seconds=1) + self.assertEqual(tm.tm_gmtoff, utc_offset) + self.assertEqual(tm.tm_zone, parsed_tm.tm_zone) + self.assertEqual(tm.tm_zone, dt.tzname()) + else: + self.skipTest('needs tm_zone support') + + def _test_strptime_timestamp_nondst_utc_offset_changes(self): + # Moscow Time has been UTC+4 year-round since 27 March + # 2011, but will change to UTC+3 permanently on 25 October + # 2014. http://en.wikipedia.org/wiki/Moscow_Time + + # in other words it is a good timezone to test with + D = self._test_strptime_timestamp + D(-31546800) # winter, UTC+3, isdst=0 + D(-18500400) # summer, UTC+4, isdst=1 + D(-2689200) # winter, UTC+3, isdst=0 + D(0) # 0 timestamp: winter, UTC+3, isdst=0 + D(1293829200) # winter, UTC+3, isdst=0 + D(1306872000) # summer, UTC+4, isdst=0 #NOTE: isdst=0 in summer + D(1322683200) # winter, UTC+4, isdst=0 #NOTE: UTC+4 in winter + D(1388520000) # winter, UTC+4, isdst=0 #NOTE: UTC+4 in winter + D(1401566400) # summer, UTC+4, isdst=0 #NOTE: isdst=0 in summer + #XXX zoneinfo still returns UTC+4 + D(1417377600) # winter, UTC+3, isdst=0 + D(1420056000) # winter, UTC+3, isdst=0 + D(1433102400) # summer, UTC+3, isdst=0 #NOTE: UTC+3, isdst=0 in summer + D(1448913600) # winter, UTC+3, isdst=0 + + @unittest.skipIf(sys.platform.startswith('win'), + "Windows does not use Olson's TZ database") + @unittest.skipUnless(os.path.exists('/usr/share/zoneinfo') or + os.path.exists('/usr/lib/zoneinfo'), + "Can't find the Olson's TZ database") + @support.run_with_tz('Europe/Moscow') + def test_strptime_timestamp_nondst_utc_offset_changes_moscow_tz(self): + self._test_strptime_timestamp_nondst_utc_offset_changes() + + def test_strptime_timestamp_nondst_utc_offset_changes_local_tz(self): + self._test_strptime_timestamp_nondst_utc_offset_changes() + + def _test_strptime_timestamp_range(self): + # test min/max values + D = self._test_strptime_timestamp + epsilon = timedelta(seconds=1) # %s accepts integer seconds + for dt in [self.theclass.min + epsilon, + self.theclass.max - epsilon]: + D(int(dt.timestamp())) + + @support.run_with_tz('UTC') + def test_strptime_timestamp_range_utc(self): + self._test_strptime_timestamp_range() + + @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') + def test_strptime_timestamp_range_nonzero_utc_offset(self): + with self.assertRaises(ValueError): # out of range + self._test_strptime_timestamp_range() + + def test_strptime_timestamp_invalid_args(self): + f = self.theclass.strptime + f('0', '%s') # no error + with self.assertRaises(ValueError): + f('0.1', '%s') # reject floats + + dt = f('0.1', '%s.%f') # no error + self.assertEqual(dt.microsecond, 100000) + + unicode_zero = '\U0001d7ec' + self.assertEqual(int(unicode_zero), 0) # no error + with self.assertRaises(ValueError): + f(unicode_zero, '%s') # accept only C standard digits + def test_tz_aware_arithmetic(self): import random