Completed
Push — main ( e04df9...e9b147 )
by Yohann
15s queued 13s
created

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

Complexity

Conditions 3

Size

Total Lines 82
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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