Passed
Pull Request — main (#155)
by Oliver
03:16 queued 01:36
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 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