Skip to content

Commit c5a0346

Browse files
authored
Fixed nonexistent DST skip hour in CronTrigger (#1060)
1 parent 307896e commit c5a0346

File tree

4 files changed

+32
-3
lines changed

4 files changed

+32
-3
lines changed

docs/versionhistory.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Version history
44
To find out how to migrate your application from a previous version of
55
APScheduler, see the :doc:`migration section <migration>`.
66

7+
**UNRELEASED**
8+
9+
- Fixed an issue where ``CronTrigger.next()`` returned a non-existing date on a DST change
10+
(`#1059 <https://github.com/agronholm/apscheduler/issues/1059>`_; PR by @jonasitzmann)
11+
712
**4.0.0a6**
813

914
- **BREAKING** Refactored ``AsyncpgEventBroker`` to directly accept a connection string,

src/apscheduler/_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,14 @@ def create_repr(instance: object, *attrnames: str, **kwargs) -> str:
9999

100100
rendered_attrs = ", ".join(f"{key}={value!r}" for key, value in kv_pairs)
101101
return f"{instance.__class__.__name__}({rendered_attrs})"
102+
103+
104+
def time_exists(dt: datetime) -> bool:
105+
"""
106+
Determine whether a datetime exists in its time zone.
107+
108+
:return: ``False`` if the given datetime falls within a gap created by a
109+
forward daylight savings shift, otherwise ``True``
110+
111+
"""
112+
return dt == datetime.fromtimestamp(dt.timestamp(), dt.tzinfo)

src/apscheduler/triggers/cron/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tzlocal import get_localzone
1010

1111
from ..._converters import as_aware_datetime, as_timezone
12-
from ..._utils import require_state_version, timezone_repr
12+
from ..._utils import require_state_version, time_exists, timezone_repr
1313
from ...abc import Trigger
1414
from .fields import (
1515
DEFAULT_VALUES,
@@ -232,7 +232,13 @@ def next(self) -> datetime | None:
232232
# A valid, but higher than the starting value, was found
233233
if field.real:
234234
next_time = self._set_field_value(next_time, fieldnum, next_value)
235-
fieldnum += 1
235+
if time_exists(next_time):
236+
fieldnum += 1
237+
else:
238+
# skip non-existent date
239+
next_time, fieldnum = self._increment_field_value(
240+
next_time, fieldnum
241+
)
236242
else:
237243
next_time, fieldnum = self._increment_field_value(
238244
next_time, fieldnum

tests/triggers/test_cron.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ def test_week_2(timezone, serializer, weekday):
356356
"trigger_args, start_time, start_time_fold, correct_next_date,"
357357
"correct_next_date_fold",
358358
[
359+
({"hour": 2}, datetime(2013, 3, 9, 20), 0, datetime(2013, 3, 11, 2), 0),
359360
({"hour": 8}, datetime(2013, 3, 9, 12), 0, datetime(2013, 3, 10, 8), 0),
360361
({"hour": 8}, datetime(2013, 11, 2, 12), 0, datetime(2013, 11, 3, 8), 0),
361362
(
@@ -373,7 +374,13 @@ def test_week_2(timezone, serializer, weekday):
373374
1,
374375
),
375376
],
376-
ids=["absolute_spring", "absolute_autumn", "interval_spring", "interval_autumn"],
377+
ids=[
378+
"spring_skip_hour",
379+
"absolute_spring",
380+
"absolute_autumn",
381+
"interval_spring",
382+
"interval_autumn",
383+
],
377384
)
378385
def test_dst_change(
379386
trigger_args,

0 commit comments

Comments
 (0)