From f34cf709eb35dfca70dc135f6fdc8446c1036aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Mon, 31 Jul 2023 12:19:46 -0400 Subject: [PATCH 01/27] Update icons.py --- sardes/config/icons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sardes/config/icons.py b/sardes/config/icons.py index dc0b610a..0774bd1c 100644 --- a/sardes/config/icons.py +++ b/sardes/config/icons.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright © SARDES Project Contributors -# https://github.com/cgq-qgc/sardes +# https://github.com/geo-stack/sardes # # This file is part of SARDES. # Licensed under the terms of the GNU General Public License. From 804a633177615e9218e4ff51ca2a8d1125fd1128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Mon, 31 Jul 2023 12:19:51 -0400 Subject: [PATCH 02/27] Create updates.py --- sardes/widgets/updates.py | 257 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 sardes/widgets/updates.py diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py new file mode 100644 index 00000000..8bba2486 --- /dev/null +++ b/sardes/widgets/updates.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © SARDES Project Contributors +# https://github.com/geo-stack/sardes +# +# This file is part of SARDES. +# Licensed under the terms of the GNU General Public License. +# +# Copyright (c) 2017 Spyder Project Contributors +# https://github.com/spyder-ide/spyder +# +# Some parts of this file is a derivative work of the Spyder project. +# Licensed under the terms of the MIT License. +# https://github.com/spyder-ide/spyder/master/spyder/workers/updates.py +# https://github.com/spyder-ide/spyder/blob/master/spyder/utils/programs.py +# +# Copyright (C) 2013 The IPython Development Team +# https://github.com/ipython/ipython +# +# See gwhat/__init__.py for more details. +# ----------------------------------------------------------------------------- + +# ---- Standard imports +import re +from distutils.version import LooseVersion + +# ---- Third party imports +from qtpy.QtCore import QObject, Qt, QThread, Signal +from qtpy.QtWidgets import QApplication, QMessageBox +import requests + +# ---- Local imports +from sardes import ( + __version__, __releases_url__, __releases_api__, + __namever__, __project_url__) +from sardes.config.icons import get_icon +from sardes.config.locale import _ + + +class UpdatesManager(QObject): + """ + Self contained manager that checks if updates are available on GitHub + and displays the ressults in a message box. + """ + + def __init__(self, parent=None): + super().__init__() + + self.dialog_updates = UpdatesDialog(parent) + self._latest_release = None + self._update_available = None + self._show_only_if_update = False + + self.thread_updates = QThread() + + self.worker_updates = WorkerUpdates() + self.worker_updates.moveToThread(self.thread_updates) + self.worker_updates.sig_ready.connect(self._receive_updates_check) + + self.thread_updates.started.connect(self.worker_updates.start) + + def start_updates_check(self, show_only_if_update: bool = False): + """Check if updates are available.""" + self._show_only_if_update = show_only_if_update + self.thread_updates.start() + + def _receive_updates_check(self, update_available, latest_release, error): + """Receive results from an update check.""" + self.thread_updates.quit() + if update_available is False and self._show_only_if_update is True: + return + + if error is not None: + icn = QMessageBox.Warning + msg = error + else: + icn = QMessageBox.Information + if update_available: + msg = _( + "

Sardes {} is available!

" + "

This new version can be downloaded from our " + "Releases page.

" + ).format(latest_release, __releases_url__) + else: + url_m = __project_url__ + "/milestones" + url_t = __project_url__ + "/issues" + msg = _( + "

{} is up to date

" + "

Further information about Sardes releases are " + "available on our Releases page.

" + "

The roadmap of the Sardes project can be consulted " + "on our Milestones page.

" + "

Please help Sardes by reporting bugs or proposing " + "new features on our Issues Tracker.

" + ).format(__namever__, __releases_url__, url_m, url_t) + + self.dialog_updates.setText(msg) + self.dialog_updates.setIcon(icn) + self.dialog_updates.exec_() + + +class UpdatesDialog(QMessageBox): + """ + Dialog to display update checks. + """ + + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle(_('Updates')) + self.setWindowIcon(get_icon('master')) + self.setMinimumSize(800, 700) + self.setWindowFlags( + Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint) + + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) + + +class WorkerUpdates(QObject): + """ + Worker that checks for releases using the Github API. + """ + sig_ready = Signal(object, object, object) + + def __init__(self): + super(WorkerUpdates, self).__init__() + self.error = None + self.latest_release = None + self.update_available = False + + def start(self): + """Main method of the WorkerUpdates worker.""" + self.update_available = False + self.latest_release = __version__ + self.error = None + + try: + page = requests.get(__releases_api__) + data = page.json() + except requests.exceptions.HTTPError: + self.error = _( + "Unable to retrieve information because of an http error.") + except requests.exceptions.ConnectionError: + self.error = ( + "Unable to connect to the internet. Make " + "sure that your connection is working properly.") + except requests.exceptions.Timeout: + self.error = ( + "Unable to retrieve information because the " + "connection timed out.") + except Exception: + self.error = ( + "Unable to check for updates because of " + "an unexpected error.") + + releases = [item['tag_name'] for item in data] + result = check_update_available(__version__, releases) + self.update_available, self.latest_release = result + + self.sig_ready.emit( + self.update_available, self.latest_release, self.error) + + +def check_update_available(version, releases): + """ + Checks if there is an update available. + + It takes as parameters the current version of GWHAT and a list of + valid cleaned releases in chronological order (what github api returns + by default). Example: ['2.3.4', '2.3.3' ...] + + Copyright (c) Spyder Project Contributors + Licensed under the terms of the MIT License + """ + if is_stable_version(version): + # Remove non stable versions from the list. + releases = [r for r in releases if is_stable_version(r)] + + if len(releases) == 0: + return False, None + + latest_release = releases[0] + if version.endswith('dev'): + return (False, latest_release) + else: + return (check_version(version, latest_release, '<'), + latest_release) + + +def check_version(actver, version, cmp_op): + """ + Check version string of an active module against a required version. + + If dev/prerelease tags result in TypeError for string-number comparison, + it is assumed that the dependency is satisfied. Users on dev branches are + responsible for keeping their own packages up to date. + + Copyright (C) 2013 The IPython Development Team + Licensed under the terms of the BSD License + """ + if isinstance(version, tuple): + version = '.'.join([str(i) for i in version]) + + # Hacks needed so that LooseVersion understands that (for example) + # version = '3.0.0' is in fact bigger than actver = '3.0.0rc1' + if (is_stable_version(version) and not is_stable_version(actver) and + actver.startswith(version) and version != actver): + version = version + 'zz' + elif (is_stable_version(actver) and not is_stable_version(version) and + version.startswith(actver) and version != actver): + actver = actver + 'zz' + + try: + if cmp_op == '>': + return LooseVersion(actver) > LooseVersion(version) + elif cmp_op == '>=': + return LooseVersion(actver) >= LooseVersion(version) + elif cmp_op == '=': + return LooseVersion(actver) == LooseVersion(version) + elif cmp_op == '<': + return LooseVersion(actver) < LooseVersion(version) + elif cmp_op == '<=': + return LooseVersion(actver) <= LooseVersion(version) + else: + return False + except TypeError: + return True + + +def is_stable_version(version): + """ + Returns wheter this is a stable version or not. A stable version has no + letters in the final component, but only numbers. + + Stable version example: 1.2, 1.3.4, 1.0.5 + Not stable version: 1.2alpha, 1.3.4beta, 0.1.0rc1, 3.0.0dev + + Copyright (c) 2017 Spyder Project Contributors + Licensed under the terms of the MIT License + """ + if not isinstance(version, tuple): + version = version.split('.') + last_part = version[-1] + + if not re.search('[a-zA-Z]', last_part): + return True + else: + return False + + +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) + updates_manager = UpdatesManager() + updates_manager.start_updates_check(show_only_if_update=False) + sys.exit(app.exec_()) From e341ed7445089a603dfb541a12873c6e5de5fde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 1 Aug 2023 21:40:34 -0400 Subject: [PATCH 03/27] Setup update manager in mainwindow --- sardes/app/mainwindow.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sardes/app/mainwindow.py b/sardes/app/mainwindow.py index ea6eafef..ac72ca02 100644 --- a/sardes/app/mainwindow.py +++ b/sardes/app/mainwindow.py @@ -99,6 +99,10 @@ def __init__(self, splash=None, sys_capture_manager=None): self.db_connection_manager) print("Table models manager set up succesfully.") + # Setup the update manager. + from sardes.widgets.updates import UpdatesManager + self.updates_manager = UpdatesManager(parent=self) + self.setup() def set_splash(self, message): @@ -299,6 +303,10 @@ class Separator(object): shortcut='Ctrl+Shift+I', context=Qt.ApplicationShortcut ) + update_action = create_action( + self, _('Check for updates...'), icon='update', + triggered=lambda: self.updates_manager.start_updates_check() + ) exit_action = create_action( self, _('Exit'), icon='exit', triggered=self.close, shortcut='Ctrl+Shift+Q', context=Qt.ApplicationShortcut From b23f01e53beec391733feb43811a1b812574045e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 1 Aug 2023 21:40:57 -0400 Subject: [PATCH 04/27] Reorder actions in menu --- sardes/app/mainwindow.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/sardes/app/mainwindow.py b/sardes/app/mainwindow.py index ac72ca02..6ba5ad56 100644 --- a/sardes/app/mainwindow.py +++ b/sardes/app/mainwindow.py @@ -266,6 +266,15 @@ class Separator(object): triggered=self.confdialog.show ) + # Create show console action. + self.console_action = None + if self.console is not None: + self.console_action = create_action( + self, _('Sardes Console...'), icon='console', + shortcut='Ctrl+Shift+J', context=Qt.ApplicationShortcut, + triggered=self.console.show + ) + # Create the panes and toolbars menus and actions self.panes_menu = QMenu(_("Panes"), self) self.panes_menu.setIcon(get_icon('panes')) @@ -286,13 +295,6 @@ class Separator(object): triggered=self.reset_window_layout) # Create help related actions and menus. - self.console_action = None - if self.console is not None: - self.console_action = create_action( - self, _('Sardes Console...'), icon='console', - shortcut='Ctrl+Shift+J', context=Qt.ApplicationShortcut, - triggered=self.console.show - ) report_action = create_action( self, _('Report an issue...'), icon='bug', shortcut='Ctrl+Shift+R', context=Qt.ApplicationShortcut, @@ -314,11 +316,12 @@ class Separator(object): # Add the actions and menus to the options menu. options_menu_items = [ - self.lang_menu, preferences_action, Separator(), - self.panes_menu, self.toolbars_menu, + self.lang_menu, preferences_action, self.console_action, + Separator(), self.panes_menu, self.toolbars_menu, self.lock_dockwidgets_and_toolbars_action, self.reset_window_layout_action, Separator(), - self.console_action, report_action, about_action, exit_action + report_action, update_action, about_action, + Separator(), exit_action ] for item in options_menu_items: if isinstance(item, Separator): From bcda1ceb12df5abd269bcb953e43671e95daa290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 1 Aug 2023 21:41:07 -0400 Subject: [PATCH 05/27] Add update icons --- sardes/config/icons.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sardes/config/icons.py b/sardes/config/icons.py index 0774bd1c..3fcac3f7 100644 --- a/sardes/config/icons.py +++ b/sardes/config/icons.py @@ -272,6 +272,9 @@ 'undo': [ ('mdi.undo-variant',), {'color': ICON_COLOR}], + 'update': [ + ('mdi.update',), + {'color': ICON_COLOR, 'scale_factor': 1.3}], 'update_blue': [ ('mdi.update',), {'color': BLUE, 'scale_factor': 1.3}], From 281bf60e014281ba1520eeff82d2650a5ba3865b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 1 Aug 2023 21:41:35 -0400 Subject: [PATCH 06/27] Add a startup_check mode --- sardes/widgets/updates.py | 72 ++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py index 8bba2486..22194ee4 100644 --- a/sardes/widgets/updates.py +++ b/sardes/widgets/updates.py @@ -19,6 +19,7 @@ # # See gwhat/__init__.py for more details. # ----------------------------------------------------------------------------- +from __future__ import annotations # ---- Standard imports import re @@ -26,7 +27,7 @@ # ---- Third party imports from qtpy.QtCore import QObject, Qt, QThread, Signal -from qtpy.QtWidgets import QApplication, QMessageBox +from qtpy.QtWidgets import QApplication, QMessageBox, QCheckBox import requests # ---- Local imports @@ -35,6 +36,7 @@ __namever__, __project_url__) from sardes.config.icons import get_icon from sardes.config.locale import _ +from sardes.config.main import CONF class UpdatesManager(QObject): @@ -47,9 +49,7 @@ def __init__(self, parent=None): super().__init__() self.dialog_updates = UpdatesDialog(parent) - self._latest_release = None - self._update_available = None - self._show_only_if_update = False + self._startup_check = False self.thread_updates = QThread() @@ -59,16 +59,26 @@ def __init__(self, parent=None): self.thread_updates.started.connect(self.worker_updates.start) - def start_updates_check(self, show_only_if_update: bool = False): + def start_updates_check(self, startup_check: bool = False): """Check if updates are available.""" - self._show_only_if_update = show_only_if_update + self._startup_check = startup_check self.thread_updates.start() - def _receive_updates_check(self, update_available, latest_release, error): + def _receive_updates_check(self, releases: list[str], error: str): """Receive results from an update check.""" self.thread_updates.quit() - if update_available is False and self._show_only_if_update is True: - return + + update_available, latest_release = check_update_available( + __version__, releases) + + if self._startup_check: + if update_available is False: + return + + last_shown_update = CONF.get( + 'main', 'last_shown_update', __version__) + if check_version(latest_release, last_shown_update, '<='): + return if error is not None: icn = QMessageBox.Warning @@ -94,10 +104,18 @@ def _receive_updates_check(self, update_available, latest_release, error): "new features on our Issues Tracker.

" ).format(__namever__, __releases_url__, url_m, url_t) + if self._startup_check: + # Add some space between text and checkbox. + msg += "
" + self.dialog_updates.chkbox.setVisible(self._startup_check) + self.dialog_updates.setText(msg) self.dialog_updates.setIcon(icn) self.dialog_updates.exec_() + if self.dialog_updates.chkbox.isChecked(): + CONF.set('main', 'last_update_shown', latest_release) + class UpdatesDialog(QMessageBox): """ @@ -110,31 +128,32 @@ def __init__(self, parent=None): self.setWindowTitle(_('Updates')) self.setWindowIcon(get_icon('master')) self.setMinimumSize(800, 700) + self.setWindowFlags( Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint) + self.chkbox = QCheckBox(_("Do not show this message again.")) + self.setCheckBox(self.chkbox) + self.setStandardButtons(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok) class WorkerUpdates(QObject): """ - Worker that checks for releases using the Github API. + Worker that fetch available releases using the Github API. """ - sig_ready = Signal(object, object, object) + sig_ready = Signal(object, object) def __init__(self): super(WorkerUpdates, self).__init__() - self.error = None - self.latest_release = None - self.update_available = False + self._error = None + self._releases = None def start(self): """Main method of the WorkerUpdates worker.""" - self.update_available = False - self.latest_release = __version__ - self.error = None - + error = None + releases = [] try: page = requests.get(__releases_api__) data = page.json() @@ -155,14 +174,13 @@ def start(self): "an unexpected error.") releases = [item['tag_name'] for item in data] - result = check_update_available(__version__, releases) - self.update_available, self.latest_release = result - self.sig_ready.emit( - self.update_available, self.latest_release, self.error) + self._error = error + self._releases = releases + self.sig_ready.emit(releases, error) -def check_update_available(version, releases): +def check_update_available(version: str, releases: list[str]): """ Checks if there is an update available. @@ -173,13 +191,13 @@ def check_update_available(version, releases): Copyright (c) Spyder Project Contributors Licensed under the terms of the MIT License """ + if len(releases) == 0: + return False, None + if is_stable_version(version): # Remove non stable versions from the list. releases = [r for r in releases if is_stable_version(r)] - if len(releases) == 0: - return False, None - latest_release = releases[0] if version.endswith('dev'): return (False, latest_release) @@ -253,5 +271,5 @@ def is_stable_version(version): import sys app = QApplication(sys.argv) updates_manager = UpdatesManager() - updates_manager.start_updates_check(show_only_if_update=False) + updates_manager.start_updates_check(startup_check=False) sys.exit(app.exec_()) From e02f62446d8d31bc21cad4a549e17b0e1103c4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 1 Aug 2023 22:01:15 -0400 Subject: [PATCH 07/27] Check for updates on startup --- sardes/app/mainwindow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sardes/app/mainwindow.py b/sardes/app/mainwindow.py index 6ba5ad56..7e302a89 100644 --- a/sardes/app/mainwindow.py +++ b/sardes/app/mainwindow.py @@ -624,6 +624,8 @@ def show(self): if self.databases_plugin.get_option('auto_connect_to_database'): self.databases_plugin.connect_to_database() + self.updates_manager.start_updates_check(startup_check=True) + class ExceptHook(QObject): """ From 03ed91b154a6e75424d35734c9dcf58fe2b5c1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 1 Aug 2023 22:02:24 -0400 Subject: [PATCH 08/27] Improve muted update logic --- sardes/widgets/updates.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py index 22194ee4..60b50298 100644 --- a/sardes/widgets/updates.py +++ b/sardes/widgets/updates.py @@ -70,15 +70,14 @@ def _receive_updates_check(self, releases: list[str], error: str): update_available, latest_release = check_update_available( __version__, releases) + muted_updates = CONF.get('main', 'muted_updates', []) if self._startup_check: if update_available is False: return - - last_shown_update = CONF.get( - 'main', 'last_shown_update', __version__) - if check_version(latest_release, last_shown_update, '<='): - return + for release in muted_updates: + if check_version(latest_release, release, '='): + return if error is not None: icn = QMessageBox.Warning @@ -114,7 +113,9 @@ def _receive_updates_check(self, releases: list[str], error: str): self.dialog_updates.exec_() if self.dialog_updates.chkbox.isChecked(): - CONF.set('main', 'last_update_shown', latest_release) + muted_updates.append(latest_release) + muted_updates = list(set(muted_updates)) + CONF.set('main', 'muted_updates', muted_updates) class UpdatesDialog(QMessageBox): From 43cd3ca61be7565d26a87c3defc2a6f4ff67b330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 2 Aug 2023 22:35:12 -0400 Subject: [PATCH 09/27] Remove the 'v' from release tag --- sardes/widgets/updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py index 60b50298..c48e9cad 100644 --- a/sardes/widgets/updates.py +++ b/sardes/widgets/updates.py @@ -174,7 +174,7 @@ def start(self): "Unable to check for updates because of " "an unexpected error.") - releases = [item['tag_name'] for item in data] + releases = [item['tag_name'][1:] for item in data] self._error = error self._releases = releases From adf28270d656834c340b73ee70d712403655fce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 10 Aug 2023 11:33:48 -0400 Subject: [PATCH 10/27] Complete missing typehints and type checking --- sardes/config/icons.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sardes/config/icons.py b/sardes/config/icons.py index 3fcac3f7..2fd87a35 100644 --- a/sardes/config/icons.py +++ b/sardes/config/icons.py @@ -6,7 +6,7 @@ # This file is part of SARDES. # Licensed under the terms of the GNU General Public License. # ---------------------------------------------------------------------------- - +from __future__ import annotations # ---- Standard imports import os @@ -331,7 +331,7 @@ def get_standard_icon(constant): return style.standardIcon(constant) -def get_standard_iconsize(constant): +def get_standard_iconsize(constant: str) -> int: """ Return the standard size of various component of the gui. @@ -342,3 +342,8 @@ def get_standard_iconsize(constant): return style.pixelMetric(QStyle.PM_MessageBoxIconSize) elif constant == 'small': return style.pixelMetric(QStyle.PM_SmallIconSize) + else: + raise ValueError(( + "Valid values for the 'constant' parameter are " + "'messagebox' or 'small', but '{}' was provided" + ).format(constant)) From 9d8aff3dbef4b38f8e19583bd2f7c58167012fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 10 Aug 2023 14:06:41 -0400 Subject: [PATCH 11/27] Complete missing typehint --- sardes/config/icons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sardes/config/icons.py b/sardes/config/icons.py index 2fd87a35..69cd6fd4 100644 --- a/sardes/config/icons.py +++ b/sardes/config/icons.py @@ -319,7 +319,7 @@ def get_iconsize(size): return QSize(*ICON_SIZES[size]) -def get_standard_icon(constant): +def get_standard_icon(constant: str) -> QIcon: """ Return a QIcon of a standard pixmap. From b2384f60168233f8e0218afcf468ef5b682db243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 10 Aug 2023 14:07:30 -0400 Subject: [PATCH 12/27] Fix deprecated distutils.version usage --- sardes/widgets/updates.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py index c48e9cad..e6a12857 100644 --- a/sardes/widgets/updates.py +++ b/sardes/widgets/updates.py @@ -23,7 +23,7 @@ # ---- Standard imports import re -from distutils.version import LooseVersion +from packaging.version import Version # ---- Third party imports from qtpy.QtCore import QObject, Qt, QThread, Signal @@ -76,7 +76,7 @@ def _receive_updates_check(self, releases: list[str], error: str): if update_available is False: return for release in muted_updates: - if check_version(latest_release, release, '='): + if check_version(latest_release, release, '=='): return if error is not None: @@ -207,7 +207,7 @@ def check_update_available(version: str, releases: list[str]): latest_release) -def check_version(actver, version, cmp_op): +def check_version(actver: str, version: str, cmp_op: str): """ Check version string of an active module against a required version. @@ -221,26 +221,17 @@ def check_version(actver, version, cmp_op): if isinstance(version, tuple): version = '.'.join([str(i) for i in version]) - # Hacks needed so that LooseVersion understands that (for example) - # version = '3.0.0' is in fact bigger than actver = '3.0.0rc1' - if (is_stable_version(version) and not is_stable_version(actver) and - actver.startswith(version) and version != actver): - version = version + 'zz' - elif (is_stable_version(actver) and not is_stable_version(version) and - version.startswith(actver) and version != actver): - actver = actver + 'zz' - try: if cmp_op == '>': - return LooseVersion(actver) > LooseVersion(version) + return Version(actver) > Version(version) elif cmp_op == '>=': - return LooseVersion(actver) >= LooseVersion(version) - elif cmp_op == '=': - return LooseVersion(actver) == LooseVersion(version) + return Version(actver) >= Version(version) + elif cmp_op == '==': + return Version(actver) == Version(version) elif cmp_op == '<': - return LooseVersion(actver) < LooseVersion(version) + return Version(actver) < Version(version) elif cmp_op == '<=': - return LooseVersion(actver) <= LooseVersion(version) + return Version(actver) <= Version(version) else: return False except TypeError: From 03a64ff48dab21ad4aba0bbd66d03d1ebbb6c1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 10 Aug 2023 14:08:03 -0400 Subject: [PATCH 13/27] Use custom more meaningfull icons in messagebox --- sardes/widgets/updates.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py index e6a12857..0a2fb804 100644 --- a/sardes/widgets/updates.py +++ b/sardes/widgets/updates.py @@ -34,7 +34,8 @@ from sardes import ( __version__, __releases_url__, __releases_api__, __namever__, __project_url__) -from sardes.config.icons import get_icon +from sardes.config.icons import ( + get_icon, get_standard_iconsize, get_standard_icon) from sardes.config.locale import _ from sardes.config.main import CONF @@ -80,17 +81,18 @@ def _receive_updates_check(self, releases: list[str], error: str): return if error is not None: - icn = QMessageBox.Warning msg = error + icon = get_standard_icon('SP_MessageBoxWarning') else: - icn = QMessageBox.Information if update_available: + icon = get_icon('update_blue') msg = _( "

Sardes {} is available!

" "

This new version can be downloaded from our " "Releases page.

" ).format(latest_release, __releases_url__) else: + icon = get_icon('commit_changes') url_m = __project_url__ + "/milestones" url_t = __project_url__ + "/issues" msg = _( @@ -109,7 +111,8 @@ def _receive_updates_check(self, releases: list[str], error: str): self.dialog_updates.chkbox.setVisible(self._startup_check) self.dialog_updates.setText(msg) - self.dialog_updates.setIcon(icn) + self.dialog_updates.setIconPixmap( + icon.pixmap(get_standard_iconsize('messagebox'))) self.dialog_updates.exec_() if self.dialog_updates.chkbox.isChecked(): @@ -155,6 +158,7 @@ def start(self): """Main method of the WorkerUpdates worker.""" error = None releases = [] + try: page = requests.get(__releases_api__) data = page.json() From 9ece1f59659051ff37d7447c936d3bcce64ec326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 10 Aug 2023 14:29:27 -0400 Subject: [PATCH 14/27] Update file header --- sardes/widgets/updates.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sardes/widgets/updates.py b/sardes/widgets/updates.py index 0a2fb804..87a0d82a 100644 --- a/sardes/widgets/updates.py +++ b/sardes/widgets/updates.py @@ -11,13 +11,12 @@ # # Some parts of this file is a derivative work of the Spyder project. # Licensed under the terms of the MIT License. -# https://github.com/spyder-ide/spyder/master/spyder/workers/updates.py -# https://github.com/spyder-ide/spyder/blob/master/spyder/utils/programs.py # # Copyright (C) 2013 The IPython Development Team # https://github.com/ipython/ipython # -# See gwhat/__init__.py for more details. +# Some parts of this file is a derivative work of the IPython project. +# Licensed under the terms of the BSD License. # ----------------------------------------------------------------------------- from __future__ import annotations From a062d36c9e6bad8ca86e7344ba8bafdc0c183297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 10 Aug 2023 16:16:31 -0400 Subject: [PATCH 15/27] SardesPlugin: make sure show_plugin is protected when dockwindow is None --- sardes/api/plugins.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sardes/api/plugins.py b/sardes/api/plugins.py index df1625c1..72eee304 100644 --- a/sardes/api/plugins.py +++ b/sardes/api/plugins.py @@ -529,10 +529,11 @@ def show_plugin(self): This method is called by the mainwindow after it is shown. """ - is_visible = self.get_option('is_visible', False) - is_docked = self.get_option('is_docked', True) - if is_visible and not is_docked: - self.dockwindow.undock() + if self.dockwindow is not None: + is_visible = self.get_option('is_visible', False) + is_docked = self.get_option('is_docked', True) + if is_visible and not is_docked: + self.dockwindow.undock() def close_plugin(self): """ From 6b7252faac1c0ed0470cd9e4c0c4cafd32c53a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 09:29:06 -0400 Subject: [PATCH 16/27] Fix wrong docstring --- sardes/app/mainwindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sardes/app/mainwindow.py b/sardes/app/mainwindow.py index 7e302a89..059f825a 100644 --- a/sardes/app/mainwindow.py +++ b/sardes/app/mainwindow.py @@ -613,7 +613,8 @@ def setup_internal_plugins(self): def show(self): """ - Extend Qt method to call show_plugin on each installed plugin. + Extend base class method to perform specific actions of certain + plugins after the mainwindow has been shown. """ super().show() From ff9f0e22f90f6e972e8f3c89e11158e5e5b25e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 09:34:00 -0400 Subject: [PATCH 17/27] Move updates module to app --- sardes/app/mainwindow.py | 2 +- sardes/{widgets => app}/updates.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename sardes/{widgets => app}/updates.py (100%) diff --git a/sardes/app/mainwindow.py b/sardes/app/mainwindow.py index 059f825a..6bcaea2d 100644 --- a/sardes/app/mainwindow.py +++ b/sardes/app/mainwindow.py @@ -100,7 +100,7 @@ def __init__(self, splash=None, sys_capture_manager=None): print("Table models manager set up succesfully.") # Setup the update manager. - from sardes.widgets.updates import UpdatesManager + from sardes.app.updates import UpdatesManager self.updates_manager = UpdatesManager(parent=self) self.setup() diff --git a/sardes/widgets/updates.py b/sardes/app/updates.py similarity index 100% rename from sardes/widgets/updates.py rename to sardes/app/updates.py From dc9f6f6dbe598b3fa3bf303fa0c02ba5fa2457ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 10:48:57 -0400 Subject: [PATCH 18/27] Rework code to make testing easier --- sardes/app/updates.py | 62 ++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/sardes/app/updates.py b/sardes/app/updates.py index 87a0d82a..9d941b66 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -55,7 +55,8 @@ def __init__(self, parent=None): self.worker_updates = WorkerUpdates() self.worker_updates.moveToThread(self.thread_updates) - self.worker_updates.sig_ready.connect(self._receive_updates_check) + self.worker_updates.sig_releases_fetched.connect( + self._receive_updates_check) self.thread_updates.started.connect(self.worker_updates.start) @@ -146,49 +147,50 @@ class WorkerUpdates(QObject): """ Worker that fetch available releases using the Github API. """ - sig_ready = Signal(object, object) + sig_releases_fetched = Signal(object, object) def __init__(self): super(WorkerUpdates, self).__init__() - self._error = None - self._releases = None def start(self): """Main method of the WorkerUpdates worker.""" - error = None - releases = [] - - try: - page = requests.get(__releases_api__) - data = page.json() - except requests.exceptions.HTTPError: - self.error = _( - "Unable to retrieve information because of an http error.") - except requests.exceptions.ConnectionError: - self.error = ( - "Unable to connect to the internet. Make " - "sure that your connection is working properly.") - except requests.exceptions.Timeout: - self.error = ( - "Unable to retrieve information because the " - "connection timed out.") - except Exception: - self.error = ( - "Unable to check for updates because of " - "an unexpected error.") + releases, error = fetch_available_releases(__releases_api__) + self.sig_releases_fetched.emit(releases, error) - releases = [item['tag_name'][1:] for item in data] - self._error = error - self._releases = releases - self.sig_ready.emit(releases, error) +def fetch_available_releases(url: str) -> (list[str], str): + """Retrieve the list of released versions available on GitHub.""" + error = None + releases = [] + + try: + page = requests.get(__releases_api__) + data = page.json() + releases = [item['tag_name'][1:] for item in data] + except requests.exceptions.HTTPError: + error = _( + "Unable to retrieve information because of an http error.") + except requests.exceptions.ConnectionError: + error = _( + "Unable to connect to the internet. Make " + "sure that your connection is working properly.") + except requests.exceptions.Timeout: + error = _( + "Unable to retrieve information because the " + "connection timed out.") + except Exception: + error = _( + "Unable to check for updates because of " + "an unexpected error.") + + return releases, error def check_update_available(version: str, releases: list[str]): """ Checks if there is an update available. - It takes as parameters the current version of GWHAT and a list of + It takes as parameters the current version of Sardes and a list of valid cleaned releases in chronological order (what github api returns by default). Example: ['2.3.4', '2.3.3' ...] From c402d95d1e6c2e5558c3d41cfed2e57dfc52b286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 17:18:57 -0400 Subject: [PATCH 19/27] Remove not needed WorkerUpdates __init__ --- sardes/app/updates.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sardes/app/updates.py b/sardes/app/updates.py index 9d941b66..b348828e 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -149,9 +149,6 @@ class WorkerUpdates(QObject): """ sig_releases_fetched = Signal(object, object) - def __init__(self): - super(WorkerUpdates, self).__init__() - def start(self): """Main method of the WorkerUpdates worker.""" releases, error = fetch_available_releases(__releases_api__) From b91a7af504b387f7c66678c55276ac3e1fb705bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 17:19:42 -0400 Subject: [PATCH 20/27] Make sure latest release is always returned --- sardes/app/updates.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/sardes/app/updates.py b/sardes/app/updates.py index b348828e..d5632e42 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -183,7 +183,7 @@ def fetch_available_releases(url: str) -> (list[str], str): return releases, error -def check_update_available(version: str, releases: list[str]): +def check_update_available(version: str, releases: list[str]) -> (bool, str): """ Checks if there is an update available. @@ -201,12 +201,8 @@ def check_update_available(version: str, releases: list[str]): # Remove non stable versions from the list. releases = [r for r in releases if is_stable_version(r)] - latest_release = releases[0] - if version.endswith('dev'): - return (False, latest_release) - else: - return (check_version(version, latest_release, '<'), - latest_release) + latest_release = str(max([Version(r) for r in releases])) + return check_version(version, latest_release, '<'), latest_release def check_version(actver: str, version: str, cmp_op: str): From f06624f2432e0a726c0b9816fbec4875613bb033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 17:20:41 -0400 Subject: [PATCH 21/27] Simplify some variable names --- sardes/app/updates.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sardes/app/updates.py b/sardes/app/updates.py index d5632e42..ad2f082f 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -48,26 +48,26 @@ class UpdatesManager(QObject): def __init__(self, parent=None): super().__init__() - self.dialog_updates = UpdatesDialog(parent) self._startup_check = False + self.dialog = UpdatesDialog(parent) - self.thread_updates = QThread() + self.thread = QThread() - self.worker_updates = WorkerUpdates() - self.worker_updates.moveToThread(self.thread_updates) - self.worker_updates.sig_releases_fetched.connect( + self.worker = WorkerUpdates() + self.worker.moveToThread(self.thread) + self.worker.sig_releases_fetched.connect( self._receive_updates_check) - self.thread_updates.started.connect(self.worker_updates.start) + self.thread.started.connect(self.worker.start) def start_updates_check(self, startup_check: bool = False): """Check if updates are available.""" self._startup_check = startup_check - self.thread_updates.start() + self.thread.start() def _receive_updates_check(self, releases: list[str], error: str): """Receive results from an update check.""" - self.thread_updates.quit() + self.thread.quit() update_available, latest_release = check_update_available( __version__, releases) From f3efce83e000880f5d4bbc3d8841c97a113d36e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 11 Aug 2023 17:21:34 -0400 Subject: [PATCH 22/27] Update updates.py --- sardes/app/updates.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sardes/app/updates.py b/sardes/app/updates.py index ad2f082f..290c6048 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -108,14 +108,14 @@ def _receive_updates_check(self, releases: list[str], error: str): if self._startup_check: # Add some space between text and checkbox. msg += "
" - self.dialog_updates.chkbox.setVisible(self._startup_check) + self.dialog.chkbox.setVisible(self._startup_check) - self.dialog_updates.setText(msg) - self.dialog_updates.setIconPixmap( + self.dialog.setText(msg) + self.dialog.setIconPixmap( icon.pixmap(get_standard_iconsize('messagebox'))) - self.dialog_updates.exec_() + self.dialog.exec_() - if self.dialog_updates.chkbox.isChecked(): + if self.dialog.chkbox.isChecked(): muted_updates.append(latest_release) muted_updates = list(set(muted_updates)) CONF.set('main', 'muted_updates', muted_updates) From b292226c34b38eca1f9e63a46d8c91d553cda721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 12 Aug 2023 13:49:35 -0400 Subject: [PATCH 23/27] Create test_updates.py --- sardes/app/tests/test_updates.py | 159 +++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 sardes/app/tests/test_updates.py diff --git a/sardes/app/tests/test_updates.py b/sardes/app/tests/test_updates.py new file mode 100644 index 00000000..89204662 --- /dev/null +++ b/sardes/app/tests/test_updates.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © GWHAT Project Contributors +# https://github.com/jnsebgosselin/gwhat +# +# This file is part of GWHAT (Ground-Water Hydrograph Analysis Toolbox). +# Licensed under the terms of the GNU General Public License. +# ----------------------------------------------------------------------------- + +# ---- Standard imports +import os +os.environ['SARDES_PYTEST'] = 'True' + +# ---- Third parties imports +import pytest + +# ---- Local imports +from sardes.app.updates import UpdatesManager, QMessageBox +from sardes.config.main import CONF + + +# ============================================================================= +# ---- Fixtures +# ============================================================================= +@pytest.fixture +def updates_manager(qtbot): + CONF.reset_to_defaults() + updates_manager = UpdatesManager() + + updates_manager.dialog.setModal(False) + assert updates_manager.dialog.isVisible() is False + + return updates_manager + + +# ============================================================================= +# ---- Tests +# ============================================================================= +def test_update_available(updates_manager, qtbot, mocker): + """ + Assert that the worker to check for updates on the GitHub API is + working as expected when an update is available. + """ + mocker.patch('sardes.app.updates.__version__', '1.1.0rc1') + mocker.patch( + 'sardes.app.updates.fetch_available_releases', + return_value=(['0.9.0', '1.1.0rc2', '1.0.0'], None) + ) + msgbox_patcher = mocker.patch.object( + QMessageBox, 'exec_', return_value=True) + + # Note that since the current version is not stable, the '1.1.0rc2 + # update should be proposed to the user. + + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + updates_manager.start_updates_check() + + assert msgbox_patcher.call_count == 1 + assert updates_manager.dialog.chkbox.isVisible() is False + assert 'Sardes 1.1.0rc2 is available!' in updates_manager.dialog.text() + updates_manager.dialog.close() + + # Test that this is working also as expected during STARTUP. + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + updates_manager.dialog.chkbox.setChecked(True) + updates_manager.start_updates_check(startup_check=True) + + assert msgbox_patcher.call_count == 2 + assert updates_manager.dialog.chkbox.isVisible() is True + assert 'Sardes 1.1.0rc2 is available!' in updates_manager.dialog.text() + updates_manager.dialog.close() + + # Assert the update is muted as expected because the checkbox was checked + # by the user on last show. + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + updates_manager.start_updates_check(startup_check=True) + + assert msgbox_patcher.call_count == 2 + + +def test_no_update_available(updates_manager, qtbot, mocker): + """ + Assert that the worker to check for updates on the GitHub API is + working as expected when no update is available. + """ + mocker.patch('sardes.app.updates.__version__', '1.1.0') + mocker.patch( + 'sardes.app.updates.fetch_available_releases', + return_value=(['0.9.0', '1.1.0rc2', '1.0.0'], None) + ) + + # Note that since the current version is stable, the '1.1.0rc2 update + # should not be proposed to the user. + + + +# assert updates_manager.dialog.isVisible() is True +# assert updates_manager.dialog.chkbox.isVisible() is False +# assert 'is up to date' in updates_manager.dialog.text() +# updates_manager.dialog.close() + +# # Assert the updates manager is working as expected when an update +# # is available. + + +# mocker.patch('sardes.app.updates.__version__', '1.1.0rc1') + +# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): +# updates_manager.start_updates_check() + + +# # Assert the updates manager is working as expected when there is +# # an error. +# fetch_patcher.return_value = (['0.9.0', '1.1.0rc2', '1.0.0'], 'Some error') + +# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): +# updates_manager.start_updates_check() + +# assert updates_manager.dialog.isVisible() is True +# assert updates_manager.dialog.chkbox.isVisible() is False +# assert 'Some error' in updates_manager.dialog.text() +# updates_manager.dialog.close() + + +# def test_updates_manager_startup(updates_manager, qtbot, mocker): +# """ +# Assert that the worker to check for updates on the GitHub API is +# working as expected when on Sardes startup. +# """ +# mocker.patch('sardes.app.updates.__version__', '1.0.0') + +# # Assert the updates manager is working as expected when up-to-date. +# fetch_patcher = mocker.patch( +# 'sardes.app.updates.fetch_available_releases', +# return_value=(['0.9.0', '1.0.0'], None) +# ) + +# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): +# updates_manager.start_updates_check(startup_check=True) + +# assert updates_manager.dialog.isVisible() is False + +# # Assert the updates manager is working as expected when an error occured. +# fetch_patcher.return_value = (['0.9.0', '1.0.0'], 'Some error') + +# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): +# updates_manager.start_updates_check(startup_check=True) + +# assert updates_manager.dialog.isVisible() is False + +# # Assert the updates manager is working as expected when an update +# # is available. +# fetch_patcher.return_value = (['0.9.0', '1.1.0', '1.0.0'], None) + + + + +if __name__ == "__main__": + pytest.main(['-x', __file__, '-v', '-rw']) From f381cdebc3d729eaf4642ef060d59cb92d31ab58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 12 Aug 2023 14:21:02 -0400 Subject: [PATCH 24/27] Rework test_update_available for modal dialog --- sardes/app/tests/test_updates.py | 34 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/sardes/app/tests/test_updates.py b/sardes/app/tests/test_updates.py index 89204662..418f1afc 100644 --- a/sardes/app/tests/test_updates.py +++ b/sardes/app/tests/test_updates.py @@ -13,6 +13,7 @@ # ---- Third parties imports import pytest +from qtpy.QtCore import QTimer # ---- Local imports from sardes.app.updates import UpdatesManager, QMessageBox @@ -40,42 +41,37 @@ def test_update_available(updates_manager, qtbot, mocker): """ Assert that the worker to check for updates on the GitHub API is working as expected when an update is available. + + Note that since we are forcing the current version to a not stable version, + the '1.1.0rc2 update should be proposed to the user. """ mocker.patch('sardes.app.updates.__version__', '1.1.0rc1') mocker.patch( 'sardes.app.updates.fetch_available_releases', return_value=(['0.9.0', '1.1.0rc2', '1.0.0'], None) ) - msgbox_patcher = mocker.patch.object( - QMessageBox, 'exec_', return_value=True) - # Note that since the current version is not stable, the '1.1.0rc2 - # update should be proposed to the user. + def handle_dialog(on_startup: bool): + assert updates_manager.dialog.isVisible() is True + assert updates_manager.dialog.chkbox.isVisible() == on_startup + assert 'Sardes 1.1.0rc2 is available!' in updates_manager.dialog.text() + updates_manager.dialog.close() with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + QTimer.singleShot(300, lambda: handle_dialog(on_startup=False)) updates_manager.start_updates_check() - assert msgbox_patcher.call_count == 1 - assert updates_manager.dialog.chkbox.isVisible() is False - assert 'Sardes 1.1.0rc2 is available!' in updates_manager.dialog.text() - updates_manager.dialog.close() - - # Test that this is working also as expected during STARTUP. + # Test that this is working also as expected during STARTUP and mute + # the message on next startups. with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): updates_manager.dialog.chkbox.setChecked(True) + QTimer.singleShot(300, lambda: handle_dialog(on_startup=True)) updates_manager.start_updates_check(startup_check=True) - assert msgbox_patcher.call_count == 2 - assert updates_manager.dialog.chkbox.isVisible() is True - assert 'Sardes 1.1.0rc2 is available!' in updates_manager.dialog.text() - updates_manager.dialog.close() - - # Assert the update is muted as expected because the checkbox was checked - # by the user on last show. + # Assert the update is muted as expected. with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): updates_manager.start_updates_check(startup_check=True) - - assert msgbox_patcher.call_count == 2 + assert updates_manager.dialog.isVisible() is False def test_no_update_available(updates_manager, qtbot, mocker): From 75f3dd37ddcbd1560ef657cab632d375f5a1a020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 12 Aug 2023 14:23:26 -0400 Subject: [PATCH 25/27] Complete test_no_update_available --- sardes/app/tests/test_updates.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/sardes/app/tests/test_updates.py b/sardes/app/tests/test_updates.py index 418f1afc..35b31b17 100644 --- a/sardes/app/tests/test_updates.py +++ b/sardes/app/tests/test_updates.py @@ -78,6 +78,9 @@ def test_no_update_available(updates_manager, qtbot, mocker): """ Assert that the worker to check for updates on the GitHub API is working as expected when no update is available. + + Note that since we are forcing the current version to a stable version, + the '1.1.0rc2 update should be proposed to the user. """ mocker.patch('sardes.app.updates.__version__', '1.1.0') mocker.patch( @@ -85,24 +88,20 @@ def test_no_update_available(updates_manager, qtbot, mocker): return_value=(['0.9.0', '1.1.0rc2', '1.0.0'], None) ) - # Note that since the current version is stable, the '1.1.0rc2 update - # should not be proposed to the user. - - - -# assert updates_manager.dialog.isVisible() is True -# assert updates_manager.dialog.chkbox.isVisible() is False -# assert 'is up to date' in updates_manager.dialog.text() -# updates_manager.dialog.close() - -# # Assert the updates manager is working as expected when an update -# # is available. - + def handle_dialog(on_startup: bool): + assert updates_manager.dialog.isVisible() is True + assert updates_manager.dialog.chkbox.isVisible() == on_startup + assert 'is up to date' in updates_manager.dialog.text() + updates_manager.dialog.close() -# mocker.patch('sardes.app.updates.__version__', '1.1.0rc1') + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + QTimer.singleShot(300, lambda: handle_dialog(on_startup=False)) + updates_manager.start_updates_check() -# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): -# updates_manager.start_updates_check() + # Test that the 'up-to-date' message is not shown during STARTUP. + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + updates_manager.start_updates_check(startup_check=True) + assert updates_manager.dialog.isVisible() is False # # Assert the updates manager is working as expected when there is From 1c455b6fece21bb724003bcb8136c1fcdfba5ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 12 Aug 2023 14:59:27 -0400 Subject: [PATCH 26/27] Complete test_update_error --- sardes/app/tests/test_updates.py | 65 +++++++++++--------------------- sardes/app/updates.py | 8 +++- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/sardes/app/tests/test_updates.py b/sardes/app/tests/test_updates.py index 35b31b17..f9830010 100644 --- a/sardes/app/tests/test_updates.py +++ b/sardes/app/tests/test_updates.py @@ -16,7 +16,7 @@ from qtpy.QtCore import QTimer # ---- Local imports -from sardes.app.updates import UpdatesManager, QMessageBox +from sardes.app.updates import UpdatesManager from sardes.config.main import CONF @@ -28,7 +28,6 @@ def updates_manager(qtbot): CONF.reset_to_defaults() updates_manager = UpdatesManager() - updates_manager.dialog.setModal(False) assert updates_manager.dialog.isVisible() is False return updates_manager @@ -104,50 +103,30 @@ def handle_dialog(on_startup: bool): assert updates_manager.dialog.isVisible() is False -# # Assert the updates manager is working as expected when there is -# # an error. -# fetch_patcher.return_value = (['0.9.0', '1.1.0rc2', '1.0.0'], 'Some error') - -# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): -# updates_manager.start_updates_check() - -# assert updates_manager.dialog.isVisible() is True -# assert updates_manager.dialog.chkbox.isVisible() is False -# assert 'Some error' in updates_manager.dialog.text() -# updates_manager.dialog.close() - - -# def test_updates_manager_startup(updates_manager, qtbot, mocker): -# """ -# Assert that the worker to check for updates on the GitHub API is -# working as expected when on Sardes startup. -# """ -# mocker.patch('sardes.app.updates.__version__', '1.0.0') - -# # Assert the updates manager is working as expected when up-to-date. -# fetch_patcher = mocker.patch( -# 'sardes.app.updates.fetch_available_releases', -# return_value=(['0.9.0', '1.0.0'], None) -# ) - -# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): -# updates_manager.start_updates_check(startup_check=True) - -# assert updates_manager.dialog.isVisible() is False - -# # Assert the updates manager is working as expected when an error occured. -# fetch_patcher.return_value = (['0.9.0', '1.0.0'], 'Some error') - -# with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): -# updates_manager.start_updates_check(startup_check=True) - -# assert updates_manager.dialog.isVisible() is False +def test_update_error(updates_manager, qtbot, mocker): + """ + Assert that the worker to check for updates on the GitHub API is + working as expected when there is an error. + """ + mocker.patch( + 'sardes.app.updates.fetch_available_releases', + return_value=(['0.9.0', '1.1.0rc2', '1.0.0'], 'some error') + ) -# # Assert the updates manager is working as expected when an update -# # is available. -# fetch_patcher.return_value = (['0.9.0', '1.1.0', '1.0.0'], None) + def handle_dialog(on_startup: bool): + assert updates_manager.dialog.isVisible() is True + assert updates_manager.dialog.chkbox.isVisible() == on_startup + assert 'some error' in updates_manager.dialog.text() + updates_manager.dialog.close() + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + QTimer.singleShot(300, lambda: handle_dialog(on_startup=False)) + updates_manager.start_updates_check() + # Test that the error message is not shown during STARTUP. + with qtbot.waitSignal(updates_manager.worker.sig_releases_fetched): + updates_manager.start_updates_check(startup_check=True) + assert updates_manager.dialog.isVisible() is False if __name__ == "__main__": diff --git a/sardes/app/updates.py b/sardes/app/updates.py index 290c6048..493eb5d4 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -47,6 +47,7 @@ class UpdatesManager(QObject): def __init__(self, parent=None): super().__init__() + self._is_checking_for_updates = False self._startup_check = False self.dialog = UpdatesDialog(parent) @@ -62,6 +63,7 @@ def __init__(self, parent=None): def start_updates_check(self, startup_check: bool = False): """Check if updates are available.""" + self._is_checking_for_updates = True self._startup_check = startup_check self.thread.start() @@ -74,10 +76,12 @@ def _receive_updates_check(self, releases: list[str], error: str): muted_updates = CONF.get('main', 'muted_updates', []) if self._startup_check: - if update_available is False: + if update_available is False or error is not None: + self._is_checking_for_updates = False return for release in muted_updates: if check_version(latest_release, release, '=='): + self._is_checking_for_updates = False return if error is not None: @@ -120,6 +124,8 @@ def _receive_updates_check(self, releases: list[str], error: str): muted_updates = list(set(muted_updates)) CONF.set('main', 'muted_updates', muted_updates) + self._is_checking_for_updates = False + class UpdatesDialog(QMessageBox): """ From fc7ba546083c8c618989b010b7c224527ce3d333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 12 Aug 2023 15:03:23 -0400 Subject: [PATCH 27/27] Cleanup code --- sardes/app/tests/test_updates.py | 5 +++-- sardes/app/updates.py | 6 ------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/sardes/app/tests/test_updates.py b/sardes/app/tests/test_updates.py index f9830010..8c05f0de 100644 --- a/sardes/app/tests/test_updates.py +++ b/sardes/app/tests/test_updates.py @@ -27,10 +27,11 @@ def updates_manager(qtbot): CONF.reset_to_defaults() updates_manager = UpdatesManager() - assert updates_manager.dialog.isVisible() is False + yield updates_manager - return updates_manager + # To avoid: Thread: Destroyed while thread is still running. + qtbot.wait(300) # ============================================================================= diff --git a/sardes/app/updates.py b/sardes/app/updates.py index 493eb5d4..ee8c09e1 100644 --- a/sardes/app/updates.py +++ b/sardes/app/updates.py @@ -47,7 +47,6 @@ class UpdatesManager(QObject): def __init__(self, parent=None): super().__init__() - self._is_checking_for_updates = False self._startup_check = False self.dialog = UpdatesDialog(parent) @@ -63,7 +62,6 @@ def __init__(self, parent=None): def start_updates_check(self, startup_check: bool = False): """Check if updates are available.""" - self._is_checking_for_updates = True self._startup_check = startup_check self.thread.start() @@ -77,11 +75,9 @@ def _receive_updates_check(self, releases: list[str], error: str): if self._startup_check: if update_available is False or error is not None: - self._is_checking_for_updates = False return for release in muted_updates: if check_version(latest_release, release, '=='): - self._is_checking_for_updates = False return if error is not None: @@ -124,8 +120,6 @@ def _receive_updates_check(self, releases: list[str], error: str): muted_updates = list(set(muted_updates)) CONF.set('main', 'muted_updates', muted_updates) - self._is_checking_for_updates = False - class UpdatesDialog(QMessageBox): """