From a31283a5c611e3d172d58ebacc60d7d91e6a4694 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 13 Apr 2026 17:52:15 +0100 Subject: [PATCH 1/3] refactor: modernise typing to Python 3.11 native syntax (21 files) --- BlocksScreen/BlocksScreen.py | 3 +- BlocksScreen/lib/filament.py | 31 +++++++++---------- BlocksScreen/lib/files.py | 21 ++++++------- BlocksScreen/lib/moonrest.py | 9 +++--- .../lib/panels/widgets/bannerPopup.py | 2 ++ BlocksScreen/lib/panels/widgets/fansPage.py | 9 +++--- BlocksScreen/lib/panels/widgets/filesPage.py | 9 +++--- BlocksScreen/lib/panels/widgets/numpadPage.py | 3 +- .../lib/panels/widgets/optionCardWidget.py | 2 +- .../lib/panels/widgets/popupDialogWidget.py | 8 ++--- .../lib/panels/widgets/printcorePage.py | 2 ++ .../lib/panels/widgets/sensorWidget.py | 15 ++++----- BlocksScreen/lib/panels/widgets/updatePage.py | 21 +++++++------ BlocksScreen/lib/qrcode_gen.py | 3 +- BlocksScreen/lib/utils/blocks_button.py | 15 ++------- BlocksScreen/lib/utils/blocks_frame.py | 3 +- BlocksScreen/lib/utils/blocks_progressbar.py | 15 ++++++--- BlocksScreen/lib/utils/blocks_tabwidget.py | 2 +- BlocksScreen/lib/utils/check_button.py | 3 +- BlocksScreen/lib/utils/icon_button.py | 3 +- .../lib/utils/toggleAnimatedButton.py | 11 +++++-- tests/network/test_network_ui.py | 4 +-- 22 files changed, 99 insertions(+), 95 deletions(-) diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index ab198a7e..dab6cb59 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -1,6 +1,5 @@ import logging import sys -import typing from logger import CrashHandler, LogManager, install_crash_handler, setup_logging @@ -39,7 +38,7 @@ def notify(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: # type: ignore RESET = "\033[0m" -def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): +def show_splash(window: QtWidgets.QWidget | None = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") splash = QtWidgets.QSplashScreen(pixmap=logo) diff --git a/BlocksScreen/lib/filament.py b/BlocksScreen/lib/filament.py index cb4c0232..0c3d0b44 100644 --- a/BlocksScreen/lib/filament.py +++ b/BlocksScreen/lib/filament.py @@ -1,6 +1,7 @@ # Class that represents a filament spool -from typing import Optional +from __future__ import annotations + import enum @@ -29,17 +30,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,7 +57,7 @@ def temperature(self) -> int: return self._temperature @property - def weight(self) -> Optional[float]: + def weight(self) -> float | None: if self._weight is None: return return self._weight @@ -65,22 +67,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 ValueError(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..87396151 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 @@ -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.""" @@ -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/moonrest.py b/BlocksScreen/lib/moonrest.py index 2c663531..079ca577 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 @@ -128,7 +127,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", @@ -142,9 +141,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/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/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/numpadPage.py b/BlocksScreen/lib/panels/widgets/numpadPage.py index b904645c..40e222cc 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 d565bd3a..b702e840 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 e0ed9955..23fc46dc 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/updatePage.py b/BlocksScreen/lib/panels/widgets/updatePage.py index b91e41d3..15e72b00 100644 --- a/BlocksScreen/lib/panels/widgets/updatePage.py +++ b/BlocksScreen/lib/panels/widgets/updatePage.py @@ -101,7 +101,7 @@ def on_request_reload(self, service: str | None = None) -> None: """Handles reload button click, requests update status refresh""" self.show_loading(True) if service: - self.request_refresh_update.emit([service]) + self.request_refresh_update[str].emit(service) else: self.request_refresh_update.emit() @@ -197,14 +197,16 @@ def on_item_clicked(self, item: ListItem) -> None: if not _remote_version: self.remote_version_title.hide() self.remote_version_tracking.hide() - self.remote_version_title.show() - self.remote_version_tracking.show() - self.remote_version_title.setText("Remote Version: ") - self.remote_version_tracking.setText(_remote_version) + else: + self.remote_version_title.show() + self.remote_version_tracking.show() + self.remote_version_title.setText("Remote Version: ") + self.remote_version_tracking.setText(_remote_version) _curr_version = cli_data.get("version", None) if not _curr_version: # There is no version information something is seriously wrong here self.action_btn.setText("Recover") + return self.version_title.show() self.version_tracking_info.show() self.version_tracking_info.setText(_curr_version) @@ -295,7 +297,8 @@ def _setupUI(self) -> None: font_id = QtGui.QFontDatabase.addApplicationFont( ":/font/media/fonts for text/Momcake-Bold.ttf" ) - font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] + _families = QtGui.QFontDatabase.applicationFontFamilies(font_id) + font_family = _families[0] if _families else "" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, @@ -304,11 +307,9 @@ def _setupUI(self) -> None: sizePolicy.setVerticalStretch(1) self.setSizePolicy(sizePolicy) self.setObjectName("updatePage") - self.setStyleSheet( - """#updatePage { + self.setStyleSheet("""#updatePage { background-image: url(:/background/media/1st_background.png); - }""" - ) + }""") self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.update_page_content_layout = QtWidgets.QVBoxLayout() self.update_page_content_layout.setContentsMargins(15, 15, 15, 15) 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 292b5125..ec6b24c7 100644 --- a/BlocksScreen/lib/utils/blocks_button.py +++ b/BlocksScreen/lib/utils/blocks_button.py @@ -1,5 +1,6 @@ -import typing import enum +import typing + from PyQt6 import QtCore, QtGui, QtWidgets @@ -106,7 +107,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) @@ -116,17 +117,7 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): _style = self.style() if not _style or not _rect: return - # Flat button control opt = QtWidgets.QStyleOptionButton() - draw_frame = ( - not self._is_flat - or self.underMouse() - or opt.state & QtWidgets.QStyle.StateFlag.State_Sunken - ) - if draw_frame: - _style.drawControl( - QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self - ) _style.drawControl( QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self ) 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_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index 414097bb..60950b67 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -1,5 +1,6 @@ import typing -from PyQt6 import QtWidgets, QtGui, QtCore + +from PyQt6 import QtCore, QtGui, QtWidgets class CustomProgressBar(QtWidgets.QProgressBar): @@ -30,6 +31,12 @@ def __init__(self, parent=None): self.setMinimumSize(100, 100) self._inner_rect: QtCore.QRectF = QtCore.QRectF() + def reset(self) -> None: + """Reset progress to zero.""" + self.progress_value = 0 + super().reset() + self.update() + def set_padding(self, value) -> None: """Set widget padding""" self._padding = value @@ -93,8 +100,8 @@ def setValue(self, value: float) -> None: Raises: ValueError: If provided value in not between 0.0 and 1.0 """ - if not (0 <= value <= 100): - raise ValueError("Argument `value` expected value between 0.0 and 1.0 ") + if not (0.0 <= value <= 1.0): + raise ValueError("Argument `value` expected value between 0.0 and 1.0") value *= 100 self.progress_value = value self.update() @@ -159,7 +166,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 is not None and 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/check_button.py b/BlocksScreen/lib/utils/check_button.py index e5b184d5..79c998c1 100644 --- a/BlocksScreen/lib/utils/check_button.py +++ b/BlocksScreen/lib/utils/check_button.py @@ -1,4 +1,3 @@ -import typing from PyQt6 import QtCore, QtGui, QtWidgets @@ -39,7 +38,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/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/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/tests/network/test_network_ui.py b/tests/network/test_network_ui.py index 466ec14a..a60a330a 100644 --- a/tests/network/test_network_ui.py +++ b/tests/network/test_network_ui.py @@ -693,7 +693,7 @@ def test_transient_mismatch_retries(self, win, qapp): message="not compatible with device", error_code="nm_error", ) - with patch("BlocksScreen.lib.panels.networkWindow.QTimer") as mock_timer: + with patch("BlocksScreen.lib.panels.networkWindow.QtCore.QTimer") as mock_timer: w._on_operation_complete(result) mock_timer.singleShot.assert_called_once() # Loading should still be visible — retry is pending @@ -740,7 +740,7 @@ def test_wifi_on_with_saved_networks_starts_connect(self, win): ) ] nm.saved_networks = saved - with patch("BlocksScreen.lib.panels.networkWindow.QTimer") as mock_timer: + with patch("BlocksScreen.lib.panels.networkWindow.QtCore.QTimer") as mock_timer: w._handle_wifi_toggle(True) mock_timer.singleShot.assert_called() assert w._pending_operation == PendingOperation.WIFI_ON From ec206ca49220de4c5a977a8695f4f575d48fc88d Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 13 Apr 2026 18:24:29 +0100 Subject: [PATCH 2/3] revert: test_network_ui belongs with networkWindow changes --- tests/network/test_network_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/network/test_network_ui.py b/tests/network/test_network_ui.py index a60a330a..466ec14a 100644 --- a/tests/network/test_network_ui.py +++ b/tests/network/test_network_ui.py @@ -693,7 +693,7 @@ def test_transient_mismatch_retries(self, win, qapp): message="not compatible with device", error_code="nm_error", ) - with patch("BlocksScreen.lib.panels.networkWindow.QtCore.QTimer") as mock_timer: + with patch("BlocksScreen.lib.panels.networkWindow.QTimer") as mock_timer: w._on_operation_complete(result) mock_timer.singleShot.assert_called_once() # Loading should still be visible — retry is pending @@ -740,7 +740,7 @@ def test_wifi_on_with_saved_networks_starts_connect(self, win): ) ] nm.saved_networks = saved - with patch("BlocksScreen.lib.panels.networkWindow.QtCore.QTimer") as mock_timer: + with patch("BlocksScreen.lib.panels.networkWindow.QTimer") as mock_timer: w._handle_wifi_toggle(True) mock_timer.singleShot.assert_called() assert w._pending_operation == PendingOperation.WIFI_ON From 8e64037d78eb8f42710fd4c7d6a4ba758d3dfdce Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 23 Sep 2026 12:02:24 +0100 Subject: [PATCH 3/3] refactor(typing): finish py311 upgrade and fix renamed filament sensor slot crash --- BlocksScreen/events.py | 53 ++++++++---------- BlocksScreen/lib/filament.py | 6 +-- BlocksScreen/lib/files.py | 4 +- BlocksScreen/lib/klipper_message_filter.py | 2 +- BlocksScreen/lib/machine.py | 2 +- BlocksScreen/lib/network/manager.py | 2 +- BlocksScreen/lib/panels/filamentTab.py | 3 +- BlocksScreen/lib/panels/mainWindow.py | 2 +- BlocksScreen/lib/panels/utilitiesTab.py | 6 +-- BlocksScreen/lib/panels/widgets/basePopup.py | 4 +- .../lib/panels/widgets/confirmPage.py | 2 +- .../lib/panels/widgets/keyboardPage.py | 2 +- .../lib/panels/widgets/sensorsPanel.py | 9 ++-- BlocksScreen/lib/printer.py | 2 +- BlocksScreen/lib/utils/blocks_label.py | 10 ++-- BlocksScreen/lib/utils/blocks_linedit.py | 6 +-- BlocksScreen/lib/utils/blocks_progressbar.py | 2 +- BlocksScreen/lib/utils/blocks_togglebutton.py | 2 +- BlocksScreen/lib/utils/display_button.py | 8 +-- BlocksScreen/lib/utils/list_button.py | 4 +- BlocksScreen/lib/utils/list_model.py | 8 +-- BlocksScreen/logger.py | 2 +- tests/widgets/test_sensors_panel_unit.py | 54 +++++++++++++++++++ 23 files changed, 115 insertions(+), 80 deletions(-) create mode 100644 tests/widgets/test_sensors_panel_unit.py 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 0c3d0b44..7ceae1a2 100644 --- a/BlocksScreen/lib/filament.py +++ b/BlocksScreen/lib/filament.py @@ -1,7 +1,5 @@ # Class that represents a filament spool -from __future__ import annotations - import enum @@ -58,8 +56,6 @@ def temperature(self) -> int: @property def weight(self) -> float | None: - if self._weight is None: - return return self._weight @weight.setter @@ -81,5 +77,5 @@ def spool_type(self) -> SpoolMaterial | None: @spool_type.setter def spool_type(self, new) -> None: if new is not None and not isinstance(new, self.SpoolMaterial): - raise ValueError(f"Spool Material type is invalid: {new!r}") + 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 87396151..710991a9 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -28,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, @@ -108,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.` 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/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/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index b772dac6..00d57802 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -1,6 +1,5 @@ import logging from collections import deque -from typing import Deque from devices.amu import AMUManager from devices.amu.models import GateStatus @@ -52,7 +51,7 @@ def __init__( self._previous_gate_states: dict[int, bool] = {} self.pre_gate_idx = {} - self.popup_gates: Deque = deque() + self.popup_gates: deque = deque() self._spool_id_map: dict[str, dict] = {} self._current_field: QtWidgets.QLineEdit | None = None self._color_target_field = None diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 035f27f8..622655f0 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/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/keyboardPage.py b/BlocksScreen/lib/panels/widgets/keyboardPage.py index 243302f4..a370fe40 100644 --- a/BlocksScreen/lib/panels/widgets/keyboardPage.py +++ b/BlocksScreen/lib/panels/widgets/keyboardPage.py @@ -185,7 +185,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/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/utils/blocks_label.py b/BlocksScreen/lib/utils/blocks_label.py index a2290092..99782b36 100644 --- a/BlocksScreen/lib/utils/blocks_label.py +++ b/BlocksScreen/lib/utils/blocks_label.py @@ -11,10 +11,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() @@ -76,7 +76,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 @@ -85,7 +85,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 ed973ec0..34b26814 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 @@ -89,7 +87,7 @@ def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: """Handle mouse release""" super().mouseReleaseEvent(event) - def paintEvent(self, event: typing.Optional[QtGui.QPaintEvent]) -> None: + 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 a3d6e1d2..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 and self.progress_value > 0: + 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_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/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/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/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