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