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

pincer.utils.tasks.TaskScheduler.loop()   A

Complexity

Conditions 3

Size

Total Lines 86
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 86
rs 9.232
c 0
b 0
f 0
cc 3
nop 8

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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