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
33 changes: 33 additions & 0 deletions docs/docs/parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,36 @@ When passing only time information the date will default to today.
>>> pendulum.parse('12:04:23', exact=True)
Time(12, 04, 23)
```

## Typed helpers

Because `parse()` can return a `DateTime`, `Date`, `Time` or `Duration` depending on the
input, its return type is a union, which is awkward in type-checked code. If you know which
type you expect, you can use one of the typed helpers instead. Each one returns that single
type and raises a `ParserError` if the string represents something else.

```python
>>> import pendulum

>>> pendulum.parse_datetime('2012-05-03T12:04:23')
DateTime(2012, 5, 3, 12, 4, 23, tzinfo=Timezone('UTC'))

>>> pendulum.parse_datetime('P2Y3M4DT5H6M7S')
Traceback (most recent call last):
...
ParserError: Text 'P2Y3M4DT5H6M7S' does not represent a datetime, got Duration
```

`parse_date()` and `parse_time()` expect the string to represent that exact type, so pass
`exact=True` as you would to `parse()`:

```python
>>> pendulum.parse_date('2012-05-03', exact=True)
Date(2012, 5, 3)

>>> pendulum.parse_time('12:04:23', exact=True)
Time(12, 4, 23)

>>> pendulum.parse_duration('P2Y3M4DT5H6M7S')
Duration(years=2, months=3, days=4, hours=5, minutes=6, seconds=7)
```
8 changes: 8 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 Expand Up @@ -423,6 +427,10 @@ def __getattr__(name: str) -> Any:
"naive",
"now",
"parse",
"parse_date",
"parse_datetime",
"parse_duration",
"parse_time",
"set_local_timezone",
"set_locale",
"test_local_timezone",
Expand Down
63 changes: 63 additions & 0 deletions src/pendulum/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pendulum.duration import Duration
from pendulum.parsing import _Interval
from pendulum.parsing import parse as base_parse
from pendulum.parsing.exceptions import ParserError
from pendulum.tz.timezone import UTC


Expand Down Expand Up @@ -36,6 +37,68 @@ def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
return _parse(text, **options)


def parse_datetime(text: str, **options: t.Any) -> DateTime:
"""Parse a string and return a ``DateTime``.

Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
string represents something else (a date, a time, a duration, ...).
"""
parsed = parse(text, **options)
if not isinstance(parsed, pendulum.DateTime):
raise _wrong_type(text, "a datetime", parsed)

return parsed


def parse_date(text: str, **options: t.Any) -> Date:
"""Parse a string and return a ``Date``.

Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
string represents something else. Note that ``parse()`` yields a ``DateTime``
for a date string unless ``exact=True`` is passed.
"""
parsed = parse(text, **options)
# DateTime is a subclass of Date, so a datetime must not pass as a date.
if not isinstance(parsed, pendulum.Date) or isinstance(parsed, pendulum.DateTime):
raise _wrong_type(text, "a date", parsed)

return parsed


def parse_time(text: str, **options: t.Any) -> Time:
"""Parse a string and return a ``Time``.

Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
string represents something else. Note that ``parse()`` yields a ``DateTime``
for a time string unless ``exact=True`` is passed.
"""
parsed = parse(text, **options)
if not isinstance(parsed, pendulum.Time):
raise _wrong_type(text, "a time", parsed)

return parsed


def parse_duration(text: str, **options: t.Any) -> Duration:
"""Parse a string and return a ``Duration``.

Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
string represents something else.
"""
parsed = parse(text, **options)
# Interval is a subclass of Duration, so an interval must not pass as one.
if not isinstance(parsed, Duration) or isinstance(parsed, pendulum.Interval):
raise _wrong_type(text, "a duration", parsed)

return parsed


def _wrong_type(text: str, expected: str, parsed: object) -> ParserError:
return ParserError(
f"Text '{text}' does not represent {expected}, got {type(parsed).__name__}"
)


def _parse(
text: str, **options: t.Any
) -> Date | DateTime | Time | Duration | Interval[DateTime]:
Expand Down
74 changes: 74 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import pytest

import pendulum

from pendulum.parsing.exceptions import ParserError
from tests.conftest import assert_date
from tests.conftest import assert_datetime
from tests.conftest import assert_duration
Expand Down Expand Up @@ -147,3 +150,74 @@ def test_parse_with_utc_timezone() -> None:
dt = pendulum.parse("2020-02-05T20:05:37.364951Z")

assert dt.to_iso8601_string() == "2020-02-05T20:05:37.364951Z"


def test_parse_datetime() -> None:
dt = pendulum.parse_datetime("2016-10-16T12:34:56.123456+01:30")

assert isinstance(dt, pendulum.DateTime)
assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456)
assert dt.offset == 5400

# A date string still parses to a DateTime by default.
dt = pendulum.parse_datetime("2016-10-16")

assert isinstance(dt, pendulum.DateTime)
assert_datetime(dt, 2016, 10, 16, 0, 0, 0, 0)

# Options are forwarded to parse().
dt = pendulum.parse_datetime("2016-10-16T12:34:56", tz="Europe/Paris")

assert dt.tz is not None
assert dt.tz.name == "Europe/Paris"


def test_parse_datetime_raises_for_other_types() -> None:
with pytest.raises(ParserError):
pendulum.parse_datetime("P2Y3M4DT5H6M7S")

with pytest.raises(ParserError):
pendulum.parse_datetime("2016-10-16", exact=True)


def test_parse_date() -> None:
d = pendulum.parse_date("2016-10-16", exact=True)

assert isinstance(d, pendulum.Date)
assert_date(d, 2016, 10, 16)


def test_parse_date_raises_for_other_types() -> None:
# A datetime must not pass as a date, even though DateTime subclasses Date.
with pytest.raises(ParserError):
pendulum.parse_date("2016-10-16")

with pytest.raises(ParserError):
pendulum.parse_date("12:34:56", exact=True)


def test_parse_time() -> None:
t = pendulum.parse_time("12:34:56.123456", exact=True)

assert isinstance(t, pendulum.Time)
assert_time(t, 12, 34, 56, 123456)


def test_parse_time_raises_for_other_types() -> None:
with pytest.raises(ParserError):
pendulum.parse_time("2016-10-16", exact=True)


def test_parse_duration_helper() -> None:
duration = pendulum.parse_duration("P2Y3M4DT5H6M7S")

assert isinstance(duration, pendulum.Duration)
assert_duration(duration, 2, 3, 0, 4, 5, 6, 7)


def test_parse_duration_raises_for_other_types() -> None:
with pytest.raises(ParserError):
pendulum.parse_duration("2016-10-16")

with pytest.raises(ParserError):
pendulum.parse_duration("2008-05-11T15:30:00Z/2008-05-11T16:30:00Z")