Skip to content

Commit

Permalink
Fix a daylight savings time issue in CronTrigger
Browse files Browse the repository at this point in the history
(1) remove the `timedelta` operations - which are not timezone aware
(2) make sure the "fold" attribute remains when incrementing
  • Loading branch information
hlobit committed Oct 28, 2024
1 parent a79e027 commit f45bddb
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 13 deletions.
19 changes: 6 additions & 13 deletions src/apscheduler/triggers/cron/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections.abc import Sequence
from datetime import datetime, timedelta, tzinfo
from datetime import datetime, tzinfo
from typing import Any, ClassVar

import attrs
Expand Down Expand Up @@ -207,16 +207,17 @@ def _set_field_value(
else:
values[field.name] = new_value

return datetime(**values, tzinfo=self.timezone)
return datetime(**values, tzinfo=self.timezone).replace(fold=dateval.fold)

def next(self) -> datetime | None:
if self._last_fire_time:
start_time = self._last_fire_time + timedelta(microseconds=1)
next_time = datetime.fromtimestamp(
self._last_fire_time.timestamp() + 1, self.timezone
)
else:
start_time = self.start_time
next_time = self.start_time

fieldnum = 0
next_time = datetime_ceil(start_time).astimezone(self.timezone)
while 0 <= fieldnum < len(self._fields):
field = self._fields[fieldnum]
curr_value = field.get_value(next_time)
Expand Down Expand Up @@ -276,11 +277,3 @@ def __repr__(self) -> str:

fields.append(f"timezone={timezone_repr(self.timezone)!r}")
return f'CronTrigger({", ".join(fields)})'


def datetime_ceil(dateval: datetime) -> datetime:
"""Round the given datetime object upwards."""
if dateval.microsecond > 0:
return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond)

return dateval
30 changes: 30 additions & 0 deletions tests/triggers/test_cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,36 @@ def test_dst_change(
)


@pytest.mark.parametrize(
"cron_expression, start_time, correct_next_dates",
[
('0 * * * *', datetime(2024, 10, 27, 2, 0, 0, 0), [
(datetime(2024, 10, 27, 2, 0, 0, 0), 0),
(datetime(2024, 10, 27, 2, 0, 0, 0), 1),
(datetime(2024, 10, 27, 3, 0, 0, 0), 0),
]),
('1 * * * *', datetime(2024, 10, 27, 2, 1, 0, 0), [
(datetime(2024, 10, 27, 2, 1, 0, 0), 0),
(datetime(2024, 10, 27, 2, 1, 0, 0), 1),
(datetime(2024, 10, 27, 3, 1, 0, 0), 0),
]),
],
ids=["dst_change_0", "dst_change_1"],
)
def test_dst_change2(
cron_expression,
start_time,
correct_next_dates,
timezone,
):
trigger = CronTrigger.from_crontab(cron_expression, timezone=timezone)
trigger.start_time = start_time.astimezone(timezone)
for (correct_next_date, fold) in correct_next_dates:
next_date = trigger.next()
assert next_date == correct_next_date.astimezone(timezone)
assert next_date.fold == fold


def test_zero_value(timezone):
start_time = datetime(2020, 1, 1, tzinfo=timezone)
trigger = CronTrigger(
Expand Down

0 comments on commit f45bddb

Please sign in to comment.