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: 16 additions & 4 deletions src/pendulum/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
return _parse(text, **options)


def _as_datetime(value: datetime.date) -> datetime.datetime:
# An interval endpoint parsed from a date-only ISO 8601 value (e.g. the
# "2021-01-01" in "2021-01-01/P1D") is a datetime.date. Promote it to a
# midnight datetime so the interval has DateTime endpoints and so
# DateTime.add/subtract accepts the duration's time components below
# (Date.add/subtract does not take hours/minutes/seconds/microseconds).
if isinstance(value, datetime.datetime):
return value

return datetime.datetime(value.year, value.month, value.day)


def _parse(
text: str, **options: t.Any
) -> Date | DateTime | Time | Duration | Interval[DateTime]:
Expand Down Expand Up @@ -75,7 +87,9 @@ def _parse(
duration = parsed.duration

if parsed.start is not None:
dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC))
dt = pendulum.instance(
_as_datetime(parsed.start), tz=options.get("tz", UTC)
)

return pendulum.interval(
dt,
Expand All @@ -91,9 +105,7 @@ def _parse(
),
)

dt = pendulum.instance(
t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC)
)
dt = pendulum.instance(_as_datetime(parsed.end), tz=options.get("tz", UTC))

return pendulum.interval(
dt.subtract(
Expand Down
18 changes: 18 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,24 @@ def test_parse_interval() -> None:
assert interval.end.offset == 0


def test_parse_interval_with_date_and_duration() -> None:
# The interval endpoint is a date-only value, so it parses to a
# datetime.date rather than a datetime.datetime. See
# https://github.com/python-pendulum/pendulum/issues/881
interval = pendulum.parse("2021-01-01/P1DT1H")

assert isinstance(interval, pendulum.Interval)
assert_datetime(interval.start, 2021, 1, 1, 0, 0, 0, 0)
assert_datetime(interval.end, 2021, 1, 2, 1, 0, 0, 0)

# The reverse form (duration then a date endpoint) goes through subtract().
interval = pendulum.parse("P1Y/2021-01-01")

assert isinstance(interval, pendulum.Interval)
assert_datetime(interval.start, 2020, 1, 1, 0, 0, 0, 0)
assert_datetime(interval.end, 2021, 1, 1, 0, 0, 0, 0)


def test_parse_now() -> None:
assert pendulum.parse("now").timezone_name == "UTC"
assert (
Expand Down