Passed
Pull Request — main (#155)
by Oliver
03:16 queued 01:36
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 Callable, Set
0 ignored issues
show
introduced by
Imports from package typing are not grouped
Loading history...
21
22
    from .types import Coro
23
24
25
_log = logging.getLogger(__package__)
26
27
28
class TaskScheduler:
29
    """Class that scedules tasts."""
30
31
    def __init__(self, client):
32
        self.client = client
33
        self.tasks: Set[Task] = set()
34
        self._loop = asyncio.get_event_loop()
35
36
    def loop(
0 ignored issues
show
best-practice introduced by
Too many arguments (8/5)
Loading history...
37
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
38
        days=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
39
        weeks=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
40
        hours=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
41
        minutes=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
42
        seconds=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
43
        milliseconds=0,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
44
        microseconds=0
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
45
    ) -> Callable[[Coro], Task]:
46
        """A decorator to create a task that repeat the given amount of t
47
48
        :Example usage:
49
50
        .. code-block:: python
51
52
            from pincer import Client
53
            from pincer.utils import TaskScheduler
54
55
            client = Client("token")
56
            task = TaskScheduler(client)
57
58
            @task.loop(minutes=3)
59
            async def my_task(self):
60
                ...
61
62
            my_task.start()
63
            client.run()
64
65
        Parameters
66
        ----------
67
        days : :class:`int`
68
            Days to wait between iterations.
69
            |default| ``0``
70
        weeks : :class:`int`
71
            Days to wait between iterations.
72
            |default| ``0``
73
        hours : :class:`int`
74
            Days to wait between iterations.
75
            |default| ``0``
76
        minutes : :class:`int`
77
            Days to wait between iterations.
78
            |default| ``0``
79
        seconds : :class:`int`
80
            Days to wait between iterations.
81
            |default| ``0``
82
        milliseconds : :class:`int`
83
            Days to wait between iterations.
84
            |default| ``0``
85
        microseconds : :class:`int`
86
            Days to wait between iterations.
87
            |default| ``0``
88
89
        Raises
90
        ------
91
        TaskIsNotCoroutine:
92
            The task is not a coroutine.
93
        TaskInvalidDelay:
94
            The delay is 0 or negative.
95
        """
96
        def decorator(func: Coro) -> Task:
97
            if not iscoroutinefunction(func):
98
                raise TaskIsNotCoroutine(
99
                    f'Task `{func.__name__}` is not a coroutine, '
100
                    'which is required for tasks.'
101
                )
102
103
            delay = timedelta(
104
                days=days,
105
                weeks=weeks,
106
                hours=hours,
107
                minutes=minutes,
108
                seconds=seconds,
109
                microseconds=microseconds,
110
                milliseconds=milliseconds
111
            ).total_seconds()
112
113
            if delay <= 0:
114
                raise TaskInvalidDelay(
115
                    f'Task `{func.__name__}` has a delay of {delay} seconds, '
116
                    'which is invalid. Delay must be greater than zero.'
117
                )
118
119
            return Task(self, func, delay)
120
121
        return decorator
122
123
    def register(self, task: Task):
124
        """Register a task.
125
126
        Parameters
127
        ----------
128
        task : :class:`~pincer.utils.tasks.Task`
129
            The task to register.
130
        """
131
        self.tasks.add(task)
132
        self.__execute(task)
133
134
    def __execute(self, task: Task):
135
        """Execute a task."""
136
        coro = task.coro(self.client) if task.client_required else task.coro()
137
        # Execute the coroutine
138
        asyncio.ensure_future(coro)
139
140
        # Schedule the coroutine's next execution
141
        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...
142
143
    def close(self):
144
        """Gracefully stops any running task."""
145
        for task in self.tasks.copy():
146
            if task.running:
147
                task.cancel()
148
149
150
class Task:
151
    """A Task is a coroutine that is scheduled to repeat every x seconds.
152
    Use a TaskScheduler in order to create a task.
153
154
    Parameters
155
    ----------
156
    scheduler: :class:`~pincer.utils.tasks.TaskScheduler`
157
        The scheduler to use.
158
    coro: :class:`~pincer.utils.types.Coro`
159
        The coroutine to register as a task.
160
    delay: :class:`float`
161
        Delay between each iteration of the task.
162
    """
163
164
    def __init__(self, scheduler: TaskScheduler, coro: Coro, delay: float):
165
        self._scheduler = scheduler
166
        self.coro = coro
167
        self.delay = delay
168
        self._handle: TimerHandle = None
169
        self._client_required = should_pass_cls(coro)
170
171
    def __del__(self):
172
        if self.running:
173
            self.cancel()
174
        else:
175
            # Did the user forget to call task.start() ?
176
            _log.warn(
0 ignored issues
show
introduced by
Using deprecated method warn()
Loading history...
177
                "Task `%s` was not scheduled. Did you forget to start it ?",
178
                self.coro.__name__
179
            )
180
181
    @property
182
    def cancelled(self):
183
        """:class:`bool`: Check if the task has been cancelled or not."""
184
        return self.running and self._handle.cancelled()
185
186
    @property
187
    def running(self):
188
        """:class:`bool`: Check if the task is running."""
189
        return self._handle is not None
190
191
    def start(self):
192
        """Register the task in the TaskScheduler and start
193
        the execution of the task.
194
        """
195
        if self.running:
196
            raise TaskAlreadyRunning(
197
                f'Task `{self.coro.__name__}` is already running.', self
198
            )
199
200
        self._scheduler.register(self)
201
202
    def cancel(self):
203
        """Cancel the task."""
204
        if not self.running:
205
            raise TaskCancelError(
206
                f'Task `{self.coro.__name__}` is not running.', self
207
            )
208
209
        self._handle.cancel()
210
        if self in self._scheduler.tasks:
211
            self._scheduler.tasks.remove(self)
212