Passed
Pull Request — main (#155)
by Yohann
01:51
created

pincer.utils.tasks.Task.client_required()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
# Copyright Pincer 2021-Present
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# Full MIT License can be found in `LICENSE` at the project root.
3
4
from __future__ import annotations
5
6
import asyncio
7
import logging
8
from datetime import timedelta
9
from typing import TYPE_CHECKING
10
from asyncio import TimerHandle, iscoroutinefunction
11
12
from . import __package__
13
from ..exceptions import (
14
    TaskAlreadyRunning, TaskCancelError, TaskInvalidDelay,
15
    TaskIsNotCoroutine
16
)
17
from .insertion import should_pass_cls
18
19
if TYPE_CHECKING:
20
    from typing import Optional, Callable, Set
0 ignored issues
show
introduced by
Imports from package typing are not grouped
Loading history...
21
    from .types import Coro
22
23
24
_log = logging.getLogger(__package__)
25
26
27
class TaskScheduler:
28
    """Class that scedules tasts."""
29
30
    def __init__(self, client):
31
        self.client = client
32
        self.tasks: Set[Task] = set()
33
        self._loop = asyncio.get_event_loop()
34
35
    def loop(
0 ignored issues
show
best-practice introduced by
Too many arguments (8/5)
Loading history...
36
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
37
        days=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
38
        weeks=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
39
        hours=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
40
        minutes=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
41
        seconds=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
42
        milliseconds=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
43
        microseconds=0
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
44
    ) -> Callable[[Coro], Task]:
45
        """A decorator to create a task that repeat the given amount of t
46
47
        :Example usage:
48
49
        .. code-block:: python
50
51
            from pincer import Client
52
            from pincer.utils import TaskScheduler
53
54
            client = Client("token")
55
            task = TaskScheduler(client)
56
57
            @task.loop(minutes=3)
58
            async def my_task(self):
59
                ...
60
61
            my_task.start()
62
            client.run()
63
64
        Parameters
65
        ----------
66
        days : :class:`int`
67
            Days to wait between iterations.
68
            |default| ``0``
69
        weeks : :class:`int`
70
            Days to wait between iterations.
71
            |default| ``0``
72
        hours : :class:`int`
73
            Days to wait between iterations.
74
            |default| ``0``
75
        minutes : :class:`int`
76
            Days to wait between iterations.
77
            |default| ``0``
78
        seconds : :class:`int`
79
            Days to wait between iterations.
80
            |default| ``0``
81
        milliseconds : :class:`int`
82
            Days to wait between iterations.
83
            |default| ``0``
84
        microseconds : :class:`int`
85
            Days to wait between iterations.
86
            |default| ``0``
87
88
        Raises
89
        ------
90
        TaskIsNotCoroutine:
91
            The task is not a coroutine.
92
        TaskInvalidDelay:
93
            The delay is 0 or negative.
94
        """
95
        def decorator(func: Coro) -> Task:
96
            if not iscoroutinefunction(func):
97
                raise TaskIsNotCoroutine(
98
                    f'Task `{func.__name__}` is not a coroutine, '
99
                    'which is required for tasks.'
100
                )
101
102
            delay = timedelta(
103
                days=days,
104
                weeks=weeks,
105
                hours=hours,
106
                minutes=minutes,
107
                seconds=seconds,
108
                microseconds=microseconds,
109
                milliseconds=milliseconds
110
            ).total_seconds()
111
112
            if delay <= 0:
113
                raise TaskInvalidDelay(
114
                    f'Task `{func.__name__}` has a delay of {delay} seconds, '
115
                    'which is invalid. Delay must be greater than zero.'
116
                )
117
118
            return Task(self, func, delay)
119
120
        return decorator
121
122
    def register(self, task: Task):
123
        """Register a task.
124
125
        Parameters
126
        ----------
127
        task : :class:`~pincer.utils.tasks.Task`
128
            The task to register.
129
        """
130
        self.tasks.add(task)
131
        self.__execute(task)
132
133
    def __execute(self, task: Task):
134
        """Execute a task."""
135
        coro = task.coro(self.client) if task.client_required else task.coro()
136
        # Execute the coroutine
137
        asyncio.ensure_future(coro)
138
139
        # Schedule the coroutine's next execution
140
        task._handle = self._loop.call_later(task.delay, self.__execute, task)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _handle was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
141
142
    def close(self):
143
        """Gracefully stops any running task."""
144
        for task in self.tasks.copy():
145
            if task.running:
146
                task.cancel()
147
148
149
class Task:
150
    """A Task is a coroutine that is scheduled to repeat every x seconds.
151
    Use a TaskScheduler in order to create a task.
152
153
    Parameters
154
    ----------
155
    scheduler: :class:`~pincer.utils.tasks.TaskScheduler`
156
        The scheduler to use.
157
    coro: :class:`~pincer.utils.types.Coro`
158
        The coroutine to register as a task.
159
    delay: :class:`float`
160
        Delay between each iteration of the task.
161
    """
162
163
    def __init__(self, scheduler: TaskScheduler, coro: Coro, delay: float):
164
        self._scheduler = scheduler
165
        self.coro = coro
166
        self.delay = delay
167
        self._handle: TimerHandle = None
168
        self._client_required = should_pass_cls(coro)
169
170
    def __del__(self):
171
        if self.running:
172
            self.cancel()
173
        else:
174
            # Did the user forget to call task.start() ?
175
            _log.warn(
0 ignored issues
show
introduced by
Using deprecated method warn()
Loading history...
176
                "Task `%s` was not scheduled. Did you forget to start it ?",
177
                self.coro.__name__
178
            )
179
180
    @property
181
    def cancelled(self):
182
        """:class:`bool`: Check if the task has been cancelled or not."""
183
        return self.running and self._handle.cancelled()
184
185
    @property
186
    def running(self):
187
        """:class:`bool`: Check if the task is running."""
188
        return self._handle is not None
189
190
    def start(self):
191
        """Register the task in the TaskScheduler and start
192
        the execution of the task.
193
        """
194
        if self.running:
195
            raise TaskAlreadyRunning(
196
                f'Task `{self.coro.__name__}` is already running.', self
197
            )
198
199
        self._scheduler.register(self)
200
201
    def cancel(self):
202
        """Cancel the task."""
203
        if not self.running:
204
            raise TaskCancelError(
205
                f'Task `{self.coro.__name__}` is not running.', self
206
            )
207
208
        self._handle.cancel()
209
        if self in self._scheduler.tasks:
210
            self._scheduler.tasks.remove(self)
211