diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 55be8694a067ba..d032d283eb9b53 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2174,6 +2174,13 @@ Notes: .. versionchanged:: 3.7 The UTC offset is not restricted to a whole number of minutes. + .. versionchanged:: 3.7 + When the ``%z`` directive is provided to the :meth:`strptime` method, + the UTC offsets can have a colon as a separator between hours, minutes + and seconds. + For example, ``'+01:00:00'`` will be parsed as an offset of one hour. + In addition, providing ``'Z'`` is identical to ``'+00:00'``. + ``%Z`` If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty string. Otherwise ``%Z`` is replaced by the returned value, which must diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 284175d0cfe22b..f5195af90c8a4d 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -210,7 +210,7 @@ def __init__(self, locale_time=None): #XXX: Does 'Y' need to worry about having less or more than # 4 digits? 'Y': r"(?P\d\d\d\d)", - 'z': r"(?P[+-]\d\d[0-5]\d)", + 'z': r"(?P[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|Z)", 'A': self.__seqToRE(self.locale_time.f_weekday, 'A'), 'a': self.__seqToRE(self.locale_time.a_weekday, 'a'), 'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'), @@ -365,7 +365,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): month = day = 1 hour = minute = second = fraction = 0 tz = -1 - tzoffset = None + gmtoff = None + gmtoff_fraction = 0 # Default to -1 to signify that values not known; not critical to have, # though iso_week = week_of_year = None @@ -455,9 +456,24 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): iso_week = int(found_dict['V']) elif group_key == 'z': z = found_dict['z'] - tzoffset = int(z[1:3]) * 60 + int(z[3:5]) - if z.startswith("-"): - tzoffset = -tzoffset + if z == 'Z': + gmtoff = 0 + else: + if z[3] == ':': + z = z[:3] + z[4:] + if len(z) > 5: + if z[5] != ':': + msg = f"Unconsistent use of : in {found_dict['z']}" + raise ValueError(msg) + z = z[:5] + z[6:] + hours = int(z[1:3]) + minutes = int(z[3:5]) + seconds = int(z[5:7] or 0) + gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds + gmtoff_fraction = int(z[8:] or 0) + if z.startswith("-"): + gmtoff = -gmtoff + gmtoff_fraction = -gmtoff_fraction elif group_key == 'Z': # Since -1 is default value only need to worry about setting tz if # it can be something other than -1. @@ -535,10 +551,6 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): 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 leap_year_fix: # the caller didn't supply a year but asked for Feb 29th. We couldn't @@ -548,7 +560,7 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): return (year, month, day, hour, minute, second, - weekday, julian, tz, tzname, gmtoff), fraction + weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a time struct based on the input string and the @@ -559,11 +571,11 @@ def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): """Return a class cls instance based on the input string and the format string.""" - tt, fraction = _strptime(data_string, format) + tt, fraction, gmtoff_fraction = _strptime(data_string, format) tzname, gmtoff = tt[-2:] args = tt[:6] + (fraction,) if gmtoff is not None: - tzdelta = datetime_timedelta(seconds=gmtoff) + tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction) if tzname: tz = datetime_timezone(tzdelta, tzname) else: diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 4edfb42d35511e..c5f91fbe1840ba 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2147,6 +2147,10 @@ def test_strptime(self): strptime = self.theclass.strptime self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE) self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE) + self.assertEqual( + strptime("-00:02:01.000003", "%z").utcoffset(), + -timedelta(minutes=2, seconds=1, microseconds=3) + ) # Only local timezone and UTC are supported for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'), (-_time.timezone, _time.tzname[0])): diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 934318025753be..1251886779d207 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -305,7 +305,7 @@ def test_fraction(self): # Test microseconds import datetime d = datetime.datetime(2012, 12, 20, 12, 34, 56, 78987) - tup, frac = _strptime._strptime(str(d), format="%Y-%m-%d %H:%M:%S.%f") + tup, frac, _ = _strptime._strptime(str(d), format="%Y-%m-%d %H:%M:%S.%f") self.assertEqual(frac, d.microsecond) def test_weekday(self): @@ -317,6 +317,51 @@ def test_julian(self): # Test julian directives self.helper('j', 7) + def test_offset(self): + one_hour = 60 * 60 + half_hour = 30 * 60 + half_minute = 30 + (*_, offset), _, offset_fraction = _strptime._strptime("+0130", "%z") + self.assertEqual(offset, one_hour + half_hour) + self.assertEqual(offset_fraction, 0) + (*_, offset), _, offset_fraction = _strptime._strptime("-0100", "%z") + self.assertEqual(offset, -one_hour) + self.assertEqual(offset_fraction, 0) + (*_, offset), _, offset_fraction = _strptime._strptime("-013030", "%z") + self.assertEqual(offset, -(one_hour + half_hour + half_minute)) + self.assertEqual(offset_fraction, 0) + (*_, offset), _, offset_fraction = _strptime._strptime("-013030.000001", "%z") + self.assertEqual(offset, -(one_hour + half_hour + half_minute)) + self.assertEqual(offset_fraction, -1) + (*_, offset), _, offset_fraction = _strptime._strptime("+01:00", "%z") + self.assertEqual(offset, one_hour) + self.assertEqual(offset_fraction, 0) + (*_, offset), _, offset_fraction = _strptime._strptime("-01:30", "%z") + self.assertEqual(offset, -(one_hour + half_hour)) + self.assertEqual(offset_fraction, 0) + (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", "%z") + self.assertEqual(offset, -(one_hour + half_hour + half_minute)) + self.assertEqual(offset_fraction, 0) + (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", "%z") + self.assertEqual(offset, -(one_hour + half_hour + half_minute)) + self.assertEqual(offset_fraction, -1) + (*_, offset), _, offset_fraction = _strptime._strptime("Z", "%z") + self.assertEqual(offset, 0) + self.assertEqual(offset_fraction, 0) + + def test_bad_offset(self): + with self.assertRaises(ValueError): + _strptime._strptime("-01:30:30.", "%z") + with self.assertRaises(ValueError): + _strptime._strptime("-0130:30", "%z") + with self.assertRaises(ValueError): + _strptime._strptime("-01:30:30.1234567", "%z") + with self.assertRaises(ValueError): + _strptime._strptime("-01:30:30:123456", "%z") + with self.assertRaises(ValueError) as err: + _strptime._strptime("-01:3030", "%z") + self.assertEqual("Unconsistent use of : in -01:3030", str(err.exception)) + def test_timezone(self): # Test timezone directives. # When gmtime() is used with %Z, entire result of strftime() is empty. diff --git a/Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst b/Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst new file mode 100644 index 00000000000000..1580440a595d5b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst @@ -0,0 +1,3 @@ +Extended support for parsing UTC offsets. strptime '%z' can now +parse the output generated by datetime.isoformat, including seconds and +microseconds.