Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pendulum/parsing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
# Subsecond part (optional)
" (?P<subsecondsection>"
" (?:[.|,])" # Subsecond separator (optional)
r" (?P<subsecond>\d{1,9})" # Subsecond
r" (?P<subsecond>\d+)" # Subsecond (any number of digits; truncated to microseconds below)
" )?"
")?"
"$",
Expand Down
2 changes: 1 addition & 1 deletion src/pendulum/parsing/iso8601.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
# Subsecond part (optional)
" (?P<subsecondsection>"
" (?:[.,])" # Subsecond separator (optional)
r" (?P<subsecond>\d{1,9})" # Subsecond
r" (?P<subsecond>\d+)" # Subsecond (any number of digits; truncated to microseconds below)
" )?"
# Timezone offset
" (?P<tz>"
Expand Down
15 changes: 15 additions & 0 deletions tests/parsing/test_parse_iso8601.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
35 changes: 35 additions & 0 deletions tests/parsing/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down