diff --git a/src/pendulum/parsing/__init__.py b/src/pendulum/parsing/__init__.py index d88c089a6..1301f452a 100644 --- a/src/pendulum/parsing/__init__.py +++ b/src/pendulum/parsing/__init__.py @@ -49,7 +49,7 @@ # Subsecond part (optional) " (?P" " (?:[.|,])" # Subsecond separator (optional) - r" (?P\d{1,9})" # Subsecond + r" (?P\d+)" # Subsecond (any number of digits; truncated to microseconds below) " )?" ")?" "$", diff --git a/src/pendulum/parsing/iso8601.py b/src/pendulum/parsing/iso8601.py index c65d249e5..e3e980814 100644 --- a/src/pendulum/parsing/iso8601.py +++ b/src/pendulum/parsing/iso8601.py @@ -49,7 +49,7 @@ # Subsecond part (optional) " (?P" " (?:[.,])" # Subsecond separator (optional) - r" (?P\d{1,9})" # Subsecond + r" (?P\d+)" # Subsecond (any number of digits; truncated to microseconds below) " )?" # Timezone offset " (?P" diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py index ed2d39887..3f70c39e7 100644 --- a/tests/parsing/test_parse_iso8601.py +++ b/tests/parsing/test_parse_iso8601.py @@ -214,3 +214,18 @@ def test_parse_iso8601_duration_invalid(): # Must include at least one element with pytest.raises(ValueError): parse_iso8601("P") + + +def test_parse_iso8601_subsecond_beyond_nanoseconds_pure_python(): + # Regression test for the pure-Python ISO 8601 parser (used as a fallback + # when the compiled extension is unavailable): a subsecond part longer + # than 9 digits used to raise a ParserError instead of being truncated to + # microsecond precision like `datetime.fromisoformat` does. Imported + # directly so the check runs regardless of whether the Rust extension is + # built, since `pendulum.parsing.parse_iso8601` prefers the extension + # when it's available. + from pendulum.parsing.iso8601 import parse_iso8601 as py_parse_iso8601 + + parsed = py_parse_iso8601("2016-10-06T12:34:56.1234567890123+05:30") + + assert parsed == datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)) diff --git a/tests/parsing/test_parsing.py b/tests/parsing/test_parsing.py index 8151ce12f..33af076af 100644 --- a/tests/parsing/test_parsing.py +++ b/tests/parsing/test_parsing.py @@ -156,6 +156,41 @@ def test_rfc_3339_extended_nanoseconds(): assert parsed.utcoffset().total_seconds() == 19800 +def test_rfc_3339_extended_beyond_nanoseconds(): + # A subsecond part longer than 9 digits (i.e. finer than nanoseconds) is + # unusual but not invalid ISO 8601: it should still be accepted and + # truncated to microsecond precision, the same as `datetime.fromisoformat` + # does, instead of raising a ParserError. + text = "2016-10-06T12:34:56.1234567890123+05:30" + + parsed = parse(text) + + assert parsed.year == 2016 + assert parsed.month == 10 + assert parsed.day == 6 + assert parsed.hour == 12 + assert parsed.minute == 34 + assert parsed.second == 56 + assert parsed.microsecond == 123456 + assert parsed.utcoffset().total_seconds() == 19800 + + +def test_common_format_extended_beyond_nanoseconds(): + # Same as `test_rfc_3339_extended_beyond_nanoseconds()` but for the + # "common" datetime format (handled separately from the ISO 8601 parser). + text = "2016/10/06 12:34:56.1234567890123" + + parsed = parse(text) + + assert parsed.year == 2016 + assert parsed.month == 10 + assert parsed.day == 6 + assert parsed.hour == 12 + assert parsed.minute == 34 + assert parsed.second == 56 + assert parsed.microsecond == 123456 + + def test_iso_8601_date(): text = "2012"