diff --git a/BlocksScreen/events.py b/BlocksScreen/events.py index 20a870e4..22fa5601 100644 --- a/BlocksScreen/events.py +++ b/BlocksScreen/events.py @@ -1,6 +1,5 @@ """Collection of all custom events used by the application""" -import typing from PyQt6.QtCore import QEvent @@ -14,9 +13,7 @@ class WebSocketConnecting(QEvent): WebsocketConnectingEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketConnecting, self).__init__( - WebSocketConnecting.WebsocketConnectingEvent - ) + super().__init__(WebSocketConnecting.WebsocketConnectingEvent) self.data = data self.args = args self.kwargs = kwargs @@ -38,13 +35,11 @@ class WebSocketMessageReceived(QEvent): def __init__( self, - method: typing.Optional[str] = None, - data: typing.Optional[dict] = None, - metadata: typing.Optional[dict] = None, + method: str | None = None, + data: dict | None = None, + metadata: dict | None = None, ): - super(WebSocketMessageReceived, self).__init__( - WebSocketMessageReceived.WebsocketMessageReceivedEvent - ) + super().__init__(WebSocketMessageReceived.WebsocketMessageReceivedEvent) self.method = method self.data = data self.metadata = metadata @@ -65,7 +60,7 @@ class WebSocketOpen(QEvent): WebsocketOpenEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketOpen, self).__init__(WebSocketOpen.WebsocketOpenEvent) + super().__init__(WebSocketOpen.WebsocketOpenEvent) self.data = data self.args = args self.kwargs = kwargs @@ -86,7 +81,7 @@ class WebSocketError(QEvent): WebsocketErrorEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketError, self).__init__(WebSocketError.WebsocketErrorEvent) + super().__init__(WebSocketError.WebsocketErrorEvent) self.data = data self.args = args self.kwargs = kwargs @@ -107,9 +102,7 @@ class WebSocketDisconnected(QEvent): WebsocketDisconnectedEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketDisconnected, self).__init__( - WebSocketDisconnected.WebsocketDisconnectedEvent - ) + super().__init__(WebSocketDisconnected.WebsocketDisconnectedEvent) self.data = data self.args = args self.kwargs = kwargs @@ -131,7 +124,7 @@ class WebSocketClose(QEvent): WebsocketCloseEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketClose, self).__init__(WebSocketClose.WebsocketCloseEvent) + super().__init__(WebSocketClose.WebsocketCloseEvent) self.data = data self.args = args self.kwargs = kwargs @@ -153,7 +146,7 @@ class KlippyShutdown(QEvent): def __init__(self, data, *args, **kwargs): QEvent.__instancecheck__(self) - super(KlippyShutdown, self).__init__(KlippyShutdown.KlippyShutdownEvent) + super().__init__(KlippyShutdown.KlippyShutdownEvent) self.data = data self.args = args self.kwargs = kwargs @@ -180,7 +173,7 @@ class KlippyReady(QEvent): KlippyReadyEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(KlippyReady, self).__init__(KlippyReady.KlippyReadyEvent) + super().__init__(KlippyReady.KlippyReadyEvent) self.data = data self.args = args self.kwargs = kwargs @@ -201,9 +194,7 @@ class KlippyDisconnected(QEvent): KlippyDisconnectedEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(KlippyDisconnected, self).__init__( - KlippyDisconnected.KlippyDisconnectedEvent - ) + super().__init__(KlippyDisconnected.KlippyDisconnectedEvent) self.data = data self.args = args self.kwargs = kwargs @@ -224,7 +215,7 @@ class KlippyError(QEvent): KlippyErrorEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, message, *args, **kwargs): - super(KlippyError, self).__init__(KlippyError.KlippyErrorEvent) + super().__init__(KlippyError.KlippyErrorEvent) self.data = data self.message = message @@ -272,7 +263,7 @@ class PrintStart(QEvent): PrintStartEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, filename, *args, **kwargs): - super(PrintStart, self).__init__(PrintStart.PrintStartEvent) + super().__init__(PrintStart.PrintStartEvent) self.file = filename self.file_metadata = kwargs self.kwargs = kwargs @@ -293,7 +284,7 @@ class PrintComplete(QEvent): PrintCompleteEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(PrintComplete, self).__init__(PrintComplete.PrintCompleteEvent) + super().__init__(PrintComplete.PrintCompleteEvent) self.data = data self.args = args self.kwargs = kwargs @@ -314,7 +305,7 @@ class PrintPause(QEvent): PrintPauseEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(PrintPause, self).__init__(PrintPause.PrintPauseEvent) + super().__init__(PrintPause.PrintPauseEvent) self.data = data self.args = args @@ -336,7 +327,7 @@ class PrintResume(QEvent): PrintResumeEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(PrintResume, self).__init__(PrintResume.PrintResumeEvent) + super().__init__(PrintResume.PrintResumeEvent) self.data = data self.args = args @@ -358,7 +349,7 @@ class PrintCancelled(QEvent): PrintCancelledEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(PrintCancelled, self).__init__(PrintCancelled.PrintCancelledEvent) + super().__init__(PrintCancelled.PrintCancelledEvent) self.data = data self.args = args @@ -380,7 +371,7 @@ class PrintError(QEvent): PrintErrorEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(PrintError, self).__init__(PrintError.PrintErrorEvent) + super().__init__(PrintError.PrintErrorEvent) self.data = data self.args = args self.kwargs = kwargs @@ -401,7 +392,7 @@ class NetworkAdded(QEvent): NetworkAddedEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(NetworkAdded, self).__init__(NetworkAdded.NetworkAddedEvent) + super().__init__(NetworkAdded.NetworkAddedEvent) self.data = data self.args = args self.kwargs = kwargs @@ -422,7 +413,7 @@ class NetworkDeleted(QEvent): NetworkDeletedEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(NetworkDeleted, self).__init__(NetworkDeleted.NetworkDeletedEvent) + super().__init__(NetworkDeleted.NetworkDeletedEvent) self.data = data self.args = args self.kwargs = kwargs @@ -443,7 +434,7 @@ class NetworkScan(QEvent): NetworkScanEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(NetworkScan, self).__init__(NetworkScan.NetworkScanEvent) + super().__init__(NetworkScan.NetworkScanEvent) self.data = data self.args = args self.kwargs = kwargs diff --git a/BlocksScreen/lib/filament.py b/BlocksScreen/lib/filament.py index cb4c0232..7ceae1a2 100644 --- a/BlocksScreen/lib/filament.py +++ b/BlocksScreen/lib/filament.py @@ -1,6 +1,5 @@ # Class that represents a filament spool -from typing import Optional import enum @@ -29,17 +28,18 @@ def __init__( self, name: str, temperature: int, - brand: Optional[str] = None, - spool_type: Optional[SpoolMaterial] = None, - spool_weight: Optional[float] = None, + brand: str | None = None, + spool_type: SpoolMaterial | None = None, + spool_weight: float | None = None, ): if not isinstance(name, str) or not isinstance(temperature, int): raise TypeError("__init__() invalid argument type") self._name: str = name self._temperature: int = temperature - self._weight: Optional[float] = None - self._brand: Optional[str] = brand + self._weight: float | None = None + self._brand: str | None = brand + self._spool_type: Filament.SpoolMaterial | None = None if spool_type is not None and spool_type in self.SpoolMaterial: self._spool_type = spool_type @@ -55,9 +55,7 @@ def temperature(self) -> int: return self._temperature @property - def weight(self) -> Optional[float]: - if self._weight is None: - return + def weight(self) -> float | None: return self._weight @weight.setter @@ -65,22 +63,19 @@ def weight(self, new_value: float): self._weight = new_value @property - def brand(self) -> Optional[str]: + def brand(self) -> str | None: return self._brand @brand.setter - def brand(self, new_value: str) -> Optional[str]: + def brand(self, new_value: str) -> None: self._brand = new_value @property - def spool_type(self) -> Optional[SpoolMaterial]: + def spool_type(self) -> SpoolMaterial | None: return self._spool_type @spool_type.setter - def spool_type(self, new): - if new not in self.SpoolMaterial: - if isinstance(new, self.SpoolMaterial): - raise ValueError( - "Spool Material type is invalid" - ) # Correct type but invalid option + def spool_type(self, new) -> None: + if new is not None and not isinstance(new, self.SpoolMaterial): + raise TypeError(f"Spool Material type is invalid: {new!r}") self._spool_type = new diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 412f0648..710991a9 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -8,7 +8,6 @@ from pathlib import Path import events -from events import ReceivedFileData from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtGui, QtWidgets @@ -29,7 +28,7 @@ class FileAction(Enum): UNKNOWN = auto() @classmethod - def from_string(cls, action: str) -> "FileAction": + def from_string(cls, action: str) -> FileAction: """Convert Moonraker action string to enum.""" mapping = { "create_file": cls.CREATE_FILE, @@ -54,7 +53,7 @@ class FileMetadata: filename: str = "" thumbnail_images: list[QtGui.QImage] = field(default_factory=list) - filament_total: typing.Union[dict, str, float] = field(default_factory=dict) + filament_total: dict | str | float = field(default_factory=dict) estimated_time: int = 0 layer_count: int = -1 total_layer: int = -1 @@ -74,8 +73,8 @@ class FileMetadata: slicer_version: str = "Unknown" gcode_start_byte: int = 0 gcode_end_byte: int = 0 - print_start_time: typing.Optional[float] = None - job_id: typing.Optional[str] = None + print_start_time: float | None = None + job_id: str | None = None def to_dict(self) -> dict: """Convert to dictionary for signal emission.""" @@ -109,7 +108,7 @@ def to_dict(self) -> dict: @classmethod def from_dict( cls, data: dict, thumbnail_images: list[QtGui.QImage] - ) -> "FileMetadata": + ) -> FileMetadata: """ `Create FileMetadata from Moonraker API response.` @@ -255,7 +254,7 @@ def is_loaded(self) -> bool: """Check if initial load is complete.""" return self._initial_load_complete - def get_file_metadata(self, filename: str) -> typing.Optional[FileMetadata]: + def get_file_metadata(self, filename: str) -> FileMetadata | None: """Get cached metadata for a file.""" return self._files_metadata.get(filename.removeprefix("/")) @@ -279,7 +278,7 @@ def initial_load(self) -> None: self._initial_load_complete = False self.request_dir_info[str, bool].emit("", True) - def handle_filelist_changed(self, data: typing.Union[dict, list]) -> None: + def handle_filelist_changed(self, data: dict | list) -> None: """Handle notify_filelist_changed from Moonraker.""" if isinstance(data, dict) and "params" in data: data = data.get("params", []) @@ -492,7 +491,7 @@ def _process_metadata(self, data: dict) -> None: self.fileinfo.emit(metadata.to_dict()) logger.debug(f"Metadata loaded for: {filename}") - def handle_metadata_error(self, error_data: typing.Union[str, dict]) -> None: + def handle_metadata_error(self, error_data: str | dict) -> None: """ Handle metadata request error from Moonraker. @@ -538,7 +537,7 @@ def _preload_usb_contents(self, usb_path: str) -> None: self._usb_preload_queue.append(usb_path) self.ws.api.get_dir_information(usb_path, True) - def get_cached_usb_files(self, usb_path: str) -> typing.Optional[list[dict]]: + def get_cached_usb_files(self, usb_path: str) -> list[dict] | None: """ Get cached files for a USB path if available. @@ -646,7 +645,7 @@ def on_request_fileinfo(self, filename: str) -> None: @QtCore.pyqtSlot(str, bool, name="get_dir_info") def get_dir_information( self, directory: str = "", extended: bool = True - ) -> typing.Optional[list]: + ) -> list | None: """Get directory information.""" self._current_directory = directory @@ -669,8 +668,8 @@ def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: def event(self, event: QtCore.QEvent) -> bool: """Handle object-level events.""" - if event.type() == ReceivedFileData.type(): - if isinstance(event, ReceivedFileData): + if event.type() == events.ReceivedFileData.type(): + if isinstance(event, events.ReceivedFileData): self.handle_message_received(event.method, event.data, event.params) return True return super().event(event) diff --git a/BlocksScreen/lib/klipper_message_filter.py b/BlocksScreen/lib/klipper_message_filter.py index 2f69efd4..ea16b1bf 100644 --- a/BlocksScreen/lib/klipper_message_filter.py +++ b/BlocksScreen/lib/klipper_message_filter.py @@ -3,7 +3,7 @@ import enum import re from dataclasses import dataclass -from typing import Callable +from collections.abc import Callable class MessageSource(enum.Enum): diff --git a/BlocksScreen/lib/machine.py b/BlocksScreen/lib/machine.py index 3187ea87..cb9a32d0 100644 --- a/BlocksScreen/lib/machine.py +++ b/BlocksScreen/lib/machine.py @@ -14,7 +14,7 @@ class MachineControl(QtCore.QObject): service_restart = QtCore.pyqtSignal(str, name="service-restart") def __init__(self, parent: typing.Optional["QtCore.QObject"]) -> None: - super(MachineControl, self).__init__(parent) + super().__init__(parent) self.setObjectName("MachineControl") @QtCore.pyqtSlot(name="machine_restart") diff --git a/BlocksScreen/lib/moonrest.py b/BlocksScreen/lib/moonrest.py index e1e61bf4..81f46ad6 100644 --- a/BlocksScreen/lib/moonrest.py +++ b/BlocksScreen/lib/moonrest.py @@ -29,7 +29,6 @@ import logging import requests -from requests import Request, Response logger = logging.getLogger(__name__) @@ -38,7 +37,7 @@ class UncallableError(Exception): """Raised when a method is not callable""" def __init__(self, message="Unable to call method", errors=None): - super(UncallableError, self).__init__(message, errors) + super().__init__(message, errors) self.errors = errors self.message = message @@ -148,7 +147,7 @@ def _request( _headers = {"x-api-key": self._api_key} if self._api_key else {} try: if hasattr(requests, request_type): - _request_method: Request = getattr(requests, request_type) + _request_method = getattr(requests, request_type) if not callable(_request_method): raise UncallableError( "Invalid request method", @@ -162,9 +161,9 @@ def _request( headers=_headers, timeout=timeout, ) - if isinstance(response, Response): + if isinstance(response, requests.Response): response.raise_for_status() return response.json() if json_response else response.content except Exception as e: - logger.info(f"Unexpected error while sending HTTP request: {e}") + logger.info("Unexpected error while sending HTTP request: %s", e) diff --git a/BlocksScreen/lib/network/manager.py b/BlocksScreen/lib/network/manager.py index 1ef6cd82..f6a45ff7 100644 --- a/BlocksScreen/lib/network/manager.py +++ b/BlocksScreen/lib/network/manager.py @@ -55,7 +55,7 @@ def __init__(self, parent: QObject | None = None) -> None: self._shutting_down: bool = False self._worker_ready: bool = False - self._pending_futures: set["asyncio.Future"] = set() + self._pending_futures: set[asyncio.Future] = set() self._worker = NetworkManagerWorker() diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 24f20b06..16745c0a 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -120,7 +120,7 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self): """Set up UI, instantiate subsystems, and wire all inter-component signals.""" - super(MainWindow, self).__init__() + super().__init__() self.config: BlocksScreenConfig = get_configparser() self.ui = Ui_MainWindow() self.ui.setupUi(self) diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index eaf85855..576287e6 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -113,8 +113,8 @@ def __init__( self.x_inputshaper: dict = {} self.stepper_limits: dict = {} - self.current_object: typing.Optional[str] = None - self.current_process: typing.Optional[Process] = None + self.current_object: str | None = None + self.current_process: Process | None = None self.axis_in: str = "x" self.amount: int = 1 self.tb: bool = False @@ -375,7 +375,7 @@ def on_object_list(self, object_list: list) -> None: @QtCore.pyqtSlot(dict, name="on_object_config") @QtCore.pyqtSlot(list, name="on_object_config") - def on_object_config(self, config: typing.Union[dict, list]) -> None: + def on_object_config(self, config: dict | list) -> None: """Handle receiving printer object configurations""" if not config: return diff --git a/BlocksScreen/lib/panels/widgets/bannerPopup.py b/BlocksScreen/lib/panels/widgets/bannerPopup.py index 7db54547..99077539 100644 --- a/BlocksScreen/lib/panels/widgets/bannerPopup.py +++ b/BlocksScreen/lib/panels/widgets/bannerPopup.py @@ -80,6 +80,8 @@ def _calculate_target_geometry(self) -> QtCore.QRect: if isinstance(widget, QtWidgets.QMainWindow): main_window = widget break + if main_window is None: + return QtCore.QRect() parent_rect = main_window.geometry() width = int(parent_rect.width() * 0.35) height = 80 diff --git a/BlocksScreen/lib/panels/widgets/basePopup.py b/BlocksScreen/lib/panels/widgets/basePopup.py index 199f9bf2..e37b0767 100644 --- a/BlocksScreen/lib/panels/widgets/basePopup.py +++ b/BlocksScreen/lib/panels/widgets/basePopup.py @@ -1,5 +1,3 @@ -import typing - from PyQt6 import QtCore, QtGui, QtWidgets @@ -152,7 +150,7 @@ def add_widget(self, widget: QtWidgets.QWidget) -> None: layout.insertWidget(index, self.ui) self.ui.show() - def _get_mainWindow_widget(self) -> typing.Optional[QtWidgets.QMainWindow]: + def _get_mainWindow_widget(self) -> QtWidgets.QMainWindow | None: """Get the main application window""" app_instance = QtWidgets.QApplication.instance() if not app_instance: diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index 0f35ba39..efb6f2fe 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -26,7 +26,7 @@ def __init__(self, parent) -> None: self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.thumbnail: QtGui.QImage = self._blocksthumbnail - self._thumbnails: typing.List = [] + self._thumbnails: list = [] self.directory = "gcodes" self.filename = "" self.confirm_button.clicked.connect( diff --git a/BlocksScreen/lib/panels/widgets/fansPage.py b/BlocksScreen/lib/panels/widgets/fansPage.py index 925c0230..c31a8600 100644 --- a/BlocksScreen/lib/panels/widgets/fansPage.py +++ b/BlocksScreen/lib/panels/widgets/fansPage.py @@ -1,15 +1,14 @@ from PyQt6 import QtCore, QtWidgets -import typing class FansPage(QtWidgets.QWidget): def __init__( self, - parent: typing.Optional["QtWidgets.QWidget"], - flags: typing.Optional["QtCore.Qt.WindowType"], + parent: QtWidgets.QWidget | None, + flags: QtCore.Qt.WindowType | None, ) -> None: if parent is not None and flags is not None: - super(FansPage, self).__init__(parent, flags) + super().__init__(parent, flags) else: - super(FansPage, self).__init__() + super().__init__() diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index 969399ac..d731eb9c 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -1,6 +1,5 @@ import json import logging -import typing import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar @@ -34,13 +33,13 @@ class FilesPage(QtWidgets.QWidget): ICON_PATHS = { "back_folder": ":/ui/media/btn_icons/back_folder.svg", "folder": ":/ui/media/btn_icons/folderIcon.svg", - "right_arrow": ":/arrow_icons/media/btn_icons/right_arrow.svg", + "right_arrow": ":/arrow_icons/media/btn_icons/arrow_right.svg", "usb": ":/ui/media/btn_icons/usb_icon.svg", "back": ":/ui/media/btn_icons/back.svg", "refresh": ":/ui/media/btn_icons/refresh.svg", } - def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: + def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent) self._file_list: list[dict] = [] @@ -263,7 +262,7 @@ def _find_file_insert_position(self, modified_time: float) -> int: return insert_pos - def _find_file_key_by_display_name(self, display_name: str) -> typing.Optional[str]: + def _find_file_key_by_display_name(self, display_name: str) -> str | None: """Find the file key in _files_data by its display name.""" for key in self._files_data: if self._get_display_name(key) == display_name: @@ -691,7 +690,7 @@ def _add_file_to_list(self, file_item: dict) -> None: if item: self._model.add_item(item) - def _create_file_list_item(self, filedata: dict) -> typing.Optional[ListItem]: + def _create_file_list_item(self, filedata: dict) -> ListItem | None: """Create a ListItem from file metadata.""" filename = filedata.get("filename", "") if not filename: diff --git a/BlocksScreen/lib/panels/widgets/keyboardPage.py b/BlocksScreen/lib/panels/widgets/keyboardPage.py index 734ba5fb..efddaa93 100644 --- a/BlocksScreen/lib/panels/widgets/keyboardPage.py +++ b/BlocksScreen/lib/panels/widgets/keyboardPage.py @@ -186,7 +186,7 @@ def _validate_pattern(self, value: str) -> bool: return value.endswith(".") return True - def _get_mainWindow_widget(self) -> typing.Optional[QtWidgets.QMainWindow]: + def _get_mainWindow_widget(self) -> QtWidgets.QMainWindow | None: """Get the main application window""" app_instance = QtWidgets.QApplication.instance() if not app_instance: diff --git a/BlocksScreen/lib/panels/widgets/numpadPage.py b/BlocksScreen/lib/panels/widgets/numpadPage.py index 4721429a..1cc9299a 100644 --- a/BlocksScreen/lib/panels/widgets/numpadPage.py +++ b/BlocksScreen/lib/panels/widgets/numpadPage.py @@ -1,7 +1,6 @@ -from lib.utils.icon_button import IconButton from lib.utils.blocks_label import BlocksLabel +from lib.utils.icon_button import IconButton from lib.utils.numpad_button import NumpadButton - from PyQt6 import QtCore, QtGui, QtWidgets diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 6fbfe20d..e331049d 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -1,7 +1,7 @@ import typing -from PyQt6 import QtCore, QtGui, QtWidgets from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets class OptionCard(QtWidgets.QAbstractButton): diff --git a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py index 39dfa293..371372a4 100644 --- a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py +++ b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py @@ -1,6 +1,5 @@ import enum from collections import deque -from typing import Deque from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets @@ -26,9 +25,9 @@ def __init__(self, parent) -> None: super().__init__(parent) self.timeout_timer = QtCore.QTimer(self) self.timeout_timer.setSingleShot(True) - self.messages: Deque = deque() + self.messages: deque = deque() self.isShown = False - self.persistent_notifications: Deque = deque() + self.persistent_notifications: deque = deque() self.message_type: Popup.MessageType = Popup.MessageType.INFO self.default_background_color = QtGui.QColor(164, 164, 164) self.info_icon = QtGui.QPixmap(":ui/media/btn_icons/info.svg") @@ -81,7 +80,8 @@ def _calculate_target_geometry(self) -> QtCore.QRect: if isinstance(widget, QtWidgets.QMainWindow): main_window = widget break - + if main_window is None: + return QtCore.QRect() parent_rect = main_window.geometry() width = int(parent_rect.width() * 0.85) diff --git a/BlocksScreen/lib/panels/widgets/printcorePage.py b/BlocksScreen/lib/panels/widgets/printcorePage.py index c2683cd1..b5942e54 100644 --- a/BlocksScreen/lib/panels/widgets/printcorePage.py +++ b/BlocksScreen/lib/panels/widgets/printcorePage.py @@ -34,6 +34,8 @@ def _geometry_calc(self) -> None: for widget in app_instance.allWidgets(): if isinstance(widget, QtWidgets.QMainWindow): main_window = widget + if main_window is None: + return x = main_window.geometry().x() y = main_window.geometry().y() width = main_window.width() diff --git a/BlocksScreen/lib/panels/widgets/sensorWidget.py b/BlocksScreen/lib/panels/widgets/sensorWidget.py index c479e5dc..14aa0a43 100644 --- a/BlocksScreen/lib/panels/widgets/sensorWidget.py +++ b/BlocksScreen/lib/panels/widgets/sensorWidget.py @@ -37,10 +37,11 @@ class SensorState(enum.IntEnum): def __init__(self, parent, sensor_name: str): super().__init__(parent) - self.name = str(sensor_name).split(" ")[1] + _parts = str(sensor_name).split(" ", 1) + self.name = _parts[1] if len(_parts) > 1 else _parts[0] self.sensor_type: SensorWidget.SensorType = ( self.SensorType.SWITCH - if "switch" in str(sensor_name).split(" ")[0].lower() + if "switch" in _parts[0].lower() else self.SensorType.MOTION ) @@ -96,13 +97,13 @@ def text(self, new_text) -> None: self._text_label.setText(f"{new_text}") self._text = new_text - @QtCore.pyqtSlot(FilamentState, name="change_fil_sensor_state") - def change_fil_sensor_state(self, state: FilamentState): - """Invert the filament state in response to a Klipper update""" + def set_filament_state(self, state: FilamentState) -> None: + """Set the filament state directly from a Klipper update.""" if not isinstance(state, SensorWidget.FilamentState): return - self.filament_state = SensorWidget.FilamentState(not state.value) - self.update() + if self.filament_state != state: + self.filament_state = state + self.update() def toggle_button_state(self, state: ToggleAnimatedButton.State) -> None: """Called when the Klipper firmware reports an update to the filament sensor state""" diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index df63cfb5..f02f5853 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -19,7 +19,7 @@ class SensorsWindow(QtWidgets.QWidget): ) def __init__(self, parent): - super(SensorsWindow, self).__init__(parent) + super().__init__(parent) self.model = EntryListModel() self.entry_delegate = EntryDelegate() self.sensor_tracking_widget = {} @@ -46,7 +46,7 @@ def handle_available_fil_sensors(self, sensors: dict) -> None: self.reset_view_model() filtered_sensors = [ sensor - for sensor in sensors.keys() + for sensor in sensors if sensor.startswith( ("filament_switch_sensor", "filament_motion_sensor", "cutter_sensor") ) @@ -67,8 +67,7 @@ def handle_fil_state_change( _item = self.sensor_tracking_widget.get(sensor_name) if _item: if parameter == "filament_detected": - state = SensorWidget.FilamentState(not value) - _item.change_fil_sensor_state(state) + _item.set_filament_state(SensorWidget.FilamentState(value)) elif parameter == "enabled": _item.toggle_button_state(SensorWidget.SensorState(value)) @@ -108,7 +107,7 @@ def create_sensor_widget(self, name: str) -> SensorWidget: else: _item_widget.show() self.current_widget = _item_widget - name_id = str(name).split(" ")[1] + name_id = _item_widget.name item = ListItem( text=name_id, right_text="", diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index 43baf42a..2cd7a179 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -283,7 +283,7 @@ def get_config(self, section_name: str) -> dict: return _config[0].get(section_name, {}) def search_config_list( - self, search_list: list[str], _objects: typing.Optional[list] = None + self, search_list: list[str], _objects: list | None = None ) -> list: """ Search a list of printer objects recursively diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 1840f0ae..1caab81a 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -1,7 +1,6 @@ import qrcode - -from PyQt6.QtGui import QImage, QColor, QPainter from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor, QImage, QPainter BLOCKS_URL = "https://blockstec.com" RF50_MANUAL_PAGE = "https://blockstec.com/RF50" diff --git a/BlocksScreen/lib/utils/blocks_button.py b/BlocksScreen/lib/utils/blocks_button.py index 2134b32d..436a1752 100644 --- a/BlocksScreen/lib/utils/blocks_button.py +++ b/BlocksScreen/lib/utils/blocks_button.py @@ -130,7 +130,7 @@ def setProperty(self, name: str, value: typing.Any): self.text_color = QtGui.QColor(value) self.update() - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + def paintEvent(self, e: QtGui.QPaintEvent | None): """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing, True) diff --git a/BlocksScreen/lib/utils/blocks_frame.py b/BlocksScreen/lib/utils/blocks_frame.py index 7de7514e..8bd2d6b8 100644 --- a/BlocksScreen/lib/utils/blocks_frame.py +++ b/BlocksScreen/lib/utils/blocks_frame.py @@ -1,6 +1,7 @@ -from PyQt6 import QtCore, QtGui, QtWidgets import typing +from PyQt6 import QtCore, QtGui, QtWidgets + class BlocksCustomFrame(QtWidgets.QFrame): def __init__(self, parent=None): diff --git a/BlocksScreen/lib/utils/blocks_label.py b/BlocksScreen/lib/utils/blocks_label.py index 386d373b..0cff3ea4 100644 --- a/BlocksScreen/lib/utils/blocks_label.py +++ b/BlocksScreen/lib/utils/blocks_label.py @@ -13,10 +13,10 @@ def __init__(self, parent: QtWidgets.QWidget | None, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - self.icon_pixmap: typing.Optional[QtGui.QPixmap] = None + self.icon_pixmap: QtGui.QPixmap | None = None self._text: str = "" - self._background_color: typing.Optional[QtGui.QColor] = None - self._border_color: typing.Optional[QtGui.QColor] = None + self._background_color: QtGui.QColor | None = None + self._border_color: QtGui.QColor | None = None self._rounded: bool = False self._marquee: bool = True self.timer = QtCore.QTimer() @@ -79,7 +79,7 @@ def setText(self, text: str) -> None: self.update_text_metrics() @property - def background_color(self) -> typing.Optional[QtGui.QColor]: + def background_color(self) -> QtGui.QColor | None: """Widget background color""" return self._background_color @@ -88,7 +88,7 @@ def background_color(self, color: QtGui.QColor) -> None: self._background_color = color @property - def border_color(self) -> typing.Optional[QtGui.QColor]: + def border_color(self) -> QtGui.QColor | None: """Widget border color""" return self._border_color diff --git a/BlocksScreen/lib/utils/blocks_linedit.py b/BlocksScreen/lib/utils/blocks_linedit.py index c8c28f15..61f4c089 100644 --- a/BlocksScreen/lib/utils/blocks_linedit.py +++ b/BlocksScreen/lib/utils/blocks_linedit.py @@ -1,5 +1,3 @@ -import typing - from PyQt6 import QtCore, QtGui, QtWidgets @@ -10,7 +8,7 @@ class BlocksCustomLinEdit(QtWidgets.QLineEdit): TEXT_MARGIN = 10 CORNER_RADIUS = 8 - def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: + def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent) # State @@ -86,7 +84,11 @@ def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: self.clicked.emit() super().mousePressEvent(event) - def paintEvent(self, event: typing.Optional[QtGui.QPaintEvent]) -> None: + def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: + """Handle mouse release""" + super().mouseReleaseEvent(event) + + def paintEvent(self, event: QtGui.QPaintEvent | None) -> None: """Custom paint with embedded toggle button.""" painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) diff --git a/BlocksScreen/lib/utils/blocks_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index 56494700..c50b0540 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -154,7 +154,7 @@ def _draw_circular_bar( bg_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) painter.setPen(bg_pen) painter.drawArc(arc_rect, arc_start, arc_span) - if self.progress_value is not None: + if self.progress_value > 0: gradient = QtGui.QConicalGradient(arc_rect.center(), -90) gradient.setColorAt(0.0, self._bar_color) gradient.setColorAt(1.0, QtGui.QColor(100, 100, 100)) diff --git a/BlocksScreen/lib/utils/blocks_tabwidget.py b/BlocksScreen/lib/utils/blocks_tabwidget.py index 4696d967..cc403bd0 100644 --- a/BlocksScreen/lib/utils/blocks_tabwidget.py +++ b/BlocksScreen/lib/utils/blocks_tabwidget.py @@ -1,4 +1,4 @@ -from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6 import QtCore, QtGui, QtWidgets class NotificationTabBar(QtWidgets.QTabBar): diff --git a/BlocksScreen/lib/utils/blocks_togglebutton.py b/BlocksScreen/lib/utils/blocks_togglebutton.py index c97e8f1f..e95194d9 100644 --- a/BlocksScreen/lib/utils/blocks_togglebutton.py +++ b/BlocksScreen/lib/utils/blocks_togglebutton.py @@ -7,7 +7,7 @@ class NetworkWidgetbuttons(QtWidgets.QWidget): clicked = QtCore.pyqtSignal() def __init__(self, parent): - super(NetworkWidgetbuttons, self).__init__(parent) + super().__init__(parent) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self._icon_label = None diff --git a/BlocksScreen/lib/utils/check_button.py b/BlocksScreen/lib/utils/check_button.py index fcd61b5d..48f23b05 100644 --- a/BlocksScreen/lib/utils/check_button.py +++ b/BlocksScreen/lib/utils/check_button.py @@ -1,5 +1,3 @@ -import typing - from PyQt6 import QtCore, QtGui, QtWidgets @@ -45,7 +43,7 @@ def setText(self, text: str | None) -> None: self.update() return - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + def paintEvent(self, e: QtGui.QPaintEvent | None): """Re-implemented method, paint widget, optimized for performance.""" painter = QtGui.QPainter(self) diff --git a/BlocksScreen/lib/utils/display_button.py b/BlocksScreen/lib/utils/display_button.py index 1bc7b723..02c1e4e5 100644 --- a/BlocksScreen/lib/utils/display_button.py +++ b/BlocksScreen/lib/utils/display_button.py @@ -202,21 +202,21 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: QtCore.Qt.TextFlag.TextShowMnemonic | QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - str(self.text()) if self.text() else str("?"), + str(self.text()) if self.text() else "?", ) painter.drawText( _stl_rect, QtCore.Qt.TextFlag.TextShowMnemonic | QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - str(self.secondary_text) if self.secondary_text else str("?"), + str(self.secondary_text) if self.secondary_text else "?", ) painter.drawText( _mtl_rect, QtCore.Qt.TextFlag.TextShowMnemonic | QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - str("/"), + "/", ) elif self.display_format == "upper_downer": _mtl = QtCore.QRectF( @@ -369,7 +369,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: QtCore.Qt.TextFlag.TextShowMnemonic | QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - str(self.text()) if self.text() else str("?"), + str(self.text()) if self.text() else "?", ) painter.setPen(QtCore.Qt.PenStyle.NoPen) _ = painter.end() diff --git a/BlocksScreen/lib/utils/icon_button.py b/BlocksScreen/lib/utils/icon_button.py index 3880d285..55d9ebc1 100644 --- a/BlocksScreen/lib/utils/icon_button.py +++ b/BlocksScreen/lib/utils/icon_button.py @@ -1,4 +1,5 @@ import typing + from PyQt6 import QtCore, QtGui, QtWidgets @@ -135,7 +136,7 @@ def setProperty(self, name: str, value: typing.Any) -> bool: elif name == "has_text": self.has_text = value elif name == "name": - self._name = name + self._name = value elif name == "text_color": self.text_color = value return super().setProperty(name, value) diff --git a/BlocksScreen/lib/utils/list_button.py b/BlocksScreen/lib/utils/list_button.py index deb01bf1..652b2fa9 100644 --- a/BlocksScreen/lib/utils/list_button.py +++ b/BlocksScreen/lib/utils/list_button.py @@ -4,9 +4,9 @@ class ListCustomButton(QtWidgets.QPushButton): def __init__(self, parent=None) -> None: if parent: - super(ListCustomButton, self).__init__(parent) + super().__init__(parent) else: - super(ListCustomButton, self).__init__() + super().__init__() self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() self.second_icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() self.text_color: QtGui.QColor = QtGui.QColor(255, 255, 255) diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index a1a6377c..8fbebc86 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -13,12 +13,12 @@ class ListItem: _rfontsize: int = 0 _lfontsize: int = 0 - callback: typing.Optional[typing.Callable] = None + callback: typing.Callable | None = None color: str = "#dfdfdf" color_left_icon: bool = False - right_icon: typing.Optional[QtGui.QPixmap] = None - left_icon: typing.Optional[QtGui.QPixmap] = None + right_icon: QtGui.QPixmap | None = None + left_icon: QtGui.QPixmap | None = None selected: bool = False allow_check: bool = True @@ -33,7 +33,7 @@ class ListItem: notificate: bool = False # stores width and heitgh of the button so we dont need to recalculate it every time - _cache: typing.Dict[int, int] = field(default_factory=dict) + _cache: dict[int, int] = field(default_factory=dict) def clear_cache(self): """Call this if text or font size changes dynamically""" diff --git a/BlocksScreen/lib/utils/toggleAnimatedButton.py b/BlocksScreen/lib/utils/toggleAnimatedButton.py index b9555876..371802eb 100644 --- a/BlocksScreen/lib/utils/toggleAnimatedButton.py +++ b/BlocksScreen/lib/utils/toggleAnimatedButton.py @@ -1,5 +1,6 @@ import enum import typing + from PyQt6 import QtCore, QtGui, QtWidgets @@ -33,6 +34,7 @@ def __init__(self, parent) -> None: - self.handle_radius * 2 ) + self.trailPath: QtGui.QPainterPath | None = None self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() self._backgroundColor: QtGui.QColor = QtGui.QColor(223, 223, 223) self._handleColor: QtGui.QColor = QtGui.QColor(255, 100, 10) @@ -179,7 +181,7 @@ def setup_animation(self) -> None: def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: """Re-implemented method, handle mouse press events""" - if self.trailPath: + if self.trailPath is not None: if self.trailPath.contains(e.pos().toPointF()) and self.underMouse(): if not self.slide_animation.state == self.slide_animation.State.Running: self._state = ToggleAnimatedButton.State(not self._state.value) @@ -214,7 +216,10 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: rect_norm = _rect.toRectF().normalized() min_x = rect_norm.x() max_x = rect_norm.x() + rect_norm.width() - rect_norm.height() * 0.80 - progress = (self._handle_position - min_x) / (max_x - min_x) + denominator = max_x - min_x + if denominator == 0: + return + progress = (self._handle_position - min_x) / denominator progress = max(0.0, min(1.0, progress)) # Inline color interpolation (no separate functions) @@ -237,6 +242,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: self.handleColor = QtGui.QColor(int(r), int(g), int(b), int(a)) + if self.trailPath is None: + return painter.fillPath( self.trailPath, bg_color if self.isEnabled() else self.disable_bg_color, diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index ce16db74..7b3b7d50 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -122,7 +122,7 @@ def close(self) -> None: def closed(self) -> bool: return False - def __enter__(self) -> "StreamToLogger": + def __enter__(self) -> StreamToLogger: return self def __exit__(self, *args) -> None: diff --git a/tests/widgets/test_sensors_panel_unit.py b/tests/widgets/test_sensors_panel_unit.py new file mode 100644 index 00000000..802f424d --- /dev/null +++ b/tests/widgets/test_sensors_panel_unit.py @@ -0,0 +1,54 @@ +"""Unit tests for SensorsWindow routing Klipper filament sensor updates.""" + +from types import SimpleNamespace + +import pytest +from lib.panels.widgets.sensorsPanel import SensorsWindow +from lib.panels.widgets.sensorWidget import SensorWidget +from PyQt6 import QtWidgets + + +@pytest.fixture() +def parent(qtbot): + """Parent widget; SensorWidget sizes itself from it.""" + w = QtWidgets.QWidget() + qtbot.addWidget(w) + return w + + +@pytest.fixture() +def sensor(parent): + """SensorWidget for a Klipper filament_switch_sensor.""" + return SensorWidget(parent, "filament_switch_sensor runout") + + +def _route(sensor, name, parameter, value): + # unbound call skips SensorsWindow._setupUi, which needs compiled qrc fonts + panel = SimpleNamespace(sensor_tracking_widget={sensor.name: sensor}) + SensorsWindow.handle_fil_state_change(panel, name, parameter, value) # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("detected", "expected"), + [ + (True, SensorWidget.FilamentState.PRESENT), + (False, SensorWidget.FilamentState.MISSING), + ], +) +def test_filament_detected_sets_state(sensor, detected, expected): + _route(sensor, "runout", "filament_detected", detected) + assert sensor.filament_state is expected + + +def test_unknown_sensor_ignored(sensor): + before = sensor.filament_state + _route(sensor, "other", "filament_detected", not before.value) + assert sensor.filament_state is before + + +@pytest.mark.parametrize( + ("klipper_name", "short_name"), + [("filament_motion_sensor encoder", "encoder"), ("cutter_sensor", "cutter_sensor")], +) +def test_sensor_name(parent, klipper_name, short_name): + assert SensorWidget(parent, klipper_name).name == short_name