Passed
Push — main ( 3ed408...6cab1f )
by Yohann
02:20 queued 34s
created

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

Complexity

Conditions 3

Size

Total Lines 83
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 83
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 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