From 445ff6b4748c060dc55998a12651d06340e6bb11 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Wed, 28 Oct 2026 05:04:54 -0400 Subject: [PATCH] fix: clear all mypy errors Annotate untyped containers and Optional attributes, type the stdlib-shaped metric dicts in the client plugins, and add types-PyYAML/types-psutil to the dev extras plus a mypy override for nio/mattermostdriver (no stubs). Behaviour fixes found on the way: enable_kernel_timestamps() now returns False when SO_TIMESTAMP is unknown instead of raising TypeError, send_shutdown is defined at module level so cleanup() cannot hit a NameError, and a failed subprocess returncode falls back to UNKNOWN. Co-Authored-By: Claude Sonnet 5.5 --- hbd/client/config.py | 5 +++-- hbd/client/main.py | 1 + hbd/client/plugin.py | 4 ++-- hbd/client/plugins/cpu_monitor.py | 2 +- hbd/client/plugins/disk_monitor.py | 6 +++--- hbd/client/plugins/filesystem_info.py | 8 ++++---- hbd/client/plugins/memory_monitor.py | 4 ++-- hbd/client/plugins/nagios_runner.py | 7 ++++--- hbd/client/plugins/network_monitor.py | 8 ++++---- hbd/client/plugins/ping_monitor.py | 1 + hbd/common/proto.py | 2 +- hbd/server/config.py | 2 +- hbd/server/data.py | 2 +- hbd/server/dns.py | 10 +++++----- hbd/server/hbdclass.py | 7 ++++--- hbd/server/journal.py | 4 ++-- hbd/server/oauth.py | 2 +- hbd/server/threshold.py | 18 +++++++++--------- hbd/server/udp.py | 12 +++++++----- pyproject.toml | 7 +++++++ 20 files changed, 63 insertions(+), 49 deletions(-) diff --git a/hbd/client/config.py b/hbd/client/config.py index 90d698e..0b05742 100644 --- a/hbd/client/config.py +++ b/hbd/client/config.py @@ -2,15 +2,16 @@ import logging import os +from typing import Any, Dict logger = logging.getLogger(__name__) try: import yaml except Exception: - yaml = None + yaml = None # type: ignore[assignment] -CLIENT_DEFAULTS = { +CLIENT_DEFAULTS: Dict[str, Any] = { # Network settings "hb_port": 50003, # Port where hbd servers listen "interval": 10, # Heartbeat interval in seconds diff --git a/hbd/client/main.py b/hbd/client/main.py index 881fc18..a7a0b61 100644 --- a/hbd/client/main.py +++ b/hbd/client/main.py @@ -36,6 +36,7 @@ MAXRECV = 32767 # Global state running = True dorestart = False +send_shutdown = False shutdown_event: Optional[asyncio.Event] = None active_tasks: List[asyncio.Task] = [] diff --git a/hbd/client/plugin.py b/hbd/client/plugin.py index 7c5f41d..2ef6e50 100644 --- a/hbd/client/plugin.py +++ b/hbd/client/plugin.py @@ -17,7 +17,7 @@ import logging import sys from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Dict, List, Optional, Type +from typing import Any, Dict, List, Optional class Plugin(ABC): @@ -256,7 +256,7 @@ class PluginRegistry: """Get all enabled plugins.""" return [p for p in self._plugins.values() if p.enabled] - def get_by_type(self, plugin_type: Type[Plugin]) -> List[Plugin]: + def get_by_type(self, plugin_type: type) -> List[Plugin]: """Get all plugins of a specific type. Args: diff --git a/hbd/client/plugins/cpu_monitor.py b/hbd/client/plugins/cpu_monitor.py index e8f64af..99ea918 100644 --- a/hbd/client/plugins/cpu_monitor.py +++ b/hbd/client/plugins/cpu_monitor.py @@ -28,7 +28,7 @@ class CPUMonitorPlugin(MonitorPlugin): def __init__(self, config: Optional[Dict[str, Any]] = None): super().__init__(config) - self.psutil = None + self.psutil: Any = None self.per_core = config.get("per_core", False) if config else False self.interval = config.get("interval", 300) if config else 300 diff --git a/hbd/client/plugins/disk_monitor.py b/hbd/client/plugins/disk_monitor.py index 1b7389a..55be6d3 100644 --- a/hbd/client/plugins/disk_monitor.py +++ b/hbd/client/plugins/disk_monitor.py @@ -10,7 +10,7 @@ from typing import Dict, Any, Optional try: import psutil except ImportError: - psutil = None + psutil = None # type: ignore[assignment] from hbd.client.plugin import MonitorPlugin @@ -58,7 +58,7 @@ class DiskMonitorPlugin(MonitorPlugin): raise ImportError("psutil library is required for disk_monitor plugin") # Store previous I/O counters for delta calculation - self._prev_io = {} + self._prev_io: Dict[str, Any] = {} async def initialize(self): """Initialize the plugin (check psutil availability).""" @@ -114,7 +114,7 @@ class DiskMonitorPlugin(MonitorPlugin): async def _collect_metrics(self) -> Dict[str, Any]: """Collect disk metrics from psutil.""" - metrics = {} + metrics: Dict[str, Any] = {} # Collect partition usage partitions_data = {} diff --git a/hbd/client/plugins/filesystem_info.py b/hbd/client/plugins/filesystem_info.py index c652b25..b4433af 100644 --- a/hbd/client/plugins/filesystem_info.py +++ b/hbd/client/plugins/filesystem_info.py @@ -10,7 +10,7 @@ from typing import Dict, Any, Optional try: import psutil except ImportError: - psutil = None + psutil = None # type: ignore[assignment] from hbd.client.plugin import InfoPlugin @@ -98,8 +98,8 @@ class FilesystemInfoPlugin(InfoPlugin): async def _collect_info(self) -> Dict[str, Any]: """Collect filesystem information from psutil.""" - info = {} - filesystems = [] + info: Dict[str, Any] = {} + filesystems: list = [] filesystem_types = set() # Get mounted disk partitions @@ -112,7 +112,7 @@ class FilesystemInfoPlugin(InfoPlugin): if partition.fstype in self.exclude_types: continue - fs_info = { + fs_info: Dict[str, Any] = { 'device': partition.device, 'mountpoint': partition.mountpoint, 'fstype': partition.fstype, diff --git a/hbd/client/plugins/memory_monitor.py b/hbd/client/plugins/memory_monitor.py index b5e0be3..b737314 100644 --- a/hbd/client/plugins/memory_monitor.py +++ b/hbd/client/plugins/memory_monitor.py @@ -10,7 +10,7 @@ from typing import Dict, Any, Optional try: import psutil except ImportError: - psutil = None + psutil = None # type: ignore[assignment] from hbd.client.plugin import MonitorPlugin @@ -116,7 +116,7 @@ class MemoryMonitorPlugin(MonitorPlugin): async def _collect_metrics(self) -> Dict[str, Any]: """Collect memory metrics from psutil.""" - metrics = {} + metrics: Dict[str, Any] = {} # Virtual (physical) memory statistics vmem = psutil.virtual_memory() diff --git a/hbd/client/plugins/nagios_runner.py b/hbd/client/plugins/nagios_runner.py index 5f1ff6f..e6ed978 100644 --- a/hbd/client/plugins/nagios_runner.py +++ b/hbd/client/plugins/nagios_runner.py @@ -124,7 +124,7 @@ class NagiosRunnerPlugin(MonitorPlugin): Returns: Dictionary with results from all plugins """ - results = {} + results: Dict[str, Any] = {} for cmd_config in self.commands: name = cmd_config.get("name") @@ -181,7 +181,8 @@ class NagiosRunnerPlugin(MonitorPlugin): self.logger.error(f"Command timed out: {command}") return NAGIOS_UNKNOWN, f"Command timed out after {self.timeout}s", {} - status_code = proc.returncode + # communicate() has finished, so the process has exited and returncode is set + status_code = proc.returncode if proc.returncode is not None else NAGIOS_UNKNOWN if status_code < 0: return NAGIOS_UNKNOWN, f"Process killed by signal {-status_code}", {} @@ -225,7 +226,7 @@ class NagiosRunnerPlugin(MonitorPlugin): Returns: Dictionary of metric_name: value """ - perfdata = {} + perfdata: Dict[str, Any] = {} # Performance data comes after the pipe character if '|' not in output: diff --git a/hbd/client/plugins/network_monitor.py b/hbd/client/plugins/network_monitor.py index 804a866..298172f 100644 --- a/hbd/client/plugins/network_monitor.py +++ b/hbd/client/plugins/network_monitor.py @@ -10,7 +10,7 @@ from typing import Dict, Any, Optional try: import psutil except ImportError: - psutil = None + psutil = None # type: ignore[assignment] from hbd.client.plugin import MonitorPlugin from hbd.client.connectivity import get_shared @@ -63,7 +63,7 @@ class NetworkMonitorPlugin(MonitorPlugin): raise ImportError("psutil library is required for network_monitor plugin") # Store previous I/O counters for delta calculation - self._prev_io = {} + self._prev_io: Dict[str, Any] = {} async def initialize(self): """Initialize the plugin (check psutil availability).""" @@ -124,7 +124,7 @@ class NetworkMonitorPlugin(MonitorPlugin): async def _collect_metrics(self) -> Dict[str, Any]: """Collect network metrics from psutil.""" - metrics = {} + metrics: Dict[str, Any] = {} # Collect per-interface I/O counters try: @@ -169,7 +169,7 @@ class NetworkMonitorPlugin(MonitorPlugin): if self.include_connections: try: connections = psutil.net_connections(kind='inet') - conn_stats = {} + conn_stats: Dict[str, int] = {} # Count connections by state for conn in connections: diff --git a/hbd/client/plugins/ping_monitor.py b/hbd/client/plugins/ping_monitor.py index 9b16ef3..cc2b71d 100644 --- a/hbd/client/plugins/ping_monitor.py +++ b/hbd/client/plugins/ping_monitor.py @@ -67,6 +67,7 @@ class PingMonitorPlugin(MonitorPlugin): self.timeout = int(cfg.get("timeout", 5)) # hosts: dict of {hostname: {warning: x, critical: y}} or list of hostnames raw_hosts = cfg.get("hosts", {}) + self.hosts: Dict[str, Any] if isinstance(raw_hosts, list): self.hosts = {h: {} for h in raw_hosts} else: diff --git a/hbd/common/proto.py b/hbd/common/proto.py index 9db1720..0fbff4a 100644 --- a/hbd/common/proto.py +++ b/hbd/common/proto.py @@ -92,7 +92,7 @@ def stodict(msg: bytes): '!' and decodes accordingly. Returns a dict with key 'ID' set to the message ID and the parsed key/value pairs. """ - d = {} + d: Dict[str, Any] = {} if len(msg) > 0 and chr(msg[0]) == "!": # message is: b'!ID:' + compressed_payload # original code used msg[1:4].decode() for ID (3 bytes including colon) diff --git a/hbd/server/config.py b/hbd/server/config.py index 81d6e47..1ef6151 100644 --- a/hbd/server/config.py +++ b/hbd/server/config.py @@ -7,7 +7,7 @@ import os try: import yaml except Exception: - yaml = None + yaml = None # type: ignore[assignment] SERVER_DEFAULTS = { # Network settings diff --git a/hbd/server/data.py b/hbd/server/data.py index c3de820..0fe31de 100644 --- a/hbd/server/data.py +++ b/hbd/server/data.py @@ -1,4 +1,4 @@ -msgs = [] # in-memory list of recent messages for new websocket clients; also logged to file via notify.eventlog +msgs: list = [] # in-memory list of recent messages for new websocket clients; also logged to file via notify.eventlog class Data: diff --git a/hbd/server/dns.py b/hbd/server/dns.py index a7f9310..37e4bbd 100644 --- a/hbd/server/dns.py +++ b/hbd/server/dns.py @@ -2,7 +2,7 @@ from __future__ import annotations from subprocess import Popen, PIPE, STDOUT -from typing import Optional +from typing import Any, Callable, Optional import asyncio import logging @@ -79,8 +79,8 @@ async def dns_update_worker( hbdclass, cfg: dict, async_queue=None, - log: Optional[callable] = None, - pushmsg: Optional[callable] = None, + log: Optional[Callable[..., Any]] = None, + pushmsg: Optional[Callable[..., Any]] = None, loop: Optional[asyncio.AbstractEventLoop] = None, ): """Pure async DNS worker that processes updates from asyncio.Queue. @@ -164,7 +164,7 @@ async def dns_update_worker( def start_dns_worker( hbdclass, cfg: dict, - log: Optional[callable] = None, + log: Optional[Callable[..., Any]] = None, loop: Optional[asyncio.AbstractEventLoop] = None, ): """Start the async DNS worker and return the Task. @@ -176,7 +176,7 @@ def start_dns_worker( loop = asyncio.get_event_loop() # Create asyncio.Queue and wrap in a bridge for thread-safe puts - async_q = asyncio.Queue() + async_q: asyncio.Queue[Any] = asyncio.Queue() class _QueueBridge: """Thread-safe wrapper around asyncio.Queue for synchronous callers.""" diff --git a/hbd/server/hbdclass.py b/hbd/server/hbdclass.py index b3e4ade..0d5fab7 100644 --- a/hbd/server/hbdclass.py +++ b/hbd/server/hbdclass.py @@ -8,6 +8,7 @@ import time import json import copy import queue +from typing import Any, Dict num = 0 @@ -24,7 +25,7 @@ def log(host, m): class Connection: # map of addrs to names - htab = {} + htab: Dict[str, Any] = {} UNKNOWN = "unknown" UP = "up" DOWN = "down" @@ -278,8 +279,8 @@ class Connection: # class Host: # Table of Hosts - hosts = {} - dnsQ = queue.Queue() + hosts: Dict[str, "Host"] = {} + dnsQ: "queue.Queue[Any]" = queue.Queue() def __init__(self, name): global num diff --git a/hbd/server/journal.py b/hbd/server/journal.py index 7bc68d7..fe977d7 100644 --- a/hbd/server/journal.py +++ b/hbd/server/journal.py @@ -11,7 +11,7 @@ import os import asyncio from datetime import datetime from pathlib import Path -from typing import Dict, Any, Optional, Callable, Iterable, List, Tuple, Union +from typing import Dict, Any, Optional, Callable, Iterable, List, TextIO, Tuple, Union logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ class MessageJournal: self.enabled = self.config.get('journal_enabled', True) # Runtime state - self._file_handle = None + self._file_handle: Optional[TextIO] = None self._current_size = 0 self._lock = asyncio.Lock() self._initialized = False diff --git a/hbd/server/oauth.py b/hbd/server/oauth.py index ad9ec1d..1b31ec9 100644 --- a/hbd/server/oauth.py +++ b/hbd/server/oauth.py @@ -128,7 +128,7 @@ def get_providers(config: dict) -> list[ResolvedProvider]: Entries with missing required fields or unknown types are skipped with a warning log. Order follows config declaration order. """ - result = [] + result: list = [] oauth_cfg = config.get("oauth", {}) if not isinstance(oauth_cfg, dict): return result diff --git a/hbd/server/threshold.py b/hbd/server/threshold.py index 6eb3f42..720addb 100644 --- a/hbd/server/threshold.py +++ b/hbd/server/threshold.py @@ -53,16 +53,16 @@ class AlertState: self.metric_path = metric_path self.level = AlertLevel.OK self.since = time.time() - self.last_value = None + self.last_value: Any = None self.last_check = time.time() self.notification_count = 0 - self.last_notification = None - self.threshold_value = None # The threshold value that triggered alert - self.operator = None # The comparison operator (>, <, >=, etc.) + self.last_notification: Optional[float] = None + self.threshold_value: Optional[float] = None # The threshold value that triggered alert + self.operator: Optional[str] = None # The comparison operator (>, <, >=, etc.) self.hysteresis: Optional[float] = None # Hysteresis fraction used for recovery - self.formatted_message = None # Formatted display message for UI + self.formatted_message: Optional[str] = None # Formatted display message for UI self.acknowledged = False # Whether alert has been acknowledged - self.acknowledged_at = None # Timestamp when acknowledged + self.acknowledged_at: Optional[float] = None # Timestamp when acknowledged self.consecutive_count = 0 # Consecutive exceedances while still OK (for count gating) self.pending_since: Optional[float] = None # non-None while waiting out grace period before notifying @@ -362,13 +362,13 @@ class ThresholdChecker: journal: Optional MessageJournal instance for logging threshold events """ # Named threshold configurations (pre-merged: defaults + overrides): {config_name: {metric_path: ThresholdConfig}} - self.threshold_configs = {} + self.threshold_configs: Dict[str, Dict[str, ThresholdConfig]] = {} # Raw overrides only for each named config (no defaults baked in): {config_name: {metric_path: ThresholdConfig}} self.threshold_raw_configs: Dict[str, Dict[str, ThresholdConfig]] = {} # Single threshold set for backward compatibility: {metric_path: ThresholdConfig} - self.thresholds = {} + self.thresholds: Dict[str, ThresholdConfig] = {} # Host to ordered list of config names: {host_name: [config_name, ...]} self.host_config_mapping: Dict[str, List[str]] = {} @@ -1276,7 +1276,7 @@ class ThresholdChecker: def _format_display( self, - display_format: str, + display_format: Optional[str], value: Any, threshold_value: Optional[float], op_symbol: str, diff --git a/hbd/server/udp.py b/hbd/server/udp.py index 07cea9f..f53d743 100644 --- a/hbd/server/udp.py +++ b/hbd/server/udp.py @@ -5,6 +5,7 @@ import socket import struct import time import zlib +from typing import Optional import logging from platform import system as platform_system @@ -25,6 +26,7 @@ _STALE_INTERVAL_MULTIPLIER = 3 # Supported on Linux, FreeBSD, and macOS. The constant is not exposed by # Python's socket module on all platforms platform = platform_system() +_SO_TIMESTAMP: Optional[int] if platform == "Darwin": _SO_TIMESTAMP = 1024 # SO_TIMESTAMP on macOS (not in Python's socket module) elif platform == "Linux": @@ -45,6 +47,8 @@ def enable_kernel_timestamps(sock) -> bool: Returns True if the kernel will supply receive timestamps, False otherwise (unsupported platform, older kernel, or insufficient permissions). """ + if _SO_TIMESTAMP is None: + return False try: sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMP, 1) return True @@ -430,7 +434,7 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict): return cfg = ctx.get("config", {}) - hbdcls = ctx.get("hbdclass") + hbdcls = ctx["hbdclass"] msg_to_websockets = ctx.get("msg_to_websockets") DEBUG = ctx.get("DEBUG", 0) @@ -459,10 +463,8 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict): newh = False cid = msg.get("id", 0) - try: - rtt = float(msg.get("rtt")) - except TypeError: - rtt = None + rtt_raw = msg.get("rtt") + rtt = float(rtt_raw) if rtt_raw is not None else None if msg.get("ID") == "HTB": host.doesack = msg.get("acks", -1) diff --git a/pyproject.toml b/pyproject.toml index 3ec8891..7a127c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,8 @@ dev = [ "pytest-cov>=4.0", "flake8>=5.0", "mypy>=1.10", + "types-PyYAML", + "types-psutil", "black>=23.0", "isort>=5.0", "tox>=4.0", @@ -92,5 +94,10 @@ max-line-length = 111 [tool.pylint.format] max-line-length = 111 +[[tool.mypy.overrides]] +# optional notification libraries that ship neither stubs nor py.typed +module = ["nio", "nio.*", "mattermostdriver", "mattermostdriver.*"] +ignore_missing_imports = true + [tool.pytest.ini_options] testpaths = ["tests"]