diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e40955afe..202a240913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +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)) ### Fixed diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index af34cc6844..72035394cd 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 @@ -91,10 +95,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 @@ -115,6 +121,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.") @@ -166,7 +173,11 @@ 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: + task = asyncio.create_task(self.coro(*args, **kwargs)) + self._tasks.append(task) + else: + await self.coro(*args, **kwargs) self._last_iteration_failed = False backoff = ExponentialBackoff() except self._valid_exception: @@ -192,6 +203,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 @@ -218,6 +232,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 @@ -738,6 +753,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 +790,11 @@ 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. + + .. versionadded:: 2.7 + Raises ------ ValueError @@ -793,6 +814,7 @@ def decorator(func: LF) -> Loop[LF]: time=time, reconnect=reconnect, loop=loop, + overlap=overlap, ) return decorator