From ffee28c0ffd3f39ea8e09035aa708cd86ede166e Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 12 Jul 2026 16:31:05 -0700 Subject: [PATCH] fix(config): add type ignore for optionxform and handle None values in path inference - Added a type ignore comment for the assignment of optionxform in config_schema.py to suppress type checker warnings. - Updated path inference logic to return None when bot latitude or longitude is not set, ensuring safer handling of missing values. - Enhanced the decoding of path nodes to check for None before appending repeater details, preventing potential errors. --- modules/config_schema.py | 2 +- modules/path_inference.py | 53 +++++++++++++---------- modules/settings_schema.py | 8 ++-- modules/settings_store.py | 4 +- tests/unit/test_weather_alerts_nws.py | 1 - tests/unit/test_wx_count_display_width.py | 2 +- 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/modules/config_schema.py b/modules/config_schema.py index 232d345..1ecc406 100644 --- a/modules/config_schema.py +++ b/modules/config_schema.py @@ -278,7 +278,7 @@ def load_documented_keys_from_example(example_path: Path) -> dict[str, dict[str, # Active keys via ConfigParser try: cfg = configparser.ConfigParser(allow_no_value=True) - cfg.optionxform = str + cfg.optionxform = str # type: ignore[assignment] cfg.read(str(example_path), encoding="utf-8") for section in cfg.sections(): try: diff --git a/modules/path_inference.py b/modules/path_inference.py index 46c6dfa..4b62044 100644 --- a/modules/path_inference.py +++ b/modules/path_inference.py @@ -316,6 +316,9 @@ def get_node_location(node_id: str, cfg: PathInferenceConfig, *, db_manager, log def select_by_simple_proximity(repeaters_with_location, cfg: PathInferenceConfig, *, logger): """Select the closest repeater to the bot, with a strong recency bias.""" + if cfg.bot_latitude is None or cfg.bot_longitude is None: + return None, 0.0 + scored_repeaters = calculate_recency_weighted_scores(repeaters_with_location, cfg) scored_repeaters = [(r, score) for r, score in scored_repeaters if score >= MIN_RECENCY_THRESHOLD] @@ -1118,18 +1121,19 @@ def decode_path_nodes( if selection.status == 'resolved': selected_repeater = selection.repeater - decoded_path.append({ - 'node_id': node_id, - 'name': selected_repeater['name'], - 'public_key': selected_repeater['public_key'], - 'device_type': selected_repeater['device_type'], - 'role': selected_repeater.get('role', 'repeater'), - 'found': True, - 'geographic_guess': selection.confidence < 0.8, - 'collision': True, - 'matches': selection.matches, - **_loc(selected_repeater), - }) + if selected_repeater is not None: + decoded_path.append({ + 'node_id': node_id, + 'name': selected_repeater['name'], + 'public_key': selected_repeater['public_key'], + 'device_type': selected_repeater['device_type'], + 'role': selected_repeater.get('role', 'repeater'), + 'found': True, + 'geographic_guess': selection.confidence < 0.8, + 'collision': True, + 'matches': selection.matches, + **_loc(selected_repeater), + }) elif selection.status == 'collision': fallback = selection.recent_repeaters[0] decoded_path.append({ @@ -1146,18 +1150,19 @@ def decode_path_nodes( }) elif selection.status == 'single': repeater = selection.repeater - decoded_path.append({ - 'node_id': node_id, - 'name': repeater['name'], - 'public_key': repeater['public_key'], - 'device_type': repeater['device_type'], - 'role': repeater.get('role', 'repeater'), - 'found': True, - 'geographic_guess': False, - 'collision': False, - 'matches': 1, - **_loc(repeater), - }) + if repeater is not None: + decoded_path.append({ + 'node_id': node_id, + 'name': repeater['name'], + 'public_key': repeater['public_key'], + 'device_type': repeater['device_type'], + 'role': repeater.get('role', 'repeater'), + 'found': True, + 'geographic_guess': False, + 'collision': False, + 'matches': 1, + **_loc(repeater), + }) else: decoded_path.append({ 'node_id': node_id, diff --git a/modules/settings_schema.py b/modules/settings_schema.py index 67cc331..a4de13f 100644 --- a/modules/settings_schema.py +++ b/modules/settings_schema.py @@ -95,15 +95,15 @@ def validate_field(field: dict, raw: Any) -> tuple[bool, Any, Optional[str]]: if ftype in ("int", "float"): try: - coerced = int(raw) if ftype == "int" else float(raw) + num = int(raw) if ftype == "int" else float(raw) except (ValueError, TypeError): return False, None, f"{label} must be a number" lo, hi = field.get("min"), field.get("max") - if lo is not None and coerced < lo: + if lo is not None and num < lo: return False, None, f"{label} must be ≥ {lo}" - if hi is not None and coerced > hi: + if hi is not None and num > hi: return False, None, f"{label} must be ≤ {hi}" - return True, coerced, None + return True, num, None if ftype == "enum": allowed = {str(o.get("value")) for o in field.get("options", [])} diff --git a/modules/settings_store.py b/modules/settings_store.py index 8463de4..cad21fe 100644 --- a/modules/settings_store.py +++ b/modules/settings_store.py @@ -53,7 +53,7 @@ class SettingsStore(ABC): def write_sections( self, updates: dict[str, dict[str, str]], - deletes: Optional[dict[str, list[str]]] = None, + deletes: Optional[dict[str, list[str] | set[str]]] = None, ) -> dict: """Persist multiple sections at once, with optional key deletions. @@ -100,7 +100,7 @@ class ConfigIniStore(SettingsStore): def write_sections( self, updates: dict[str, dict[str, str]], - deletes: Optional[dict[str, list[str]]] = None, + deletes: Optional[dict[str, list[str] | set[str]]] = None, ) -> dict: # Mirror writes into the in-memory config so this process stays current. for section, values in (updates or {}).items(): diff --git a/tests/unit/test_weather_alerts_nws.py b/tests/unit/test_weather_alerts_nws.py index c001db7..4992e86 100644 --- a/tests/unit/test_weather_alerts_nws.py +++ b/tests/unit/test_weather_alerts_nws.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Unit tests for lazy NWS alert coverage handling (international / HTTP 400).""" -import asyncio import configparser from unittest.mock import AsyncMock, Mock diff --git a/tests/unit/test_wx_count_display_width.py b/tests/unit/test_wx_count_display_width.py index a2bd459..e10e625 100644 --- a/tests/unit/test_wx_count_display_width.py +++ b/tests/unit/test_wx_count_display_width.py @@ -3,9 +3,9 @@ import pytest -from modules.models import MeshMessage from modules.commands.alternatives.wx_international import GlobalWxCommand from modules.commands.wx_command import WxCommand +from modules.models import MeshMessage @pytest.fixture