Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-APScheduler for 
openSUSE:Factory checked in at 2026-01-08 15:28:42
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-APScheduler (Old)
 and      /work/SRC/openSUSE:Factory/.python-APScheduler.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-APScheduler"

Thu Jan  8 15:28:42 2026 rev:26 rq:1325921 version:3.11.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-APScheduler/python-APScheduler.changes    
2025-11-24 15:53:26.024904543 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-APScheduler.new.1928/python-APScheduler.changes
  2026-01-08 15:29:26.890107667 +0100
@@ -1,0 +2,11 @@
+Thu Jan  8 08:23:04 UTC 2026 - Anton Smorodskyi <[email protected]>
+
+- Update to 3.11.2:
+  * Fixed an issue where a job using a CronTrigger scheduled
+    in a repeated time interval during DST transitions could
+    cause the scheduler to get stuck in an infinite loop 
+    (#1021; PR by @soulofakuma)
+- update build and runtime dependencies to latest state in upstream
+
+
+-------------------------------------------------------------------

Old:
----
  apscheduler-3.11.1.tar.gz

New:
----
  apscheduler-3.11.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-APScheduler.spec ++++++
--- /var/tmp/diff_new_pack.3QnzsC/_old  2026-01-08 15:29:27.342126874 +0100
+++ /var/tmp/diff_new_pack.3QnzsC/_new  2026-01-08 15:29:27.342126874 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-APScheduler
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-APScheduler
-Version:        3.11.1
+Version:        3.11.2
 Release:        0
 Summary:        In-process task scheduler with Cron-like capabilities
 License:        MIT
@@ -26,21 +26,25 @@
 Source:         
https://files.pythonhosted.org/packages/source/a/apscheduler/apscheduler-%{version}.tar.gz
 BuildRequires:  %{python_module SQLAlchemy >= 1.4}
 BuildRequires:  %{python_module Twisted}
+BuildRequires:  %{python_module anyio >= 4.0}
+BuildRequires:  %{python_module attrs >= 21.3}
 BuildRequires:  %{python_module gevent}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pytest-asyncio}
+BuildRequires:  %{python_module pytest-mock}
 BuildRequires:  %{python_module pytest-tornado}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module pytz}
-BuildRequires:  %{python_module setuptools >= 36.2.7}
-BuildRequires:  %{python_module setuptools_scm >= 1.7.0}
-BuildRequires:  %{python_module tornado}
-BuildRequires:  %{python_module tzlocal >= 2.0}
+BuildRequires:  %{python_module setuptools >= 77}
+BuildRequires:  %{python_module setuptools_scm >= 6.4}
+BuildRequires:  %{python_module tzlocal >= 3.0}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
+
+Requires:       python-attrs >= 21.3
 Requires:       python-pytz
-Requires:       python-tzlocal >= 2.0
+Requires:       python-tzlocal >= 3.0
 Recommends:     python-SQLAlchemy >= 1.4
 Recommends:     python-Twisted
 Recommends:     python-gevent
@@ -49,17 +53,6 @@
 Suggests:       python-redis
 Suggests:       python-tornado >= 4.3
 BuildArch:      noarch
-%if %{with python2}
-BuildRequires:  python-funcsigs
-BuildRequires:  python-futures
-BuildRequires:  python-mock
-BuildRequires:  python-trollius
-%endif
-%ifpython2
-Requires:       python-funcsigs
-Requires:       python-futures
-Requires:       python-trollius
-%endif
 %python_subpackages
 
 %description
@@ -85,7 +78,7 @@
 
 %prep
 %setup -q -n apscheduler-%{version}
-sed -i 's/--cov//' setup.cfg
+sed -i 's/--cov//' pyproject.toml || true
 
 %build
 %pyproject_wheel
@@ -95,13 +88,11 @@
 %python_expand %fdupes %{buildroot}%{$python_sitelib}
 
 %check
-# https://github.com/agronholm/apscheduler/issues/601
-%pytest -k 'not test_broken_pool'
+%pytest -p asyncio -k "not (redis or mongodb or rethinkdb or zookeeper)"
 
 %files %{python_files}
 %license LICENSE.txt
 %doc README.rst
-%doc examples/
 %{python_sitelib}/apscheduler
 %{python_sitelib}/[Aa][Pp][Ss]cheduler-%{version}.dist-info
 

++++++ apscheduler-3.11.1.tar.gz -> apscheduler-3.11.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/.github/workflows/publish.yml 
new/apscheduler-3.11.2/.github/workflows/publish.yml
--- old/apscheduler-3.11.1/.github/workflows/publish.yml        2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/.github/workflows/publish.yml        2025-12-22 
01:39:06.000000000 +0100
@@ -14,9 +14,9 @@
     runs-on: ubuntu-latest
     environment: release
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v6
     - name: Set up Python
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
         python-version: 3.x
     - name: Install dependencies
@@ -24,7 +24,7 @@
     - name: Create packages
       run: python -m build
     - name: Archive packages
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: dist
         path: dist
@@ -38,7 +38,10 @@
       id-token: write
     steps:
     - name: Retrieve packages
-      uses: actions/download-artifact@v4
+      uses: actions/download-artifact@v6
+      with:
+        name: dist
+        path: dist
     - name: Upload packages
       uses: pypa/gh-action-pypi-publish@release/v1
 
@@ -49,7 +52,7 @@
     permissions:
       contents: write
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v6
     - id: changelog
       uses: agronholm/release-notes@v1
       with:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/.pre-commit-config.yaml 
new/apscheduler-3.11.2/.pre-commit-config.yaml
--- old/apscheduler-3.11.1/.pre-commit-config.yaml      2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/.pre-commit-config.yaml      2025-12-22 
01:39:06.000000000 +0100
@@ -5,7 +5,7 @@
 # * Run "pre-commit install".
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v5.0.0
+    rev: v6.0.0
     hooks:
       - id: check-added-large-files
       - id: check-case-conflict
@@ -20,14 +20,14 @@
       - id: trailing-whitespace
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.7.4
+    rev: v0.14.8
     hooks:
       - id: ruff
         args: [--fix, --show-fixes]
       - id: ruff-format
 
   - repo: https://github.com/codespell-project/codespell
-    rev: v2.3.0
+    rev: v2.4.1
     hooks:
       - id: codespell
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/PKG-INFO 
new/apscheduler-3.11.2/PKG-INFO
--- old/apscheduler-3.11.1/PKG-INFO     2025-10-31 19:55:16.113325000 +0100
+++ new/apscheduler-3.11.2/PKG-INFO     2025-12-22 01:39:10.236482000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: APScheduler
-Version: 3.11.1
+Version: 3.11.2
 Summary: In-process task scheduler with Cron-like capabilities
 Author-email: Alex Grönholm <[email protected]>
 License: MIT
@@ -47,6 +47,7 @@
 Provides-Extra: test
 Requires-Dist: 
APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]; extra 
== "test"
 Requires-Dist: pytest; extra == "test"
+Requires-Dist: pytest-timeout; extra == "test"
 Requires-Dist: anyio>=4.5.2; extra == "test"
 Requires-Dist: PySide6; (platform_python_implementation == "CPython" and 
python_version < "3.14") and extra == "test"
 Requires-Dist: gevent; python_version < "3.14" and extra == "test"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/docs/versionhistory.rst 
new/apscheduler-3.11.2/docs/versionhistory.rst
--- old/apscheduler-3.11.1/docs/versionhistory.rst      2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/docs/versionhistory.rst      2025-12-22 
01:39:06.000000000 +0100
@@ -4,6 +4,13 @@
 To find out how to migrate your application from a previous version of
 APScheduler, see the :doc:`migration section <migration>`.
 
+**3.11.2**
+
+- Fixed an issue where a job using a ``CronTrigger`` scheduled in a repeated 
time
+  interval during DST transitions could cause the scheduler to get stuck in an 
infinite
+  loop
+  (#1021 <https://github.com/agronholm/apscheduler/issues/1021>_; PR by 
@soulofakuma)
+
 **3.11.1**
 
 - Fixed ``scheduler.shutdown()`` not raising ``SchedulerNotRunning`` (or 
raising the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/pyproject.toml 
new/apscheduler-3.11.2/pyproject.toml
--- old/apscheduler-3.11.1/pyproject.toml       2025-10-31 19:54:58.000000000 
+0100
+++ new/apscheduler-3.11.2/pyproject.toml       2025-12-22 01:39:06.000000000 
+0100
@@ -51,6 +51,7 @@
 test = [
     "APScheduler[mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper,etcd]",
     "pytest",
+    "pytest-timeout",
     "anyio >= 4.5.2",
     "PySide6; python_implementation == 'CPython' and python_version < '3.14'",
     "gevent; python_version < '3.14'",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/src/APScheduler.egg-info/PKG-INFO 
new/apscheduler-3.11.2/src/APScheduler.egg-info/PKG-INFO
--- old/apscheduler-3.11.1/src/APScheduler.egg-info/PKG-INFO    2025-10-31 
19:55:16.000000000 +0100
+++ new/apscheduler-3.11.2/src/APScheduler.egg-info/PKG-INFO    2025-12-22 
01:39:10.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: APScheduler
-Version: 3.11.1
+Version: 3.11.2
 Summary: In-process task scheduler with Cron-like capabilities
 Author-email: Alex Grönholm <[email protected]>
 License: MIT
@@ -47,6 +47,7 @@
 Provides-Extra: test
 Requires-Dist: 
APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]; extra 
== "test"
 Requires-Dist: pytest; extra == "test"
+Requires-Dist: pytest-timeout; extra == "test"
 Requires-Dist: anyio>=4.5.2; extra == "test"
 Requires-Dist: PySide6; (platform_python_implementation == "CPython" and 
python_version < "3.14") and extra == "test"
 Requires-Dist: gevent; python_version < "3.14" and extra == "test"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/APScheduler.egg-info/requires.txt 
new/apscheduler-3.11.2/src/APScheduler.egg-info/requires.txt
--- old/apscheduler-3.11.1/src/APScheduler.egg-info/requires.txt        
2025-10-31 19:55:16.000000000 +0100
+++ new/apscheduler-3.11.2/src/APScheduler.egg-info/requires.txt        
2025-12-22 01:39:10.000000000 +0100
@@ -30,6 +30,7 @@
 [test]
 APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]
 pytest
+pytest-timeout
 anyio>=4.5.2
 pytz
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/src/apscheduler/events.py 
new/apscheduler-3.11.2/src/apscheduler/events.py
--- old/apscheduler-3.11.1/src/apscheduler/events.py    2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/events.py    2025-12-22 
01:39:06.000000000 +0100
@@ -1,26 +1,26 @@
 __all__ = (
-    "EVENT_SCHEDULER_STARTED",
-    "EVENT_SCHEDULER_SHUTDOWN",
-    "EVENT_SCHEDULER_PAUSED",
-    "EVENT_SCHEDULER_RESUMED",
+    "EVENT_ALL",
+    "EVENT_ALL_JOBS_REMOVED",
     "EVENT_EXECUTOR_ADDED",
     "EVENT_EXECUTOR_REMOVED",
     "EVENT_JOBSTORE_ADDED",
     "EVENT_JOBSTORE_REMOVED",
-    "EVENT_ALL_JOBS_REMOVED",
     "EVENT_JOB_ADDED",
-    "EVENT_JOB_REMOVED",
-    "EVENT_JOB_MODIFIED",
-    "EVENT_JOB_EXECUTED",
     "EVENT_JOB_ERROR",
+    "EVENT_JOB_EXECUTED",
+    "EVENT_JOB_MAX_INSTANCES",
     "EVENT_JOB_MISSED",
+    "EVENT_JOB_MODIFIED",
+    "EVENT_JOB_REMOVED",
     "EVENT_JOB_SUBMITTED",
-    "EVENT_JOB_MAX_INSTANCES",
-    "EVENT_ALL",
-    "SchedulerEvent",
+    "EVENT_SCHEDULER_PAUSED",
+    "EVENT_SCHEDULER_RESUMED",
+    "EVENT_SCHEDULER_SHUTDOWN",
+    "EVENT_SCHEDULER_STARTED",
     "JobEvent",
     "JobExecutionEvent",
     "JobSubmissionEvent",
+    "SchedulerEvent",
 )
 
 
@@ -76,7 +76,7 @@
         self.alias = alias
 
     def __repr__(self):
-        return "<%s (code=%d)>" % (self.__class__.__name__, self.code)
+        return f"<self.__class__.__name__ (code={self.code})>"
 
 
 class JobEvent(SchedulerEvent):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/src/apscheduler/executors/base.py 
new/apscheduler-3.11.2/src/apscheduler/executors/base.py
--- old/apscheduler-3.11.1/src/apscheduler/executors/base.py    2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/executors/base.py    2025-12-22 
01:39:06.000000000 +0100
@@ -17,8 +17,8 @@
 class MaxInstancesReachedError(Exception):
     def __init__(self, job):
         super().__init__(
-            'Job "%s" has already reached its maximum number of instances (%d)'
-            % (job.id, job.max_instances)
+            f'Job "{job.id}" has already reached its maximum number of 
instances '
+            f"({job.max_instances})"
         )
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/src/apscheduler/job.py 
new/apscheduler-3.11.2/src/apscheduler/job.py
--- old/apscheduler-3.11.1/src/apscheduler/job.py       2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/job.py       2025-12-22 
01:39:06.000000000 +0100
@@ -1,4 +1,5 @@
 from collections.abc import Iterable, Mapping
+from datetime import timezone
 from inspect import isclass, ismethod
 from uuid import uuid4
 
@@ -12,6 +13,8 @@
     ref_to_obj,
 )
 
+UTC = timezone.utc
+
 
 class Job:
     """
@@ -38,21 +41,21 @@
     """
 
     __slots__ = (
-        "_scheduler",
+        "__weakref__",
         "_jobstore_alias",
-        "id",
-        "trigger",
+        "_scheduler",
+        "args",
+        "coalesce",
         "executor",
         "func",
         "func_ref",
-        "args",
+        "id",
         "kwargs",
-        "name",
-        "misfire_grace_time",
-        "coalesce",
         "max_instances",
+        "misfire_grace_time",
+        "name",
         "next_run_time",
-        "__weakref__",
+        "trigger",
     )
 
     def __init__(self, scheduler, id=None, **kwargs):
@@ -145,7 +148,7 @@
         """
         run_times = []
         next_run_time = self.next_run_time
-        while next_run_time and next_run_time <= now:
+        while next_run_time and next_run_time.astimezone(UTC) <= 
now.astimezone(UTC):
             run_times.append(next_run_time)
             next_run_time = self.trigger.get_next_fire_time(next_run_time, now)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/apscheduler/triggers/calendarinterval.py 
new/apscheduler-3.11.2/src/apscheduler/triggers/calendarinterval.py
--- old/apscheduler-3.11.1/src/apscheduler/triggers/calendarinterval.py 
2025-10-31 19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/triggers/calendarinterval.py 
2025-12-22 01:39:06.000000000 +0100
@@ -65,15 +65,15 @@
     """
 
     __slots__ = (
-        "years",
-        "months",
-        "weeks",
+        "_time",
         "days",
-        "start_date",
         "end_date",
-        "timezone",
         "jitter",
-        "_time",
+        "months",
+        "start_date",
+        "timezone",
+        "weeks",
+        "years",
     )
 
     def __init__(
@@ -183,4 +183,4 @@
             fields.append(f"end_date='{self.end_date}'")
 
         fields.append(f"timezone={timezone_repr(self.timezone)!r}")
-        return f'{self.__class__.__name__}({", ".join(fields)})'
+        return f"{self.__class__.__name__}({', '.join(fields)})"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/apscheduler/triggers/combining.py 
new/apscheduler-3.11.2/src/apscheduler/triggers/combining.py
--- old/apscheduler-3.11.1/src/apscheduler/triggers/combining.py        
2025-10-31 19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/triggers/combining.py        
2025-12-22 01:39:06.000000000 +0100
@@ -3,7 +3,7 @@
 
 
 class BaseCombiningTrigger(BaseTrigger):
-    __slots__ = ("triggers", "jitter")
+    __slots__ = ("jitter", "triggers")
 
     def __init__(self, triggers, jitter=None):
         self.triggers = triggers
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/apscheduler/triggers/cron/__init__.py 
new/apscheduler-3.11.2/src/apscheduler/triggers/cron/__init__.py
--- old/apscheduler-3.11.1/src/apscheduler/triggers/cron/__init__.py    
2025-10-31 19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/triggers/cron/__init__.py    
2025-12-22 01:39:06.000000000 +0100
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 
 from tzlocal import get_localzone
 
@@ -16,8 +16,11 @@
     convert_to_datetime,
     datetime_ceil,
     datetime_repr,
+    datetime_utc_add,
 )
 
+UTC = timezone.utc
+
 
 class CronTrigger(BaseTrigger):
     """
@@ -62,7 +65,7 @@
         "second": BaseField,
     }
 
-    __slots__ = "timezone", "start_date", "end_date", "fields", "jitter"
+    __slots__ = "end_date", "fields", "jitter", "start_date", "timezone"
 
     def __init__(
         self,
@@ -183,9 +186,7 @@
                     i += 1
 
         difference = datetime(**values) - dateval.replace(tzinfo=None)
-        dateval = datetime.fromtimestamp(
-            dateval.timestamp() + difference.total_seconds(), self.timezone
-        )
+        dateval = datetime_utc_add(dateval, difference)
         return dateval, fieldnum
 
     def _set_field_value(self, dateval, fieldnum, new_value):
@@ -202,19 +203,23 @@
         return datetime(**values, tzinfo=self.timezone, fold=dateval.fold)
 
     def get_next_fire_time(self, previous_fire_time, now):
-        # If datetime is folded, cast in ISO format to ensure they advance 
correctly
-        if previous_fire_time and previous_fire_time.fold == 1:
-            previous_fire_time = 
datetime.fromisoformat(previous_fire_time.isoformat())
-
-        if now.fold == 1:
-            now = datetime.fromisoformat(now.isoformat()) + 
timedelta(microseconds=1)
-
         if previous_fire_time:
-            start_date = min(now, previous_fire_time + 
timedelta(microseconds=1))
+            start_date = min(
+                now.astimezone(UTC),
+                datetime_utc_add(
+                    previous_fire_time, timedelta(microseconds=1)
+                ).astimezone(UTC),
+            ).astimezone(self.timezone)
             if start_date == previous_fire_time:
-                start_date += timedelta(microseconds=1)
+                start_date = datetime_utc_add(start_date, 
timedelta(microseconds=1))
         else:
-            start_date = max(now, self.start_date) if self.start_date else now
+            start_date = (
+                max(now.astimezone(UTC), 
self.start_date.astimezone(UTC)).astimezone(
+                    self.timezone
+                )
+                if self.start_date
+                else now
+            )
 
         fieldnum = 0
         next_date = datetime_ceil(start_date).astimezone(self.timezone)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/apscheduler/triggers/cron/expressions.py 
new/apscheduler-3.11.2/src/apscheduler/triggers/cron/expressions.py
--- old/apscheduler-3.11.1/src/apscheduler/triggers/cron/expressions.py 
2025-10-31 19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/triggers/cron/expressions.py 
2025-12-22 01:39:06.000000000 +0100
@@ -2,10 +2,10 @@
 
 __all__ = (
     "AllExpression",
+    "LastDayOfMonthExpression",
     "RangeExpression",
-    "WeekdayRangeExpression",
     "WeekdayPositionExpression",
-    "LastDayOfMonthExpression",
+    "WeekdayRangeExpression",
 )
 
 import re
@@ -68,7 +68,7 @@
 
     def __str__(self):
         if self.step:
-            return "*/%d" % self.step
+            return f"*/{self.step}"
         return "*"
 
     def __repr__(self):
@@ -136,18 +136,18 @@
 
     def __str__(self):
         if self.last != self.first and self.last is not None:
-            range = "%d-%d" % (self.first, self.last)
+            range = f"{self.first}-{self.last}"
         else:
             range = str(self.first)
 
         if self.step:
-            return "%s/%d" % (range, self.step)
+            return f"{range}/{self.step}"
 
         return range
 
     def __repr__(self):
         args = [str(self.first)]
-        if self.last != self.first and self.last is not None or self.step:
+        if (self.last != self.first and self.last is not None) or self.step:
             args.append(str(self.last))
 
         if self.step:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/apscheduler/triggers/cron/fields.py 
new/apscheduler-3.11.2/src/apscheduler/triggers/cron/fields.py
--- old/apscheduler-3.11.1/src/apscheduler/triggers/cron/fields.py      
2025-10-31 19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/triggers/cron/fields.py      
2025-12-22 01:39:06.000000000 +0100
@@ -1,13 +1,13 @@
 """Fields represent CronTrigger options which map to 
:class:`~datetime.datetime` fields."""
 
 __all__ = (
-    "MIN_VALUES",
-    "MAX_VALUES",
     "DEFAULT_VALUES",
+    "MAX_VALUES",
+    "MIN_VALUES",
     "BaseField",
-    "WeekField",
     "DayOfMonthField",
     "DayOfWeekField",
+    "WeekField",
 )
 
 import re
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apscheduler-3.11.1/src/apscheduler/triggers/interval.py 
new/apscheduler-3.11.2/src/apscheduler/triggers/interval.py
--- old/apscheduler-3.11.1/src/apscheduler/triggers/interval.py 2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/triggers/interval.py 2025-12-22 
01:39:06.000000000 +0100
@@ -29,12 +29,12 @@
     """
 
     __slots__ = (
-        "timezone",
-        "start_date",
         "end_date",
         "interval",
         "interval_length",
         "jitter",
+        "start_date",
+        "timezone",
     )
 
     def __init__(
@@ -79,7 +79,7 @@
             next_fire_time = self.start_date.timestamp()
         else:
             timediff = now.timestamp() - self.start_date.timestamp()
-            next_interval_num = int(ceil(timediff / self.interval_length))
+            next_interval_num = ceil(timediff / self.interval_length)
             next_fire_time = (
                 self.start_date.timestamp() + self.interval_length * 
next_interval_num
             )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/src/apscheduler/util.py 
new/apscheduler-3.11.2/src/apscheduler/util.py
--- old/apscheduler-3.11.1/src/apscheduler/util.py      2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/src/apscheduler/util.py      2025-12-22 
01:39:06.000000000 +0100
@@ -1,21 +1,21 @@
 """This module contains several handy functions primarily meant for internal 
use."""
 
 __all__ = (
-    "asint",
     "asbool",
+    "asint",
     "astimezone",
+    "check_callable_args",
     "convert_to_datetime",
-    "datetime_to_utc_timestamp",
-    "utc_timestamp_to_datetime",
     "datetime_ceil",
+    "datetime_to_utc_timestamp",
     "get_callable_name",
-    "obj_to_ref",
-    "ref_to_obj",
+    "localize",
     "maybe_ref",
-    "check_callable_args",
     "normalize",
-    "localize",
+    "obj_to_ref",
+    "ref_to_obj",
     "undefined",
+    "utc_timestamp_to_datetime",
 )
 
 import re
@@ -35,6 +35,8 @@
 else:
     from zoneinfo import ZoneInfo
 
+UTC = timezone.utc
+
 
 class _Undefined:
     def __nonzero__(self):
@@ -236,10 +238,32 @@
 
     """
     if dateval.microsecond > 0:
-        return dateval + timedelta(seconds=1, 
microseconds=-dateval.microsecond)
+        return datetime_utc_add(
+            dateval, timedelta(seconds=1, microseconds=-dateval.microsecond)
+        )
+
     return dateval
 
 
+def datetime_utc_add(dateval: datetime, tdelta: timedelta) -> datetime:
+    """
+    Adds an timedelta to a datetime in UTC for correct datetime arithmetic 
across
+    Daylight Saving Time changes
+
+    :param dateval: The date to add to
+    :type dateval: datetime
+    :param operand: The timedelta to add to the datetime
+    :type operand: timedelta
+    :return: The sum of the datetime and the timedelta
+    :rtype: datetime
+    """
+    original_tz = dateval.tzinfo
+    if original_tz is None:
+        return dateval + tdelta
+
+    return (dateval.astimezone(UTC) + tdelta).astimezone(original_tz)
+
+
 def datetime_repr(dateval):
     return dateval.strftime("%Y-%m-%d %H:%M:%S %Z") if dateval else "None"
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/tests/test_job.py 
new/apscheduler-3.11.2/tests/test_job.py
--- old/apscheduler-3.11.1/tests/test_job.py    2025-10-31 19:54:58.000000000 
+0100
+++ new/apscheduler-3.11.2/tests/test_job.py    2025-12-22 01:39:06.000000000 
+0100
@@ -1,4 +1,5 @@
 import gc
+import sys
 import weakref
 from datetime import datetime, timedelta
 from functools import partial
@@ -11,6 +12,11 @@
 from apscheduler.triggers.date import DateTrigger
 from apscheduler.util import localize
 
+if sys.version_info < (3, 9):
+    from backports.zoneinfo import ZoneInfo
+else:
+    from zoneinfo import ZoneInfo
+
 
 def dummyfunc():
     pass
@@ -103,6 +109,32 @@
     assert run_times == expected_times
 
 
[email protected](5)
+def test_get_run_times_dst_transition(create_job):
+    """Tests that Job._get_run_times does not run into an endless loop due to 
datetime comparison"""
+    timezone = ZoneInfo("US/Eastern")
+    next_run_time = datetime(2025, 11, 2, 1, 0, 10, tzinfo=timezone)
+    now = datetime(2025, 11, 2, 1, 0, 10, 10, tzinfo=timezone)
+    next_next_run_time = datetime(2025, 11, 2, 1, 0, 10, fold=1, 
tzinfo=timezone)
+    job = create_job(
+        trigger="cron",
+        trigger_args={"timezone": timezone, "hour": 1, "minute": 0, "second": 
10},
+        next_run_time=next_next_run_time,
+        func=dummyfunc,
+    )
+    job.next_run_time = next_run_time
+
+    run_times = job._get_run_times(now)
+    assert len(run_times) == 1
+    assert str(run_times[0]) == str(next_run_time)
+
+    run_times = job._get_run_times(now.replace(fold=1))
+    assert len(run_times) == 2
+    assert list(map(str, run_times)) == list(
+        map(str, [next_run_time, next_next_run_time])
+    )
+
+
 def test_private_modify_bad_id(job):
     """Tests that only strings are accepted for job IDs."""
     del job.id
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/tests/test_jobstores.py 
new/apscheduler-3.11.2/tests/test_jobstores.py
--- old/apscheduler-3.11.1/tests/test_jobstores.py      2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/tests/test_jobstores.py      2025-12-22 
01:39:06.000000000 +0100
@@ -297,7 +297,7 @@
 
     """
     jobs = [
-        create_add_job(jobstore, dummy_job, datetime(2014, 2, 26), "job%d" % i)
+        create_add_job(jobstore, dummy_job, datetime(2014, 2, 26), f"job{i}")
         for i in range(3)
     ]
     jobs[index].next_run_time = (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/tests/test_util.py 
new/apscheduler-3.11.2/tests/test_util.py
--- old/apscheduler-3.11.1/tests/test_util.py   2025-10-31 19:54:58.000000000 
+0100
+++ new/apscheduler-3.11.2/tests/test_util.py   2025-12-22 01:39:06.000000000 
+0100
@@ -190,8 +190,7 @@
             ValueError, convert_to_datetime, "2009-8-1", None, "argname"
         )
         assert str(exc.value) == (
-            'The "tz" argument must be specified if argname has no timezone '
-            "information"
+            'The "tz" argument must be specified if argname has no timezone 
information'
         )
 
     def test_text_timezone(self):
@@ -460,8 +459,7 @@
         func = eval("lambda x, *, y, z=1: None")
         exc = pytest.raises(ValueError, check_callable_args, func, [1], {})
         assert str(exc.value) == (
-            "The following keyword-only arguments have not been supplied in "
-            "kwargs: y"
+            "The following keyword-only arguments have not been supplied in 
kwargs: y"
         )
 
     def test_wrapped_func(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apscheduler-3.11.1/tests/triggers/test_cron.py 
new/apscheduler-3.11.2/tests/triggers/test_cron.py
--- old/apscheduler-3.11.1/tests/triggers/test_cron.py  2025-10-31 
19:54:58.000000000 +0100
+++ new/apscheduler-3.11.2/tests/triggers/test_cron.py  2025-12-22 
01:39:06.000000000 +0100
@@ -48,8 +48,7 @@
 def test_cron_trigger_4(timezone):
     trigger = CronTrigger(year="2012", month="2", day="last", 
timezone=timezone)
     assert repr(trigger) == (
-        "<CronTrigger (year='2012', month='2', day='last', "
-        "timezone='Europe/Berlin')>"
+        "<CronTrigger (year='2012', month='2', day='last', 
timezone='Europe/Berlin')>"
     )
     start_date = localize(datetime(2012, 2, 1), timezone)
     correct_next_date = localize(datetime(2012, 2, 29), timezone)
@@ -265,31 +264,59 @@
 
 
 @pytest.mark.parametrize(
-    "trigger_args, start_date, start_date_fold, correct_next_date",
+    "trigger_args, start_date, start_date_fold, correct_next_date, 
correct_next_date_fold",
     [
-        ({"hour": 8}, datetime(2013, 3, 9, 12), False, datetime(2013, 3, 10, 
8)),
-        ({"hour": 8}, datetime(2013, 11, 2, 12), True, datetime(2013, 11, 3, 
8)),
+        ({"hour": 8}, datetime(2013, 3, 9, 12), 0, datetime(2013, 3, 10, 8), 
0),
+        ({"hour": 8}, datetime(2013, 11, 2, 12), 1, datetime(2013, 11, 3, 8), 
0),
+        (
+            {"hour": 1, "minute": 30},
+            datetime(2013, 11, 3, 0, 30),
+            0,
+            datetime(2013, 11, 3, 1, 30),
+            0,
+        ),
+        (
+            {"hour": 1, "minute": 30},
+            datetime(2013, 11, 3, 1, 30, 5),
+            0,
+            datetime(2013, 11, 3, 1, 30),
+            1,
+        ),
+        (
+            {"hour": 1, "minute": 30},
+            datetime(2013, 11, 3, 1, 30, 5),
+            1,
+            datetime(2013, 11, 4, 1, 30),
+            0,
+        ),
         (
             {"minute": "*/30"},
             datetime(2013, 3, 10, 1, 35),
             1,
             datetime(2013, 3, 10, 3),
+            0,
         ),
         (
             {"minute": "*/30"},
             datetime(2013, 11, 3, 1, 35),
             0,
             datetime(2013, 11, 3, 1),
+            1,
         ),
     ],
     ids=[
         "absolute_spring",
         "absolute_autumn",
+        "absolute_autumn_from_before_into_repeated_interval",
+        "absolute_autumn_from_repeated_into_repeated_interval",
+        "absolute_autumn_from_repeated_interval_to_after",
         "interval_spring",
         "interval_autumn",
     ],
 )
-def test_dst_change(trigger_args, start_date, start_date_fold, 
correct_next_date):
+def test_dst_change(
+    trigger_args, start_date, start_date_fold, correct_next_date, 
correct_next_date_fold
+):
     """
     Making sure that CronTrigger works correctly when crossing the DST switch 
threshold.
     Note that you should explicitly compare datetimes as strings to avoid the 
internal datetime
@@ -299,8 +326,16 @@
     timezone = ZoneInfo("US/Eastern")
     trigger = CronTrigger(timezone=timezone, **trigger_args)
     start_date = start_date.replace(tzinfo=timezone, fold=start_date_fold)
-    correct_next_date = correct_next_date.replace(tzinfo=timezone, fold=1)
+    correct_next_date = correct_next_date.replace(
+        tzinfo=timezone, fold=correct_next_date_fold
+    )
     assert str(trigger.get_next_fire_time(None, start_date)) == 
str(correct_next_date)
+    assert str(trigger.get_next_fire_time(start_date, start_date)) == str(
+        correct_next_date
+    )
+    assert str(trigger.get_next_fire_time(start_date, correct_next_date)) == 
str(
+        correct_next_date
+    )
 
 
 def test_dst_change_2(timezone):
@@ -310,7 +345,7 @@
     """
     timezone = ZoneInfo("Europe/Helsinki")
     trigger = CronTrigger(minute=30, timezone=timezone)
-    start_date = datetime(2017, 10, 29, 3, 30, tzinfo=timezone, fold=1)
+    start_date = datetime(2017, 10, 29, 3, 30, 0, 5, tzinfo=timezone, fold=1)
     correct_next_date = datetime(2017, 10, 29, 4, 30, tzinfo=timezone, fold=0)
     assert str(trigger.get_next_fire_time(None, start_date)) == 
str(correct_next_date)
     assert str(trigger.get_next_fire_time(start_date, start_date)) == str(

Reply via email to