diff --git a/oslo_utils/tests/test_timeutils.py b/oslo_utils/tests/test_timeutils.py index e886c20c..09cd031f 100644 --- a/oslo_utils/tests/test_timeutils.py +++ b/oslo_utils/tests/test_timeutils.py @@ -62,6 +62,26 @@ class TimeUtilsTest(test_base.BaseTestCase): ) self.assertEqual(skynet_self_aware_time_ms_utc, expect) + def test_parse_isotime_naive_treated_as_utc(self): + """Verify naive timestamps are treated as UTC (documented behavior).""" + # Naive string without timezone should be treated as UTC + result = timeutils.parse_isotime('2012-02-14T20:53:07') + self.assertIsNotNone(result.tzinfo) + assert result.tzinfo is not None # for type checker + self.assertEqual(result.tzinfo.tzname(None), 'UTC') + + # Explicit Z should also be UTC + result_z = timeutils.parse_isotime('2012-02-14T20:53:07Z') + assert result_z.tzinfo is not None # for type checker + self.assertEqual(result_z.tzinfo.tzname(None), 'UTC') + + # Explicit offset should be respected + result_offset = timeutils.parse_isotime('2012-02-14T20:53:07+05:30') + self.assertIsNotNone(result_offset.tzinfo) + assert result_offset.tzinfo is not None # for type checker + offset = result_offset.tzinfo.utcoffset(None) + self.assertEqual(offset, datetime.timedelta(hours=5, minutes=30)) + def test_parse_strtime(self): perfect_time_format = self.skynet_self_aware_time_perfect_str expect = timeutils.parse_strtime(perfect_time_format) diff --git a/oslo_utils/timeutils.py b/oslo_utils/timeutils.py index c677fde9..6d5f7af5 100644 --- a/oslo_utils/timeutils.py +++ b/oslo_utils/timeutils.py @@ -41,7 +41,36 @@ now = time.monotonic def parse_isotime(timestr: str) -> datetime.datetime: - """Parse time from ISO 8601 format.""" + """Parse time from ISO 8601 format. + + :param timestr: ISO 8601 formatted datetime string + :returns: A timezone-aware datetime.datetime instance + :raises ValueError: When the string cannot be parsed as ISO 8601 + + .. note:: + For historical reasons, datetime strings without explicit timezone + designators (Z, +HH:MM, -HH:MM) are treated as UTC timestamps rather + than naive/local time as specified by ISO 8601. This behavior is + preserved for backward compatibility. + + For ISO 8601 compliant parsing that returns naive datetime objects + when no timezone is specified, consider using + ``datetime.datetime.fromisoformat()`` (Python 3.7+) or the ``iso8601`` + library directly with ``default_timezone=None``. + + Examples: + + >>> # Strings without timezone designators are treated as UTC + >>> parse_isotime('2012-02-14T20:53:07') + datetime.datetime(2012, 2, 14, 20, 53, 7, tzinfo=) + + >>> # Explicit timezone designators are respected + >>> parse_isotime('2012-02-14T20:53:07Z') + datetime.datetime(2012, 2, 14, 20, 53, 7, tzinfo=) + + >>> parse_isotime('2012-02-14T20:53:07+05:30') + datetime.datetime(2012, 2, 14, 20, 53, 7, tzinfo=<+05:30>) + """ try: return iso8601.parse_date(timestr) except iso8601.ParseError as e: diff --git a/releasenotes/notes/document-parse-isotime-utc-behavior-1638124-8f3a1b5e9c4d2f1a.yaml b/releasenotes/notes/document-parse-isotime-utc-behavior-1638124-8f3a1b5e9c4d2f1a.yaml new file mode 100644 index 00000000..53900fd0 --- /dev/null +++ b/releasenotes/notes/document-parse-isotime-utc-behavior-1638124-8f3a1b5e9c4d2f1a.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + `Bug #1638124 `_: + The ``parse_isotime()`` function's docstring now clearly documents that + datetime strings without explicit timezone designators (Z, +HH:MM, -HH:MM) + are treated as UTC timestamps rather than naive/local time as specified + by ISO 8601. This behavior exists for historical reasons and backward + compatibility. The documentation now points users to alternatives + (``datetime.fromisoformat()`` or the ``iso8601`` library directly with + ``default_timezone=None``) for ISO 8601 compliant parsing.