Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove redundant self._next_triggers construction and add AndTrigger test. #914

Merged
1 change: 1 addition & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ APScheduler, see the :doc:`migration section <migration>`.
(PR by MohammadAmin Vahedinia)
- Fixed the shutdown procedure of the Redis event broker
- Fixed ``SQLAlchemyDataStore`` not respecting custom schema name when creating enums
- Fixed skipped intervals with overlapping schedules in ``AndTrigger``. (#911 <https://github.com/agronholm/apscheduler/issues/911>_; PR by Bennett Meares)

**4.0.0a4**

Expand Down
1 change: 0 additions & 1 deletion src/apscheduler/triggers/combining.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ def next(self) -> datetime | None:

# If all the fire times were within the threshold, return the earliest one
if latest_fire_time - earliest_fire_time <= self.threshold:
self._next_fire_times = [t.next() for t in self.triggers]
return earliest_fire_time
else:
raise MaxIterationsReached
Expand Down
124 changes: 123 additions & 1 deletion tests/triggers/test_combining.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

import pytest

from apscheduler import MaxIterationsReached
from apscheduler.triggers.calendarinterval import CalendarIntervalTrigger
from apscheduler.triggers.combining import AndTrigger, OrTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger

Expand Down Expand Up @@ -62,6 +64,126 @@ def test_repr(self, timezone, serializer):
"threshold=1.0, max_iterations=10000)"
)

@pytest.mark.parametrize(
"left_kwargs,right_kwargs,start_time,expected_datetimes",
[
(
{"_class": "IntervalTrigger", "hours": 6},
{"_class": "IntervalTrigger", "hours": 12},
datetime(2024, 5, 1, 0, 0, 0, tzinfo=timezone.utc),
[
datetime(2024, 5, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 1, 12, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 2, 0, 0, 0, tzinfo=timezone.utc),
],
),
(
{"_class": "IntervalTrigger", "days": 1},
{"_class": "IntervalTrigger", "weeks": 1},
datetime(2024, 5, 1, 0, 0, 0, tzinfo=timezone.utc),
[
datetime(2024, 5, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 8, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 15, 0, 0, 0, tzinfo=timezone.utc),
],
),
(
{
"_class": "CronTrigger",
"day_of_week": "mon-fri",
"hour": "*",
"timezone": timezone.utc,
},
{"_class": "IntervalTrigger", "hours": 12},
datetime(2024, 5, 3, 0, 0, 0, tzinfo=timezone.utc),
[
datetime(2024, 5, 3, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 3, 12, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 6, 0, 0, 0, tzinfo=timezone.utc),
],
),
(
{
"_class": "CronTrigger",
"day_of_week": "mon-fri",
"timezone": timezone.utc,
},
{"_class": "IntervalTrigger", "days": 4},
datetime(2024, 5, 13, 0, 0, 0, tzinfo=timezone.utc),
[
datetime(2024, 5, 13, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 17, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 21, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 29, 0, 0, 0, tzinfo=timezone.utc),
],
),
(
{
"_class": "CalendarIntervalTrigger",
"months": 1,
"timezone": timezone.utc,
},
{
"_class": "CronTrigger",
"day_of_week": "mon-fri",
"timezone": timezone.utc,
},
datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
[
datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 3, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 4, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 5, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 7, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 8, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 10, 1, 0, 0, 0, tzinfo=timezone.utc),
datetime(2024, 11, 1, 0, 0, 0, tzinfo=timezone.utc),
],
),
],
)
def test_overlapping_triggers(
self, left_kwargs, right_kwargs, start_time, expected_datetimes
):
"""
Verify that the `AndTrigger` fires at the intersection of two triggers.
"""
trigger_classes = {
"CronTrigger": CronTrigger,
"IntervalTrigger": IntervalTrigger,
"CalendarIntervalTrigger": CalendarIntervalTrigger,
}
left_trigger_class = trigger_classes[left_kwargs.pop("_class")]
right_trigger_class = trigger_classes[right_kwargs.pop("_class")]

left_kwargs.update(
{
(
"start_time"
if left_trigger_class is not CalendarIntervalTrigger
else "start_date"
): start_time,
}
)
right_kwargs.update(
{
(
"start_time"
if right_trigger_class is not CalendarIntervalTrigger
else "start_date"
): start_time,
}
)

left_trigger = left_trigger_class(**left_kwargs)
right_trigger = right_trigger_class(**right_kwargs)
and_trigger = AndTrigger([left_trigger, right_trigger])

for expected_datetime in expected_datetimes:
next_datetime = and_trigger.next()
assert next_datetime == expected_datetime


class TestOrTrigger:
def test_two_datetriggers(self, timezone, serializer):
Expand Down
Loading