Skip to content

Commit

Permalink
Take timezone into account when working with last_run
Browse files Browse the repository at this point in the history
  • Loading branch information
SijmenHuizenga committed Oct 29, 2023
1 parent 51d762e commit 2ace108
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 8 deletions.
10 changes: 8 additions & 2 deletions schedule/__init__.py
Expand Up @@ -765,7 +765,8 @@ def _schedule_next_run(self) -> None:
# Make sure we run at the specified time *today* (or *this hour*)
# as well. This accounts for when a job takes so long it finished
# in the next period.
if not self.last_run or (self.next_run - self.last_run) > self.period:
last_run_tz = self._to_at_timezone(self.last_run)
if not last_run_tz or (self.next_run - last_run_tz) > self.period:
if (
self.unit == "days"
and self.next_run.time() > now.time()
Expand Down Expand Up @@ -797,11 +798,16 @@ def _schedule_next_run(self) -> None:
# it preserves the moment in time and changes the local timestamp.
# This method applies pytz normalization but preserves the local timestamp, in fact changing the moment in time.
def _normalize_preserve_timestamp(self, input: datetime.datetime) -> datetime.datetime:
if self.at_time_zone is None:
if self.at_time_zone is None or input is None:
return input
normalized = self.at_time_zone.normalize(input)
return normalized.replace(day=input.day, hour=input.hour, minute=input.minute, second=input.second, microsecond=input.microsecond)

def _to_at_timezone(self, input: datetime.datetime) -> datetime.datetime:
if self.at_time_zone is None or input is None:
return input
return input.astimezone(self.at_time_zone)

def _is_overdue(self, when: datetime.datetime):
return self.cancel_after is not None and when > self.cancel_after

Expand Down
62 changes: 56 additions & 6 deletions test_schedule.py
Expand Up @@ -503,26 +503,76 @@ def test_next_run_time_day_end(self):
assert job.next_run.hour == 23

def test_next_run_time_hour_end(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")

self.tst_next_run_time_hour_end(None, 0)

def test_next_run_time_hour_end_london(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")

self.tst_next_run_time_hour_end("Europe/London", 0)

def test_next_run_time_hour_end_katmandu(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")

# 12:00 in Berlin is 15:45 in Kathmandu
# this test schedules runs at :10 minutes, so job runs at
# 16:10 in Kathmandu, which is 13:25 in Berlin
# in local time we don't run at :10, but at :25, offset of 15 minutes
self.tst_next_run_time_hour_end("Asia/Kathmandu", 15)

def tst_next_run_time_hour_end(self, tz, offsetMinutes):
mock_job = make_mock_job()

# So a job scheduled to run at :10 in Kathmandu, runs always 25 minutes
with mock_datetime(2010, 10, 10, 12, 0, 0):
job = every().hour.at(":10").do(mock_job)
job = every().hour.at(":10", tz).do(mock_job)
assert job.next_run.hour == 12
assert job.next_run.minute == 10
assert job.next_run.minute == 10 + offsetMinutes

with mock_datetime(2010, 10, 10, 13, 0, 0):
job.run()
assert job.next_run.hour == 13
assert job.next_run.minute == 10
assert job.next_run.minute == 10 + offsetMinutes

with mock_datetime(2010, 10, 10, 13, 15, 0):
with mock_datetime(2010, 10, 10, 13, 30, 0):
job.run()
assert job.next_run.hour == 14
assert job.next_run.minute == 10
assert job.next_run.minute == 10 + offsetMinutes


def test_next_run_time_minute_end(self):
self.tst_next_run_time_minute_end(None)

def test_next_run_time_minute_end_london(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")

self.tst_next_run_time_minute_end("Europe/London")

def test_next_run_time_minute_end_katmhandu(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")

self.tst_next_run_time_minute_end("Asia/Kathmandu")

def tst_next_run_time_minute_end(self, tz):
mock_job = make_mock_job()
with mock_datetime(2010, 10, 10, 10, 10, 0):
job = every().minute.at(":15").do(mock_job)
job = every().minute.at(":15", tz).do(mock_job)
assert job.next_run.minute == 10
assert job.next_run.second == 15

Expand Down

0 comments on commit 2ace108

Please sign in to comment.