diff -r 6c122e4e1cb2 Lib/datetime.py --- a/Lib/datetime.py Sun Feb 14 03:25:48 2016 +0000 +++ b/Lib/datetime.py Mon Feb 15 22:01:39 2016 +1030 @@ -6,6 +6,7 @@ import time as _time import math as _math +import re def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -146,6 +147,12 @@ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] _DAYNAMES = [None, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +datetime_re = re.compile( + r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' + r'[T ](?P\d{1,2}):(?P\d{1,2})' + r'(?::(?P\d{1,2})(?:(?P\.\d{1,7})\d*)?)?' + r'(?PZ|[+-]\d{2}(?::?\d{2})?)?\$' +) def _build_struct_time(y, m, d, hh, mm, ss, dstflag): wday = (_ymd2ord(y, m, d) + 6) % 7 @@ -1401,6 +1408,38 @@ return result @classmethod + def fromisoformat(cls, value): + """Parses a string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + Microseconds values after 6 digits are discared. + + Raises ValueError if the input is not well formatted or not a valid datetime. + """ + match = datetime_re.match(value) + if not match: + raise ValueError('invalid {} iso8601 string : {}'.format(cls.__name__, value)) + kw = match.groupdict() + tzinfo = kw.pop('tzinfo') + if tzinfo == 'Z': + tzinfo = timezone.utc + elif tzinfo is not None: + offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0 + offset_hours = int(tzinfo[1:3]) + offset = timedelta(hours=offset_hours, minutes=offset_mins) + if tzinfo[0] == '-': + offset = -offset + tzinfo = timezone(offset) + us = kw.pop('microsecond', None) + kw = {k: int(v) for k, v in kw.items() if v is not None} + if us: + us = round(float(us), 6) + kw['microsecond'] = int(us * 1e6) + kw['tzinfo'] = tzinfo + return cls(**kw) + + @classmethod def utcfromtimestamp(cls, t): """Construct a naive UTC datetime from a POSIX timestamp.""" return cls._fromtimestamp(t, True, None) diff -r 6c122e4e1cb2 Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py Sun Feb 14 03:25:48 2016 +0000 +++ b/Lib/test/datetimetester.py Mon Feb 15 22:01:39 2016 +1030 @@ -1847,6 +1847,35 @@ self.assertEqual(expected.tm_min, got.minute) self.assertEqual(expected.tm_sec, got.second) + def test_fromisoformat(self): + # Valid inputs + parse_datetime = self.theclass.fromisoformat + self.assertEqual(parse_datetime('2012-04-23T09:15:00'), + datetime(2012, 4, 23, 9, 15)) + self.assertEqual(parse_datetime('2012-4-9 4:8:16'), + datetime(2012, 4, 9, 4, 8, 16)) + self.assertEqual(parse_datetime('2012-04-23T09:15:00Z'), + datetime(2012, 4, 23, 9, 15, 0, 0, timezone(timedelta(minutes=0)))) + self.assertEqual(parse_datetime('2012-4-9 4:8:16-0320'), + datetime(2012, 4, 9, 4, 8, 16, 0, timezone(timedelta(hours=-3, minutes=-20)))) + self.assertEqual(parse_datetime('2012-04-23T10:20:30.400+02:30'), + datetime(2012, 4, 23, 10, 20, 30, 400000, timezone(timedelta(hours=2, minutes=30)))) + self.assertEqual(parse_datetime('2012-04-23T10:20:30.400+02'), + datetime(2012, 4, 23, 10, 20, 30, 400000, timezone(timedelta(hours=2)))) + self.assertEqual(parse_datetime('2012-04-23T10:20:30.400-02'), + datetime(2012, 4, 23, 10, 20, 30, 400000, timezone(timedelta(hours=-2)))) + # rounding high + self.assertEqual(parse_datetime('2012-04-23T10:20:30.4000059'), + datetime(2012, 4, 23, 10, 20, 30, 400006)) + # rounding low + testing long usec decimal we don't care about + self.assertEqual(parse_datetime('2012-04-23T10:20:30.400003434'), + datetime(2012, 4, 23, 10, 20, 30, 400003)) + # Invalid inputs + with self.assertRaises(ValueError): + parse_datetime('20120423091500') + parse_datetime('2012-04-56T09:15:90') + + def test_fromtimestamp(self): import time