Skip to content

Commit c81bf8b

Browse files
committed
Add typed parse helpers for datetime, date, time and duration
1 parent 0d71391 commit c81bf8b

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

docs/docs/parsing.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,36 @@ When passing only time information the date will default to today.
112112
>>> pendulum.parse('12:04:23', exact=True)
113113
Time(12, 04, 23)
114114
```
115+
116+
## Typed helpers
117+
118+
Because `parse()` can return a `DateTime`, `Date`, `Time` or `Duration` depending on the
119+
input, its return type is a union, which is awkward in type-checked code. If you know which
120+
type you expect, you can use one of the typed helpers instead. Each one returns that single
121+
type and raises a `ParserError` if the string represents something else.
122+
123+
```python
124+
>>> import pendulum
125+
126+
>>> pendulum.parse_datetime('2012-05-03T12:04:23')
127+
DateTime(2012, 5, 3, 12, 4, 23, tzinfo=Timezone('UTC'))
128+
129+
>>> pendulum.parse_datetime('P2Y3M4DT5H6M7S')
130+
Traceback (most recent call last):
131+
...
132+
ParserError: Text 'P2Y3M4DT5H6M7S' does not represent a datetime, got Duration
133+
```
134+
135+
`parse_date()` and `parse_time()` expect the string to represent that exact type, so pass
136+
`exact=True` as you would to `parse()`:
137+
138+
```python
139+
>>> pendulum.parse_date('2012-05-03', exact=True)
140+
Date(2012, 5, 3)
141+
142+
>>> pendulum.parse_time('12:04:23', exact=True)
143+
Time(12, 4, 23)
144+
145+
>>> pendulum.parse_duration('P2Y3M4DT5H6M7S')
146+
Duration(years=2, months=3, days=4, hours=5, minutes=6, seconds=7)
147+
```

src/pendulum/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
from pendulum.helpers import week_starts_at
3232
from pendulum.interval import Interval
3333
from pendulum.parser import parse as parse
34+
from pendulum.parser import parse_date as parse_date
35+
from pendulum.parser import parse_datetime as parse_datetime
36+
from pendulum.parser import parse_duration as parse_duration
37+
from pendulum.parser import parse_time as parse_time
3438
from pendulum.time import Time
3539
from pendulum.tz import UTC
3640
from pendulum.tz import fixed_timezone
@@ -423,6 +427,10 @@ def __getattr__(name: str) -> Any:
423427
"naive",
424428
"now",
425429
"parse",
430+
"parse_date",
431+
"parse_datetime",
432+
"parse_duration",
433+
"parse_time",
426434
"set_local_timezone",
427435
"set_locale",
428436
"test_local_timezone",

src/pendulum/parser.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pendulum.duration import Duration
1010
from pendulum.parsing import _Interval
1111
from pendulum.parsing import parse as base_parse
12+
from pendulum.parsing.exceptions import ParserError
1213
from pendulum.tz.timezone import UTC
1314

1415

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

3839

40+
def parse_datetime(text: str, **options: t.Any) -> DateTime:
41+
"""Parse a string and return a ``DateTime``.
42+
43+
Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
44+
string represents something else (a date, a time, a duration, ...).
45+
"""
46+
parsed = parse(text, **options)
47+
if not isinstance(parsed, pendulum.DateTime):
48+
raise _wrong_type(text, "a datetime", parsed)
49+
50+
return parsed
51+
52+
53+
def parse_date(text: str, **options: t.Any) -> Date:
54+
"""Parse a string and return a ``Date``.
55+
56+
Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
57+
string represents something else. Note that ``parse()`` yields a ``DateTime``
58+
for a date string unless ``exact=True`` is passed.
59+
"""
60+
parsed = parse(text, **options)
61+
# DateTime is a subclass of Date, so a datetime must not pass as a date.
62+
if not isinstance(parsed, pendulum.Date) or isinstance(parsed, pendulum.DateTime):
63+
raise _wrong_type(text, "a date", parsed)
64+
65+
return parsed
66+
67+
68+
def parse_time(text: str, **options: t.Any) -> Time:
69+
"""Parse a string and return a ``Time``.
70+
71+
Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
72+
string represents something else. Note that ``parse()`` yields a ``DateTime``
73+
for a time string unless ``exact=True`` is passed.
74+
"""
75+
parsed = parse(text, **options)
76+
if not isinstance(parsed, pendulum.Time):
77+
raise _wrong_type(text, "a time", parsed)
78+
79+
return parsed
80+
81+
82+
def parse_duration(text: str, **options: t.Any) -> Duration:
83+
"""Parse a string and return a ``Duration``.
84+
85+
Accepts the same options as ``parse()`` but raises a ``ParserError`` if the
86+
string represents something else.
87+
"""
88+
parsed = parse(text, **options)
89+
# Interval is a subclass of Duration, so an interval must not pass as one.
90+
if not isinstance(parsed, Duration) or isinstance(parsed, pendulum.Interval):
91+
raise _wrong_type(text, "a duration", parsed)
92+
93+
return parsed
94+
95+
96+
def _wrong_type(text: str, expected: str, parsed: object) -> ParserError:
97+
return ParserError(
98+
f"Text '{text}' does not represent {expected}, got {type(parsed).__name__}"
99+
)
100+
101+
39102
def _parse(
40103
text: str, **options: t.Any
41104
) -> Date | DateTime | Time | Duration | Interval[DateTime]:

tests/test_parsing.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import annotations
22

3+
import pytest
4+
35
import pendulum
46

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

149152
assert dt.to_iso8601_string() == "2020-02-05T20:05:37.364951Z"
153+
154+
155+
def test_parse_datetime() -> None:
156+
dt = pendulum.parse_datetime("2016-10-16T12:34:56.123456+01:30")
157+
158+
assert isinstance(dt, pendulum.DateTime)
159+
assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456)
160+
assert dt.offset == 5400
161+
162+
# A date string still parses to a DateTime by default.
163+
dt = pendulum.parse_datetime("2016-10-16")
164+
165+
assert isinstance(dt, pendulum.DateTime)
166+
assert_datetime(dt, 2016, 10, 16, 0, 0, 0, 0)
167+
168+
# Options are forwarded to parse().
169+
dt = pendulum.parse_datetime("2016-10-16T12:34:56", tz="Europe/Paris")
170+
171+
assert dt.tz is not None
172+
assert dt.tz.name == "Europe/Paris"
173+
174+
175+
def test_parse_datetime_raises_for_other_types() -> None:
176+
with pytest.raises(ParserError):
177+
pendulum.parse_datetime("P2Y3M4DT5H6M7S")
178+
179+
with pytest.raises(ParserError):
180+
pendulum.parse_datetime("2016-10-16", exact=True)
181+
182+
183+
def test_parse_date() -> None:
184+
d = pendulum.parse_date("2016-10-16", exact=True)
185+
186+
assert isinstance(d, pendulum.Date)
187+
assert_date(d, 2016, 10, 16)
188+
189+
190+
def test_parse_date_raises_for_other_types() -> None:
191+
# A datetime must not pass as a date, even though DateTime subclasses Date.
192+
with pytest.raises(ParserError):
193+
pendulum.parse_date("2016-10-16")
194+
195+
with pytest.raises(ParserError):
196+
pendulum.parse_date("12:34:56", exact=True)
197+
198+
199+
def test_parse_time() -> None:
200+
t = pendulum.parse_time("12:34:56.123456", exact=True)
201+
202+
assert isinstance(t, pendulum.Time)
203+
assert_time(t, 12, 34, 56, 123456)
204+
205+
206+
def test_parse_time_raises_for_other_types() -> None:
207+
with pytest.raises(ParserError):
208+
pendulum.parse_time("2016-10-16", exact=True)
209+
210+
211+
def test_parse_duration_helper() -> None:
212+
duration = pendulum.parse_duration("P2Y3M4DT5H6M7S")
213+
214+
assert isinstance(duration, pendulum.Duration)
215+
assert_duration(duration, 2, 3, 0, 4, 5, 6, 7)
216+
217+
218+
def test_parse_duration_raises_for_other_types() -> None:
219+
with pytest.raises(ParserError):
220+
pendulum.parse_duration("2016-10-16")
221+
222+
with pytest.raises(ParserError):
223+
pendulum.parse_duration("2008-05-11T15:30:00Z/2008-05-11T16:30:00Z")

0 commit comments

Comments
 (0)