From df458b7257e76dd13618ac360058fe0a801af701 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 10:03:54 -0800 Subject: [PATCH 1/9] Added fromisoformat() to date and datetime --- adafruit_datetime.py | 42 +++++++++++++++++++++++++++++++++ examples/datetime_simpletest.py | 6 +++++ 2 files changed, 48 insertions(+) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index c9d34fd..dd69e62 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -29,6 +29,7 @@ # pylint: disable=too-many-lines import time as _time import math as _math +import re as _re from micropython import const __version__ = "0.0.0-auto.0" @@ -657,6 +658,18 @@ def fromordinal(cls, ordinal): y, m, d = _ord2ymd(ordinal) return cls(y, m, d) + @classmethod + def fromisoformat(cls, date_string): + """Return a date object constructed from an ISO date format.""" + match = _re.match( + r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])", date_string + ) + if match: + y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3)) + return cls(y, m, d) + else: + raise ValueError("Not a valid ISO Date") + @classmethod def today(cls): """Return the current local date.""" @@ -1206,6 +1219,35 @@ def _fromtimestamp(cls, t, utc, tz): def fromtimestamp(cls, timestamp, tz=None): return cls._fromtimestamp(timestamp, tz is not None, tz) + @classmethod + def fromisoformat(cls, date_string, tz=None): + """Return a date object constructed from an ISO date format. + YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] + """ + match = _re.match( + r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(T([0-9][0-9]))?(:([0-9][0-9]))?(:([0-9][0-9]))?(\.[0-9][0-9][0-9][0-9][0-9][0-9])?", + date_string, + ) + if match: + y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3)) + hh = int(match.group(5)) if match.group(5) else 0 + mm = int(match.group(5)) if match.group(7) else 0 + ss = int(match.group(9)) if match.group(9) else 0 + us = round((float("0" + match.group(10)) if match.group(10) else 0) * 1e6) + result = cls( + y, + m, + d, + hh, + mm, + ss, + us, + tz, + ) + return result + else: + raise ValueError("Not a valid ISO Date") + @classmethod def now(cls, timezone=None): """Return the current local date and time.""" diff --git a/examples/datetime_simpletest.py b/examples/datetime_simpletest.py index 4c42b82..cf97a6b 100644 --- a/examples/datetime_simpletest.py +++ b/examples/datetime_simpletest.py @@ -6,6 +6,7 @@ # All rights reserved. # SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. # SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries # SPDX-License-Identifier: Python-2.0 # Example of working with a `datetime` object @@ -28,3 +29,8 @@ print(it) print("Today is: ", dt.ctime()) + +iso_date_string = "2020-04-05T05:04:45.752301" +print("Creating new datetime from ISO Date:", iso_date_string) +isodate = datetime.fromisoformat(iso_date_string) +print("Formatted back out as ISO Date: ", isodate.isoformat()) From 169afbe2c0dc4f32ec95193e897d18a7bad460f2 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 10:10:21 -0800 Subject: [PATCH 2/9] Linted --- adafruit_datetime.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index dd69e62..ff96c6c 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -667,8 +667,7 @@ def fromisoformat(cls, date_string): if match: y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3)) return cls(y, m, d) - else: - raise ValueError("Not a valid ISO Date") + raise ValueError("Not a valid ISO Date") @classmethod def today(cls): @@ -1225,7 +1224,10 @@ def fromisoformat(cls, date_string, tz=None): YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] """ match = _re.match( - r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(T([0-9][0-9]))?(:([0-9][0-9]))?(:([0-9][0-9]))?(\.[0-9][0-9][0-9][0-9][0-9][0-9])?", + ( + r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(T([0-9][0-9]))?(:([0-9][0-9]))" + r"?(:([0-9][0-9]))?(\.[0-9][0-9][0-9][0-9][0-9][0-9])?" + ), date_string, ) if match: @@ -1245,8 +1247,7 @@ def fromisoformat(cls, date_string, tz=None): tz, ) return result - else: - raise ValueError("Not a valid ISO Date") + raise ValueError("Not a valid ISO Date") @classmethod def now(cls, timezone=None): From 4252da7a695e55d4a12a36b45269a5d41a128ff0 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 10:16:17 -0800 Subject: [PATCH 3/9] Make docs happy --- adafruit_datetime.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index ff96c6c..ae77116 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -660,7 +660,10 @@ def fromordinal(cls, ordinal): @classmethod def fromisoformat(cls, date_string): - """Return a date object constructed from an ISO date format.""" + """Return a date object constructed from an ISO date format. + Valid format is YYYY-MM-DD + + """ match = _re.match( r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])", date_string ) @@ -1220,8 +1223,9 @@ def fromtimestamp(cls, timestamp, tz=None): @classmethod def fromisoformat(cls, date_string, tz=None): - """Return a date object constructed from an ISO date format. - YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] + """Return a datetime object constructed from an ISO date format. + Valid format is YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] + """ match = _re.match( ( From da727d5ee10b4181f012706c503d7f0b96126807 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 10:42:40 -0800 Subject: [PATCH 4/9] Allow 3 and 6 decimal floats and update valid format string --- adafruit_datetime.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index ae77116..b5378c2 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -1224,13 +1224,13 @@ def fromtimestamp(cls, timestamp, tz=None): @classmethod def fromisoformat(cls, date_string, tz=None): """Return a datetime object constructed from an ISO date format. - Valid format is YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] + Valid format is YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]]] """ match = _re.match( ( - r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(T([0-9][0-9]))?(:([0-9][0-9]))" - r"?(:([0-9][0-9]))?(\.[0-9][0-9][0-9][0-9][0-9][0-9])?" + r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(T([0-9][0-9]))?(:([0-9][0-9]))?" + r"(:([0-9][0-9]))?(\.([0-9][0-9][0-9])([0-9][0-9][0-9])?)?" ), date_string, ) @@ -1239,7 +1239,12 @@ def fromisoformat(cls, date_string, tz=None): hh = int(match.group(5)) if match.group(5) else 0 mm = int(match.group(5)) if match.group(7) else 0 ss = int(match.group(9)) if match.group(9) else 0 - us = round((float("0" + match.group(10)) if match.group(10) else 0) * 1e6) + us = 0 + print(match.group(10)) + if match.group(11): + us += int(match.group(11)) * 1000 + if match.group(12): + us += int(match.group(12)) result = cls( y, m, From aceaa3594fc58b4e33c379e0157553a9fb1d4918 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 10:47:34 -0800 Subject: [PATCH 5/9] Finally fixed what build docs was complaining about --- adafruit_datetime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index b5378c2..d089b53 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -661,7 +661,7 @@ def fromordinal(cls, ordinal): @classmethod def fromisoformat(cls, date_string): """Return a date object constructed from an ISO date format. - Valid format is YYYY-MM-DD + Valid format is ``YYYY-MM-DD`` """ match = _re.match( @@ -1224,7 +1224,7 @@ def fromtimestamp(cls, timestamp, tz=None): @classmethod def fromisoformat(cls, date_string, tz=None): """Return a datetime object constructed from an ISO date format. - Valid format is YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]]] + Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]]]`` """ match = _re.match( From f611e90e5cae87765e385bf810ed057460aebd7d Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 14:51:49 -0800 Subject: [PATCH 6/9] Implemented Timezone Offset functionality and enabled unit tests --- adafruit_datetime.py | 131 ++++++++++++++++++++++++++++++----------- tests/test_date.py | 13 ++++ tests/test_datetime.py | 3 - 3 files changed, 110 insertions(+), 37 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index d089b53..7af0f40 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -665,12 +665,12 @@ def fromisoformat(cls, date_string): """ match = _re.match( - r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])", date_string + r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$", date_string ) if match: y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3)) return cls(y, m, d) - raise ValueError("Not a valid ISO Date") + raise ValueError("Invalid isoformat string") @classmethod def today(cls): @@ -922,6 +922,90 @@ def tzinfo(self): """ return self._tzinfo + @staticmethod + def _parse_iso_string(string_to_parse, segments): + results = [] + + for regex in segments: + match = _re.match(regex, string_to_parse) + if match: + for grp in range(regex.count("(")): + results.append(int(match.group(grp + 1))) + string_to_parse = string_to_parse[len(match.group(0)) :] + elif string_to_parse: # Only raise an error if we're not done yet + raise ValueError("Invalid isoformat string") + if string_to_parse: + raise ValueError("Invalid isoformat string") + return results + + # pylint: disable=too-many-locals + @classmethod + def fromisoformat(cls, time_string): + """Return a time object constructed from an ISO date format. + Valid format is ``HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]`` + + """ + match = _re.match(r"(.*)[\-\+]", time_string) + offset_string = None + if match: + offset_string = time_string[len(match.group(1)) :] + time_string = match.group(1) + + time_segments = ( + r"([0-9][0-9])", + r":([0-9][0-9])", + r":([0-9][0-9])", + r"\.([0-9][0-9][0-9])", + r"([0-9][0-9][0-9])", + ) + offset_segments = ( + r"([\-\+][0-9][0-9]):([0-9][0-9])", + r":([0-9][0-9])", + r"\.([0-9][0-9][0-9][0-9][0-9][0-9])", + ) + + results = cls._parse_iso_string(time_string, time_segments) + if len(results) < 1: + raise ValueError("Invalid isoformat string") + if len(results) < len(time_segments): + results += [None] * (len(time_segments) - len(results)) + if offset_string: + results += cls._parse_iso_string(offset_string, offset_segments) + + hh = results[0] + mm = results[1] if len(results) >= 2 and results[1] is not None else 0 + ss = results[2] if len(results) >= 3 and results[2] is not None else 0 + us = 0 + if len(results) >= 4 and results[3] is not None: + us += results[3] * 1000 + if len(results) >= 5 and results[4] is not None: + us += results[4] + tz = None + if len(results) >= 7: + offset_hh = results[5] + multiplier = -1 if offset_hh < 0 else 1 + offset_mm = results[6] * multiplier + offset_ss = (results[7] if len(results) >= 8 else 0) * multiplier + offset_us = (results[8] if len(results) >= 9 else 0) * multiplier + offset = timedelta( + hours=offset_hh, + minutes=offset_mm, + seconds=offset_ss, + microseconds=offset_us, + ) + tz = timezone(offset, name="utcoffset") + + result = cls( + hh, + mm, + ss, + us, + tz, + ) + return result + + # pylint: enable=too-many-locals + # Instance methods def isoformat(self, timespec="auto"): """Return a string representing the time in ISO 8601 format, one of: @@ -1222,41 +1306,20 @@ def fromtimestamp(cls, timestamp, tz=None): return cls._fromtimestamp(timestamp, tz is not None, tz) @classmethod - def fromisoformat(cls, date_string, tz=None): + def fromisoformat(cls, date_string): """Return a datetime object constructed from an ISO date format. - Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]]]`` + Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]`` """ - match = _re.match( - ( - r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(T([0-9][0-9]))?(:([0-9][0-9]))?" - r"(:([0-9][0-9]))?(\.([0-9][0-9][0-9])([0-9][0-9][0-9])?)?" - ), - date_string, - ) - if match: - y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3)) - hh = int(match.group(5)) if match.group(5) else 0 - mm = int(match.group(5)) if match.group(7) else 0 - ss = int(match.group(9)) if match.group(9) else 0 - us = 0 - print(match.group(10)) - if match.group(11): - us += int(match.group(11)) * 1000 - if match.group(12): - us += int(match.group(12)) - result = cls( - y, - m, - d, - hh, - mm, - ss, - us, - tz, - ) - return result - raise ValueError("Not a valid ISO Date") + if "T" in date_string: + date_string, time_string = date_string.split("T") + dateval = date.fromisoformat(date_string) + timeval = time.fromisoformat(time_string) + else: + dateval = date.fromisoformat(date_string) + timeval = time() + + return cls.combine(dateval, timeval) @classmethod def now(cls, timezone=None): diff --git a/tests/test_date.py b/tests/test_date.py index 8b9626c..2e5e09c 100644 --- a/tests/test_date.py +++ b/tests/test_date.py @@ -88,6 +88,19 @@ def test_fromtimestamp(self): self.assertEqual(d.month, month) self.assertEqual(d.day, day) + def test_fromisoformat(self): + # Try an arbitrary fixed value. + iso_date_string = "1999-09-19" + d = cpy_date.fromisoformat(iso_date_string) + self.assertEqual(d.year, 1999) + self.assertEqual(d.month, 9) + self.assertEqual(d.day, 19) + + def test_fromisoformat_bad_formats(self): + # Try an arbitrary fixed value. + self.assertRaises(ValueError, cpy_date.fromisoformat, "99-09-19") + self.assertRaises(ValueError, cpy_date.fromisoformat, "1999-13-19") + # TODO: Test this when timedelta is added in @unittest.skip("Skip for CircuitPython - timedelta() not yet implemented.") def test_today(self): diff --git a/tests/test_datetime.py b/tests/test_datetime.py index a3e90b0..d21c58e 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -1161,8 +1161,6 @@ def test_fromisoformat_timespecs(self): dt_rt = self.theclass.fromisoformat(dtstr) self.assertEqual(dt, dt_rt) - # TODO - @unittest.skip("fromisoformat not implemented") def test_fromisoformat_fails_datetime(self): # Test that fromisoformat() fails on invalid values bad_strs = [ @@ -1219,7 +1217,6 @@ def test_fromisoformat_utc(self): self.assertIs(dt.tzinfo, timezone.utc) - # TODO @unittest.skip("fromisoformat not implemented") def test_fromisoformat_subclass(self): class DateTimeSubclass(self.theclass): From 0d1acd12d4f30960ab23f16caaa2bd49cb7d155c Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 15:50:33 -0800 Subject: [PATCH 7/9] Passing another test. Fixed the separator to be any character --- adafruit_datetime.py | 11 +++++++++-- tests/test_datetime.py | 2 -- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index 7af0f40..3dad513 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -1262,6 +1262,11 @@ def tzinfo(self): """ return self._tzinfo + @property + def fold(self): + """Fold.""" + return self._fold + # Class methods # pylint: disable=protected-access @@ -1311,8 +1316,10 @@ def fromisoformat(cls, date_string): Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]`` """ - if "T" in date_string: - date_string, time_string = date_string.split("T") + time_string = None + if len(date_string) > 10: + time_string = date_string[11:] + date_string = date_string[:10] dateval = date.fromisoformat(date_string) timeval = time.fromisoformat(time_string) else: diff --git a/tests/test_datetime.py b/tests/test_datetime.py index d21c58e..de64167 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -1033,8 +1033,6 @@ def __new__(cls, *args, **kwargs): self.assertIsInstance(dt, DateTimeSubclass) self.assertEqual(dt.extra, 7) - # TODO - @unittest.skip("timezone not implemented") def test_fromisoformat_datetime(self): # Test that isoformat() is reversible base_dates = [(1, 1, 1), (1900, 1, 1), (2004, 11, 12), (2017, 5, 30)] From 4e7900b62dd2c7b6b5e5c76b3f04a509948e994d Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 15:55:53 -0800 Subject: [PATCH 8/9] Enabled more unit tests --- tests/test_datetime.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_datetime.py b/tests/test_datetime.py index de64167..c92e142 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -1095,8 +1095,6 @@ def test_fromisoformat_timezone(self): dt_rt = self.theclass.fromisoformat(dtstr) assert dt == dt_rt, dt_rt - # TODO - @unittest.skip("fromisoformat not implemented") def test_fromisoformat_separators(self): separators = [ " ", @@ -1118,8 +1116,6 @@ def test_fromisoformat_separators(self): dt_rt = self.theclass.fromisoformat(dtstr) self.assertEqual(dt, dt_rt) - # TODO - @unittest.skip("fromisoformat not implemented") def test_fromisoformat_ambiguous(self): # Test strings like 2018-01-31+12:15 (where +12:15 is not a time zone) separators = ["+", "-"] @@ -1215,7 +1211,6 @@ def test_fromisoformat_utc(self): self.assertIs(dt.tzinfo, timezone.utc) - @unittest.skip("fromisoformat not implemented") def test_fromisoformat_subclass(self): class DateTimeSubclass(self.theclass): pass From 898031f37828128892404431efcef6f638f6b470 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 18 Feb 2021 16:30:07 -0800 Subject: [PATCH 9/9] Fix error message to match original datetime lib and enabled more unit tests --- adafruit_datetime.py | 57 ++++++++++++++++++++++++++---------------- tests/test_datetime.py | 6 ++--- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/adafruit_datetime.py b/adafruit_datetime.py index 3dad513..735ac3b 100755 --- a/adafruit_datetime.py +++ b/adafruit_datetime.py @@ -63,6 +63,8 @@ ) _DAYNAMES = (None, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") +_INVALID_ISO_ERROR = "Invalid isoformat string: '{}'" + # Utility functions - universal def _cmp(obj_x, obj_y): return 0 if obj_x == obj_y else 1 if obj_x > obj_y else -1 @@ -670,7 +672,7 @@ def fromisoformat(cls, date_string): if match: y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3)) return cls(y, m, d) - raise ValueError("Invalid isoformat string") + raise ValueError(_INVALID_ISO_ERROR.format(date_string)) @classmethod def today(cls): @@ -926,16 +928,17 @@ def tzinfo(self): def _parse_iso_string(string_to_parse, segments): results = [] + remaining_string = string_to_parse for regex in segments: - match = _re.match(regex, string_to_parse) + match = _re.match(regex, remaining_string) if match: for grp in range(regex.count("(")): results.append(int(match.group(grp + 1))) - string_to_parse = string_to_parse[len(match.group(0)) :] - elif string_to_parse: # Only raise an error if we're not done yet - raise ValueError("Invalid isoformat string") - if string_to_parse: - raise ValueError("Invalid isoformat string") + remaining_string = remaining_string[len(match.group(0)) :] + elif remaining_string: # Only raise an error if we're not done yet + raise ValueError() + if remaining_string: + raise ValueError() return results # pylint: disable=too-many-locals @@ -945,6 +948,8 @@ def fromisoformat(cls, time_string): Valid format is ``HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]`` """ + # Store the original string in an error message + original_string = time_string match = _re.match(r"(.*)[\-\+]", time_string) offset_string = None if match: @@ -964,13 +969,16 @@ def fromisoformat(cls, time_string): r"\.([0-9][0-9][0-9][0-9][0-9][0-9])", ) - results = cls._parse_iso_string(time_string, time_segments) - if len(results) < 1: - raise ValueError("Invalid isoformat string") - if len(results) < len(time_segments): - results += [None] * (len(time_segments) - len(results)) - if offset_string: - results += cls._parse_iso_string(offset_string, offset_segments) + try: + results = cls._parse_iso_string(time_string, time_segments) + if len(results) < 1: + raise ValueError(_INVALID_ISO_ERROR.format(original_string)) + if len(results) < len(time_segments): + results += [None] * (len(time_segments) - len(results)) + if offset_string: + results += cls._parse_iso_string(offset_string, offset_segments) + except ValueError as error: + raise ValueError(_INVALID_ISO_ERROR.format(original_string)) from error hh = results[0] mm = results[1] if len(results) >= 2 and results[1] is not None else 0 @@ -1316,15 +1324,20 @@ def fromisoformat(cls, date_string): Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]`` """ + original_string = date_string + time_string = None - if len(date_string) > 10: - time_string = date_string[11:] - date_string = date_string[:10] - dateval = date.fromisoformat(date_string) - timeval = time.fromisoformat(time_string) - else: - dateval = date.fromisoformat(date_string) - timeval = time() + try: + if len(date_string) > 10: + time_string = date_string[11:] + date_string = date_string[:10] + dateval = date.fromisoformat(date_string) + timeval = time.fromisoformat(time_string) + else: + dateval = date.fromisoformat(date_string) + timeval = time() + except ValueError as error: + raise ValueError(_INVALID_ISO_ERROR.format(original_string)) from error return cls.combine(dateval, timeval) diff --git a/tests/test_datetime.py b/tests/test_datetime.py index c92e142..140bd84 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -1128,7 +1128,7 @@ def test_fromisoformat_ambiguous(self): self.assertEqual(dt, dt_rt) # TODO - @unittest.skip("fromisoformat not implemented") + @unittest.skip("_format_time not fully implemented") def test_fromisoformat_timespecs(self): datetime_bases = [(2009, 12, 4, 8, 17, 45, 123456), (2009, 12, 4, 8, 17, 45, 0)] @@ -1193,14 +1193,12 @@ def test_fromisoformat_fails_datetime(self): with self.assertRaises(ValueError): self.theclass.fromisoformat(bad_str) - # TODO - @unittest.skip("fromisoformat not implemented") def test_fromisoformat_fails_surrogate(self): # Test that when fromisoformat() fails with a surrogate character as # the separator, the error message contains the original string dtstr = "2018-01-03\ud80001:0113" - with self.assertRaisesRegex(ValueError, re.escape(repr(dtstr))): + with self.assertRaisesRegex(ValueError, repr(dtstr)): self.theclass.fromisoformat(dtstr) # TODO