diff --git a/schedule/__init__.py b/schedule/__init__.py index 8e12eeb7..8fd7d8f2 100644 --- a/schedule/__init__.py +++ b/schedule/__init__.py @@ -468,6 +468,24 @@ def tag(self, *tags: Hashable): self.tags.update(tags) return self + def with_time_zone(self, tz: str): + import pytz + + if self.unit != "seconds": + raise ScheduleValueError( + "Timezone should be defined in at()" + ) + + if isinstance(tz, str): + self.at_time_zone = pytz.timezone(tz) + elif isinstance(tz, pytz.BaseTzInfo): + self.at_time_zone = tz + else: + raise ScheduleValueError( + "Timezone must be string or pytz.timezone object" + ) + return self + def at(self, time_str: str, tz: Optional[str] = None): """ Specify a particular time that the job should be run at. @@ -689,7 +707,7 @@ def run(self): logger.debug("Running job %s", self) ret = self.job_func() - self.last_run = datetime.datetime.now() + self.last_run = datetime.datetime.now(self.at_time_zone) self._schedule_next_run() if self._is_overdue(self.next_run): @@ -789,6 +807,15 @@ def _correct_utc_offset( moment = self.at_time_zone.normalize(moment) offset_after_normalize = moment.utcoffset() + # Check fall back for DST + if self.last_run is not None: + last_execution_dst = self.last_run.dst() + moment_dst = moment.dst() + if last_execution_dst > moment_dst: + if self.unit in ["minutes", "hours"]: + moment -= last_execution_dst - moment_dst + return moment + if offset_before_normalize == offset_after_normalize: # There was no change in the utc-offset, datetime didn't change. return moment diff --git a/test_schedule.py b/test_schedule.py index f497826d..404da043 100644 --- a/test_schedule.py +++ b/test_schedule.py @@ -1239,6 +1239,61 @@ def test_align_utc_offset_after_fold_fixate(self): assert aligned_time.minute == 30 assert aligned_time.day == 27 + def test_fall_back_for_daylight_saving_time(self): + mock_job = make_mock_job() + # 26 October 2025, 03:00:00 clocks were turned back 1 hour + with mock_datetime(2025, 10, 26, 2, 58, second=40, fold=0): + job_object = every().minute.at(":30", tz="Europe/Madrid").do(mock_job) + with mock_datetime(2025, 10, 26, 2, 59, second=40, fold=0): + schedule.run_pending() + assert job_object.next_run.hour == 2 + + def test_fall_back_for_daylight_saving_time_2(self): + mock_job = make_mock_job() + # 26 October 2025, 03:00:00 clocks were turned back 1 hour + with mock_datetime(2025, 10, 26, 2, 59, second=30, fold=1): + assert every().minute.at(":30").do(mock_job).next_run.hour == 3 + + def test_fall_back_for_daylight_saving_time_3(self): + mock_job = make_mock_job() + # 26 October 2025, 03:00:00 clocks were turned back 1 hour + with mock_datetime(2025, 10, 26, 1, 59, second=30, fold=0): + assert every().minute.at(":30").do(mock_job).next_run.hour == 2 + + def test_fall_back_for_daylight_saving_time_4(self): + mock_job = make_mock_job() + # 26 October 2025, 03:00:00 clocks were turned back 1 hour + with mock_datetime(2025, 10, 26, 1, 30, second=0, fold=0): + job_object = every().hour.at(":30", tz="Europe/Madrid").do(mock_job) + assert job_object.next_run.hour == 2 + assert job_object.next_run.minute == 30 + with mock_datetime(2025, 10, 26, 2, 59, second=40, fold=0): + schedule.run_pending() + assert job_object.next_run.hour == 2 + assert job_object.next_run.minute == 30 + with mock_datetime(2025, 10, 26, 2, 59, second=40, fold=1): + schedule.run_pending() + assert job_object.next_run.hour == 3 + assert job_object.next_run.minute == 30 + + def test_fall_back_for_daylight_saving_time_5(self): + mock_job = make_mock_job() + # 26 October 2025, 03:00:00 clocks were turned back 1 hour + with mock_datetime(2025, 10, 26, 2, 59, second=52, fold=0): + job_object = every(5).seconds.with_time_zone(tz="Europe/Madrid").do(mock_job) + with mock_datetime(2025, 10, 26, 2, 59, second=59, fold=0): + schedule.run_pending() + assert job_object.next_run.hour == 2 + + def test_fall_back_for_daylight_saving_time_6(self): + mock_job = make_mock_job() + # 26 October 2025, 03:00:00 clocks were turned back 1 hour + with mock_datetime(2025, 10, 26, 2, 59, second=52, fold=1): + job_object = every(5).seconds.with_time_zone(tz="Europe/Madrid").do(mock_job) + with mock_datetime(2025, 10, 26, 2, 59, second=59, fold=1): + schedule.run_pending() + assert job_object.next_run.hour == 3 + def test_daylight_saving_time(self): mock_job = make_mock_job() # 27 March 2022, 02:00:00 clocks were turned forward 1 hour