Skip to content

Commit 26f58f1

Browse files
committed
Make pylint happier
1 parent 65dcc38 commit 26f58f1

14 files changed

+282
-95
lines changed

src/powersensor_local/EventBuffer.py

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,90 @@
1+
"""Common helper for small commandline utils."""
12
import asyncio
23
import signal
34
from abc import ABC, abstractmethod
45

56
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+
"""
614
exiting: bool = False
715
@abstractmethod
816
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+
"""
1023

1124
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+
"""
1229
await self.on_exit()
1330
self.exiting = True
1431

1532
@abstractmethod
1633
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+
"""
1840

1941
# Signal handler for Ctrl+C
2042
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+
"""
2149
signal.signal(signal.SIGINT, self.__handle_sigint)
2250

2351
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+
"""
2459
print(f"\nReceived signal: {signum}")
2560
print(f"Signal name: {signal.Signals(signum).name}")
2661
print(f"Interrupted at: {frame.f_code.co_filename}:{frame.f_lineno}")
2762
signal.signal(signal.SIGINT, signal.SIG_DFL)
2863
asyncio.create_task(self._do_exit())
2964

3065
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+
"""
3172
loop = asyncio.new_event_loop()
3273
asyncio.run(self.main())
3374
loop.stop()
3475

3576
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+
"""
3789
while not self.exiting:
3890
await asyncio.sleep(seconds)

src/powersensor_local/async_event_emitter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""Small helper class for pub/sub functionality with async handlers."""
12
from typing import Callable
23

34
class AsyncEventEmitter:
@@ -31,5 +32,5 @@ async def emit(self, event_name: str, *args):
3132
for callback in self._listeners[event_name]:
3233
try:
3334
await callback(event_name, *args)
34-
except BaseException as e:
35+
except BaseException as e: # pylint: disable=W0718
3536
await self.emit('exception', e)

src/powersensor_local/devices.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
"""Abstraction interface for unified event stream from Powersensor devices"""
12
import asyncio
23
import sys
34

45
from datetime import datetime, timezone
56
from pathlib import Path
6-
project_root = str(Path(__file__).parents[1])
7-
if project_root not in sys.path:
8-
sys.path.append(project_root)
7+
PROJECT_ROOT = str(Path(__file__).parents[1])
8+
if PROJECT_ROOT not in sys.path:
9+
sys.path.append(PROJECT_ROOT)
910

11+
# pylint: disable=C0413
1012
from powersensor_local.legacy_discovery import LegacyDiscovery
1113
from powersensor_local.plug_api import PlugApi
1214

@@ -22,9 +24,9 @@ def __init__(self, bcast_addr='<broadcast>'):
2224
"""Creates a fresh instance, without scanning for devices."""
2325
self._event_cb = None
2426
self._discovery = LegacyDiscovery(bcast_addr)
25-
self._devices = dict()
27+
self._devices = {}
2628
self._timer = None
27-
self._plug_apis = dict()
29+
self._plug_apis = {}
2830

2931
async def start(self, async_event_cb):
3032
"""Registers the async event callback function and starts the scan
@@ -81,7 +83,7 @@ async def stop(self):
8183
To restart the event streaming, call start() again."""
8284
for plug in self._plug_apis.values():
8385
await plug.disconnect()
84-
self._plug_apis = dict()
86+
self._plug_apis = {}
8587
self._event_cb = None
8688
if self._timer:
8789
self._timer.terminate()
@@ -175,21 +177,24 @@ def __init__(self, mac):
175177
self._last_active = datetime.now(timezone.utc)
176178

177179
def mark_active(self):
180+
"""Updates the last activity time to prevent expiry."""
178181
self._last_active = datetime.now(timezone.utc)
179182

180183
def has_expired(self):
184+
"""Checks whether the last activity time is past the expiry."""
181185
now = datetime.now(timezone.utc)
182186
delta = now - self._last_active
183187
return delta.total_seconds() > EXPIRY_TIMEOUT_S
184188

185-
class _Timer:
189+
class _Timer: # pylint: disable=R0903
186190
def __init__(self, interval_s, callback):
187191
self._terminate = False
188192
self._interval = interval_s
189193
self._callback = callback
190194
self._task = asyncio.create_task(self._run())
191195

192196
def terminate(self):
197+
"""Disables the timer and cancels the associated task."""
193198
self._terminate = True
194199
self._task.cancel()
195200

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""A simple fixed‑size buffer that stores event dictionaries."""
2+
from typing import Any
3+
4+
5+
class EventBuffer:
6+
"""A simple fixed‑size buffer that stores event dictionaries.
7+
8+
Parameters
9+
----------
10+
keep : int
11+
The maximum number of events to retain in the buffer. When a new event
12+
is appended and this limit would be exceeded, the oldest event (the
13+
one at index 0) is removed.
14+
"""
15+
def __init__(self, keep: int):
16+
self._keep = keep
17+
self._evs = []
18+
19+
def find_by_key(self, key: str, value: Any):
20+
"""Return the first event that contains ``key`` with the given ``value``.
21+
22+
Parameters
23+
----------
24+
key : str
25+
The dictionary key to search for.
26+
value : Any
27+
The value that the key must match.
28+
29+
Returns
30+
-------
31+
dict | None
32+
The matching event dictionary, or ``None`` if no match is found.
33+
"""
34+
for ev in self._evs:
35+
if key in ev and ev[key] == value:
36+
return ev
37+
return None
38+
39+
def append(self, ev: dict):
40+
"""Add an event to the buffer.
41+
42+
If adding the new event would exceed ``self._keep``, the oldest event
43+
is removed to keep the buffer size bounded.
44+
45+
Parameters
46+
----------
47+
ev : dict
48+
The event dictionary to append.
49+
"""
50+
self._evs.append(ev)
51+
if len(self._evs) > self._keep:
52+
del self._evs[0]
53+
54+
def evict_older(self, key: str, value: float):
55+
"""Remove events that are older than a given timestamp.
56+
57+
Events are considered *older* if they contain ``key`` and its value is
58+
less than or equal to the provided ``value``. Eviction stops as soon
59+
as an event that does not satisfy this condition is encountered (the
60+
buffer is ordered by insertion time).
61+
62+
Parameters
63+
----------
64+
key : str
65+
The timestamp key to inspect in each event.
66+
value : float
67+
The cutoff timestamp; events with timestamps <= this value are removed.
68+
"""
69+
while len(self._evs) > 0:
70+
ev = self._evs[0]
71+
if key in ev and ev[key] <= value:
72+
del self._evs[0]
73+
else:
74+
return

src/powersensor_local/events.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
import sys
88
from pathlib import Path
99

10-
project_root = str(Path(__file__).parents[1])
11-
if project_root not in sys.path:
12-
sys.path.append(project_root)
10+
PROJECT_ROOT = str(Path(__file__).parents[1])
11+
if PROJECT_ROOT not in sys.path:
12+
sys.path.append(PROJECT_ROOT)
1313

14+
# pylint: disable=C0413
1415
from powersensor_local.devices import PowersensorDevices
1516
from powersensor_local.abstract_event_handler import AbstractEventHandler
1617

1718
class EventLoopRunner(AbstractEventHandler):
19+
"""Main logic wrapper."""
1820
def __init__(self):
1921
self.devices: typing.Union[PowersensorDevices, None] = PowersensorDevices()
2022

@@ -23,6 +25,7 @@ async def on_exit(self):
2325
await self.devices.stop()
2426

2527
async def on_message(self, obj):
28+
"""Callback for printing received events."""
2629
print(obj)
2730
if obj['event'] == 'device_found':
2831
self.devices.subscribe(obj['mac'])
@@ -40,6 +43,7 @@ async def main(self):
4043
await self.wait()
4144

4245
def app():
46+
"""Application entry point."""
4347
EventLoopRunner().run()
4448

4549
if __name__ == "__main__":

src/powersensor_local/legacy_discovery.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""The legacy alternative to using mDNS discovery."""
12
import asyncio
23
import json
34
import socket
@@ -13,7 +14,7 @@ def __init__(self, broadcast_addr = '<broadcast>'):
1314
"""
1415
super().__init__()
1516
self._dst_addr = broadcast_addr
16-
self._found = dict()
17+
self._found = {}
1718

1819
async def scan(self, timeout_sec = 2.0):
1920
"""Scans the local network for discoverable devices.
@@ -25,7 +26,7 @@ async def scan(self, timeout_sec = 2.0):
2526
"id": "aabbccddeeff",
2627
}
2728
"""
28-
self._found = dict()
29+
self._found = {}
2930

3031
loop = asyncio.get_running_loop()
3132
transport, _ = await loop.create_datagram_endpoint(
@@ -45,6 +46,7 @@ async def scan(self, timeout_sec = 2.0):
4546
return list(self._found.values())
4647

4748
def protocol_factory(self):
49+
"""UDP protocol factory."""
4850
return self
4951

5052
def datagram_received(self, data, addr):

0 commit comments

Comments
 (0)