From 716e85b42a6a08b3f00fd7cc68f3e05c3dff0e22 Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:26:21 -0700 Subject: [PATCH] Parse timezone names with multiple slash-separated components AI assistance: OpenAI Codex was used for investigation, implementation, regression tests, and automated validation. --- docs/docs/instantiation.md | 8 +++++ src/pendulum/formatting/formatter.py | 2 +- tests/datetime/test_from_format.py | 46 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/docs/instantiation.md b/docs/docs/instantiation.md index a49c6a26c..23aa5bd2c 100644 --- a/docs/docs/instantiation.md +++ b/docs/docs/instantiation.md @@ -119,6 +119,14 @@ It also accepts a `tz` keyword argument to specify the timezone: '1975-05-21T22:00:00+01:00' ``` +Use the `z` token to read a timezone name from the input, including names with multiple slashes: + +```python +>>> dt = pendulum.from_format('2024-01-15 America/Argentina/Buenos_Aires', 'YYYY-MM-DD z') +>>> dt.timezone_name +'America/Argentina/Buenos_Aires' +``` + The final helper is for working with unix timestamps. `from_timestamp()` will create a `DateTime` instance equal to the given timestamp and will set the timezone as well or default it to `UTC`. diff --git a/src/pendulum/formatting/formatter.py b/src/pendulum/formatting/formatter.py index b09977d0d..600bd3473 100644 --- a/src/pendulum/formatting/formatter.py +++ b/src/pendulum/formatting/formatter.py @@ -42,7 +42,7 @@ "['a-z\u00a0-\u05ff\u0700-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]+" r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}" ) -_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?" +_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)*" class Formatter: diff --git a/tests/datetime/test_from_format.py b/tests/datetime/test_from_format.py index ecf59632a..362f69c45 100644 --- a/tests/datetime/test_from_format.py +++ b/tests/datetime/test_from_format.py @@ -45,6 +45,52 @@ def test_from_format_with_square_bracket_in_timezone(): ) +@pytest.mark.parametrize( + "zone", + [ + "America/Argentina/Buenos_Aires", + "America/Indiana/Indianapolis", + "America/Kentucky/Louisville", + "America/North_Dakota/New_Salem", + "Europe/Paris", + "UTC", + ], +) +@pytest.mark.parametrize("month", [1, 7]) +@pytest.mark.parametrize( + "fmt", + [ + "YYYY-MM-DD HH:mm:ss.SSSSSS z", + "z YYYY-MM-DD HH:mm:ss.SSSSSS", + "z/YYYY-MM-DD HH:mm:ss.SSSSSS", + ], +) +def test_from_format_with_nested_timezone(zone: str, month: int, fmt: str) -> None: + expected = pendulum.datetime(2024, month, 15, 12, 34, 56, 123456, tz=zone) + + actual = pendulum.from_format(expected.format(fmt), fmt, tz="Europe/London") + + assert actual == expected + assert actual.timezone_name == zone + assert actual.utcoffset() == expected.utcoffset() + + +@pytest.mark.parametrize( + "zone", + [ + "America/Argentina/Not_A_City", + "America/Argentina/Buenos_Aires/Extra", + "America//Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires/", + "/America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires extra", + ], +) +def test_from_format_rejects_invalid_nested_timezone(zone: str) -> None: + with pytest.raises(ValueError): + pendulum.from_format(f"2024-01-15 12:34:56 {zone}", "YYYY-MM-DD HH:mm:ss z") + + def test_from_format_with_escaped_elements(): d = pendulum.from_format("1975-05-21T22:32:11+00:00", "YYYY-MM-DD[T]HH:mm:ssZ") assert_datetime(d, 1975, 5, 21, 22, 32, 11)