chore: bump minimum Python from 3.8 to 3.9 (#336)

* chore: bump minimum Python from 3.8 to 3.9 and adopt PEP 585 builtins

- Update requires-python, classifiers, mypy target, and black target in pyproject.toml
- Replace Python 3.8 with 3.9 in CI matrix and nightly workflow
- Replace typing.List/Dict/Tuple/Set with builtin list/dict/tuple/set (PEP 585)
- Update all 3.8 version references in README, docs, website, and install.sh
- Fix test_wayland_no_tools to mock IBus availability
- Remove List from common_types re-exports and update test

* style: reformat test files for Black 26.x and pin version in CI

Black 26.x introduced parenthesized context manager formatting.
Reformat 7 test files and pin Black>=26,<27 in CI to prevent
future drift from unpinned version upgrades.
This commit is contained in:
Jatin K Malik
2026-03-21 12:15:10 -07:00
committed by GitHub
parent e478f7e353
commit fa17b88f74
27 changed files with 265 additions and 227 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ This module provides type definitions to avoid circular imports.
"""
from enum import Enum, auto
from typing import Callable, List, Optional, Protocol # noqa: F401
from typing import Callable, Optional, Protocol # noqa: F401
class RecognitionState(Enum):
@@ -7,7 +7,6 @@ This module processes text commands from speech recognition, such as
import logging
import re
from typing import List, Tuple
logger = logging.getLogger(__name__)
@@ -99,7 +98,7 @@ class CommandProcessor:
)
self.format_cmd_regex = re.compile(format_cmd_pattern, re.IGNORECASE)
def process_text(self, text: str) -> Tuple[str, List[str]]:
def process_text(self, text: str) -> tuple[str, list[str]]:
"""
Process text commands in the recognized text.
@@ -13,7 +13,7 @@ import queue
import sys
import threading
import time
from typing import Callable, List, Optional
from typing import Callable, Optional
from ..common_types import RecognitionState
from ..ui.audio_feedback import play_error_sound, play_start_sound, play_stop_sound
@@ -518,9 +518,9 @@ class SpeechRecognitionManager:
self._voice_commands_preference = kwargs.get("voice_commands_enabled")
self._voice_commands_enabled = self._resolve_voice_commands_enabled()
self.text_callbacks: List[Callable[[str], None]] = []
self.state_callbacks: List[Callable[[RecognitionState], None]] = []
self.action_callbacks: List[Callable[[str], None]] = []
self.text_callbacks: list[Callable[[str], None]] = []
self.state_callbacks: list[Callable[[RecognitionState], None]] = []
self.action_callbacks: list[Callable[[str], None]] = []
# Download progress tracking
self._download_progress_callback: Optional[Callable[[float, float, str], None]] = None
@@ -537,7 +537,7 @@ class SpeechRecognitionManager:
# Audio diagnostics tracking
self._last_audio_level = 0.0
self._audio_level_callbacks: List[Callable[[float], None]] = []
self._audio_level_callbacks: list[Callable[[float], None]] = []
# Recording control flags
self.should_record = False
@@ -699,7 +699,7 @@ class SpeechRecognitionManager:
self.state = RecognitionState.ERROR
raise
def _transcribe_with_whisper(self, audio_buffer: List[bytes]) -> str:
def _transcribe_with_whisper(self, audio_buffer: list[bytes]) -> str:
"""
Transcribe audio buffer using Whisper.
@@ -911,7 +911,7 @@ class SpeechRecognitionManager:
self.state = RecognitionState.ERROR
raise
def _transcribe_with_whispercpp(self, audio_buffer: List[bytes]) -> str:
def _transcribe_with_whispercpp(self, audio_buffer: list[bytes]) -> str:
"""
Transcribe audio buffer using whisper.cpp.
@@ -1397,11 +1397,11 @@ class SpeechRecognitionManager:
except ValueError:
logger.warning(f"Callback {callback} not found in text_callbacks.")
def get_text_callbacks(self) -> List[Callable[[str], None]]:
def get_text_callbacks(self) -> list[Callable[[str], None]]:
"""Get a copy of the current text callbacks list."""
return list(self.text_callbacks)
def set_text_callbacks(self, callbacks: List[Callable[[str], None]]):
def set_text_callbacks(self, callbacks: list[Callable[[str], None]]):
"""Set the text callbacks list (used for temporarily replacing callbacks)."""
self.text_callbacks = list(callbacks)
@@ -1829,7 +1829,7 @@ class SpeechRecognitionManager:
self._process_audio_buffer(audio_buffer)
def _process_audio_buffer(self, audio_buffer: List[bytes]):
def _process_audio_buffer(self, audio_buffer: list[bytes]):
"""Process an immutable audio segment for transcription and commands."""
if not audio_buffer:
return
@@ -1959,7 +1959,7 @@ class SpeechRecognitionManager:
self._update_state(RecognitionState.LISTENING)
logger.info("DEBUG: _perform_recognition thread exiting")
def _enqueue_audio_segment(self, audio_buffer: List[bytes]):
def _enqueue_audio_segment(self, audio_buffer: list[bytes]):
"""Queue an audio segment for asynchronous transcription."""
segment = audio_buffer.copy()
if not segment:
+6 -6
View File
@@ -7,7 +7,7 @@ This module handles loading, saving, and accessing user preferences.
import json
import logging
import os
from typing import Any, Dict
from typing import Any
logger = logging.getLogger(__name__)
@@ -109,7 +109,7 @@ class ConfigManager:
except Exception as e:
logger.error(f"Failed to load config: {e}")
def _check_needs_migration(self, user_config: Dict) -> bool:
def _check_needs_migration(self, user_config: dict) -> bool:
"""Check if the user config needs migration to add per-engine model sizes."""
sr_config = user_config.get("speech_recognition", {})
# Need migration if we have model_size but not the per-engine keys
@@ -117,7 +117,7 @@ class ConfigManager:
"vosk_model_size" not in sr_config or "whisper_model_size" not in sr_config
)
def _migrate_config(self, user_config: Dict):
def _migrate_config(self, user_config: dict):
"""Migrate old config formats to the current format."""
sr_config = self.config.get("speech_recognition", {})
user_sr_config = user_config.get("speech_recognition", {})
@@ -210,7 +210,7 @@ class ConfigManager:
logger.error(f"Failed to set config value: {e}")
return False
def get_settings(self) -> Dict[str, Any]:
def get_settings(self) -> dict[str, Any]:
"""Get the entire configuration dictionary."""
return self.config
@@ -267,7 +267,7 @@ class ConfigManager:
return enabled
def update_speech_recognition_settings(self, settings: Dict[str, Any]):
def update_speech_recognition_settings(self, settings: dict[str, Any]):
"""Update multiple speech recognition settings at once."""
if "speech_recognition" not in self.config:
self.config["speech_recognition"] = {}
@@ -293,7 +293,7 @@ class ConfigManager:
self.config["sound_effects"] = {}
self.config["sound_effects"]["enabled"] = enabled
def _update_dict_recursive(self, target: Dict, source: Dict):
def _update_dict_recursive(self, target: dict, source: dict):
"""
Update a dictionary recursively.
@@ -11,7 +11,7 @@ import os
import select
import threading
import time
from typing import Dict, List, Optional, Set
from typing import Optional
# Try to import evdev
try:
@@ -41,7 +41,7 @@ KEY_LEFTMETA = 125 # Super/Windows key
KEY_RIGHTMETA = 126
# Map modifier key names to evdev key codes
MODIFIER_KEY_CODES: Dict[str, Set[int]] = {
MODIFIER_KEY_CODES: dict[str, set[int]] = {
"ctrl": {KEY_LEFTCTRL, KEY_RIGHTCTRL},
"alt": {KEY_LEFTALT, KEY_RIGHTALT},
"shift": {KEY_LEFTSHIFT, KEY_RIGHTSHIFT},
@@ -55,7 +55,7 @@ MODIFIER_KEY_CODES: Dict[str, Set[int]] = {
}
def find_keyboard_devices() -> List[str]:
def find_keyboard_devices() -> list[str]:
"""
Find all keyboard input devices.
@@ -149,22 +149,22 @@ class EvdevKeyboardBackend(KeyboardBackend):
mode: The shortcut mode ("toggle" or "push_to_talk")
"""
super().__init__(shortcut, mode)
self.devices: List[InputDevice] = []
self.device_fds: List[int] = []
self.devices: list[InputDevice] = []
self.device_fds: list[int] = []
self.running = False
self.monitor_thread: Optional[threading.Thread] = None
self.last_trigger_time = 0
self.last_key_press_time = 0
self.double_tap_threshold = 0.3 # seconds
self.key_pressed_devices: Set[int] = set()
self.key_pressed_devices: set[int] = set()
self._devices_lock = threading.Lock()
if not EVDEV_AVAILABLE:
logger.error("python-evdev not available")
def _get_target_key_codes(self) -> Set[int]:
def _get_target_key_codes(self) -> set[int]:
"""Get the evdev key codes for the configured modifier."""
return MODIFIER_KEY_CODES.get(self._modifier_key, set())
+4 -4
View File
@@ -9,7 +9,7 @@ import logging
import threading
from datetime import datetime
from pathlib import Path
from typing import Callable, List, Optional
from typing import Callable, Optional
logger = logging.getLogger(__name__)
@@ -58,8 +58,8 @@ class LoggingManager:
max_records: Maximum number of log records to keep in memory
"""
self.max_records = max_records
self.log_records: List[LogRecord] = []
self.log_callbacks: List[Callable[[LogRecord], None]] = []
self.log_records: list[LogRecord] = []
self.log_callbacks: list[Callable[[LogRecord], None]] = []
self.lock = threading.Lock()
# Create logs directory
@@ -130,7 +130,7 @@ class LoggingManager:
level_filter: Optional[str] = None,
module_filter: Optional[str] = None,
last_n: Optional[int] = None,
) -> List[LogRecord]:
) -> list[LogRecord]:
"""
Get log records with optional filtering.
+5 -5
View File
@@ -8,7 +8,7 @@ for whisper.cpp, supporting Vulkan, CUDA, and CPU backends.
import logging
import os
import subprocess
from typing import Dict, Optional, Tuple
from typing import Optional
logger = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ class ComputeBackend:
CPU = "cpu"
def detect_vulkan_support() -> Tuple[bool, Optional[str]]:
def detect_vulkan_support() -> tuple[bool, Optional[str]]:
"""
Detect if Vulkan is available and get device info.
@@ -91,7 +91,7 @@ def detect_vulkan_support() -> Tuple[bool, Optional[str]]:
return False, None
def detect_cuda_support() -> Tuple[bool, Optional[str]]:
def detect_cuda_support() -> tuple[bool, Optional[str]]:
"""
Detect if NVIDIA CUDA is available and get device info.
@@ -119,7 +119,7 @@ def detect_cuda_support() -> Tuple[bool, Optional[str]]:
return False, None
def detect_compute_backend() -> Tuple[str, str]:
def detect_compute_backend() -> tuple[str, str]:
"""
Detect the best available compute backend.
@@ -177,7 +177,7 @@ def detect_cpu_info() -> str:
return "CPU"
def get_recommended_model() -> Tuple[str, str]:
def get_recommended_model() -> tuple[str, str]:
"""
Get the recommended whisper.cpp model based on system configuration.