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
20 changes: 19 additions & 1 deletion docs/docs/parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The library natively supports the RFC 3339 format, most ISO 8601 formats and som

>>> dt = pendulum.parse('1975-05-21T22:00:00')
>>> print(dt)
'1975-05-21T22:00:00+00:00
'1975-05-21T22:00:00+00:00'

# You can pass a tz keyword to specify the timezone
>>> dt = pendulum.parse('1975-05-21T22:00:00', tz='Europe/Paris')
Expand Down Expand Up @@ -112,3 +112,21 @@ When passing only time information the date will default to today.
>>> pendulum.parse('12:04:23', exact=True)
Time(12, 04, 23)
```
### Strict parsing helpers

Pendulum also provides helper functions for strict parsing of specific types:

```python
>>> import pendulum

>>> pendulum.parse_datetime("2016-10-16T12:34:56")
DateTime(...)

>>> pendulum.parse_date("2016-10-16")
Date(2016, 10, 16)

>>> pendulum.parse_time("12:34:56")
Time(12, 34, 56)

>>> pendulum.parse_duration("PT2H")
Duration(...)
4 changes: 4 additions & 0 deletions src/pendulum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
from pendulum.helpers import week_starts_at
from pendulum.interval import Interval
from pendulum.parser import parse as parse
from pendulum.parser import parse_date as parse_date
from pendulum.parser import parse_datetime as parse_datetime
from pendulum.parser import parse_duration as parse_duration
from pendulum.parser import parse_time as parse_time
from pendulum.time import Time
from pendulum.tz import UTC
from pendulum.tz import fixed_timezone
Expand Down
58 changes: 55 additions & 3 deletions src/pendulum/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

import pendulum

from pendulum.date import Date
from pendulum.datetime import DateTime
from pendulum.duration import Duration
from pendulum.parsing import _Interval
from pendulum.parsing import parse as base_parse
from pendulum.time import Time
from pendulum.tz.timezone import UTC


if t.TYPE_CHECKING:
from pendulum.date import Date
from pendulum.datetime import DateTime
from pendulum.interval import Interval
from pendulum.time import Time

with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"

Expand Down Expand Up @@ -134,3 +134,55 @@ def _parse(
)

raise NotImplementedError


def parse_datetime(text: str, **options: t.Any) -> DateTime:
"""
Parses a string into a DateTime.

:param text: The string to parse.
"""
parsed = parse(text, **options)
if not isinstance(parsed, DateTime):
raise ValueError(f"Invalid datetime string: {text}")
return parsed


def parse_date(text: str, **options: t.Any) -> Date:
"""
Parses a string into a Date.

:param text: The string to parse.
"""
parsed = parse(text, exact = True, **options)
if not isinstance(parsed, Date):
if isinstance(parsed, DateTime):
return parsed.date()
raise ValueError(f"Invalid date string: {text}")
return parsed


def parse_time(text: str, **options: t.Any) -> Time:
"""
Parses a string into a Time.

:param text: The string to parse.
"""
parsed = parse(text, exact = True, **options)
if not isinstance(parsed, Time):
if isinstance(parsed, DateTime):
return parsed.time()
raise ValueError(f"Invalid time string: {text}")
return parsed


def parse_duration(text: str, **options: t.Any) -> Duration:
"""
Parses a string into a Duration.

:param text: The string to parse.
"""
parsed = parse(text, **options)
if not isinstance(parsed, Duration):
raise ValueError(f"Invalid duration string: {text}")
return parsed
105 changes: 105 additions & 0 deletions tests/parsing/test_strict_parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations

import pytest

from pendulum.parser import parse_date
from pendulum.parser import parse_datetime
from pendulum.parser import parse_duration
from pendulum.parser import parse_time


# tests for parse_datetime


def test_parse_datetime_valid() -> None:
dt = parse_datetime("2026-03-19T11:28:37")
assert dt.year == 2026


def test_parse_datetime_accepts_date() -> None:
dt = parse_datetime("2026-03-19")
assert dt.hour == 0


def test_parse_datetime_rejects_duration() -> None:
with pytest.raises(ValueError):
parse_datetime("PT2H")


def test_parse_datetime_rejects_interval() -> None:
with pytest.raises(ValueError):
parse_datetime("2026-03-19T12:00:00/2026-03-19T13:00:00")


def test_parse_datetime_accepts_only_year() -> None:
dt = parse_datetime("2026")
assert dt.year == 2026


# tests for parse_date


def test_parse_date_valid() -> None:
d = parse_date("2026-03-19")
assert d.day == 19


def test_parse_date_accepts_datetime() -> None:
d = parse_date("2026-03-19T11:28:37")
assert d.day == 19


def test_parse_date_rejects_time() -> None:
with pytest.raises(ValueError):
parse_date("11:28:37")


def test_parse_date_rejects_duration() -> None:
with pytest.raises(ValueError):
parse_date("PT2H")


def test_parse_date_rejects_intervals() -> None:
with pytest.raises(ValueError):
parse_date("2026-03-19T12:00:00/2026-03-19T13:00:00")


# tests for parse_time


def test_parse_time_valid() -> None:
t = parse_time("11:28:37")
assert t.hour == 11


def test_parse_time_accepts_datetime() -> None:
t = parse_time("2026-03-19T11:28:37")
assert t.minute == 28


def test_parse_time_rejects_date() -> None:
with pytest.raises(ValueError):
parse_time("2026-03-19")


def test_parse_time_rejects_duration() -> None:
with pytest.raises(ValueError):
parse_time("PT2H")


def test_parse_time_rejects_interval() -> None:
with pytest.raises(ValueError):
parse_time("2026-03-19T12:00:00/2026-03-19T13:00:00")


# tests for parse_duration


def test_parse_duration_valid() -> None:
dur = parse_duration("PT2H")
assert dur.hours == 2


def test_parse_duration_rejects_datetime() -> None:
with pytest.raises(ValueError):
parse_duration("2026-03-19T11:28:37")