diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e469cf4..eeb5843 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -29,7 +29,8 @@ jobs: - name: Setup xvfb (Linux) if: runner.os == 'Linux' run: | - sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 + sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 libglib2.0-0 libgl1-mesa-dev + sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev # start xvfb in the background sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 & - name: Install Python dependencies diff --git a/CHANGELOG b/CHANGELOG index 6deb16b..ca6ed76 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ -0.16.12 +0.17.0 + - BREAKING CHANGE: migrate codebase from PyQt5 to PyQt6 - tests: allow to use different DCOR instance for testing - tests: make tests independent of testing user 0.16.11 diff --git a/dcoraid/__main__.py b/dcoraid/__main__.py index 2484b85..9991949 100644 --- a/dcoraid/__main__.py +++ b/dcoraid/__main__.py @@ -10,14 +10,14 @@ def main(splash=True): setup_logging("dclab") setup_logging("requests", level=logging.INFO) - from PyQt5.QtWidgets import QApplication + from PyQt6.QtWidgets import QApplication if platform.win32_ver()[0] == "7": # Use software OpenGL on Windows 7, because sometimes the # window content becomes plain white. # Not sure whether this actually works. - from PyQt5.QtCore import Qt, QCoreApplication - from PyQt5.QtGui import QGuiApplication + from PyQt6.QtCore import Qt, QCoreApplication + from PyQt6.QtGui import QGuiApplication QApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) QGuiApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) @@ -25,18 +25,18 @@ def main(splash=True): app = QApplication(sys.argv) if splash: - from PyQt5.QtWidgets import QSplashScreen - from PyQt5.QtGui import QPixmap - from PyQt5.QtCore import QEventLoop + from PyQt6.QtWidgets import QSplashScreen + from PyQt6.QtGui import QPixmap + from PyQt6.QtCore import QEventLoop ref_splash = resources.files("dcoraid.img") / "splash.png" with resources.as_file(ref_splash) as splash_path: splash_pix = QPixmap(str(splash_path)) splash = QSplashScreen(splash_pix) splash.setMask(splash_pix.mask()) splash.show() - app.processEvents(QEventLoop.AllEvents, 300) + app.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 300) - from PyQt5 import QtCore, QtGui + from PyQt6 import QtCore, QtGui from .gui import DCORAid # Set Application Icon @@ -45,14 +45,14 @@ def main(splash=True): app.setWindowIcon(QtGui.QIcon(str(icon_path))) # Use dots as decimal separators - QtCore.QLocale.setDefault(QtCore.QLocale(QtCore.QLocale.C)) + QtCore.QLocale.setDefault(QtCore.QLocale(QtCore.QLocale.Language.C)) window = DCORAid() if splash: splash.finish(window) - sys.exit(app.exec_()) + sys.exit(app.exec()) if __name__ == "__main__": diff --git a/dcoraid/gui/api.py b/dcoraid/gui/api.py index ae368d2..27a2c3f 100644 --- a/dcoraid/gui/api.py +++ b/dcoraid/gui/api.py @@ -5,7 +5,7 @@ import shutil import tempfile -from PyQt5 import QtCore +from PyQt6 import QtCore from ..api import CKANAPI diff --git a/dcoraid/gui/browse_public/widget_browse_public.py b/dcoraid/gui/browse_public/widget_browse_public.py index 5701ae0..c190ddd 100644 --- a/dcoraid/gui/browse_public/widget_browse_public.py +++ b/dcoraid/gui/browse_public/widget_browse_public.py @@ -2,7 +2,7 @@ from importlib import resources -from PyQt5 import uic, QtCore, QtWidgets +from PyQt6 import uic, QtCore, QtWidgets from ...common import ConnectionTimeoutErrors from ...dbmodel import APIInterrogator @@ -29,7 +29,7 @@ def __init__(self, *args, **kwargs): @QtCore.pyqtSlot() def on_public_search(self): - self.setCursor(QtCore.Qt.WaitCursor) + self.setCursor(QtCore.Qt.CursorShape.WaitCursor) api = get_ckan_api( public=not self.checkBox_public_include_private.isChecked()) try: @@ -44,7 +44,7 @@ def on_public_search(self): self, f"Failed to connect to {api.server}", tb.format_exc(limit=1)) - self.setCursor(QtCore.Qt.ArrowCursor) + self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) @staticmethod def find_main_window(): diff --git a/dcoraid/gui/dbview/drag_table_widget.py b/dcoraid/gui/dbview/drag_table_widget.py index bfff881..a9c19e0 100644 --- a/dcoraid/gui/dbview/drag_table_widget.py +++ b/dcoraid/gui/dbview/drag_table_widget.py @@ -1,5 +1,5 @@ -from PyQt5 import QtWidgets, QtCore, QtGui -from PyQt5.QtCore import Qt +from PyQt6 import QtWidgets, QtCore, QtGui +from PyQt6.QtCore import Qt class DragTableWidget(QtWidgets.QTableWidget): @@ -9,7 +9,7 @@ def mouseMoveEvent(self, e): urls = [] for item in self.selectedItems(): - data = item.data(Qt.UserRole + 1) + data = item.data(Qt.ItemDataRole.UserRole + 1) urls.append(QtCore.QUrl(data)) mime_data = QtCore.QMimeData() @@ -20,4 +20,4 @@ def mouseMoveEvent(self, e): drag.setHotSpot(e.pos() - self.rect().topLeft()) # This magic is somehow required to get drag working. - dropAction = drag.exec_(Qt.CopyAction) # noqa: F841 + dropAction = drag.exec(Qt.CopyAction) # noqa: F841 diff --git a/dcoraid/gui/dbview/filter_base.py b/dcoraid/gui/dbview/filter_base.py index 57a426a..b01d58d 100644 --- a/dcoraid/gui/dbview/filter_base.py +++ b/dcoraid/gui/dbview/filter_base.py @@ -1,7 +1,7 @@ import copy from importlib import resources -from PyQt5 import QtCore, QtGui, QtWidgets, uic +from PyQt6 import QtCore, QtGui, QtWidgets, uic class FilterBase(QtWidgets.QWidget): @@ -22,7 +22,8 @@ def __init__(self, *args, **kwargs): # resize first column header = self.tableWidget.horizontalHeader() - header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) + header.setSectionResizeMode(0, + QtWidgets.QHeaderView.ResizeMode.Stretch) # trigger user selection change signal self.tableWidget.itemSelectionChanged.connect(self.on_entry_selected) @@ -38,9 +39,10 @@ def __init__(self, *args, **kwargs): self.tableWidget.setDragEnabled(False) # disable drag self.tableWidget.setDragDropOverwriteMode(False) # don't overwrite self.tableWidget.setDragDropMode( - QtWidgets.QAbstractItemView.NoDragDrop) # drag & drop disabled + # drag & drop disabled + QtWidgets.QAbstractItemView.DragDropMode.NoDragDrop) self.tableWidget.setDefaultDropAction( - QtCore.Qt.IgnoreAction) # no drop by default + QtCore.Qt.DropAction.IgnoreAction) # no drop by default def get_entry_actions(self, row, entry): """This is defined in the subclasses (Circle, Collection, etc)""" @@ -87,8 +89,8 @@ def set_entry(self, row, entry): horz_layout.setContentsMargins(2, 0, 2, 0) spacer = QtWidgets.QSpacerItem(0, 0, - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Minimum) + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum) horz_layout.addItem(spacer) for action in self.get_entry_actions(row, entry): diff --git a/dcoraid/gui/dbview/filter_chain.py b/dcoraid/gui/dbview/filter_chain.py index f035057..e864237 100644 --- a/dcoraid/gui/dbview/filter_chain.py +++ b/dcoraid/gui/dbview/filter_chain.py @@ -2,7 +2,7 @@ from importlib import resources -from PyQt5 import QtCore, QtWidgets, uic +from PyQt6 import QtCore, QtWidgets, uic from ..tools import ShowWaitCursor from ..api import get_ckan_api diff --git a/dcoraid/gui/dbview/filter_views.py b/dcoraid/gui/dbview/filter_views.py index 287fc99..12f2abf 100644 --- a/dcoraid/gui/dbview/filter_views.py +++ b/dcoraid/gui/dbview/filter_views.py @@ -1,8 +1,8 @@ from functools import partial import webbrowser -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import Qt +from PyQt6 import QtCore, QtWidgets +from PyQt6.QtCore import Qt from ..api import get_ckan_api from ..tools import ShowWaitCursor @@ -64,7 +64,7 @@ def download_collection(self, collection_name, condensed=False): for res_dict in ds_dict.get("resources", []): self.download_resource.emit(res_dict["id"], condensed) QtWidgets.QApplication.processEvents( - QtCore.QEventLoop.AllEvents, + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) def get_entry_actions(self, row, entry): @@ -108,7 +108,7 @@ def download_dataset(self, dataset_id, condensed=False): for res_dict in ds_dict.get("resources", []): self.download_resource.emit(res_dict["id"], condensed) QtWidgets.QApplication.processEvents( - QtCore.QEventLoop.AllEvents, + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) def get_entry_actions(self, row, entry): @@ -139,7 +139,7 @@ def __init__(self, *args, **kwargs): self.checkBox.setChecked(True) self.tableWidget.setDragEnabled(True) self.tableWidget.setDragDropMode( - QtWidgets.QAbstractItemView.DragOnly) + QtWidgets.QAbstractItemView.DragDropMode.DragOnly) self.label_info.setText("Tip: You can drag and drop your selection " "from the resources list to Shape-Out!") @@ -172,4 +172,4 @@ def set_entry_label(self, row, entry): item = self.tableWidget.item(row, 0) api = get_ckan_api() dcor_url = f"{api.server}/api/3/action/dcserv?id={entry['id']}" - item.setData(Qt.UserRole + 1, dcor_url) + item.setData(Qt.ItemDataRole.UserRole + 1, dcor_url) diff --git a/dcoraid/gui/download/widget_download.py b/dcoraid/gui/download/widget_download.py index f85d0d8..d75b7e2 100644 --- a/dcoraid/gui/download/widget_download.py +++ b/dcoraid/gui/download/widget_download.py @@ -9,8 +9,8 @@ import subprocess import webbrowser -from PyQt5 import uic, QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QStandardPaths +from PyQt6 import uic, QtCore, QtGui, QtWidgets +from PyQt6.QtCore import QStandardPaths from ...download import DownloadQueue @@ -37,7 +37,7 @@ def __init__(self, *args, **kwargs): #: path to persistent shelf to be able to resume uploads on startup self.shelf_path = os_path.join( QStandardPaths.writableLocation( - QStandardPaths.AppLocalDataLocation), + QStandardPaths.StandardLocation.AppLocalDataLocation), "persistent_download_jobs") #: DownloadQueue instance @@ -85,7 +85,7 @@ def initialize(self): @QtCore.pyqtSlot(str, bool) def download_resource(self, resource_id, condensed=False): fallback = QStandardPaths.writableLocation( - QStandardPaths.DownloadLocation) + QStandardPaths.StandardLocation.DownloadLocation) dl_path = self.settings.value("downloads/default path", fallback) self.widget_jobs.jobs.new_job(resource_id, dl_path, condensed) @@ -182,14 +182,14 @@ def update_job_status(self): job.traceback = traceback.format_exc() QtWidgets.QApplication.processEvents( - QtCore.QEventLoop.AllEvents, - 300) + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) # spacing (did not work in __init__) header = self.horizontalHeader() header.setSectionResizeMode( - 0, QtWidgets.QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode( + 1, QtWidgets.QHeaderView.ResizeMode.Stretch) self._busy_updating_widgets = False @@ -219,9 +219,11 @@ def set_actions_item(self, row, col, job): horz_layout = QtWidgets.QHBoxLayout(widact) horz_layout.setContentsMargins(2, 0, 2, 0) - spacer = QtWidgets.QSpacerItem(0, 0, - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Minimum) + spacer = QtWidgets.QSpacerItem( + 0, 0, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum + ) horz_layout.addItem(spacer) if job.state == "error": actions = [ diff --git a/dcoraid/gui/logs/widget_log.py b/dcoraid/gui/logs/widget_log.py index 805d618..abd635b 100644 --- a/dcoraid/gui/logs/widget_log.py +++ b/dcoraid/gui/logs/widget_log.py @@ -9,7 +9,7 @@ import time import traceback -from PyQt5 import uic, QtCore, QtWidgets +from PyQt6 import uic, QtCore, QtWidgets from ..._version import version diff --git a/dcoraid/gui/main.py b/dcoraid/gui/main.py index 4838d62..36d057c 100644 --- a/dcoraid/gui/main.py +++ b/dcoraid/gui/main.py @@ -12,7 +12,7 @@ import requests_toolbelt import urllib3 -from PyQt5 import uic, QtCore, QtGui, QtWidgets +from PyQt6 import uic, QtCore, QtGui, QtWidgets from ..api import APIOutdatedError from ..common import ConnectionTimeoutErrors @@ -57,20 +57,19 @@ def __init__(self, *args, **kwargs): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) super(DCORAid, self).__init__(*args, **kwargs) # if "--version" was specified, print the version and exit if "--version" in sys.argv: print(__version__) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, - 300) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) sys.exit(0) #: DCOR-Aid settings self.settings = QtCore.QSettings() - self.settings.setIniCodec("utf-8") ref_ui = resources.files("dcoraid.gui") / "main.ui" with resources.as_file(ref_ui) as path_ui: uic.loadUi(path_ui, self) @@ -111,8 +110,8 @@ def __init__(self, *args, **kwargs): self.user_filter_chain.download_resource.connect( self.panel_download.download_resource) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, - 300) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) # Run wizard if necessary if ((self.settings.value("user scenario", "") != "anonymous") @@ -137,8 +136,8 @@ def closeEvent(self, event): self.panel_upload.prepare_quit() self.panel_download.prepare_quit() self.status_widget.prepare_quit() - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, - 300) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) event.accept() @QtCore.pyqtSlot() @@ -214,7 +213,7 @@ def on_action_software(self): sw_text += "Modules:\n" for lib in libs: sw_text += "- {} {}\n".format(lib.__name__, lib.__version__) - sw_text += "- PyQt5 {}\n".format(QtCore.QT_VERSION_STR) + sw_text += "- PyQt6 {}\n".format(QtCore.QT_VERSION_STR) sw_text += "\n Breeze icon theme by the KDE Community (LGPL)." sw_text += "\n Font-Awesome icons by Fort Awesome (CC BY 4.0)." if hasattr(sys, 'frozen'): @@ -225,7 +224,7 @@ def on_action_software(self): @QtCore.pyqtSlot() def on_refresh_private_data(self): - self.tab_user.setCursor(QtCore.Qt.WaitCursor) + self.tab_user.setCursor(QtCore.Qt.CursorShape.WaitCursor) api = get_ckan_api() data = DBExtract() if api.is_available() and api.api_key: @@ -244,12 +243,12 @@ def on_refresh_private_data(self): self, f"Failed to connect to {api.server}", tb.format_exc(limit=1)) - self.tab_user.setCursor(QtCore.Qt.ArrowCursor) + self.tab_user.setCursor(QtCore.Qt.CursorShape.ArrowCursor) @QtCore.pyqtSlot() def on_wizard(self): self.wizard = SetupWizard(self) - self.wizard.exec_() + self.wizard.exec() def excepthook(etype, value, trace): @@ -283,13 +282,13 @@ def excepthook(etype, value, trace): ) errorbox = QtWidgets.QMessageBox() - errorbox.setIcon(QtWidgets.QMessageBox.Critical) + errorbox.setIcon(QtWidgets.QMessageBox.Icon.Critical) errorbox.addButton(QtWidgets.QPushButton('Close'), - QtWidgets.QMessageBox.YesRole) + QtWidgets.QMessageBox.ButtonRole.YesRole) errorbox.addButton(QtWidgets.QPushButton( - 'Copy text && Close'), QtWidgets.QMessageBox.NoRole) + 'Copy text && Close'), QtWidgets.QMessageBox.ButtonRole.NoRole) errorbox.setText(exception) - ret = errorbox.exec_() + ret = errorbox.exec() if ret == 1: cb = QtWidgets.QApplication.clipboard() cb.clear(mode=cb.Clipboard) diff --git a/dcoraid/gui/maintenance/widget_maintenance.py b/dcoraid/gui/maintenance/widget_maintenance.py index 155ce68..f3653e0 100644 --- a/dcoraid/gui/maintenance/widget_maintenance.py +++ b/dcoraid/gui/maintenance/widget_maintenance.py @@ -3,8 +3,8 @@ from importlib import resources -from PyQt5 import uic, QtCore, QtWidgets -from PyQt5.QtCore import QStandardPaths +from PyQt6 import uic, QtCore, QtWidgets +from PyQt6.QtCore import QStandardPaths from ...api import dataset_draft_remove_all from ...upload import PersistentUploadJobList @@ -43,7 +43,7 @@ def on_clear_cache(self): queue = mw.panel_upload.jobs dirs = queue.find_zombie_caches() msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setIcon(QtWidgets.QMessageBox.Icon.Information) if dirs: [shutil.rmtree(pp, ignore_errors=True) for pp in dirs] details = [f"- {pp}" for pp in dirs] @@ -53,7 +53,7 @@ def on_clear_cache(self): else: msg.setText("No zombie cache data found.") msg.setWindowTitle("Nothing to do") - msg.exec_() + msg.exec() @QtCore.pyqtSlot() def on_remove_drafts(self): @@ -61,7 +61,7 @@ def on_remove_drafts(self): # get all dataset IDs that should not be removed pers_job_path = os_path.join( QStandardPaths.writableLocation( - QStandardPaths.AppLocalDataLocation), + QStandardPaths.StandardLocation.AppLocalDataLocation), "persistent_upload_jobs") pers_datasets = PersistentUploadJobList(pers_job_path) # perform deletion @@ -69,7 +69,7 @@ def on_remove_drafts(self): api=get_ckan_api(), ignore_dataset_ids=pers_datasets) msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setIcon(QtWidgets.QMessageBox.Icon.Information) del_titles = [f"{d['name']}" for d in deleted] ign_titles = [f"{d['name']}" for d in ignored] if del_titles + ign_titles: @@ -83,4 +83,4 @@ def on_remove_drafts(self): else: msg.setText("No drafts found.") msg.setWindowTitle("Nothing to do") - msg.exec_() + msg.exec() diff --git a/dcoraid/gui/preferences/dlg_preferences.py b/dcoraid/gui/preferences/dlg_preferences.py index fa3816f..eade857 100644 --- a/dcoraid/gui/preferences/dlg_preferences.py +++ b/dcoraid/gui/preferences/dlg_preferences.py @@ -6,8 +6,9 @@ import string import traceback as tb -from PyQt5 import uic, QtCore, QtWidgets -from PyQt5.QtCore import QStandardPaths +from PyQt6 import uic, QtCore, QtWidgets +from PyQt6.QtCore import QStandardPaths +from PyQt6.QtWidgets import QMessageBox from ...api import NoAPIKeyError, CKANAPI from ...common import ConnectionTimeoutErrors @@ -66,16 +67,16 @@ def ask_change_server_or_api_key(self): ...because it implies a restart of DCOR-Aid. """ - button_reply = QtWidgets.QMessageBox.question( + button_reply = QMessageBox.question( self, 'DCOR-Aid restart required', "Changing the server or API token requires a restart of " + "DCOR-Aid. If you choose 'No', then the original server " + "and API token are NOT changed. Do you really want to quit " + "DCOR-Aid?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) - if button_reply == QtWidgets.QMessageBox.Yes: + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No) + if button_reply == QMessageBox.StandardButton.Yes: return True else: return False @@ -83,10 +84,10 @@ def ask_change_server_or_api_key(self): @QtCore.pyqtSlot() def on_toggle_api_password_view(self): cur_em = self.lineEdit_api_key.echoMode() - if cur_em == QtWidgets.QLineEdit.Normal: - new_em = QtWidgets.QLineEdit.PasswordEchoOnEdit + if cur_em == QtWidgets.QLineEdit.EchoMode.Normal: + new_em = QtWidgets.QLineEdit.EchoMode.PasswordEchoOnEdit else: - new_em = QtWidgets.QLineEdit.Normal + new_em = QtWidgets.QLineEdit.EchoMode.Normal self.lineEdit_api_key.setEchoMode(new_em) @QtCore.pyqtSlot() @@ -95,7 +96,7 @@ def on_api_token_renew(self): api_key = self.settings.value("auth/api key") if len(api_key) == 36: # deprecated API key - ret = QtWidgets.QMessageBox.question( + ret = QMessageBox.question( self, "Deprecated API key", "You are using an API key instead of an API token. " @@ -103,7 +104,7 @@ def on_api_token_renew(self): + "DCOR-Aid can only remove it locally. A new API token " + "will be created. Proceed?" ) - if ret != QtWidgets.QMessageBox.Yes: + if ret != QMessageBox.StandardButton.Yes: # Abort return # Create a new token @@ -132,14 +133,14 @@ def on_api_token_revoke(self): api_key = self.settings.value("auth/api key") if len(api_key) == 36: # deprecated API key - ret = QtWidgets.QMessageBox.question( + ret = QMessageBox.question( self, "Deprecated API key", "You are using an API key instead of an API token. " + "API keys are deprecated and cannot be invalidated. " + "DCOR-Aid can only remove it locally. Proceed?" ) - if ret != QtWidgets.QMessageBox.Yes: + if ret != QMessageBox.StandardButton.Yes: # Abort return else: @@ -167,7 +168,7 @@ def on_downloads_browse(self): def on_downloads_init(self): fallback = QStandardPaths.writableLocation( - QStandardPaths.DownloadLocation) + QStandardPaths.StandardLocation.DownloadLocation) dl_path = self.settings.value("downloads/default path", fallback) self.lineEdit_downloads_path.setText(dl_path) @@ -180,12 +181,12 @@ def on_uploads_apply(self): path_cache = self.lineEdit_uploads_cache.text() self.settings.setValue("uploads/cache path", path_cache) if path_cache != current: - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Warning) + msg = QMessageBox() + msg.setIcon(QMessageBox.Warning) msg.setText("In order for the new cache path to be used, please " "restart DCOR-Aid!") msg.setWindowTitle("Please restart DCOR-Aid") - msg.exec_() + msg.exec() def on_uploads_browse(self): default = self.settings.value("uploads/cache path", ".") @@ -199,7 +200,7 @@ def on_uploads_browse(self): @QtCore.pyqtSlot() def on_uploads_init(self): fallback = QStandardPaths.writableLocation( - QStandardPaths.CacheLocation) + QStandardPaths.StandardLocation.CacheLocation) cache_path = self.settings.value("uploads/cache path", fallback) if not pathlib.Path(cache_path).exists(): cache_path = fallback @@ -225,7 +226,7 @@ def on_show_server(self): self.lineEdit_api_key.setText(self.settings.value("auth/api key", "")) self.tabWidget.setCurrentIndex(0) # server settings self.lineEdit_api_key.setEchoMode( - QtWidgets.QLineEdit.PasswordEchoOnEdit) + QtWidgets.QLineEdit.EchoMode.PasswordEchoOnEdit) self.show() self.activateWindow() @@ -237,12 +238,12 @@ def on_show_user(self): user_dict = api.get_user_dict() except tuple(list(ConnectionTimeoutErrors) + [NoAPIKeyError]): self.logger.error(tb.format_exc()) - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Warning) + msg = QMessageBox() + msg.setIcon(QMessageBox.Warning) msg.setText("No connection or wrong server or invalid API token!") msg.setWindowTitle("Warning") msg.setDetailedText(tb.format_exc()) - msg.exec_() + msg.exec() self.on_show_server() else: self.lineEdit_user_id.setText(user_dict["name"]) @@ -268,12 +269,12 @@ def on_update_user(self): user_dict = api.get_user_dict() except (ConnectionError, NoAPIKeyError): self.logger.error(tb.format_exc()) - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Warning) + msg = QMessageBox() + msg.setIcon(QMessageBox.Warning) msg.setText("No connection or wrong server or invalid API token!") msg.setWindowTitle("Warning") msg.setDetailedText(tb.format_exc()) - msg.exec_() + msg.exec() self.on_show_server() update_dict = {} update_dict["id"] = user_dict["id"] @@ -307,12 +308,12 @@ def on_update_server(self): api.get_user_dict() # raises an exception if credentials are wrong except BaseException: self.logger.error(tb.format_exc()) - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Critical) + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Critical) msg.setText("Bad server / API token combination!") msg.setWindowTitle("Error") msg.setDetailedText(tb.format_exc()) - msg.exec_() + msg.exec() else: if old_server != server or old_api_key != api_key: if self.ask_change_server_or_api_key(): diff --git a/dcoraid/gui/status_widget.py b/dcoraid/gui/status_widget.py index a913fa6..5b031eb 100644 --- a/dcoraid/gui/status_widget.py +++ b/dcoraid/gui/status_widget.py @@ -3,7 +3,7 @@ import requests import traceback -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from ..common import ConnectionTimeoutErrors @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): self.toolButton_user = QtWidgets.QToolButton() self.toolButton_user.setText("Initialization...") self.toolButton_user.setToolButtonStyle( - QtCore.Qt.ToolButtonTextBesideIcon) + QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.toolButton_user.setAutoRaise(True) self.layout.addWidget(self.toolButton_user) self.toolButton_user.clicked.connect(self.clicked) @@ -56,7 +56,8 @@ def __init__(self, *args, **kwargs): def get_favicon(server): dldir = pathlib.Path( QtCore.QStandardPaths.writableLocation( - QtCore.QStandardPaths.AppDataLocation)) / "favicons" + QtCore.QStandardPaths.StandardLocation.AppDataLocation) + ) / "favicons" dldir.mkdir(exist_ok=True, parents=True) favname = dldir / (server.split("://")[1] + "_favicon.ico") diff --git a/dcoraid/gui/tools/wait_cursor.py b/dcoraid/gui/tools/wait_cursor.py index aff913f..208d84a 100644 --- a/dcoraid/gui/tools/wait_cursor.py +++ b/dcoraid/gui/tools/wait_cursor.py @@ -1,16 +1,16 @@ import functools -from PyQt5.QtWidgets import QApplication -from PyQt5.QtGui import QCursor -from PyQt5.QtCore import Qt, QEventLoop +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QCursor +from PyQt6.QtCore import Qt, QEventLoop class ShowWaitCursor: def __enter__(self): - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) # This overloaded function call makes sure that all events, # even those triggered during the function call, are processed. - QApplication.processEvents(QEventLoop.AllEvents, 50) + QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 50) return self def __exit__(self, type, value, traceback): diff --git a/dcoraid/gui/updater.py b/dcoraid/gui/updater.py index fcd2ee2..9103aa8 100644 --- a/dcoraid/gui/updater.py +++ b/dcoraid/gui/updater.py @@ -6,7 +6,7 @@ import urllib.request from dclab.external.packaging import parse as parse_version -from PyQt5 import QtCore +from PyQt6 import QtCore class UpdateWorker(QtCore.QObject): diff --git a/dcoraid/gui/upload/circle_mgr.py b/dcoraid/gui/upload/circle_mgr.py index 7f99699..9c75a31 100644 --- a/dcoraid/gui/upload/circle_mgr.py +++ b/dcoraid/gui/upload/circle_mgr.py @@ -1,6 +1,6 @@ from functools import lru_cache -from PyQt5 import QtWidgets +from PyQt6 import QtWidgets from ..api import get_ckan_api @@ -40,7 +40,7 @@ def ask_for_new_circle(parent_widget): + "a colleague to add you to a Circle (Your user name is " + "'{}').".format(ud["name"]) + "\n\nTo proceed with Circle creation, please choose a name:", - QtWidgets.QLineEdit.Normal, + QtWidgets.QLineEdit.EchoMode.Normal, "{}'s Circle".format(name)) if ok_pressed and text != '': cname = "user-circle-{}".format(ud["name"]) diff --git a/dcoraid/gui/upload/dlg_upload.py b/dcoraid/gui/upload/dlg_upload.py index 80516fd..fb79b04 100644 --- a/dcoraid/gui/upload/dlg_upload.py +++ b/dcoraid/gui/upload/dlg_upload.py @@ -3,7 +3,7 @@ from importlib import resources import traceback as tb -from PyQt5 import uic, QtCore, QtGui, QtWidgets +from PyQt6 import uic, QtCore, QtGui, QtWidgets from ...api import dataset_create @@ -38,10 +38,10 @@ def __init__(self, *args, **kwargs): # Dialog box buttons self.pushButton_proceed = self.buttonBox.button( - QtWidgets.QDialogButtonBox.Apply) + QtWidgets.QDialogButtonBox.StandardButton.Apply) self.pushButton_proceed.setText("Proceed with upload / Enqueue job") self.pushButton_cancel = self.buttonBox.button( - QtWidgets.QDialogButtonBox.Cancel) + QtWidgets.QDialogButtonBox.StandardButton.Cancel) # Keep identifier self.identifier = self.instance_counter @@ -85,7 +85,7 @@ def __init__(self, *args, **kwargs): self.comboBox_vis.addItem("Private", "private") # Shortcut for testing - self.shortcut = QtWidgets.QShortcut( + self.shortcut = QtGui.QShortcut( QtGui.QKeySequence("Ctrl+Alt+Shift+E"), self) self.shortcut.activated.connect(self._autofill_for_testing) @@ -102,8 +102,8 @@ def __init__(self, *args, **kwargs): self.comboBox_preset.addItems(sorted(self.presets.keys())) # Restrict characters for line edit - regex = QtCore.QRegExp(job.VALID_RESOURCE_REGEXP) - validator = QtGui.QRegExpValidator(regex) + regex = QtCore.QRegularExpression(job.VALID_RESOURCE_REGEXP) + validator = QtGui.QRegularExpressionValidator(regex) self.lineEdit_res_filename.setValidator(validator) # Signals and slots @@ -235,7 +235,7 @@ def on_add_resources(self, files=None): # Select the first item ix = self.rvmodel.index(0, 0) sm = self.listView_resources.selectionModel() - sm.select(ix, QtCore.QItemSelectionModel.Select) + sm.select(ix, QtCore.QItemSelectionModel.SelectionFlag.Select) @QtCore.pyqtSlot() def on_preset_load(self): @@ -322,7 +322,7 @@ def on_proceed(self): + "Please select 'No' if you would like to change things. " + "Select 'Yes' to proceed without changes - be aware that " + "you will not be able to change anything later on.") - if choice != QtWidgets.QMessageBox.Yes: + if choice != QtWidgets.QMessageBox.StandardButton.Yes: return if not self.rvmodel.supplements_were_edited(): choice = QtWidgets.QMessageBox.question( @@ -333,7 +333,7 @@ def on_proceed(self): + "appear when you click on a resource. Please select 'No' " + "if you would like to go back (recommended). Select 'Yes' " + "to proceed without changes (no future changes possible).") - if choice != QtWidgets.QMessageBox.Yes: + if choice != QtWidgets.QMessageBox.StandardButton.Yes: return # Try to create the dataset and display any issues with the metadata @@ -343,7 +343,7 @@ def on_proceed(self): except BaseException: self.logger.error(tb.format_exc()) msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setIcon(QtWidgets.QMessageBox.Icon.Critical) msg.setText("It was not possible to create a dataset draft. " "If this is not a connection problem, please consider " "creating an issue on GitHub.") msg.setWindowTitle("Dataset creation failed") msg.setDetailedText(tb.format_exc()) - msg.exec_() + msg.exec() return self.setHidden(True) # Remember the dataset identifier diff --git a/dcoraid/gui/upload/resource_schema_preset.py b/dcoraid/gui/upload/resource_schema_preset.py index 5f2a35a..f898933 100644 --- a/dcoraid/gui/upload/resource_schema_preset.py +++ b/dcoraid/gui/upload/resource_schema_preset.py @@ -1,7 +1,7 @@ import json import pathlib -from PyQt5.QtCore import QStandardPaths +from PyQt6.QtCore import QStandardPaths class PersistentResourceSchemaPresets: @@ -10,7 +10,8 @@ def __init__(self): self._presets = {} # This is a roaming path on Windows self.path = pathlib.Path(QStandardPaths.writableLocation( - QStandardPaths.AppDataLocation)) / "upload_resource_schema_presets" + QStandardPaths.StandardLocation.AppDataLocation) + ) / "upload_resource_schema_presets" self.path.mkdir(exist_ok=True, parents=True) for pp in self.path.glob("*.json"): with pp.open() as fd: diff --git a/dcoraid/gui/upload/resources_model.py b/dcoraid/gui/upload/resources_model.py index b25726b..89f1d0b 100644 --- a/dcoraid/gui/upload/resources_model.py +++ b/dcoraid/gui/upload/resources_model.py @@ -4,7 +4,7 @@ from functools import lru_cache import pathlib -from PyQt5 import QtCore, QtGui +from PyQt6 import QtCore, QtGui from ...upload import job @@ -31,9 +31,9 @@ def add_resources(self, rslist): self.resources[ff] = {} self.layoutChanged.emit() - def data(self, index, role=QtCore.Qt.DisplayRole): + def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): """Return data for 'View'""" - if role == QtCore.Qt.DisplayRole: + if role == QtCore.Qt.ItemDataRole.DisplayRole: _, data = self.get_data_for_index(index) return data["file"]["filename"] diff --git a/dcoraid/gui/upload/widget_schema.py b/dcoraid/gui/upload/widget_schema.py index 29170d9..33ad6d0 100644 --- a/dcoraid/gui/upload/widget_schema.py +++ b/dcoraid/gui/upload/widget_schema.py @@ -1,4 +1,4 @@ -from PyQt5 import QtCore, QtWidgets +from PyQt6 import QtCore, QtWidgets from .widget_supplement_item import RSSItem, RSSTagsItem, TitleItem @@ -113,9 +113,11 @@ def populate_schema(self, schema_dict): self.schema_widgets[sec] = widget_list # Finally add a stretch spacer in case there are not enough # items. - spacer_item = QtWidgets.QSpacerItem(20, 0, - QtWidgets.QSizePolicy.Minimum, - QtWidgets.QSizePolicy.Expanding) + spacer_item = QtWidgets.QSpacerItem( + 20, 0, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Expanding + ) self.verticalLayout.addItem(spacer_item) def set_schema(self, schema_dict): diff --git a/dcoraid/gui/upload/widget_supplement_item.py b/dcoraid/gui/upload/widget_supplement_item.py index de82b9e..f94683c 100644 --- a/dcoraid/gui/upload/widget_supplement_item.py +++ b/dcoraid/gui/upload/widget_supplement_item.py @@ -1,6 +1,6 @@ from importlib import resources -from PyQt5 import uic, QtCore, QtWidgets +from PyQt6 import uic, QtCore, QtWidgets class TitleItem(QtWidgets.QWidget): diff --git a/dcoraid/gui/upload/widget_tablecell_actions.py b/dcoraid/gui/upload/widget_tablecell_actions.py index 54fb921..e080fe9 100644 --- a/dcoraid/gui/upload/widget_tablecell_actions.py +++ b/dcoraid/gui/upload/widget_tablecell_actions.py @@ -1,7 +1,7 @@ from importlib import resources import webbrowser -from PyQt5 import uic, QtCore, QtWidgets +from PyQt6 import uic, QtCore, QtWidgets class TableCellActions(QtWidgets.QWidget): @@ -38,7 +38,7 @@ def on_delete(self): @QtCore.pyqtSlot() def on_error(self): msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setIcon(QtWidgets.QMessageBox.Icon.Critical) msg.setText("There was an error during data transfer. If this happens " + "often or with a particular type of dataset, please " + "" @@ -46,7 +46,7 @@ def on_error(self): "to see details required for fixing the problem.") msg.setWindowTitle(f"Job {self.job.dataset_id[:5]} error") msg.setDetailedText(self.job.traceback) - msg.exec_() + msg.exec() @QtCore.pyqtSlot() def on_retry(self): diff --git a/dcoraid/gui/upload/widget_upload.py b/dcoraid/gui/upload/widget_upload.py index ab6bd4e..d8dbdfb 100644 --- a/dcoraid/gui/upload/widget_upload.py +++ b/dcoraid/gui/upload/widget_upload.py @@ -9,8 +9,8 @@ import time import traceback as tb -from PyQt5 import uic, QtCore, QtWidgets -from PyQt5.QtCore import QStandardPaths +from PyQt6 import uic, QtCore, QtGui, QtWidgets +from PyQt6.QtCore import QStandardPaths from ...api import APINotFoundError from ...upload import queue, task @@ -44,10 +44,10 @@ def __init__(self, *args, **kwargs): # menu button for adding tasks menu = QtWidgets.QMenu() - act1 = QtWidgets.QAction("Select task files from disk", self) + act1 = QtGui.QAction("Select task files from disk", self) act1.setData("single") menu.addAction(act1) - act2 = QtWidgets.QAction( + act2 = QtGui.QAction( "Recursively find and load task files from a folder", self) act2.setData("bulk") menu.addAction(act2) @@ -57,7 +57,7 @@ def __init__(self, *args, **kwargs): #: path to persistent shelf to be able to resume uploads on startup self.shelf_path = os_path.join( QStandardPaths.writableLocation( - QStandardPaths.AppLocalDataLocation), + QStandardPaths.StandardLocation.AppLocalDataLocation), "persistent_upload_jobs") # UploadQueue instance @@ -80,7 +80,7 @@ def __del__(self): def cache_dir(self): """path to cache directory (compression)""" fallback = QStandardPaths.writableLocation( - QStandardPaths.CacheLocation) + QStandardPaths.StandardLocation.CacheLocation) settings = QtCore.QSettings() cache_path = settings.value("uploads/cache path", fallback) return cache_path @@ -138,7 +138,7 @@ def initialize(self, retry_if_fail=True): queue.DCORAidQueueMissingResourceWarning)] if w: msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setIcon(QtWidgets.QMessageBox.Icon.Information) msg.setText( "You have created upload jobs in a previous session " + "and some of the resources cannot be located on " @@ -153,7 +153,7 @@ def initialize(self, retry_if_fail=True): msg.setDetailedText( "\n\n".join([str(wi.message) for wi in w])) msg.setWindowTitle("Resources for uploads missing") - msg.exec_() + msg.exec() if self.parent().parent().isVisible(): self.widget_jobs.set_job_list(self.jobs) # upload finished signal @@ -221,10 +221,10 @@ def on_upload_manual_ready(self, upload_dialog): resource_names=names, supplements=supps) - @QtCore.pyqtSlot(QtWidgets.QAction) + @QtCore.pyqtSlot(QtGui.QAction) def on_upload_task( self, - action: QtWidgets.QAction | pathlib.Path | list[pathlib.Path]): + action: QtGui.QAction | pathlib.Path | list[pathlib.Path]): """Import an UploadJob task file and add it to the queue This functionality is mainly used for automation. Another @@ -249,14 +249,14 @@ def on_upload_task( self, "Select folder to search for DCOR-Aid task files", ".", - QtWidgets.QFileDialog.ShowDirsOnly, + QtWidgets.QFileDialog.Option.ShowDirsOnly, ) files = pathlib.Path(tdir).rglob("*.dcoraid-task") # Keep track of a persistent task ID to dataset ID dictionary path_id_dict = os_path.join( QStandardPaths.writableLocation( - QStandardPaths.AppLocalDataLocation), + QStandardPaths.StandardLocation.AppLocalDataLocation), "map_task_to_dataset_id.txt") map_task_to_dataset_id = task.PersistentTaskDatasetIDDict(path_id_dict) api = get_ckan_api() @@ -320,7 +320,7 @@ def on_upload_task( f"Would you like to create a new dataset for this " f"task file on '{api.server}' (select 'No' if in doubt)?" ) - if ret == QtWidgets.QMessageBox.Yes: + if ret == QtWidgets.QMessageBox.StandardButton.Yes: # retry, this time forcing the creation of a new dataset upload_job = task.load_task(force_dataset_creation=True, **load_kw) @@ -375,8 +375,10 @@ class UploadTableWidget(QtWidgets.QTableWidget): def __init__(self, *args, **kwargs): super(UploadTableWidget, self).__init__(*args, **kwargs) self.logger = logging.getLogger(__name__).getChild("UploadTableWidget") - self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.setSelectionMode( + QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + self.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) self._jobs = None @@ -476,8 +478,9 @@ def update_job_status(self): # spacing (did not work in __init__) header = self.horizontalHeader() header.setSectionResizeMode( - 0, QtWidgets.QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode( + 1, QtWidgets.QHeaderView.ResizeMode.Stretch) # enable updates again self.setUpdatesEnabled(True) diff --git a/dcoraid/gui/wizard/__init__.py b/dcoraid/gui/wizard/__init__.py index 25a6e7e..c09fa29 100644 --- a/dcoraid/gui/wizard/__init__.py +++ b/dcoraid/gui/wizard/__init__.py @@ -3,7 +3,8 @@ import uuid from dclab.rtdc_dataset.fmt_dcor import access_token -from PyQt5 import uic, QtCore, QtWidgets +from PyQt6 import uic, QtCore, QtWidgets +from PyQt6.QtWidgets import QMessageBox, QWizard from ...api import NoAPIKeyError, APINotFoundError, CKANAPI @@ -73,7 +74,7 @@ def __init__(self, *args, **kwargs): self.pushButton_path_access_token.clicked.connect( self.on_browse_access_token) - self.button(QtWidgets.QWizard.FinishButton).clicked.connect( + self.button(QWizard.WizardButton.FinishButton).clicked.connect( self._finalize) @QtCore.pyqtSlot() @@ -94,18 +95,19 @@ def _finalize(self): + "of DCOR-Aid. If you choose 'No', then the original " \ + "server and API token are NOT changed. Do you " \ + "really want to quit DCOR-Aid?" - button_reply = QtWidgets.QMessageBox.question( + button_reply = QMessageBox.question( self, 'DCOR-Aid restart required', msg, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) - if button_reply == QtWidgets.QMessageBox.Yes: + QMessageBox.StandardButton.Yes | + QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No) + if button_reply == QMessageBox.StandardButton.Yes: proceed = True else: proceed = False else: - QtWidgets.QMessageBox.information( + QMessageBox.information( self, "DCOR-Aid restart required", "Please restart DCOR-Aid to proceed." @@ -127,7 +129,7 @@ def _finalize(self): settings.remove("auth/certificate") settings.sync() QtWidgets.QApplication.processEvents( - QtCore.QEventLoop.AllEvents, 300) + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) QtWidgets.QApplication.quit() sys.exit(0) # if the above does not work diff --git a/pyproject.toml b/pyproject.toml index 0ad6560..1af7c99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -GUI = ["pyqt5"] +GUI = ["PyQt6"] [project.scripts] dcoraid = "dcoraid.__main__:main" diff --git a/tests/conftest.py b/tests/conftest.py index 985e9b3..bdc4035 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,8 @@ import tempfile import time -from PyQt5 import QtCore -from PyQt5.QtCore import QStandardPaths +from PyQt6 import QtCore +from PyQt6.QtCore import QStandardPaths import pytest from dcoraid.api import APIConflictError @@ -28,13 +28,13 @@ def cleanup_dcoraid_tasks(): # remove persistent upload jobs shelf_path = os_path.join( QStandardPaths.writableLocation( - QStandardPaths.AppLocalDataLocation), + QStandardPaths.StandardLocation.AppLocalDataLocation), "persistent_upload_jobs") shutil.rmtree(shelf_path, ignore_errors=True) # remove persistent upload id dict path_id_dict = os_path.join( QStandardPaths.writableLocation( - QStandardPaths.AppLocalDataLocation), + QStandardPaths.StandardLocation.AppLocalDataLocation), "map_task_to_dataset_id.txt") path_id_dict = pathlib.Path(path_id_dict) if path_id_dict.exists(): @@ -47,9 +47,8 @@ def pytest_configure(config): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) settings = QtCore.QSettings() - settings.setIniCodec("utf-8") settings.setValue("check for updates", "0") settings.setValue("user scenario", "dcor-dev") settings.setValue("auth/server", common.SERVER) @@ -70,9 +69,8 @@ def pytest_unconfigure(config): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) settings = QtCore.QSettings() - settings.setIniCodec("utf-8") settings.remove("debug/without timers") settings.remove("check for updates") settings.sync() diff --git a/tests/requirements.txt b/tests/requirements.txt index 7cbe096..3887dfe 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,4 @@ -pyqt5 +PyQt6 pytest pytest-qt pluggy>=1.0 diff --git a/tests/test_gui.py b/tests/test_gui.py index 7f13e3b..64a62f7 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -11,8 +11,8 @@ from dcoraid.gui.upload import widget_upload import pytest -from PyQt5 import QtCore, QtWidgets, QtTest -from PyQt5.QtWidgets import QInputDialog, QMessageBox +from PyQt6 import QtCore, QtGui, QtWidgets, QtTest +from PyQt6.QtWidgets import QInputDialog, QMessageBox from . import common @@ -24,25 +24,27 @@ def mw(qtbot): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) settings = QtCore.QSettings() - settings.setIniCodec("utf-8") settings.setValue("auth/server", common.SERVER) QtTest.QTest.qWait(100) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) # Code that will run before your test mw = DCORAid() qtbot.addWidget(mw) QtWidgets.QApplication.setActiveWindow(mw) QtTest.QTest.qWait(100) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) # Run test yield mw # Make sure that all daemons are gone mw.close() # It is extremely weird, but this seems to be important to avoid segfaults! QtTest.QTest.qWait(100) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) @pytest.mark.filterwarnings("ignore::UserWarning", @@ -52,7 +54,7 @@ def test_gui_anonymous(qtbot): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) settings = QtCore.QSettings() spath = pathlib.Path(settings.fileName()) # temporarily move settings to temporary location @@ -73,7 +75,8 @@ def test_gui_anonymous(qtbot): mw = DCORAid() qtbot.addWidget(mw) QtWidgets.QApplication.setActiveWindow(mw) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 3000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 3000) # sanity check assert mw.settings.value("user scenario") == "anonymous" assert mw.settings.value("auth/server") == "dcor.mpl.mpg.de" @@ -87,7 +90,8 @@ def test_gui_anonymous(qtbot): shutil.copy2(stmp, spath) mw.close() QtTest.QTest.qWait(500) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 500) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 500) def test_gui_mydata_dataset_add_to_collection(mw, qtbot): @@ -145,22 +149,23 @@ def test_gui_start_with_bad_server(qtbot): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) settings = QtCore.QSettings() - settings.setIniCodec("utf-8") settings.setValue("auth/server", "WRONG-dcor-dev.mpl.mpg.de") try: mw = DCORAid() qtbot.addWidget(mw) QtWidgets.QApplication.setActiveWindow(mw) QtTest.QTest.qWait(200) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) # just make sure that DCOR-Aid thinks it is offline assert not mw.panel_upload.isEnabled() assert not mw.panel_download.isEnabled() mw.close() QtTest.QTest.qWait(500) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) except BaseException: raise finally: @@ -172,9 +177,8 @@ def test_gui_start_with_bad_api_key(qtbot): QtCore.QCoreApplication.setOrganizationName("DCOR") QtCore.QCoreApplication.setOrganizationDomain("dcor.mpl.mpg.de") QtCore.QCoreApplication.setApplicationName("dcoraid") - QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat) settings = QtCore.QSettings() - settings.setIniCodec("utf-8") good_key = settings.value("auth/api key") bad_key = good_key[:-2] + "00" settings.setValue("auth/api key", bad_key) @@ -183,14 +187,16 @@ def test_gui_start_with_bad_api_key(qtbot): qtbot.addWidget(mw) QtWidgets.QApplication.setActiveWindow(mw) QtTest.QTest.qWait(200) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) # just make sure that DCOR-Aid thinks it is offline assert not mw.panel_upload.isEnabled() # downloads should still be possible assert mw.panel_download.isEnabled() mw.close() QtTest.QTest.qWait(500) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 5000) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 5000) except BaseException: raise finally: @@ -208,7 +214,7 @@ def test_gui_upload_simple(mw, qtbot): # Avoid message boxes with mock.patch.object(QMessageBox, "question", - return_value=QMessageBox.Yes): + return_value=QMessageBox.StandardButton.Yes): # Commence upload dlg.on_proceed() assert dlg.dataset_id is not None @@ -224,7 +230,7 @@ def test_gui_upload_task(mw, qtbot): return_value=([tpath], None)): with mock.patch.object(QMessageBox, "information", return_value=None): - act = QtWidgets.QAction("some unimportant text") + act = QtGui.QAction("some unimportant text") act.setData("single") mw.panel_upload.on_upload_task(action=act) uj = mw.panel_upload.jobs[-1] @@ -243,9 +249,9 @@ def test_gui_upload_task_bad_dataset_id_no(mw, qtbot): QtWidgets.QFileDialog, "getOpenFileNames", return_value=([tpath], None)), \ mock.patch.object(QMessageBox, "question", - return_value=QMessageBox.No), \ + return_value=QMessageBox.StandardButton.No), \ mock.patch.object(QMessageBox, "information", return_value=None): - act = QtWidgets.QAction("some unimportant text") + act = QtGui.QAction("some unimportant text") act.setData("single") mw.panel_upload.on_upload_task(action=act) if len(mw.panel_upload.jobs): @@ -264,9 +270,9 @@ def test_gui_upload_task_bad_dataset_id_yes(mw, qtbot): QtWidgets.QFileDialog, "getOpenFileNames", return_value=([tpath], None)), \ mock.patch.object(QMessageBox, "question", - return_value=QMessageBox.Yes), \ + return_value=QMessageBox.StandardButton.Yes), \ mock.patch.object(QMessageBox, "information", return_value=None): - act = QtWidgets.QAction("some unimportant text") + act = QtGui.QAction("some unimportant text") act.setData("single") mw.panel_upload.on_upload_task(action=act) uj = mw.panel_upload.jobs[-1] @@ -301,7 +307,7 @@ def test_gui_upload_task_missing_circle_multiple(mw, qtbot): mock.patch.object(QtWidgets.QInputDialog, "getItem", return_value=(defaults["circle"], True)), \ mock.patch.object(QMessageBox, "information", return_value=None): - act = QtWidgets.QAction("some unimportant text") + act = QtGui.QAction("some unimportant text") act.setData("bulk") request_circle = widget_upload.circle_mgr.request_circle with mock.patch.object(widget_upload.circle_mgr, @@ -325,7 +331,7 @@ def test_gui_upload_private(mw, qtbot): dlg.comboBox_vis.setCurrentIndex(dlg.comboBox_vis.findData("private")) # Avoid message boxes with mock.patch.object(QMessageBox, "question", - return_value=QMessageBox.Yes), \ + return_value=QMessageBox.StandardButton.Yes), \ mock.patch.object(QMessageBox, "information", return_value=None): # Commence upload dlg.on_proceed() @@ -349,7 +355,8 @@ def test_gui_upload_task_missing_circle(mw, qtbot): dataset_dict.pop("owner_org") tpath = common.make_upload_task(task_id=task_id, dataset_dict=dataset_dict) - QtWidgets.QApplication.processEvents(QtCore.QEventLoop.AllEvents, 300) + QtWidgets.QApplication.processEvents( + QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300) defaults = common.get_test_defaults() with mock.patch.object( QtWidgets.QFileDialog, "getOpenFileNames", @@ -357,7 +364,7 @@ def test_gui_upload_task_missing_circle(mw, qtbot): mock.patch.object(QtWidgets.QInputDialog, "getItem", return_value=(defaults["circle"], True)), \ mock.patch.object(QMessageBox, "information", return_value=None): - act = QtWidgets.QAction("some unimportant text") + act = QtGui.QAction("some unimportant text") act.setData("single") mw.panel_upload.on_upload_task(action=act) uj = mw.panel_upload.jobs[-1]