From 9dc617d4a112e9bf31e29ed8d3928139d6fa6d07 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:44:23 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=86=95=20feat(loop):=20add=20optional?= =?UTF-8?q?=20overlap=20support=20to=20allow=20concurrent=20loop=20executi?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a new overlap parameter to the loop decorator and Loop class to control whether loop iterations can run concurrently. When set to True, the next iteration will not wait for the previous one to finish, allowing overlapping executions—useful for long-running tasks that should not delay subsequent runs. Defaults to False to preserve current behavior. Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ext/tasks/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index af34cc6844..3dc3e24d35 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -91,10 +91,12 @@ def __init__( count: int | None, reconnect: bool, loop: asyncio.AbstractEventLoop, + overlap: bool ) -> None: self.coro: LF = coro self.reconnect: bool = reconnect self.loop: asyncio.AbstractEventLoop = loop + self.overlap: bool = overlap self.count: int | None = count self._current_loop = 0 self._handle: SleepHandle = MISSING @@ -166,7 +168,10 @@ async def _loop(self, *args: Any, **kwargs: Any) -> None: self._last_iteration = self._next_iteration self._next_iteration = self._get_next_sleep_time() try: - await self.coro(*args, **kwargs) + if self.overlap: + asyncio.create_task(self.coro(*args, **kwargs)) + else: + await self.coro(*args, **kwargs) self._last_iteration_failed = False backoff = ExponentialBackoff() except self._valid_exception: @@ -738,6 +743,7 @@ def loop( count: int | None = None, reconnect: bool = True, loop: asyncio.AbstractEventLoop = MISSING, + overlap: bool = False, ) -> Callable[[LF], Loop[LF]]: """A decorator that schedules a task in the background for you with optional reconnect logic. The decorator returns a :class:`Loop`. @@ -774,6 +780,9 @@ def loop( The loop to use to register the task, if not given defaults to :func:`asyncio.get_event_loop`. + overlap: :class:`bool` + Whether to allow the next iteration of the loop to run even if the previous one has not completed. + Raises ------ ValueError @@ -793,6 +802,7 @@ def decorator(func: LF) -> Loop[LF]: time=time, reconnect=reconnect, loop=loop, + overlap=overlap ) return decorator From bfda3aaf62ac9a33ec1816f204fb0cfe6a919582 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:46:22 +0000 Subject: [PATCH 2/9] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ext/tasks/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 3dc3e24d35..cc002323ca 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -91,7 +91,7 @@ def __init__( count: int | None, reconnect: bool, loop: asyncio.AbstractEventLoop, - overlap: bool + overlap: bool, ) -> None: self.coro: LF = coro self.reconnect: bool = reconnect @@ -802,7 +802,7 @@ def decorator(func: LF) -> Loop[LF]: time=time, reconnect=reconnect, loop=loop, - overlap=overlap + overlap=overlap, ) return decorator From 8dc95c4db83eab9a136eea9bd5e4bbc829cd0db6 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:51:00 +0300 Subject: [PATCH 3/9] Update CHANGELOG.md Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38725e4141..c5b961f0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ These changes are available on the `master` branch, but have not yet been releas ([#2714](https://github.com/Pycord-Development/pycord/pull/2714)) - Added the ability to pass a `datetime.time` object to `format_dt` ([#2747](https://github.com/Pycord-Development/pycord/pull/2747)) +- Added the ability to pass an `overlap` parameter to the `loop` decorator and `Loop` class, allowing concurrent iterations if enabled ([#2765](https://github.com/Pycord-Development/pycord/pull/2765)) ### Fixed From 7dbf3a07c0c3bf27dcd47be0ee281107c9cfc2bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:51:24 +0000 Subject: [PATCH 4/9] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b961f0a4..a3ea3e4e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2714](https://github.com/Pycord-Development/pycord/pull/2714)) - Added the ability to pass a `datetime.time` object to `format_dt` ([#2747](https://github.com/Pycord-Development/pycord/pull/2747)) -- Added the ability to pass an `overlap` parameter to the `loop` decorator and `Loop` class, allowing concurrent iterations if enabled ([#2765](https://github.com/Pycord-Development/pycord/pull/2765)) +- Added the ability to pass an `overlap` parameter to the `loop` decorator and `Loop` + class, allowing concurrent iterations if enabled + ([#2765](https://github.com/Pycord-Development/pycord/pull/2765)) ### Fixed From 1f1ef8ab41497dfe42f04f4729fb7ceb14d7b865 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:51:47 +0300 Subject: [PATCH 5/9] Update __init__.py Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ext/tasks/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index cc002323ca..c014cb60e5 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -783,6 +783,8 @@ def loop( overlap: :class:`bool` Whether to allow the next iteration of the loop to run even if the previous one has not completed. + .. versionadded:: 2.7 + Raises ------ ValueError From 570ae99cec957753ca7e6bc2197970c04fd45310 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:07:03 +0300 Subject: [PATCH 6/9] fix- TypeError: Loop.__init__() missing 1 required positional argument: 'overlap' Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ext/tasks/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index c014cb60e5..13504bf414 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -223,6 +223,7 @@ def __get__(self, obj: T, objtype: type[T]) -> Loop[LF]: count=self.count, reconnect=self.reconnect, loop=self.loop, + overlap=self.overlap, ) copy._injected = obj copy._before_loop = self._before_loop From c8f3bb4990c900d0e5ac6f9cdc255bbdde2212df Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:26:48 +0300 Subject: [PATCH 7/9] fix cancellation using overlap=true Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ext/tasks/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 13504bf414..37b44bf186 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -117,6 +117,7 @@ def __init__( self._is_being_cancelled = False self._has_failed = False self._stop_next_iteration = False + self._tasks: list[asyncio.Task[Any]] = [] if self.count is not None and self.count <= 0: raise ValueError("count must be greater than 0 or None.") @@ -169,7 +170,8 @@ async def _loop(self, *args: Any, **kwargs: Any) -> None: self._next_iteration = self._get_next_sleep_time() try: if self.overlap: - asyncio.create_task(self.coro(*args, **kwargs)) + task = asyncio.create_task(self.coro(*args, **kwargs)) + self._tasks.append(task) else: await self.coro(*args, **kwargs) self._last_iteration_failed = False @@ -197,6 +199,9 @@ async def _loop(self, *args: Any, **kwargs: Any) -> None: except asyncio.CancelledError: self._is_being_cancelled = True + for task in self._tasks: + task.cancel() + await asyncio.gather(*self._tasks, return_exceptions=True) raise except Exception as exc: self._has_failed = True From 463a249b89baaa241e2f0ca352c39f003b8d7077 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 18:20:48 +0300 Subject: [PATCH 8/9] fix Exception in callback Future.set_result(True) handle: Traceback (most recent call last): File "/usr/local/lib/python3.11/asyncio/events.py", line 84, in _run self._context.run(self._callback, *self._args) asyncio.exceptions.InvalidStateError: invalid state Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ext/tasks/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 37b44bf186..4f820cd5b7 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -59,10 +59,14 @@ def __init__( relative_delta = discord.utils.compute_timedelta(dt) self.handle = loop.call_later(relative_delta, future.set_result, True) + def _set_result_safe(self): + if not self.future.done(): + self.future.set_result(True) + def recalculate(self, dt: datetime.datetime) -> None: self.handle.cancel() relative_delta = discord.utils.compute_timedelta(dt) - self.handle = self.loop.call_later(relative_delta, self.future.set_result, True) + self.handle = loop.call_later(relative_delta, self._set_result_safe) def wait(self) -> asyncio.Future[Any]: return self.future From 26a8ff1ad3344975176607ef2c6bc3b131633792 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 18 Apr 2025 18:25:07 +0300 Subject: [PATCH 9/9] fix identation Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ext/tasks/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 4f820cd5b7..72035394cd 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -60,8 +60,8 @@ def __init__( self.handle = loop.call_later(relative_delta, future.set_result, True) def _set_result_safe(self): - if not self.future.done(): - self.future.set_result(True) + if not self.future.done(): + self.future.set_result(True) def recalculate(self, dt: datetime.datetime) -> None: self.handle.cancel()