|
| 1 | +"""Common helper for small commandline utils.""" |
1 | 2 | import asyncio |
2 | 3 | import signal |
3 | 4 | from abc import ABC, abstractmethod |
4 | 5 |
|
5 | 6 | class AbstractEventHandler(ABC): |
| 7 | + """Base class to handle signals and the asyncio loop. |
| 8 | +
|
| 9 | + Subclasses must implement :py:meth:`on_exit` and :py:meth:`main`. The |
| 10 | + ``run`` method starts an event loop, registers a SIGINT handler and |
| 11 | + executes the :py:meth:`main` coroutine. The loop is stopped when a |
| 12 | + SIGINT is received and the :py:meth:`on_exit` coroutine has finished. |
| 13 | + """ |
6 | 14 | exiting: bool = False |
7 | 15 | @abstractmethod |
8 | 16 | async def on_exit(self): |
9 | | - pass |
| 17 | + """Called when a SIGINT is received. |
| 18 | +
|
| 19 | + Subclasses should override this method to perform any cleanup |
| 20 | + (e.g. closing connections, flushing buffers). It is awaited before |
| 21 | + the handler sets :pyattr:`exiting` to ``True``. |
| 22 | + """ |
10 | 23 |
|
11 | 24 | async def _do_exit(self): |
| 25 | + """Internal helper that runs ``on_exit`` and marks the handler as |
| 26 | + exiting. This coroutine is scheduled by :py:meth:`__handle_sigint` |
| 27 | + when a SIGINT signal arrives. |
| 28 | + """ |
12 | 29 | await self.on_exit() |
13 | 30 | self.exiting = True |
14 | 31 |
|
15 | 32 | @abstractmethod |
16 | 33 | async def main(self): |
17 | | - pass |
| 34 | + """Main coroutine to be executed by the event loop. |
| 35 | +
|
| 36 | + Subclasses must implement this method. It should contain the |
| 37 | + application's primary logic and can call :py:meth:`wait` to keep |
| 38 | + the loop alive until a SIGINT is received. |
| 39 | + """ |
18 | 40 |
|
19 | 41 | # Signal handler for Ctrl+C |
20 | 42 | def register_sigint_handler(self): |
| 43 | + """Register the SIGINT (Ctrl‑C) handler. |
| 44 | +
|
| 45 | + This method sets :py:meth:`__handle_sigint` as the callback for |
| 46 | + ``signal.SIGINT``. It should be called before :py:meth:`run` if |
| 47 | + a custom handler is required. |
| 48 | + """ |
21 | 49 | signal.signal(signal.SIGINT, self.__handle_sigint) |
22 | 50 |
|
23 | 51 | def __handle_sigint(self, signum, frame): |
| 52 | + """Internal SIGINT callback. |
| 53 | +
|
| 54 | + Prints diagnostic information and schedules :py:meth:`_do_exit` |
| 55 | + as a task in the running event loop. After the first SIGINT |
| 56 | + the default handler is restored to allow a second Ctrl‑C to |
| 57 | + terminate immediately. |
| 58 | + """ |
24 | 59 | print(f"\nReceived signal: {signum}") |
25 | 60 | print(f"Signal name: {signal.Signals(signum).name}") |
26 | 61 | print(f"Interrupted at: {frame.f_code.co_filename}:{frame.f_lineno}") |
27 | 62 | signal.signal(signal.SIGINT, signal.SIG_DFL) |
28 | 63 | asyncio.create_task(self._do_exit()) |
29 | 64 |
|
30 | 65 | def run(self): |
| 66 | + """Start the event loop and execute :py:meth:`main`. |
| 67 | +
|
| 68 | + A new event loop is created, the SIGINT handler is registered, |
| 69 | + and :py:meth:`main` is run with ``asyncio.run``. The loop is |
| 70 | + stopped once the coroutine completes (normally or after a SIGINT). |
| 71 | + """ |
31 | 72 | loop = asyncio.new_event_loop() |
32 | 73 | asyncio.run(self.main()) |
33 | 74 | loop.stop() |
34 | 75 |
|
35 | 76 | async def wait(self, seconds=1): |
36 | | - # Keep the event loop running until Ctrl+C is pressed |
| 77 | + """Keep the event loop alive until a SIGINT is received. |
| 78 | +
|
| 79 | + Parameters |
| 80 | + ---------- |
| 81 | + seconds : int, optional |
| 82 | + Number of seconds to sleep between checks. The default is |
| 83 | + ``1`` which balances responsiveness with CPU usage. |
| 84 | +
|
| 85 | + This coroutine can be awaited by subclasses in their |
| 86 | + :py:meth:`main` implementation to block until the handler |
| 87 | + exits. |
| 88 | + """ |
37 | 89 | while not self.exiting: |
38 | 90 | await asyncio.sleep(seconds) |
0 commit comments