From ce1220a32ba5220e4b4f2052571c83bae5a863c5 Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Sun, 15 Jun 2025 10:52:15 +0100 Subject: [PATCH] Make empty durations an error in pure-Python parser Some of Debian's test runners noticed that the pydantic-extra-types tests are failing on 32-bit architectures: ______________________ test_invalid_zero_duration_string _______________________ def test_invalid_zero_duration_string(): """'P' is not a valid ISO 8601 duration and should raise a validation error.""" > with pytest.raises(ValidationError): E Failed: DID NOT RAISE tests/test_pendulum_dt.py:447: Failed Debian currently has pendulum 3.0.0, which disabled the Rust extensions if `struct.calcsize("P") == 4`, and the Rust and Python parsers disagree about how to handle an empty duration: the Rust parser reports an error, while the Python parser returns `Duration()`. 3.1.0 removes that particular limitation on using Rust extensions on 32-bit architectures, but the parser discrepancy still seems to be present. I don't have access to the full text of the standard, but Wikipedia's summary says 'However, at least one element must be present, thus "P" is not a valid representation for a duration of 0 seconds', so I think the Rust parser is correct. Adjust the Python parser to match. --- src/pendulum/parsing/iso8601.py | 2 +- tests/parsing/test_parse_iso8601.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pendulum/parsing/iso8601.py b/src/pendulum/parsing/iso8601.py index 908cdc75..c65d249e 100644 --- a/src/pendulum/parsing/iso8601.py +++ b/src/pendulum/parsing/iso8601.py @@ -265,7 +265,7 @@ def parse_iso8601( def _parse_iso8601_duration(text: str, **options: str) -> Duration | None: m = ISO8601_DURATION.fullmatch(text) - if not m: + if not m or (not m.group("w") and not m.group("ymd") and not m.group("hms")): return None years = 0 diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py index c15b9bd9..ed2d3988 100644 --- a/tests/parsing/test_parse_iso8601.py +++ b/tests/parsing/test_parse_iso8601.py @@ -90,7 +90,7 @@ def test_parse_iso8601(text: str, expected: date) -> None: assert parse_iso8601(text) == expected -def test_parse_ios8601_invalid(): +def test_parse_iso8601_invalid(): # Invalid month with pytest.raises(ValueError): parse_iso8601("20161306T123456") @@ -193,7 +193,7 @@ def test_parse_ios8601_invalid(): ("P2Y30M4DT5H6M7S", (2, 30, 0, 4, 5, 6, 7, 0)), ], ) -def test_parse_ios8601_duration( +def test_parse_iso8601_duration( text: str, expected: tuple[int, int, int, int, int, int, int, int] ) -> None: parsed = parse_iso8601(text) @@ -208,3 +208,9 @@ def test_parse_ios8601_duration( parsed.remaining_seconds, parsed.microseconds, ) == expected + + +def test_parse_iso8601_duration_invalid(): + # Must include at least one element + with pytest.raises(ValueError): + parse_iso8601("P")