From 1dd214679bb8f33c5cefd80557c028b95b2f5403 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 6 Apr 2023 12:41:46 -0400 Subject: [PATCH 1/3] Drop PyQt in favor of PySide --- .flake8 | 2 +- pyproject.toml | 11 ++- res/design.ui | 19 ++++- scripts/compile_resources.ps1 | 14 +++- scripts/requirements-dev.txt | 4 +- scripts/requirements.txt | 9 +- ...olledWorker.py => AutoControlledThread.py} | 5 +- src/AutoSplit.py | 84 ++++++++----------- src/error_messages.py | 4 +- src/hotkeys.py | 2 +- src/menu_bar.py | 10 +-- src/region_selection.py | 26 +++--- src/user_profile.py | 4 +- typings/PyQt6/QtTest.pyi | 13 --- 14 files changed, 103 insertions(+), 104 deletions(-) rename src/{AutoControlledWorker.py => AutoControlledThread.py} (94%) delete mode 100644 typings/PyQt6/QtTest.pyi diff --git a/.flake8 b/.flake8 index 93d41a1a..b883f620 100644 --- a/.flake8 +++ b/.flake8 @@ -29,7 +29,7 @@ per-file-ignores= ; Type re-exports ; mypy 3.7 Union issue *.pyi: Q000,E701,E704,E501,N8,A001,A002,A003,CCE002,F401,Y037 -; PyQt methods +; Qt methods, we can't control the names ignore-names=closeEvent,paintEvent,keyPressEvent,mousePressEvent,mouseMoveEvent,mouseReleaseEvent ; McCabe max-complexity is also taken care of by Pylint and doesn't fail the build there ; So this is the hard limit diff --git a/pyproject.toml b/pyproject.toml index 2350488c..50e8ff37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ reportUnusedCallResult = "none" reportMissingTypeStubs = "warning" # False positives with TYPE_CHECKING reportImportCycles = "information" -# False positives with PyQt .connect. pylint(no-member) works instead +# False positives with PySide .connect. pylint(no-member) works instead reportFunctionMemberAccess = "none" # Extra runtime safety reportUnnecessaryComparison = "warning" @@ -87,7 +87,14 @@ ignore-paths = [ # We expect stub files to be incomplete or contain useless statements "^.*.pyi$", ] -extension-pkg-allow-list = ["PyQt6", "PySide6", "win32ui", "win32.win32gui"] +extension-pkg-allow-list = [ + "PySide6.QtCore", + "PySide6.QtGui", + "PySide6.QtTest", + "PySide6.QtWidgets", + "win32ui", + "win32.win32gui", +] [tool.pylint.FORMAT] max-line-length = 120 diff --git a/res/design.ui b/res/design.ui index 7d67e0e0..afb56b1d 100644 --- a/res/design.ui +++ b/res/design.ui @@ -32,9 +32,7 @@ - :/resources/icon.ico - :/resources/icon.ico - + :/resources/icon.ico:/resources/icon.ico @@ -974,6 +972,9 @@ F1 + + Qt::ApplicationShortcut + @@ -990,6 +991,9 @@ Ctrl+S + + Qt::ApplicationShortcut + @@ -998,6 +1002,9 @@ Ctrl+O + + Qt::ApplicationShortcut + @@ -1006,6 +1013,9 @@ Ctrl+Shift+S + + Qt::ApplicationShortcut + @@ -1019,6 +1029,9 @@ Ctrl+, + + Qt::ApplicationShortcut + QAction::PreferencesRole diff --git a/scripts/compile_resources.ps1 b/scripts/compile_resources.ps1 index 909036fe..c7eba061 100644 --- a/scripts/compile_resources.ps1 +++ b/scripts/compile_resources.ps1 @@ -2,11 +2,17 @@ $originalDirectory = $pwd Set-Location "$PSScriptRoot/.." New-Item -Force -ItemType directory ./src/gen | Out-Null -pyuic6 './res/about.ui' -o './src/gen/about.py' -pyuic6 './res/design.ui' -o './src/gen/design.py' -pyuic6 './res/settings.ui' -o './src/gen/settings.py' -pyuic6 './res/update_checker.ui' -o './src/gen/update_checker.py' +pyside6-uic './res/about.ui' -o './src/gen/about.py' +pyside6-uic './res/design.ui' -o './src/gen/design.py' +pyside6-uic './res/settings.ui' -o './src/gen/settings.py' +pyside6-uic './res/update_checker.ui' -o './src/gen/update_checker.py' pyside6-rcc './res/resources.qrc' -o './src/gen/resources_rc.py' +$files = Get-ChildItem ./src/gen/ *.py +foreach ($file in $files) { + (Get-Content $file.PSPath) | + ForEach-Object { $_ -replace 'import resources_rc', 'from . import resources_rc' } | + Set-Content $file.PSPath +} Write-Host 'Generated code from .ui files' $build_vars_path = "$PSScriptRoot/../src/gen/build_vars.py" diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 2e2e8c1c..20cba59a 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -25,9 +25,9 @@ autopep8>=2.0.0 # New checks isort unify # -# Run `./scripts/designer.ps1` to quickly open the bundled PyQt Designer. +# Run `./scripts/designer.ps1` to quickly open the bundled Qt Designer. # Can also be downloaded externally as a non-python package -qt6-applications +# qt6-applications # Types types-d3dshot types-keyboard diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 62c64e99..c78d8aba 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -19,16 +19,17 @@ packaging Pillow>=9.2 # gnome-screeshot checks psutil PyAutoGUI -# 6.4.1 fixes the rare Illegal Operation issue from RTADan, but the dev wheels are currently broken :/ -# 2.0.0-beta.2 contains the fixes if anyone needs it. -PyQt6>=6.4.0 # Python 3.11 support +# PySide6-Essentials>6.4.2 # Python 3.11 support, +# Dev PySide6 past March 7th fixes https://bugreports.qt.io/browse/PYSIDE-2189 and https://bugreports.qt.io/browse/PYSIDE-1603 +# https://download.qt.io/snapshots/ci/pyside/dev/?C=M;O=D +https://download.qt.io/snapshots/ci/pyside/dev/0142b21d429a7cf0ef78a5bf9a0d972c2a7223b2/split_wheels/PySide6_Essentials-6.5.0a1.dev1678202304-cp37-abi3-win_amd64.whl +https://download.qt.io/snapshots/ci/pyside/dev/0142b21d429a7cf0ef78a5bf9a0d972c2a7223b2/split_wheels/shiboken6-6.5.0a1.dev1678202304-cp37-abi3-win_amd64.whl requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534 toml # # Build and compile resources pyinstaller>=5.5 # Python 3.11 support pyinstaller-hooks-contrib>=2022.9 # opencv-python 4.6 support. Changes for pywintypes and comtypes -PySide6-Essentials>=6.4.0.1 # Python 3.11 support # # https://peps.python.org/pep-0508/#environment-markers # diff --git a/src/AutoControlledWorker.py b/src/AutoControlledThread.py similarity index 94% rename from src/AutoControlledWorker.py rename to src/AutoControlledThread.py index 5b3238c1..f5e518a8 100644 --- a/src/AutoControlledWorker.py +++ b/src/AutoControlledThread.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from PyQt6 import QtCore +from PySide6 import QtCore import error_messages import user_profile @@ -11,11 +11,12 @@ from AutoSplit import AutoSplit -class AutoControlledWorker(QtCore.QObject): +class AutoControlledThread(QtCore.QThread): def __init__(self, autosplit: AutoSplit): self.autosplit = autosplit super().__init__() + @QtCore.Slot() def run(self): while True: try: diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 4d239b82..c408fc39 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -6,19 +6,21 @@ import os import signal import sys +from collections.abc import Callable from time import time from types import FunctionType +from typing import NoReturn, Optional import certifi import cv2 from psutil import process_iter -from PyQt6 import QtCore, QtGui -from PyQt6.QtTest import QTest -from PyQt6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QWidget +from PySide6 import QtCore, QtGui +from PySide6.QtTest import QTest +from PySide6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QWidget import error_messages import user_profile -from AutoControlledWorker import AutoControlledWorker +from AutoControlledThread import AutoControlledThread from AutoSplitImage import START_KEYWORD, AutoSplitImage, ImageType from capture_method import CaptureMethodBase, CaptureMethodEnum from gen import about, design, settings, update_checker @@ -48,16 +50,16 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow): # pylint: disable=too-many- is_auto_controlled = "--auto-controlled" in sys.argv # Signals - start_auto_splitter_signal = QtCore.pyqtSignal() - reset_signal = QtCore.pyqtSignal() - skip_split_signal = QtCore.pyqtSignal() - undo_split_signal = QtCore.pyqtSignal() - pause_signal = QtCore.pyqtSignal() - after_setting_hotkey_signal = QtCore.pyqtSignal() - update_checker_widget_signal = QtCore.pyqtSignal(str, bool) - load_start_image_signal = QtCore.pyqtSignal([], [bool], [bool, bool]) + start_auto_splitter_signal = QtCore.Signal() + reset_signal = QtCore.Signal() + skip_split_signal = QtCore.Signal() + undo_split_signal = QtCore.Signal() + pause_signal = QtCore.Signal() + after_setting_hotkey_signal = QtCore.Signal() + update_checker_widget_signal = QtCore.Signal(str, bool) + load_start_image_signal = QtCore.Signal(type[Optional[bool]], type[Optional[bool]]) # Use this signal when trying to show an error from outside the main thread - show_error_signal = QtCore.pyqtSignal(FunctionType) + show_error_signal = QtCore.Signal(FunctionType) # Timers timer_live_image = QtCore.QTimer() @@ -97,13 +99,15 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow): # pylint: disable=too-many- reset_image: AutoSplitImage | None = None split_images: list[AutoSplitImage] = [] split_image: AutoSplitImage | None = None - update_auto_control: QtCore.QThread | None = None + update_auto_control: AutoControlledThread | None = None - def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-statements - super().__init__(parent) + def __init__(self): # pylint: disable=too-many-statements + super().__init__() # Setup global error handling - self.show_error_signal.connect(lambda errorMessageBox: errorMessageBox()) + def _show_error_signal_slot(error_message_box: Callable[..., object]): + return error_message_box() + self.show_error_signal.connect(_show_error_signal_slot) sys.excepthook = error_messages.make_excepthook(self) self.setupUi(self) @@ -133,10 +137,7 @@ def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-s print(f"{AUTOSPLIT_VERSION}\n{os.getpid()}", flush=True) # Use and Start the thread that checks for updates from LiveSplit - self.update_auto_control = QtCore.QThread() - worker = AutoControlledWorker(self) - worker.moveToThread(self.update_auto_control) - self.update_auto_control.started.connect(worker.run) + self.update_auto_control = AutoControlledThread(self) self.update_auto_control.start() # split image folder line edit text @@ -149,23 +150,10 @@ def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-s self.action_about_qt_for_python.triggered.connect(about_qt_for_python) self.action_check_for_updates.triggered.connect(lambda: check_for_updates(self)) self.action_settings.triggered.connect(lambda: open_settings(self)) - # PyQt6 typing is wrong - self.action_save_profile.triggered.connect( - lambda: user_profile.save_settings(self), # pyright: ignore[reportGeneralTypeIssues] - ) - self.action_save_profile_as.triggered.connect( - lambda: user_profile.save_settings_as(self), # pyright: ignore[reportGeneralTypeIssues] - ) + self.action_save_profile.triggered.connect(lambda: user_profile.save_settings(self)) + self.action_save_profile_as.triggered.connect(lambda: user_profile.save_settings_as(self)) self.action_load_profile.triggered.connect(lambda: user_profile.load_settings(self)) - # Shortcut context can't be set through the designer because of a bug in pyuic6 that generates invalid code - # Email sent to pyqt@riverbankcomputing.com - self.action_view_help.setShortcutContext(QtCore.Qt.ShortcutContext.ApplicationShortcut) - self.action_settings.setShortcutContext(QtCore.Qt.ShortcutContext.ApplicationShortcut) - self.action_save_profile.setShortcutContext(QtCore.Qt.ShortcutContext.ApplicationShortcut) - self.action_save_profile_as.setShortcutContext(QtCore.Qt.ShortcutContext.ApplicationShortcut) - self.action_load_profile.setShortcutContext(QtCore.Qt.ShortcutContext.ApplicationShortcut) - # Connecting button clicks to functions self.browse_button.clicked.connect(self.__browse) self.select_region_button.clicked.connect(lambda: select_region(self)) @@ -193,12 +181,12 @@ def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-s # connect signals to functions self.after_setting_hotkey_signal.connect(lambda: after_setting_hotkey(self)) self.start_auto_splitter_signal.connect(self.__auto_splitter) - self.update_checker_widget_signal.connect( - lambda latest_version, check_on_open: open_update_checker(self, latest_version, check_on_open), - ) + + def _update_checker_widget_signal_slot(latest_version: str, check_on_open: bool): + return open_update_checker(self, latest_version, check_on_open) + self.update_checker_widget_signal.connect(_update_checker_widget_signal_slot) + self.load_start_image_signal.connect(self.__load_start_image) - self.load_start_image_signal[bool].connect(self.__load_start_image) - self.load_start_image_signal[bool, bool].connect(self.__load_start_image) self.reset_signal.connect(self.reset) self.skip_split_signal.connect(self.skip_split) self.undo_split_signal.connect(self.undo_split) @@ -780,7 +768,7 @@ def gui_changes_on_reset(self, safe_to_reload_start_image: bool = False): QApplication.processEvents() if safe_to_reload_start_image: - self.load_start_image_signal[bool, bool].emit(False, False) + self.load_start_image_signal.emit(False, False) def __get_capture_for_comparison(self): """ @@ -865,7 +853,7 @@ def __update_split_image(self, specific_image: AutoSplitImage | None = None): loop_tuple = self.split_images_and_loop_number[self.split_image_number] self.image_loop_value_label.setText(f"{loop_tuple[1]}/{loop_tuple[0].loops}") - def closeEvent(self, a0: QtGui.QCloseEvent | None = None): + def closeEvent(self, event: QtGui.QCloseEvent | None = None): """ Exit safely when closing the window """ @@ -874,8 +862,8 @@ def exit_program(): if self.update_auto_control: self.update_auto_control.terminate() self.capture_method.close(self) - if a0 is not None: - a0.accept() + if event is not None: + event.accept() if self.is_auto_controlled: # stop main thread (which is probably blocked reading input) via an interrupt signal os.kill(os.getpid(), signal.SIGINT) @@ -884,7 +872,7 @@ def exit_program(): # Simulates LiveSplit quitting without asking. See "TODO" at update_auto_control Worker # This also more gracefully exits LiveSplit # Users can still manually save their settings - if a0 is None: + if event is None: exit_program() if user_profile.have_settings_changed(self): @@ -905,11 +893,11 @@ def exit_program(): if user_profile.save_settings(self): exit_program() else: - a0.ignore() + event.ignore() if warning is QMessageBox.StandardButton.No: exit_program() if warning is QMessageBox.StandardButton.Cancel: - a0.ignore() + event.ignore() else: exit_program() diff --git a/src/error_messages.py b/src/error_messages.py index 0c271e03..79148806 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -8,7 +8,7 @@ from types import TracebackType from typing import TYPE_CHECKING -from PyQt6 import QtCore, QtWidgets +from PySide6 import QtCore, QtWidgets from utils import FROZEN, GITHUB_REPOSITORY @@ -177,7 +177,7 @@ def excepthook(exception_type: type[BaseException], exception: BaseException, _t # HACK: Can happen when starting the region selector while capturing with WindowsGraphicsCapture if ( exception_type is SystemError - and str(exception) == " returned a result with an error set" + and str(exception) == " returned a result with an error set" ): return # Whithin LiveSplit excepthook needs to use MainWindow's signals to show errors diff --git a/src/hotkeys.py b/src/hotkeys.py index 3648416f..9fcd0427 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -5,7 +5,7 @@ import keyboard import pyautogui -from PyQt6 import QtWidgets +from PySide6 import QtWidgets import error_messages from utils import fire_and_forget, is_digit diff --git a/src/menu_bar.py b/src/menu_bar.py index fe980639..de1d0c1f 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -6,7 +6,7 @@ import requests from packaging.version import parse as version_parse -from PyQt6 import QtCore, QtWidgets +from PySide6 import QtCore, QtWidgets from requests.exceptions import RequestException import error_messages @@ -14,7 +14,7 @@ from capture_method import ( CAPTURE_METHODS, CameraInfo, CaptureMethodEnum, change_capture_method, get_all_video_capture_devices, ) -from gen import about, design, resources_rc, settings as settings_ui, update_checker # noqa: F401 +from gen import about, design, settings as settings_ui, update_checker from hotkeys import HOTKEYS, Hotkey, set_hotkey from utils import ( AUTOSPLIT_VERSION, FIRST_WIN_11_BUILD, GITHUB_REPOSITORY, WINDOWS_BUILD_NUMBER, decimal, fire_and_forget, @@ -104,7 +104,6 @@ def about_qt(): def about_qt_for_python(): webbrowser.open("https://wiki.qt.io/Qt_for_Python") - webbrowser.open("https://www.riverbankcomputing.com/software/pyqt") def check_for_updates(autosplit: AutoSplit, check_on_open: bool = False): @@ -211,10 +210,7 @@ def __set_readme_link(self): # HACK: This is a workaround because custom_image_settings_info_label # simply will not open links with a left click no matter what we tried. self.readme_link_button.clicked.connect( - # PyQt6 typing is wrong - lambda: webbrowser.open( # pyright: ignore[reportGeneralTypeIssues] - f"https://github.com/{GITHUB_REPOSITORY}#readme", - ), + lambda: webbrowser.open(f"https://github.com/{GITHUB_REPOSITORY}#readme"), ) self.readme_link_button.setStyleSheet("border: 0px; background-color:rgba(0,0,0,0%);") diff --git a/src/region_selection.py b/src/region_selection.py index 998ba92a..1a5b21e2 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -8,8 +8,8 @@ import cv2 import numpy as np -from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtTest import QTest +from PySide6 import QtCore, QtGui, QtWidgets +from PySide6.QtTest import QTest from win32 import win32gui from win32con import SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN from winsdk._winrt import initialize_with_window @@ -303,8 +303,8 @@ def __init__(self): self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) self.show() - def keyPressEvent(self, a0: QtGui.QKeyEvent): - if a0.key() == QtCore.Qt.Key.Key_Escape: + def keyPressEvent(self, event: QtGui.QKeyEvent): + if event.key() == QtCore.Qt.Key.Key_Escape: self.close() @@ -313,9 +313,9 @@ class SelectWindowWidget(BaseSelectWidget): Widget to select a window and obtain its bounds """ - def mouseReleaseEvent(self, a0: QtGui.QMouseEvent): - self._x = int(a0.position().x()) + self.geometry().x() - self._y = int(a0.position().y()) + self.geometry().y() + def mouseReleaseEvent(self, event: QtGui.QMouseEvent): + self._x = int(event.position().x()) + self.geometry().x() + self._y = int(event.position().y()) + self.geometry().y() self.close() @@ -339,23 +339,23 @@ def height(self): def width(self): return self._right - self._x - def paintEvent(self, a0: QtGui.QPaintEvent): + def paintEvent(self, event: QtGui.QPaintEvent): if self.__begin != self.__end: qpainter = QtGui.QPainter(self) qpainter.setPen(QtGui.QPen(QtGui.QColor("red"), 2)) qpainter.setBrush(QtGui.QColor("opaque")) qpainter.drawRect(QtCore.QRect(self.__begin, self.__end)) - def mousePressEvent(self, a0: QtGui.QMouseEvent): - self.__begin = a0.position().toPoint() + def mousePressEvent(self, event: QtGui.QMouseEvent): + self.__begin = event.position().toPoint() self.__end = self.__begin self.update() - def mouseMoveEvent(self, a0: QtGui.QMouseEvent): - self.__end = a0.position().toPoint() + def mouseMoveEvent(self, event: QtGui.QMouseEvent): + self.__end = event.position().toPoint() self.update() - def mouseReleaseEvent(self, a0: QtGui.QMouseEvent): + def mouseReleaseEvent(self, event: QtGui.QMouseEvent): if self.__begin != self.__end: # The coordinates are pulled relative to the top left of the set geometry, # so the added virtual screen offsets convert them back to the virtual screen coordinates diff --git a/src/user_profile.py b/src/user_profile.py index b737a543..fe0fae43 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, TypedDict, cast import toml -from PyQt6 import QtCore, QtWidgets +from PySide6 import QtCore, QtWidgets import error_messages from capture_method import CAPTURE_METHODS, CaptureMethodEnum, Region, change_capture_method @@ -200,7 +200,7 @@ def load_check_for_updates_on_open(autosplit: AutoSplit): value = QtCore \ .QSettings("AutoSplit", "Check For Updates On Open") \ .value("check_for_updates_on_open", True, type=bool) - autosplit.action_check_for_updates_on_open.setChecked(value) + autosplit.action_check_for_updates_on_open.setChecked(value) # pyright: ignore[reportGeneralTypeIssues] # Type not infered by PySide6 # noqa: E501 # pylint: disable=line-too-long def set_check_for_updates_on_open(design_window: design.Ui_MainWindow, value: bool): diff --git a/typings/PyQt6/QtTest.pyi b/typings/PyQt6/QtTest.pyi deleted file mode 100644 index 83a6c435..00000000 --- a/typings/PyQt6/QtTest.pyi +++ /dev/null @@ -1,13 +0,0 @@ -import typing - -import PyQt6.sip - -# Email sent to pyqt@riverbankcomputing.com - - -class QTest(PyQt6.sip.simplewrapper): - @typing.overload - @staticmethod - def qWait(ms: int) -> None: ... - @typing.overload - def qWait(self, ms: int) -> None: ... From 7682adc67edd996810cd8249e50662fca1cde1e8 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 6 Apr 2023 12:42:05 -0400 Subject: [PATCH 2/3] Small CI update --- .github/workflows/lint-and-build.yml | 1 + .vscode/settings.json | 1 + pyproject.toml | 5 +++-- src/AutoSplit.py | 7 ++++++- src/error_messages.py | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index e8ca03ee..8d6dcd17 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -12,6 +12,7 @@ on: branches: - main - master + - dev* paths: - "**.py" - "**.ui" diff --git a/.vscode/settings.json b/.vscode/settings.json index 807a07f0..c3f005d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -91,6 +91,7 @@ "warning": "Warning", "info": "Information" }, + "pylint.importStrategy": "fromEnvironment", // Use the new Flake8 extension instead "python.linting.flake8Enabled": false, // Partial codes don't work yet: https://github.com/microsoft/vscode-flake8/issues/7 diff --git a/pyproject.toml b/pyproject.toml index 50e8ff37..8296f0f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ recursive = true aggressive = 3 ignore = [ "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) - "E70" # Allow ... on same line as def + "E70", # Allow ... on same line as def ] # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file @@ -17,7 +17,7 @@ enableTypeIgnoreComments = false # Extra strict reportImplicitStringConcatenation = "error" reportCallInDefaultInitializer = "error" -reportMissingSuperCall = "none" # False positives on base classes +reportMissingSuperCall = "none" # False positives on base classes reportPropertyTypeMismatch = "error" reportUninitializedInstanceVariable = "error" reportUnnecessaryTypeIgnoreComment = "error" @@ -146,3 +146,4 @@ line_length = 120 combine_as_imports = true include_trailing_comma = true multi_line_output = 5 +skip_glob = "src/gen/**" diff --git a/src/AutoSplit.py b/src/AutoSplit.py index c408fc39..d9314ab4 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -38,11 +38,14 @@ ) CHECK_FPS_ITERATIONS = 10 +DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 2 # Needed when compiled, along with the custom hook-requests PyInstaller hook os.environ["REQUESTS_CA_BUNDLE"] = certifi.where() myappid = f"Toufool.AutoSplit.v{AUTOSPLIT_VERSION}" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) +# qt.qpa.window: SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: COM error 0x5: Access is denied. # noqa: E501 # pylint: disable=line-too-long +# ctypes.windll.user32.SetProcessDpiAwarenessContext(2) class AutoSplit(QMainWindow, design.Ui_MainWindow): # pylint: disable=too-many-instance-attributes @@ -682,6 +685,8 @@ def __similarity_threshold_loop(self, number_of_split_images: int, dummy_splits_ QTest.qWait(wait_delta_ms) + return False + def __pause_loop(self, stop_time: float, message: str): """ Wait for a certain time and show the timer to the user. @@ -858,7 +863,7 @@ def closeEvent(self, event: QtGui.QCloseEvent | None = None): Exit safely when closing the window """ - def exit_program(): + def exit_program() -> NoReturn: if self.update_auto_control: self.update_auto_control.terminate() self.capture_method.close(self) diff --git a/src/error_messages.py b/src/error_messages.py index 79148806..d80670e9 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -6,7 +6,7 @@ import sys import traceback from types import TracebackType -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NoReturn from PySide6 import QtCore, QtWidgets @@ -185,7 +185,7 @@ def excepthook(exception_type: type[BaseException], exception: BaseException, _t return excepthook -def handle_top_level_exceptions(exception: Exception): +def handle_top_level_exceptions(exception: Exception) -> NoReturn: message = f"AutoSplit encountered an unrecoverable exception and will likely now close. {CREATE_NEW_ISSUE_MESSAGE}" # Print error to console if not running in executable if FROZEN: From d1fe78686704b2b3b0611ad2b85539a22ff9631c Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 7 Apr 2023 21:42:24 -0400 Subject: [PATCH 3/3] Use PySide6 v6.5 from PyPI --- scripts/requirements.txt | 10 ++++------ src/AutoSplit.py | 2 +- src/region_selection.py | 19 +++++++++++-------- src/user_profile.py | 32 +++++++++++++++++++------------- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index c78d8aba..8eaa7abe 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -5,10 +5,12 @@ # Usage: ./scripts/install.ps1 # # If you're having issues with the libraries, you might want to first run: -# pip uninstall -y -r ./scripts/requirements-dev.txt +# pip uninstall -y -r ./scripts/requirements-dev.txt PySide6 shiboken6 # # Creating an AutoSplit executable with PyInstaller: ./scripts/build.ps1 # +# PySide6 dev wheels: https://download.qt.io/snapshots/ci/pyside/dev/?C=M;O=D +# # Dependencies: certifi ImageHash>=4.3.1 # Contains type information + setup as package not module @@ -19,11 +21,7 @@ packaging Pillow>=9.2 # gnome-screeshot checks psutil PyAutoGUI -# PySide6-Essentials>6.4.2 # Python 3.11 support, -# Dev PySide6 past March 7th fixes https://bugreports.qt.io/browse/PYSIDE-2189 and https://bugreports.qt.io/browse/PYSIDE-1603 -# https://download.qt.io/snapshots/ci/pyside/dev/?C=M;O=D -https://download.qt.io/snapshots/ci/pyside/dev/0142b21d429a7cf0ef78a5bf9a0d972c2a7223b2/split_wheels/PySide6_Essentials-6.5.0a1.dev1678202304-cp37-abi3-win_amd64.whl -https://download.qt.io/snapshots/ci/pyside/dev/0142b21d429a7cf0ef78a5bf9a0d972c2a7223b2/split_wheels/shiboken6-6.5.0a1.dev1678202304-cp37-abi3-win_amd64.whl +PySide6-Essentials>=6.5 # fixes https://bugreports.qt.io/browse/PYSIDE-2189 and https://bugreports.qt.io/browse/PYSIDE-1603 requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534 toml # diff --git a/src/AutoSplit.py b/src/AutoSplit.py index d9314ab4..00330aed 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -16,7 +16,7 @@ from psutil import process_iter from PySide6 import QtCore, QtGui from PySide6.QtTest import QTest -from PySide6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QWidget +from PySide6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox import error_messages import user_profile diff --git a/src/region_selection.py b/src/region_selection.py index 1a5b21e2..04d22723 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -4,7 +4,7 @@ import ctypes.wintypes import os from math import ceil -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import cv2 import numpy as np @@ -12,7 +12,7 @@ from PySide6.QtTest import QTest from win32 import win32gui from win32con import SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN -from winsdk._winrt import initialize_with_window +from winsdk._winrt import initialize_with_window # pylint: disable=no-name-in-module from winsdk.windows.foundation import AsyncStatus, IAsyncOperation from winsdk.windows.graphics.capture import GraphicsCaptureItem, GraphicsCapturePicker @@ -165,12 +165,15 @@ def align_region(autosplit: AutoSplit): error_messages.region() return # This is the image used for aligning the capture region to the best fit for the user. - template_filename = QtWidgets.QFileDialog.getOpenFileName( - autosplit, - "Select Reference Image", - "", - IMREAD_EXT_FILTER, - )[0] + template_filename = cast( + str, # https://bugreports.qt.io/browse/PYSIDE-2285 + QtWidgets.QFileDialog.getOpenFileName( + autosplit, + "Select Reference Image", + "", + IMREAD_EXT_FILTER, + )[0], + ) # Return if the user presses cancel if not template_filename: diff --git a/src/user_profile.py b/src/user_profile.py index fe0fae43..daae8221 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -81,13 +81,16 @@ def save_settings_as(autosplit: AutoSplit): @return: The save settings filepath selected. Empty if cancelled """ # User picks save destination - save_settings_file_path = QtWidgets.QFileDialog.getSaveFileName( - autosplit, - "Save Settings As", - autosplit.last_successfully_loaded_settings_file_path - or os.path.join(auto_split_directory, "settings.toml"), - "TOML (*.toml)", - )[0] + save_settings_file_path = cast( + str, # https://bugreports.qt.io/browse/PYSIDE-2285 + QtWidgets.QFileDialog.getSaveFileName( + autosplit, + "Save Settings As", + autosplit.last_successfully_loaded_settings_file_path + or os.path.join(auto_split_directory, "settings.toml"), + "TOML (*.toml)", + )[0], + ) # If user cancels save destination window, don't save settings if not save_settings_file_path: @@ -156,12 +159,15 @@ def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str def load_settings(autosplit: AutoSplit, from_path: str = ""): - load_settings_file_path = from_path or QtWidgets.QFileDialog.getOpenFileName( - autosplit, - "Load Profile", - os.path.join(auto_split_directory, "settings.toml"), - "TOML (*.toml)", - )[0] + load_settings_file_path = from_path or cast( + str, # https://bugreports.qt.io/browse/PYSIDE-2285 + QtWidgets.QFileDialog.getOpenFileName( + autosplit, + "Load Profile", + os.path.join(auto_split_directory, "settings.toml"), + "TOML (*.toml)", + )[0], + ) if not (load_settings_file_path and __load_settings_from_file(autosplit, load_settings_file_path)): return