From 0732948c63d92bd95e04ba5b3758ab35b5c5a45e Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sun, 10 Dec 2023 20:07:12 -0800
Subject: [PATCH 01/22] Remove application update mechanism from Application
plugin and move required files to Update Manager plugin.
---
spyder/config/main.py | 1 -
spyder/plugins/application/confpage.py | 3 -
spyder/plugins/application/container.py | 260 +-----------------
spyder/plugins/application/plugin.py | 34 +--
spyder/plugins/statusbar/plugin.py | 4 +-
.../updatemanager}/tests/__init__.py | 0
.../updatemanager}/tests/test_update.py | 0
.../updatemanager}/updates.py | 0
.../widgets/__init__.py | 0
.../widgets/install.py | 0
.../widgets/status.py | 0
spyder/workers/__init__.py | 7 -
12 files changed, 5 insertions(+), 304 deletions(-)
rename spyder/{workers => plugins/updatemanager}/tests/__init__.py (100%)
rename spyder/{workers => plugins/updatemanager}/tests/test_update.py (100%)
rename spyder/{workers => plugins/updatemanager}/updates.py (100%)
rename spyder/plugins/{application => updatemanager}/widgets/__init__.py (100%)
rename spyder/plugins/{application => updatemanager}/widgets/install.py (100%)
rename spyder/plugins/{application => updatemanager}/widgets/status.py (100%)
delete mode 100644 spyder/workers/__init__.py
diff --git a/spyder/config/main.py b/spyder/config/main.py
index 1458e45564c..2a8480c3390 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -78,7 +78,6 @@
'custom_margin': 0,
'use_custom_cursor_blinking': False,
'show_internal_errors': True,
- 'check_updates_on_startup': True,
'cursor/width': 2,
'completion/size': (300, 180),
'report_error/remember_token': False,
diff --git a/spyder/plugins/application/confpage.py b/spyder/plugins/application/confpage.py
index b55a2083f58..0d7907b373a 100644
--- a/spyder/plugins/application/confpage.py
+++ b/spyder/plugins/application/confpage.py
@@ -65,8 +65,6 @@ def setup_page(self):
prompt_box = newcb(_("Prompt when exiting"), 'prompt_on_exit')
popup_console_box = newcb(_("Show internal Spyder errors to report "
"them to Github"), 'show_internal_errors')
- check_updates = newcb(_("Check for updates on startup"),
- 'check_updates_on_startup')
# Decide if it's possible to activate or not single instance mode
# ??? Should we allow multiple instances for macOS?
@@ -88,7 +86,6 @@ def setup_page(self):
advanced_layout.addWidget(single_instance_box)
advanced_layout.addWidget(prompt_box)
advanced_layout.addWidget(popup_console_box)
- advanced_layout.addWidget(check_updates)
advanced_widget = QWidget()
advanced_widget.setLayout(advanced_layout)
diff --git a/spyder/plugins/application/container.py b/spyder/plugins/application/container.py
index cce31fa770a..e51b88fafc8 100644
--- a/spyder/plugins/application/container.py
+++ b/spyder/plugins/application/container.py
@@ -12,12 +12,10 @@
# Standard library imports
import os
-import subprocess
import sys
import glob
# Third party imports
-from packaging.version import parse
from qtpy.QtCore import Qt, QThread, QTimer, Signal, Slot
from qtpy.QtGui import QGuiApplication
from qtpy.QtWidgets import QAction, QMessageBox, QPushButton
@@ -28,18 +26,13 @@
from spyder.api.translations import _
from spyder.api.widgets.main_container import PluginMainContainer
from spyder.utils.installers import InstallerMissingDependencies
-from spyder.config.utils import is_anaconda
-from spyder.config.base import (get_conf_path, get_debug_level,
- is_conda_based_app)
-from spyder.plugins.application.widgets.status import ApplicationUpdateStatus
+from spyder.config.base import get_conf_path, get_debug_level
from spyder.plugins.console.api import ConsoleActions
-from spyder.utils.conda import is_anaconda_pkg, get_spyder_conda_channel
from spyder.utils.environ import UserEnvDialog
from spyder.utils.qthelpers import start_file, DialogManager
from spyder.widgets.about import AboutDialog
from spyder.widgets.dependencies import DependenciesDialog
from spyder.widgets.helperwidgets import MessageCheckBox
-from spyder.workers.updates import WorkerUpdates
class ApplicationPluginMenus:
@@ -61,7 +54,6 @@ class ApplicationActions:
SpyderDocumentationVideoAction = "spyder_documentation_video_action"
SpyderTroubleshootingAction = "spyder_troubleshooting_action"
SpyderDependenciesAction = "spyder_dependencies_action"
- SpyderCheckUpdatesAction = "spyder_check_updates_action"
SpyderSupportAction = "spyder_support_action"
SpyderAbout = "spyder_about_action"
@@ -94,9 +86,6 @@ def __init__(self, name, plugin, parent=None):
self.current_dpi = None
self.dpi_messagebox = None
- # Keep track of the downloaded installer executable for updates
- self.installer_path = None
-
# ---- PluginMainContainer API
# -------------------------------------------------------------------------
def setup(self):
@@ -107,19 +96,6 @@ def setup(self):
# Attributes
self.dialog_manager = DialogManager()
- self.application_update_status = None
- if is_conda_based_app():
- self.application_update_status = ApplicationUpdateStatus(
- parent=self)
- (self.application_update_status.sig_check_for_updates_requested
- .connect(self.check_updates))
- (self.application_update_status.sig_install_on_close_requested
- .connect(self.set_installer_path))
- self.application_update_status.set_no_status()
- self.give_updates_feedback = False
- self.thread_updates = None
- self.worker_updates = None
- self.updates_timer = None
# Actions
# Documentation actions
@@ -155,10 +131,6 @@ def setup(self):
_("Dependencies..."),
triggered=self.show_dependencies,
icon=self.create_icon('advanced'))
- self.check_updates_action = self.create_action(
- ApplicationActions.SpyderCheckUpdatesAction,
- _("Check for updates..."),
- triggered=self.check_updates)
self.support_group_action = self.create_action(
ApplicationActions.SpyderSupportAction,
_("Spyder support..."),
@@ -226,20 +198,10 @@ def update_actions(self):
def on_close(self):
"""To call from Spyder when the plugin is closed."""
self.dialog_manager.close_all()
- if self.updates_timer is not None:
- self.updates_timer.stop()
- if self.thread_updates is not None:
- self.thread_updates.quit()
- self.thread_updates.wait()
if self.dependencies_thread is not None:
self.dependencies_thread.quit()
self.dependencies_thread.wait()
- # Run installer after Spyder is closed
- cmd = ('start' if os.name == 'nt' else 'open')
- if self.installer_path:
- subprocess.Popen(' '.join([cmd, self.installer_path]), shell=True)
-
@Slot()
def show_about(self):
"""Show Spyder About dialog."""
@@ -251,226 +213,6 @@ def show_user_env_variables(self):
"""Show Windows current user environment variables."""
self.dialog_manager.show(UserEnvDialog(self))
- # ---- Updates
- # -------------------------------------------------------------------------
-
- def _check_updates_ready(self):
- """Show results of the Spyder update checking process."""
-
- # `feedback` = False is used on startup, so only positive feedback is
- # given. `feedback` = True is used when after startup (when using the
- # menu action, and gives feeback if updates are, or are not found.
- feedback = self.give_updates_feedback
-
- # Get results from worker
- update_available = self.worker_updates.update_available
- latest_release = self.worker_updates.latest_release
- error_msg = self.worker_updates.error
-
- url_i = 'https://docs.spyder-ide.org/current/installation.html'
-
- # Define the custom QMessageBox
- box = MessageCheckBox(icon=QMessageBox.Information,
- parent=self)
- box.setWindowTitle(_("New Spyder version"))
- box.setAttribute(Qt.WA_ShowWithoutActivating)
- box.set_checkbox_text(_("Check for updates at startup"))
- box.setStandardButtons(QMessageBox.Ok)
- box.setDefaultButton(QMessageBox.Ok)
- box.setTextFormat(Qt.RichText)
-
- # Adjust the checkbox depending on the stored configuration
- option = 'check_updates_on_startup'
- box.set_checked(self.get_conf(option))
-
- header = _(
- "
Spyder {} is available!
"
- ).format(latest_release)
-
- if error_msg is not None:
- box.setText(error_msg)
- box.set_check_visible(False)
- box.show()
- if self.application_update_status:
- self.application_update_status.set_no_status()
- elif update_available:
- if self.application_update_status:
- self.application_update_status.set_status_pending(
- latest_release)
-
- # Update using our installers
- if parse(latest_release) >= parse("6.0.0"):
- box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
- box.setDefaultButton(QMessageBox.Yes)
-
- if not is_conda_based_app():
- installers_url = url_i + "#standalone-installers"
- msg = (
- header +
- _("Would you like to automatically download and "
- "install it using Spyder's installer?"
- "
"
- "We recommend our own installer "
- "because it's more stable and makes updating easy. "
- "This will leave your existing Spyder installation "
- "untouched.").format(installers_url)
- )
- else:
- msg = (
- header +
- _("Would you like to automatically download "
- "and install it?")
- )
-
- box.setText(msg)
- box.exec_()
- if box.result() == QMessageBox.Yes:
- self.application_update_status.start_installation(
- latest_release=latest_release)
-
- # Manual update
- if (
- not box.result() # The installer dialog was skipped
- or (
- box.result() == QMessageBox.No
- and not is_conda_based_app()
- )
- ):
- # Update-at-startup checkbox visible only if manual update
- # is first message box
- box.set_check_visible(not box.result())
- box.setStandardButtons(QMessageBox.Ok)
- box.setDefaultButton(QMessageBox.Ok)
-
- msg = ""
- if not box.result():
- msg += header
-
- if os.name == "nt":
- if is_anaconda():
- msg += _("Run the following command or commands in "
- "the Anaconda prompt to update manually:"
- "
")
- else:
- msg += _("Run the following command in a cmd prompt "
- "to update manually:
")
- else:
- if is_anaconda():
- msg += _("Run the following command or commands in a "
- "terminal to update manually:
")
- else:
- msg += _("Run the following command in a terminal to "
- "update manually:
")
-
- if is_anaconda():
- channel, __ = get_spyder_conda_channel()
- is_pypi = channel == 'pypi'
-
- if is_anaconda_pkg() and not is_pypi:
- msg += "conda update anaconda
"
-
- if is_pypi:
- dont_mix_pip_conda_video = (
- "https://youtu.be/Ul79ihg41Rs"
- )
-
- msg += (
- "pip install --upgrade spyder
"
- "
"
- )
-
- msg += _(
- "Important note: You installed Spyder with "
- "pip in a Conda environment, which is not a good "
- "idea. See our video for more "
- "details about it."
- ).format(dont_mix_pip_conda_video)
- else:
- if channel == 'pkgs/main':
- channel = ''
- else:
- channel = f'-c {channel}'
-
- msg += (
- f"conda install {channel} "
- f"spyder={latest_release}"
- f"
"
- )
-
- msg += _(
- "Important note: Since you installed "
- "Spyder with Anaconda, please don't use pip "
- "to update it as that will break your "
- "installation."
- )
- else:
- msg += "pip install --upgrade spyder
"
-
- msg += _(
- "
For more information, visit our "
- "installation guide."
- ).format(url_i)
-
- box.setText(msg)
- box.show()
- elif feedback:
- box.setText(_("Spyder is up to date."))
- box.show()
- if self.application_update_status:
- self.application_update_status.set_no_status()
- else:
- if self.application_update_status:
- self.application_update_status.set_no_status()
-
- self.set_conf(option, box.is_checked())
-
- # Enable check_updates_action after the thread has finished
- self.check_updates_action.setDisabled(False)
-
- # Provide feeback when clicking menu if check on startup is on
- self.give_updates_feedback = True
-
- @Slot()
- def check_updates(self, startup=False):
- """Check for spyder updates on github releases using a QThread."""
- # Disable check_updates_action while the thread is working
- self.check_updates_action.setDisabled(True)
- # !!! >>> Disable signals until alpha1
- if is_conda_based_app():
- self.application_update_status.blockSignals(True)
- return
- # !!! <<< Disable signals until alpha1
- if self.application_update_status:
- self.application_update_status.set_status_checking()
-
- if self.thread_updates is not None:
- self.thread_updates.quit()
- self.thread_updates.wait()
-
- self.thread_updates = QThread(None)
- self.worker_updates = WorkerUpdates(self, startup=startup)
- self.worker_updates.sig_ready.connect(self._check_updates_ready)
- self.worker_updates.sig_ready.connect(self.thread_updates.quit)
- self.worker_updates.moveToThread(self.thread_updates)
- self.thread_updates.started.connect(self.worker_updates.start)
-
- # Delay starting this check to avoid blocking the main window
- # while loading.
- # Fixes spyder-ide/spyder#15839
- if startup:
- self.updates_timer = QTimer(self)
- self.updates_timer.setInterval(60000)
- self.updates_timer.setSingleShot(True)
- self.updates_timer.timeout.connect(self.thread_updates.start)
- self.updates_timer.start()
- else:
- self.thread_updates.start()
-
- @Slot(str)
- def set_installer_path(self, installer_path):
- """Set installer executable path to be run when closing."""
- self.installer_path = installer_path
-
# ---- Dependencies
# -------------------------------------------------------------------------
def _set_dependencies(self):
diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py
index 11c968f195a..503631f9620 100644
--- a/spyder/plugins/application/plugin.py
+++ b/spyder/plugins/application/plugin.py
@@ -23,7 +23,7 @@
from spyder.api.plugin_registration.decorators import (
on_plugin_available, on_plugin_teardown)
from spyder.api.widgets.menus import SpyderMenu, MENU_SEPARATOR
-from spyder.config.base import (DEV, get_module_path, get_debug_level,
+from spyder.config.base import (get_module_path, get_debug_level,
running_under_pytest)
from spyder.plugins.application.confpage import ApplicationConfigPage
from spyder.plugins.application.container import (
@@ -98,22 +98,8 @@ def on_editor_available(self):
editor = self.get_plugin(Plugins.Editor)
self.get_container().sig_load_log_file.connect(editor.load)
- @on_plugin_available(plugin=Plugins.StatusBar)
- def on_statusbar_available(self):
- # Add status widget if created
- if self.application_update_status:
- statusbar = self.get_plugin(Plugins.StatusBar)
- statusbar.add_status_widget(self.application_update_status)
-
# -------------------------- PLUGIN TEARDOWN ------------------------------
- @on_plugin_teardown(plugin=Plugins.StatusBar)
- def on_statusbar_teardown(self):
- # Remove status widget if created
- if self.application_update_status:
- statusbar = self.get_plugin(Plugins.StatusBar)
- statusbar.remove_status_widget(self.application_update_status.ID)
-
@on_plugin_teardown(plugin=Plugins.Preferences)
def on_preferences_teardown(self):
preferences = self.get_plugin(Plugins.Preferences)
@@ -147,11 +133,6 @@ def on_mainwindow_visible(self):
if not running_under_pytest():
container.compute_dependencies()
- # Check for updates
- if DEV is None and self.get_conf('check_updates_on_startup'):
- container.give_updates_feedback = False
- container.check_updates(startup=True)
-
# Handle DPI scale and window changes to show a restart message.
# Don't activate this functionality on macOS because it's being
# triggered in the wrong situations.
@@ -220,8 +201,7 @@ def _populate_help_menu_support_section(self):
mainmenu = self.get_plugin(Plugins.MainMenu)
for support_action in [
self.trouble_action, self.report_action,
- self.dependencies_action, self.check_updates_action,
- self.support_group_action]:
+ self.dependencies_action, self.support_group_action]:
mainmenu.add_item_to_application_menu(
support_action,
menu_id=ApplicationMenus.Help,
@@ -261,7 +241,6 @@ def _depopulate_help_menu_support_section(self):
ApplicationActions.SpyderTroubleshootingAction,
ConsoleActions.SpyderReportAction,
ApplicationActions.SpyderDependenciesAction,
- ApplicationActions.SpyderCheckUpdatesAction,
ApplicationActions.SpyderSupportAction]:
mainmenu.remove_item_from_application_menu(
support_action,
@@ -414,11 +393,6 @@ def dependencies_action(self):
"""Show Spyder's Dependencies dialog box."""
return self.get_container().dependencies_action
- @property
- def check_updates_action(self):
- """Check if a new version of Spyder is available."""
- return self.get_container().check_updates_action
-
@property
def support_group_action(self):
"""Open Spyder's Google support group in the browser."""
@@ -453,7 +427,3 @@ def report_action(self):
def debug_logs_menu(self):
return self.get_container().get_menu(
ApplicationPluginMenus.DebugLogsMenu)
-
- @property
- def application_update_status(self):
- return self.get_container().application_update_status
diff --git a/spyder/plugins/statusbar/plugin.py b/spyder/plugins/statusbar/plugin.py
index 4d7d6ded8c1..f1736040f3d 100644
--- a/spyder/plugins/statusbar/plugin.py
+++ b/spyder/plugins/statusbar/plugin.py
@@ -46,7 +46,7 @@ class StatusBar(SpyderPluginV2):
'clock_status', 'cpu_status', 'memory_status', 'read_write_status',
'eol_status', 'encoding_status', 'cursor_position_status',
'vcs_status', 'lsp_status', 'completion_status',
- 'interpreter_status', 'application_update_status'}
+ 'interpreter_status'}
# ---- SpyderPluginV2 API
@staticmethod
@@ -216,7 +216,7 @@ def _organize_status_widgets(self):
'clock_status', 'cpu_status', 'memory_status', 'read_write_status',
'eol_status', 'encoding_status', 'cursor_position_status',
'vcs_status', 'lsp_status', 'completion_status',
- 'interpreter_status', 'application_update_status']
+ 'interpreter_status']
external_left = list(self.EXTERNAL_LEFT_WIDGETS.keys())
# Remove all widgets from the statusbar, except the external right
diff --git a/spyder/workers/tests/__init__.py b/spyder/plugins/updatemanager/tests/__init__.py
similarity index 100%
rename from spyder/workers/tests/__init__.py
rename to spyder/plugins/updatemanager/tests/__init__.py
diff --git a/spyder/workers/tests/test_update.py b/spyder/plugins/updatemanager/tests/test_update.py
similarity index 100%
rename from spyder/workers/tests/test_update.py
rename to spyder/plugins/updatemanager/tests/test_update.py
diff --git a/spyder/workers/updates.py b/spyder/plugins/updatemanager/updates.py
similarity index 100%
rename from spyder/workers/updates.py
rename to spyder/plugins/updatemanager/updates.py
diff --git a/spyder/plugins/application/widgets/__init__.py b/spyder/plugins/updatemanager/widgets/__init__.py
similarity index 100%
rename from spyder/plugins/application/widgets/__init__.py
rename to spyder/plugins/updatemanager/widgets/__init__.py
diff --git a/spyder/plugins/application/widgets/install.py b/spyder/plugins/updatemanager/widgets/install.py
similarity index 100%
rename from spyder/plugins/application/widgets/install.py
rename to spyder/plugins/updatemanager/widgets/install.py
diff --git a/spyder/plugins/application/widgets/status.py b/spyder/plugins/updatemanager/widgets/status.py
similarity index 100%
rename from spyder/plugins/application/widgets/status.py
rename to spyder/plugins/updatemanager/widgets/status.py
diff --git a/spyder/workers/__init__.py b/spyder/workers/__init__.py
deleted file mode 100644
index 839eae7ce43..00000000000
--- a/spyder/workers/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (c) 2009- Spyder Project Contributors
-#
-# Distributed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-# -----------------------------------------------------------------------------
From e45b82673af48dd3a81fdde1ddc3077b8a69265b Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sun, 10 Dec 2023 20:14:52 -0800
Subject: [PATCH 02/22] Rename plugin files * updates.py -> workers.py *
widgets/install.py -> widgets/update.py * tests/test_update.py ->
tests/test_update_manager.py
---
.../tests/{test_update.py => test_update_manager.py} | 0
spyder/plugins/updatemanager/widgets/{install.py => update.py} | 0
spyder/plugins/updatemanager/{updates.py => workers.py} | 0
3 files changed, 0 insertions(+), 0 deletions(-)
rename spyder/plugins/updatemanager/tests/{test_update.py => test_update_manager.py} (100%)
rename spyder/plugins/updatemanager/widgets/{install.py => update.py} (100%)
rename spyder/plugins/updatemanager/{updates.py => workers.py} (100%)
diff --git a/spyder/plugins/updatemanager/tests/test_update.py b/spyder/plugins/updatemanager/tests/test_update_manager.py
similarity index 100%
rename from spyder/plugins/updatemanager/tests/test_update.py
rename to spyder/plugins/updatemanager/tests/test_update_manager.py
diff --git a/spyder/plugins/updatemanager/widgets/install.py b/spyder/plugins/updatemanager/widgets/update.py
similarity index 100%
rename from spyder/plugins/updatemanager/widgets/install.py
rename to spyder/plugins/updatemanager/widgets/update.py
diff --git a/spyder/plugins/updatemanager/updates.py b/spyder/plugins/updatemanager/workers.py
similarity index 100%
rename from spyder/plugins/updatemanager/updates.py
rename to spyder/plugins/updatemanager/workers.py
From 27f463b20c1960ade078542dbfba1b34e6407539 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 30 Oct 2023 21:24:23 -0700
Subject: [PATCH 03/22] Initial commit for Update Manager Plugin
---
setup.py | 1 +
spyder/api/plugins/enum.py | 1 +
spyder/config/main.py | 5 +
spyder/plugins/statusbar/plugin.py | 4 +-
spyder/plugins/updatemanager/__init__.py | 12 +
spyder/plugins/updatemanager/api.py | 11 +
spyder/plugins/updatemanager/confpage.py | 42 ++
spyder/plugins/updatemanager/container.py | 105 +++
spyder/plugins/updatemanager/plugin.py | 131 ++++
.../plugins/updatemanager/scripts/install.bat | 88 +++
.../plugins/updatemanager/scripts/install.sh | 60 ++
.../plugins/updatemanager/tests/__init__.py | 2 +-
.../tests/test_update_manager.py | 174 +++--
.../plugins/updatemanager/widgets/__init__.py | 2 +-
.../plugins/updatemanager/widgets/status.py | 118 ++-
.../plugins/updatemanager/widgets/update.py | 709 ++++++++++++------
spyder/plugins/updatemanager/workers.py | 260 +++----
17 files changed, 1206 insertions(+), 519 deletions(-)
create mode 100644 spyder/plugins/updatemanager/__init__.py
create mode 100644 spyder/plugins/updatemanager/api.py
create mode 100644 spyder/plugins/updatemanager/confpage.py
create mode 100644 spyder/plugins/updatemanager/container.py
create mode 100644 spyder/plugins/updatemanager/plugin.py
create mode 100644 spyder/plugins/updatemanager/scripts/install.bat
create mode 100755 spyder/plugins/updatemanager/scripts/install.sh
diff --git a/setup.py b/setup.py
index 3286d73728f..f7f5dc3b908 100644
--- a/setup.py
+++ b/setup.py
@@ -312,6 +312,7 @@ def run(self):
'switcher = spyder.plugins.switcher.plugin:Switcher',
'toolbar = spyder.plugins.toolbar.plugin:Toolbar',
'tours = spyder.plugins.tours.plugin:Tours',
+ 'update_manager = spyder.plugins.updatemanager.plugin:UpdateManager',
'variable_explorer = spyder.plugins.variableexplorer.plugin:VariableExplorer',
'workingdir = spyder.plugins.workingdirectory.plugin:WorkingDirectory',
]
diff --git a/spyder/api/plugins/enum.py b/spyder/api/plugins/enum.py
index a74cccdfa94..9d68767ed33 100644
--- a/spyder/api/plugins/enum.py
+++ b/spyder/api/plugins/enum.py
@@ -41,6 +41,7 @@ class Plugins:
Switcher = 'switcher'
Toolbar = "toolbar"
Tours = 'tours'
+ UpdateManager = 'update_manager'
VariableExplorer = 'variable_explorer'
WorkingDirectory = 'workingdir'
diff --git a/spyder/config/main.py b/spyder/config/main.py
index 2a8480c3390..c4cf64d951a 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -83,6 +83,11 @@
'report_error/remember_token': False,
'show_dpi_message': True,
}),
+ ('update_manager',
+ {
+ 'check_updates_on_startup': True,
+ 'check_stable_only': True,
+ }),
('toolbar',
{
'enable': True,
diff --git a/spyder/plugins/statusbar/plugin.py b/spyder/plugins/statusbar/plugin.py
index f1736040f3d..bd3bd86e4e0 100644
--- a/spyder/plugins/statusbar/plugin.py
+++ b/spyder/plugins/statusbar/plugin.py
@@ -46,7 +46,7 @@ class StatusBar(SpyderPluginV2):
'clock_status', 'cpu_status', 'memory_status', 'read_write_status',
'eol_status', 'encoding_status', 'cursor_position_status',
'vcs_status', 'lsp_status', 'completion_status',
- 'interpreter_status'}
+ 'interpreter_status', 'update_manager_status'}
# ---- SpyderPluginV2 API
@staticmethod
@@ -216,7 +216,7 @@ def _organize_status_widgets(self):
'clock_status', 'cpu_status', 'memory_status', 'read_write_status',
'eol_status', 'encoding_status', 'cursor_position_status',
'vcs_status', 'lsp_status', 'completion_status',
- 'interpreter_status']
+ 'interpreter_status', 'update_manager_status']
external_left = list(self.EXTERNAL_LEFT_WIDGETS.keys())
# Remove all widgets from the statusbar, except the external right
diff --git a/spyder/plugins/updatemanager/__init__.py b/spyder/plugins/updatemanager/__init__.py
new file mode 100644
index 00000000000..2079205facb
--- /dev/null
+++ b/spyder/plugins/updatemanager/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+spyder.plugins.updatemanager
+============================
+
+Application Update Manager Plugin.
+"""
diff --git a/spyder/plugins/updatemanager/api.py b/spyder/plugins/updatemanager/api.py
new file mode 100644
index 00000000000..8365533093f
--- /dev/null
+++ b/spyder/plugins/updatemanager/api.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder update manager API.
+"""
+
+from spyder.plugins.updatemanager.container import UpdateManagerActions
diff --git a/spyder/plugins/updatemanager/confpage.py b/spyder/plugins/updatemanager/confpage.py
new file mode 100644
index 00000000000..e1b73d6f5dc
--- /dev/null
+++ b/spyder/plugins/updatemanager/confpage.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+General entry in Preferences.
+
+For historical reasons (dating back to Spyder 2) the main class here is called
+`MainConfigPage` and its associated entry in our config system is called
+`main`.
+"""
+
+from qtpy.QtWidgets import QGroupBox, QVBoxLayout
+
+from spyder.config.base import _
+from spyder.api.preferences import PluginConfigPage
+
+
+class UpdateManagerConfigPage(PluginConfigPage):
+ def setup_page(self):
+ """Setup config page widgets and options."""
+ updates_group = QGroupBox(_("Updates"))
+ check_updates = self.create_checkbox(
+ _("Check for updates on startup"),
+ 'check_updates_on_startup'
+ )
+ stable_only = self.create_checkbox(
+ _("Check for stable releases only"),
+ 'check_stable_only'
+ )
+
+ updates_layout = QVBoxLayout()
+ updates_layout.addWidget(check_updates)
+ updates_layout.addWidget(stable_only)
+ updates_group.setLayout(updates_layout)
+
+ vlayout = QVBoxLayout()
+ vlayout.addWidget(updates_group)
+ vlayout.addStretch(1)
+ self.setLayout(vlayout)
diff --git a/spyder/plugins/updatemanager/container.py b/spyder/plugins/updatemanager/container.py
new file mode 100644
index 00000000000..dc94cd5fa8f
--- /dev/null
+++ b/spyder/plugins/updatemanager/container.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Container Widget.
+
+Holds references for base actions in the Application of Spyder.
+"""
+
+# Standard library imports
+import logging
+
+# Third party imports
+from qtpy.QtCore import Slot
+
+# Local imports
+from spyder.api.translations import _
+from spyder.api.widgets.main_container import PluginMainContainer
+from spyder.plugins.updatemanager.widgets.status import UpdateManagerStatus
+from spyder.plugins.updatemanager.widgets.update import (
+ UpdateManagerWidget, NO_STATUS
+)
+from spyder.utils.qthelpers import DialogManager
+
+# Logger setup
+logger = logging.getLogger(__name__)
+
+
+# Actions
+class UpdateManagerActions:
+ SpyderCheckUpdateAction = "spyder_check_update_action"
+
+
+class UpdateManagerContainer(PluginMainContainer):
+
+ def __init__(self, name, plugin, parent=None):
+ super().__init__(name, plugin, parent)
+
+ self.install_on_close = False
+
+ def setup(self):
+ self.dialog_manager = DialogManager()
+ self.update_manager = UpdateManagerWidget(parent=self)
+ self.update_manager_status = UpdateManagerStatus(parent=self)
+
+ # Actions
+ self.check_update_action = self.create_action(
+ UpdateManagerActions.SpyderCheckUpdateAction,
+ _("Check for updates..."),
+ triggered=self.start_check_update
+ )
+
+ # Signals
+ self.update_manager.sig_set_status.connect(self.set_status)
+ self.update_manager.sig_disable_actions.connect(
+ self.check_update_action.setDisabled)
+ self.update_manager.sig_block_status_signals.connect(
+ self.update_manager_status.blockSignals)
+ self.update_manager.sig_download_progress.connect(
+ self.update_manager_status.set_download_progress)
+ self.update_manager.sig_install_on_close.connect(
+ self.set_install_on_close)
+ self.update_manager.sig_quit_requested.connect(self.sig_quit_requested)
+
+ self.update_manager_status.sig_check_update.connect(
+ self.start_check_update)
+ self.update_manager_status.sig_start_update.connect(self.start_update)
+ self.update_manager_status.sig_show_progress_dialog.connect(
+ self.update_manager.show_progress_dialog)
+
+ self.set_status(NO_STATUS)
+
+ def update_actions(self):
+ pass
+
+ def set_status(self, status, latest_version=None):
+ """Set Update Manager status"""
+ self.update_manager_status.set_value(status)
+
+ @Slot()
+ def start_check_update(self, startup=False):
+ """Check for spyder updates."""
+ self.update_manager.start_check_update(startup=startup)
+
+ @Slot()
+ def start_update(self):
+ """Start the update process"""
+ self.update_manager.start_update()
+
+ def set_install_on_close(self, install_on_close):
+ """Set whether start install on close."""
+ self.install_on_close = install_on_close
+
+ def on_close(self):
+ """To call from Spyder when the plugin is closed."""
+ self.update_manager.cleanup_threads()
+
+ # Run installer after Spyder is closed
+ if self.install_on_close:
+ self.update_manager.start_install()
+
+ self.dialog_manager.close_all()
diff --git a/spyder/plugins/updatemanager/plugin.py b/spyder/plugins/updatemanager/plugin.py
new file mode 100644
index 00000000000..23ed346abeb
--- /dev/null
+++ b/spyder/plugins/updatemanager/plugin.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Update Manager Plugin.
+"""
+
+# Local imports
+from spyder.api.plugins import Plugins, SpyderPluginV2
+from spyder.api.translations import _
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
+from spyder.config.base import DEV
+from spyder.plugins.updatemanager.confpage import UpdateManagerConfigPage
+from spyder.plugins.updatemanager.container import (UpdateManagerActions,
+ UpdateManagerContainer)
+from spyder.plugins.mainmenu.api import ApplicationMenus, HelpMenuSections
+
+
+class UpdateManager(SpyderPluginV2):
+ NAME = 'update_manager'
+ REQUIRES = [Plugins.Preferences]
+ OPTIONAL = [Plugins.Help, Plugins.MainMenu, Plugins.Shortcuts,
+ Plugins.StatusBar]
+ CONTAINER_CLASS = UpdateManagerContainer
+ CONF_SECTION = 'update_manager'
+ CONF_FILE = False
+ CONF_WIDGET_CLASS = UpdateManagerConfigPage
+ CAN_BE_DISABLED = False
+
+ @staticmethod
+ def get_name():
+ return _('Update Manager')
+
+ @classmethod
+ def get_icon(cls):
+ return cls.create_icon('genprefs')
+
+ @staticmethod
+ def get_description():
+ return _('Manage application updates.')
+
+ # --------------------- PLUGIN INITIALIZATION -----------------------------
+
+ def on_initialize(self):
+ pass
+
+ @on_plugin_available(plugin=Plugins.Preferences)
+ def on_preferences_available(self):
+ # Register conf page
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.register_plugin_preferences(self)
+
+ @on_plugin_available(plugin=Plugins.MainMenu)
+ def on_main_menu_available(self):
+ if self.is_plugin_enabled(Plugins.Shortcuts):
+ if self.is_plugin_available(Plugins.Shortcuts):
+ self._populate_help_menu()
+ else:
+ self._populate_help_menu()
+
+ @on_plugin_available(plugin=Plugins.StatusBar)
+ def on_statusbar_available(self):
+ # Add status widget
+ statusbar = self.get_plugin(Plugins.StatusBar)
+ statusbar.add_status_widget(self.update_manager_status)
+
+ # -------------------------- PLUGIN TEARDOWN ------------------------------
+
+ @on_plugin_teardown(plugin=Plugins.StatusBar)
+ def on_statusbar_teardown(self):
+ # Remove status widget if created
+ statusbar = self.get_plugin(Plugins.StatusBar)
+ statusbar.remove_status_widget(self.update_manager_status.ID)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ self._depopulate_help_menu()
+
+ def on_close(self, _unused=True):
+ # The container is closed directly in the plugin registry
+ pass
+
+ def on_mainwindow_visible(self):
+ """Actions after the mainwindow in visible."""
+ container = self.get_container()
+
+ # Check for updates on startup
+ if DEV is None and self.get_conf('check_updates_on_startup'):
+ container.start_check_updates(startup=True)
+
+ # ---- Private API
+ # ------------------------------------------------------------------------
+ def _populate_help_menu(self):
+ """Add update action and menu to the Help menu."""
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.add_item_to_application_menu(
+ self.check_update_action,
+ menu_id=ApplicationMenus.Help,
+ section=HelpMenuSections.Support,
+ before_section=HelpMenuSections.ExternalDocumentation)
+
+ @property
+ def _window(self):
+ return self.main.window()
+
+ def _depopulate_help_menu(self):
+ """Remove update action from the Help main menu."""
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ UpdateManagerActions.SpyderCheckUpdateAction,
+ menu_id=ApplicationMenus.Help)
+
+ # ---- Public API
+ # ------------------------------------------------------------------------
+ @property
+ def check_update_action(self):
+ """Check if a new version of Spyder is available."""
+ return self.get_container().check_update_action
+
+ @property
+ def update_manager_status(self):
+ return self.get_container().update_manager_status
diff --git a/spyder/plugins/updatemanager/scripts/install.bat b/spyder/plugins/updatemanager/scripts/install.bat
new file mode 100644
index 00000000000..ad26ae98f08
--- /dev/null
+++ b/spyder/plugins/updatemanager/scripts/install.bat
@@ -0,0 +1,88 @@
+:: This script updates or installs a new version of Spyder
+@echo off
+
+:: Create variables from arguments
+:parse
+IF "%~1"=="" GOTO endparse
+IF "%~1"=="-p" set prefix=%2 & SHIFT
+IF "%~1"=="-i" set install_exe=%2 & SHIFT
+IF "%~1"=="-c" set conda=%2 & SHIFT
+IF "%~1"=="-v" set spy_ver=%2 & SHIFT
+SHIFT
+GOTO parse
+:endparse
+
+:: Enforce encoding
+chcp 65001>nul
+
+IF not "%conda%"=="" IF not "%spy_ver%"=="" (
+ call :update_subroutine
+ call :launch_spyder
+ goto exit
+)
+
+IF not "%install_exe%"=="" (
+ call :install_subroutine
+ goto exit
+)
+
+:exit
+exit %ERRORLEVEL%
+
+:install_subroutine
+ echo Installing Spyder from: %install_exe%
+
+ call :wait_for_spyder_quit
+
+ :: Uninstall Spyder
+ for %%I in ("%prefix%\..\..") do set "conda_root=%%~fI"
+
+ echo Install will proceed after the current Spyder version is uninstalled.
+ start %conda_root%\Uninstall-Spyder.exe
+
+ :: Must wait for uninstaller to appear on tasklist
+ :wait_for_uninstall_start
+ tasklist /fi "ImageName eq Un_A.exe" /fo csv 2>NUL | find /i "Un_A.exe">NUL
+ IF "%ERRORLEVEL%"=="1" (
+ timeout /t 1 /nobreak > nul
+ goto wait_for_uninstall_start
+ )
+ echo Uninstall in progress...
+
+ :wait_for_uninstall
+ timeout /t 1 /nobreak > nul
+ tasklist /fi "ImageName eq Un_A.exe" /fo csv 2>NUL | find /i "Un_A.exe">NUL
+ IF "%ERRORLEVEL%"=="0" goto wait_for_uninstall
+ echo Uninstall complete.
+
+ start %install_exe%
+ goto :EOF
+
+:update_subroutine
+ echo Updating Spyder
+
+ call :wait_for_spyder_quit
+
+ %conda% install -p %prefix% -c conda-forge --override-channels -y spyder=%spy_ver%
+ set /P CONT=Press any key to exit...
+ goto :EOF
+
+:wait_for_spyder_quit
+ echo Waiting for Spyder to quit...
+ :loop
+ tasklist /fi "ImageName eq spyder.exe" /fo csv 2>NUL | find /i "spyder.exe">NUL
+ IF "%ERRORLEVEL%"=="0" (
+ timeout /t 1 /nobreak > nul
+ goto loop
+ )
+ echo Spyder is quit.
+ goto :EOF
+
+:launch_spyder
+ echo %prefix% | findstr /b "%USERPROFILE%" > nul && (
+ set shortcut_root=%APPDATA%
+ ) || (
+ set shortcut_root=%ALLUSERSPROFILE%
+ )
+ start "" /B "%shortcut_root%\Microsoft\Windows\Start Menu\Programs\spyder\Spyder.lnk"
+ goto :EOF
diff --git a/spyder/plugins/updatemanager/scripts/install.sh b/spyder/plugins/updatemanager/scripts/install.sh
new file mode 100755
index 00000000000..381da1cc44e
--- /dev/null
+++ b/spyder/plugins/updatemanager/scripts/install.sh
@@ -0,0 +1,60 @@
+#!/bin/bash -i
+set -e
+unset HISTFILE # Do not write to history with interactive shell
+
+while getopts "i:c:p:v:" option; do
+ case "$option" in
+ (i) install_exe=$OPTARG ;;
+ (c) conda=$OPTARG ;;
+ (p) prefix=$OPTARG ;;
+ (v) spy_ver=$OPTARG ;;
+ esac
+done
+shift $(($OPTIND - 1))
+
+update_spyder(){
+ $conda install -p $prefix -c conda-forge --override-channels -y spyder=$spy_ver
+ read -p "Press any key to exit..."
+}
+
+launch_spyder(){
+ if [[ "$OSTYPE" = "darwin"* ]]; then
+ shortcut=/Applications/Spyder.app
+ [[ "$prefix" = "$HOME"* ]] && open -a $HOME$shortcut || open -a $shortcut
+ elif [[ -n "$(which gtk-launch)" ]]; then
+ gtk-launch spyder_spyder
+ else
+ nohup $prefix/bin/spyder &>/dev/null &
+ fi
+}
+
+install_spyder(){
+ # First uninstall Spyder
+ uninstall_script="$prefix/../../uninstall-spyder.sh"
+ if [[ -f "$uninstall_script" ]]; then
+ echo "Uninstalling Spyder..."
+ $uninstall_script
+ fi
+
+ # Run installer
+ [[ "$OSTYPE" = "darwin"* ]] && open $install_exe || sh $install_exe
+}
+
+while [[ $(pgrep spyder 2> /dev/null) ]]; do
+ echo "Waiting for Spyder to quit..."
+ sleep 1
+done
+
+echo "Spyder quit."
+
+if [[ -e "$conda" && -d "$prefix" && -n "$spy_ver" ]]; then
+ update_spyder
+ launch_spyder
+elif [[ -e "$install_exe" ]]; then
+ install_spyder
+fi
+
+if [[ "$OSTYPE" = "darwin"* ]]; then
+ # Close the Terminal window that was opened for this process
+ osascript -e 'tell application "Terminal" to close first window' &
+fi
diff --git a/spyder/plugins/updatemanager/tests/__init__.py b/spyder/plugins/updatemanager/tests/__init__.py
index 92b07bcf58c..f984ad47da2 100644
--- a/spyder/plugins/updatemanager/tests/__init__.py
+++ b/spyder/plugins/updatemanager/tests/__init__.py
@@ -6,4 +6,4 @@
# (see LICENSE.txt for details)
# -----------------------------------------------------------------------------
-"""Tests for workers."""
+"""Tests for Update Manager Plugin."""
diff --git a/spyder/plugins/updatemanager/tests/test_update_manager.py b/spyder/plugins/updatemanager/tests/test_update_manager.py
index eaaf9fc3f52..0e57ed91c7e 100644
--- a/spyder/plugins/updatemanager/tests/test_update_manager.py
+++ b/spyder/plugins/updatemanager/tests/test_update_manager.py
@@ -4,108 +4,144 @@
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
-import sys
+import os
+import logging
import pytest
-from spyder.workers.updates import WorkerUpdates
+from spyder.config.base import running_in_ci
+from spyder.plugins.updatemanager import workers
+from spyder.plugins.updatemanager.workers import WorkerUpdate
+from spyder.plugins.updatemanager.widgets import update
+from spyder.plugins.updatemanager.widgets.update import UpdateManagerWidget
+
+logging.basicConfig()
+
+
+@pytest.fixture(autouse=True)
+def capture_logging(caplog):
+ caplog.set_level(10, "spyder.plugins.updatemanager")
+
+
+@pytest.fixture
+def worker():
+ return WorkerUpdate(None)
+
+
+# ---- Test WorkerUpdate
+
+@pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"])
+def test_updates_appenv(qtbot, mocker, version):
+ """
+ Test whether or not we offer updates for our installers according to the
+ current Spyder version.
+
+ Uses UpdateManagerWidget in order to also test QThread.
+ """
+ mocker.patch.object(update, "__version__", new=version)
+ # Do not execute start_update after check_update completes.
+ mocker.patch.object(
+ UpdateManagerWidget, "start_update", new=lambda x: None)
+ mocker.patch.object(workers, "__version__", new=version)
+ mocker.patch.object(workers, "is_anaconda", return_value=True)
+ mocker.patch.object(workers, "is_conda_based_app", return_value=True)
+ mocker.patch.object(
+ workers, "get_spyder_conda_channel",
+ return_value=("conda-forge", "https://conda.anaconda.org/conda-forge"))
+
+ um = UpdateManagerWidget(None)
+ um.start_check_update()
+ qtbot.waitUntil(um.update_thread.isFinished)
+
+ _update = um.update_worker.update_available
+ assert _update if version.split('.')[0] == '1' else not _update
+ # assert len(worker.releases) > 1
+ assert len(um.update_worker.releases) > 1
-@pytest.mark.parametrize("is_anaconda", [True, False])
-@pytest.mark.parametrize("is_pypi", [True, False])
@pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"])
@pytest.mark.parametrize(
- "spyder_conda_channel", [
+ "channel", [
("pkgs/main", "https://repo.anaconda.com/pkgs/main"),
- ("conda-forge", "https://conda.anaconda.org/conda-forge")
+ ("conda-forge", "https://conda.anaconda.org/conda-forge"),
+ ("pypi", "https://conda.anaconda.org/pypi")
]
)
-def test_updates(qtbot, mocker, is_anaconda, is_pypi, version,
- spyder_conda_channel):
+def test_updates_condaenv(qtbot, worker, mocker, version, channel):
"""
- Test whether or not we offer updates for Anaconda and PyPI according to the
+ Test whether or not we offer updates for our installers according to the
current Spyder version.
"""
- mocker.patch(
- "spyder.workers.updates.is_anaconda",
- return_value=is_anaconda
+ mocker.patch.object(workers, "__version__", new=version)
+ mocker.patch.object(workers, "is_anaconda", return_value=True)
+ mocker.patch.object(workers, "is_conda_based_app", return_value=False)
+ mocker.patch.object(
+ workers, "get_spyder_conda_channel", return_value=channel
)
- if is_anaconda:
- if is_pypi:
- channel = ("pypi", "https://conda.anaconda.org/pypi")
- else:
- channel = spyder_conda_channel
+ with qtbot.waitSignal(worker.sig_ready, timeout=5000):
+ worker.start()
- mocker.patch(
- "spyder.workers.updates.get_spyder_conda_channel",
- return_value=channel
- )
-
- worker = WorkerUpdates(None, False, version=version)
- worker.start()
-
- update = worker.update_available
- assert update if version.split('.')[0] == '1' else not update
+ _update = worker.update_available
+ assert _update if version.split('.')[0] == '1' else not _update
assert len(worker.releases) == 1
-@pytest.mark.skipif(sys.platform == 'darwin', reason="Fails frequently on Mac")
@pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"])
-def test_updates_for_installers(qtbot, mocker, version):
+def test_updates_pipenv(qtbot, worker, mocker, version):
"""
- Test whether or not we offer updates for our installers according to the
- current Spyder version.
+ Test updates for pip installed Spyder
"""
- mocker.patch("spyder.workers.updates.is_anaconda", return_value=False)
- mocker.patch("spyder.workers.updates.is_conda_based_app",
- return_value=True)
+ mocker.patch.object(workers, "__version__", new=version)
+ mocker.patch.object(workers, "is_anaconda", return_value=False)
+ mocker.patch.object(workers, "is_conda_based_app", return_value=False)
+ mocker.patch.object(
+ workers, "get_spyder_conda_channel",
+ return_value=("pypi", "https://conda.anaconda.org/pypi")
+ )
- worker = WorkerUpdates(None, False, version=version)
- worker.start()
+ with qtbot.waitSignal(worker.sig_ready, timeout=5000):
+ worker.start()
- update = worker.update_available
- assert update if version.split('.')[0] == '1' else not update
- assert len(worker.releases) > 1
+ _update = worker.update_available
+ assert _update if version.split('.')[0] == '1' else not _update
+ assert len(worker.releases) == 1
-def test_no_update_development(qtbot, mocker):
- """Test we don't offer updates for development versions."""
- mocker.patch(
- "spyder.workers.updates.get_spyder_conda_channel",
- return_value=("pypi", "https://conda.anaconda.org/pypi")
- )
+@pytest.mark.parametrize("release", ["4.0.1", "4.0.1a1"])
+@pytest.mark.parametrize("version", ["4.0.0a1", "4.0.0"])
+@pytest.mark.parametrize("stable_only", [True, False])
+def test_update_non_stable(qtbot, mocker, version, release, stable_only):
+ """Test we offer unstable updates."""
+ mocker.patch.object(workers, "__version__", new=version)
- worker = WorkerUpdates(None, False, version="3.3.2.dev0",
- releases=['3.3.1'])
- worker.start()
- assert not worker.update_available
+ worker = WorkerUpdate(stable_only)
+ worker.releases = [release]
+ worker._check_update_available()
+ _update = worker.update_available
+ assert not _update if "a" in release and stable_only else _update
-def test_update_pre_to_pre(qtbot, mocker):
- """Test we offer updates between prereleases."""
- mocker.patch(
- "spyder.workers.updates.get_spyder_conda_channel",
- return_value=("pypi", "https://conda.anaconda.org/pypi")
- )
- worker = WorkerUpdates(None, False, version="4.0.0a1",
- releases=['4.0.0b5'])
- worker.start()
- assert worker.update_available
+# ---- Test WorkerDownloadInstaller
+@pytest.mark.skipif(not running_in_ci(), reason="Download only in CI")
+def test_download(qtbot, mocker):
+ """
+ Test download spyder installer.
+ Uses UpdateManagerWidget in order to also test QThread.
+ """
+ um = UpdateManagerWidget(None)
+ um.latest_release = "6.0.0a2"
+ um._set_installer_path()
+ # Do not execute _start_install after download completes.
+ mocker.patch.object(
+ UpdateManagerWidget, "_confirm_install", new=lambda x: None)
-def test_update_pre_to_final(qtbot, mocker):
- """Test we offer updates from prereleases to the final versions."""
- mocker.patch(
- "spyder.workers.updates.get_spyder_conda_channel",
- return_value=("pypi", "https://conda.anaconda.org/pypi")
- )
+ um._start_download()
+ qtbot.waitUntil(um.download_thread.isFinished, timeout=60000)
- worker = WorkerUpdates(None, False, version="4.0.0b3",
- releases=['4.0.0'])
- worker.start()
- assert worker.update_available
+ assert os.path.exists(um.installer_path)
if __name__ == "__main__":
diff --git a/spyder/plugins/updatemanager/widgets/__init__.py b/spyder/plugins/updatemanager/widgets/__init__.py
index 5a3f944db10..e35c9c7b676 100644
--- a/spyder/plugins/updatemanager/widgets/__init__.py
+++ b/spyder/plugins/updatemanager/widgets/__init__.py
@@ -5,4 +5,4 @@
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
-"""Widgets for the Application plugin."""
+"""Widgets for the Update Manager plugin."""
diff --git a/spyder/plugins/updatemanager/widgets/status.py b/spyder/plugins/updatemanager/widgets/status.py
index 051ed90b4c5..35fff755a53 100644
--- a/spyder/plugins/updatemanager/widgets/status.py
+++ b/spyder/plugins/updatemanager/widgets/status.py
@@ -20,10 +20,9 @@
from spyder.api.translations import _
from spyder.api.widgets.menus import SpyderMenu
from spyder.api.widgets.status import StatusBarWidget
-from spyder.config.base import is_conda_based_app
-from spyder.plugins.application.widgets.install import (
- UpdateInstallerDialog, NO_STATUS, DOWNLOADING_INSTALLER, INSTALLING,
- PENDING, CHECKING)
+from spyder.plugins.updatemanager.widgets.update import (
+ NO_STATUS, DOWNLOADING_INSTALLER, PENDING,
+ CHECKING, DOWNLOAD_FINISHED, INSTALL_ON_CLOSE)
from spyder.utils.icon_manager import ima
from spyder.utils.qthelpers import add_actions, create_action
@@ -32,24 +31,25 @@
logger = logging.getLogger(__name__)
-class ApplicationUpdateStatus(StatusBarWidget):
- """Status bar widget for application update status."""
- BASE_TOOLTIP = _("Application update status")
- ID = 'application_update_status'
+class UpdateManagerStatus(StatusBarWidget):
+ """Status bar widget for update manager."""
+ BASE_TOOLTIP = _("Update manager status")
+ ID = 'update_manager_status'
- sig_check_for_updates_requested = Signal()
- """
- Signal to request checking for updates.
- """
+ sig_check_update = Signal()
+ """Signal to request checking for updates."""
- sig_install_on_close_requested = Signal(str)
+ sig_start_update = Signal()
+ """Signal to start update process"""
+
+ sig_show_progress_dialog = Signal(bool)
"""
- Signal to request running the downloaded installer on close.
+ Signal to show progress dialog
Parameters
----------
- installer_path: str
- Path to instal
+ show: bool
+ True to show, False to hide
"""
CUSTOM_WIDGET_CLASS = QLabel
@@ -59,9 +59,6 @@ def __init__(self, parent):
self.tooltip = self.BASE_TOOLTIP
super().__init__(parent, show_spinner=True)
- # Installation dialog
- self.installer = UpdateInstallerDialog(self)
-
# Check for updates action menu
self.menu = SpyderMenu(self)
@@ -70,32 +67,24 @@ def __init__(self, parent):
self.custom_widget.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
# Signals
- self.sig_clicked.connect(self.show_installation_dialog_or_menu)
-
- # Installer widget signals
- self.installer.sig_download_progress.connect(
- self.set_download_progress)
- self.installer.sig_installation_status.connect(
- self.set_value)
- self.installer.sig_install_on_close_requested.connect(
- self.sig_install_on_close_requested)
+ self.sig_clicked.connect(self.show_dialog_or_menu)
def set_value(self, value):
- """Return update installation state."""
- if value == DOWNLOADING_INSTALLER or value == INSTALLING:
- self.tooltip = _("Update installation will continue in the "
- "background.\n"
- "Click here to show the installation "
- "dialog again.")
- if value == DOWNLOADING_INSTALLER:
- self.spinner.hide()
- self.spinner.stop()
- self.custom_widget.show()
- else:
+ """Set update manager status."""
+ if value == DOWNLOADING_INSTALLER:
+ self.tooltip = _(
+ "Downloading update will continue in the background.\n"
+ "Click here to show the download dialog again."
+ )
+ self.spinner.hide()
+ self.spinner.stop()
+ self.custom_widget.show()
+ elif value == CHECKING:
+ self.tooltip = self.BASE_TOOLTIP
+ if self.custom_widget:
self.custom_widget.hide()
- self.spinner.show()
- self.spinner.start()
- self.installer.show()
+ self.spinner.show()
+ self.spinner.start()
elif value == PENDING:
self.tooltip = value
self.custom_widget.hide()
@@ -111,7 +100,7 @@ def set_value(self, value):
self.setVisible(True)
self.update_tooltip()
value = f"Spyder: {value}"
- logger.debug(f"Application Update Status: {value}")
+ logger.debug(f"Update manager status: {value}")
super().set_value(value)
def get_tooltip(self):
@@ -121,47 +110,24 @@ def get_tooltip(self):
def get_icon(self):
return ima.icon('spyder_about')
- def start_installation(self, latest_release):
- self.installer.start_installation(latest_release)
-
- def set_download_progress(self, current_value, total):
- percentage_progress = 0
- if total > 0:
- percentage_progress = round((current_value/total) * 100)
- self.custom_widget.setText(f"{percentage_progress}%")
-
- def set_status_pending(self, latest_release):
- self.set_value(PENDING)
- self.installer.save_latest_release(latest_release)
-
- def set_status_checking(self):
- self.set_value(CHECKING)
- self.spinner.show()
- self.spinner.start()
-
- def set_no_status(self):
- self.set_value(NO_STATUS)
- self.spinner.hide()
- self.spinner.stop()
+ def set_download_progress(self, percent_progress):
+ """Set download progress in status bar"""
+ self.custom_widget.setText(f"{percent_progress}%")
@Slot()
- def show_installation_dialog_or_menu(self):
- """Show installation dialog or menu."""
+ def show_dialog_or_menu(self):
+ """Show download dialog or status bar menu."""
value = self.value.split(":")[-1].strip()
- if (
- self.tooltip != self.BASE_TOOLTIP
- and value != PENDING
- and is_conda_based_app()
- ):
- self.installer.show()
- elif value == PENDING and is_conda_based_app():
- self.installer.continue_installation()
+ if value == DOWNLOADING_INSTALLER:
+ self.sig_show_progress_dialog.emit(True)
+ elif value in (PENDING, DOWNLOAD_FINISHED, INSTALL_ON_CLOSE):
+ self.sig_start_update.emit()
elif value == NO_STATUS:
self.menu.clear()
check_for_updates_action = create_action(
self,
text=_("Check for updates..."),
- triggered=self.sig_check_for_updates_requested.emit
+ triggered=self.sig_check_update.emit
)
add_actions(self.menu, [check_for_updates_action])
rect = self.contentsRect()
diff --git a/spyder/plugins/updatemanager/widgets/update.py b/spyder/plugins/updatemanager/widgets/update.py
index 4d05efe25e4..a3ecc3d4bc5 100644
--- a/spyder/plugins/updatemanager/widgets/update.py
+++ b/spyder/plugins/updatemanager/widgets/update.py
@@ -4,25 +4,32 @@
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
-"""Update installation widgets."""
+"""Update Manager widgets."""
# Standard library imports
import logging
import os
+import os.path as osp
+import sys
import subprocess
+from tempfile import gettempdir
+import platform
# Third-party imports
-from qtpy.QtCore import Qt, QThread, Signal
-from qtpy.QtWidgets import (QDialog, QHBoxLayout, QMessageBox,
- QLabel, QProgressBar, QPushButton, QVBoxLayout,
- QWidget)
+from packaging.version import parse
+from qtpy.QtCore import Qt, QThread, QTimer, Signal
+from qtpy.QtWidgets import QMessageBox, QWidget, QProgressBar, QPushButton
# Local imports
from spyder import __version__
+from spyder.api.config.mixins import SpyderConfigurationAccessor
from spyder.api.translations import _
from spyder.config.base import is_conda_based_app
-from spyder.utils.icon_manager import ima
-from spyder.workers.updates import WorkerDownloadInstaller
+from spyder.config.utils import is_anaconda
+from spyder.plugins.updatemanager.workers import (WorkerUpdate,
+ WorkerDownloadInstaller)
+from spyder.utils.conda import find_conda, is_anaconda_pkg
+from spyder.widgets.helperwidgets import MessageCheckBox
# Logger setup
logger = logging.getLogger(__name__)
@@ -36,6 +43,7 @@
PENDING = _("Update available")
CHECKING = _("Checking for updates")
CANCELLED = _("Cancelled update")
+INSTALL_ON_CLOSE = _("Install on close")
INSTALL_INFO_MESSAGES = {
DOWNLOADING_INSTALLER: _("Downloading Spyder {version}"),
@@ -44,87 +52,52 @@
FINISHED: _("Finished installing Spyder {version}"),
PENDING: _("Spyder {version} available to download"),
CHECKING: _("Checking for new Spyder version"),
- CANCELLED: _("Spyder update cancelled")
+ CANCELLED: _("Spyder update cancelled"),
+ INSTALL_ON_CLOSE: _("Install Spyder {version} on close")
}
+HEADER = _("Spyder {} is available!
")
+URL_I = 'https://docs.spyder-ide.org/current/installation.html'
+TMPDIR = osp.join(gettempdir(), 'spyder')
-class UpdateInstallation(QWidget):
- """Update progress installation widget."""
- def __init__(self, parent):
- super().__init__(parent)
- action_layout = QVBoxLayout()
- progress_layout = QHBoxLayout()
- self._progress_widget = QWidget(self)
- self._progress_widget.setFixedHeight(50)
- self._progress_bar = QProgressBar(self)
- self._progress_bar.setFixedWidth(180)
- self.cancel_button = QPushButton()
- self.cancel_button.setIcon(ima.icon('DialogCloseButton'))
- self.cancel_button.setFixedHeight(25)
- self.cancel_button.setFixedWidth(25)
- progress_layout.addWidget(self._progress_bar, alignment=Qt.AlignLeft)
- progress_layout.addWidget(self.cancel_button)
- progress_layout.setAlignment(Qt.AlignVCenter)
- self._progress_widget.setLayout(progress_layout)
-
- self._progress_label = QLabel(_('Downloading'))
-
- self.install_info = QLabel(
- _("Downloading Spyder update
"))
-
- button_layout = QHBoxLayout()
- self.ok_button = QPushButton(_('OK'))
- button_layout.addStretch()
- button_layout.addWidget(self.ok_button)
- button_layout.addStretch()
- action_layout.addStretch()
- action_layout.addWidget(self._progress_label)
- action_layout.addWidget(self._progress_widget)
- action_layout.addWidget(self.install_info)
- action_layout.addSpacing(10)
- action_layout.addLayout(button_layout)
- action_layout.addStretch()
-
- # Layout
- general_layout = QHBoxLayout()
- general_layout.addLayout(action_layout)
-
- self.setLayout(general_layout)
-
- def update_installation_status(self, status, latest_version):
- """Update installation status (downloading, installing, finished)."""
- self._progress_label.setText(status)
- self.install_info.setText(INSTALL_INFO_MESSAGES[status].format(
- version=latest_version))
- if status == INSTALLING:
- self._progress_bar.setRange(0, 0)
- self.cancel_button.setEnabled(False)
-
- def update_installation_progress(self, current_value, total):
- """Update installation progress bar."""
- self._progress_bar.setMaximum(total)
- self._progress_bar.setValue(current_value)
+class UpdateManagerWidget(QWidget, SpyderConfigurationAccessor):
+ """Check for updates widget"""
+
+ sig_disable_actions = Signal(bool)
+ """
+ Signal to disable plugin actions during check for update
+
+ Parameters
+ ----------
+ disable: bool
+ True to disable, False to re-enable
+ """
+ sig_block_status_signals = Signal(bool)
+ """
+ Signal to block signals from update manager status during
+ check for update
-class UpdateInstallerDialog(QDialog):
- """Update installer dialog."""
+ Parameters
+ ----------
+ block: bool
+ True to block, False to unblock
+ """
- sig_download_progress = Signal(int, int)
+ sig_download_progress = Signal(int)
"""
- Signal to get the download progress.
+ Signal to send the download progress.
Parameters
----------
- current_value: int
- Size of the data downloaded until now.
- total: int
- Total size of the file expected to be downloaded.
+ percent_progress: int
+ Percent of the data downloaded until now.
"""
- sig_installation_status = Signal(str, str)
+ sig_set_status = Signal(str, str)
"""
- Signal to get the current status of the update installation.
+ Signal to set the status of update manager.
Parameters
----------
@@ -134,184 +107,482 @@ class UpdateInstallerDialog(QDialog):
Latest release version detected.
"""
- sig_install_on_close_requested = Signal(str)
+ sig_install_on_close = Signal(bool)
"""
- Signal to request running the downloaded installer on close.
+ Signal to request running the install process on close.
Parameters
----------
- installer_path: str
- Path to the installer executable.
+ install_on_close: bool
+ Whether to install on close.
+ """
+
+ sig_quit_requested = Signal()
+ """
+ This signal can be emitted to request the main application to quit.
"""
def __init__(self, parent):
+ super().__init__(parent)
+ self.CONF_SECTION = parent.CONF_SECTION if parent else 'update_manager'
+
+ self.startup = None
+ self.update_thread = None
+ self.update_worker = None
+ self.update_timer = None
+ self.latest_release = None
+ self.major_update = None
+
self.cancelled = False
- self.status = NO_STATUS
self.download_thread = None
self.download_worker = None
+ self.progress_dialog = None
self.installer_path = None
+ self.installer_size_path = None
- super().__init__(parent)
- self.setWindowFlags(Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint)
- self._parent = parent
- self._installation_widget = UpdateInstallation(self)
- self.latest_release_version = ""
-
- # Layout
- installer_layout = QVBoxLayout()
- installer_layout.addWidget(self._installation_widget)
- self.setLayout(installer_layout)
-
- # Signals
- self.sig_download_progress.connect(
- self._installation_widget.update_installation_progress)
- self.sig_installation_status.connect(
- self._installation_widget.update_installation_status)
-
- self._installation_widget.ok_button.clicked.connect(
- self.close_installer)
- self._installation_widget.cancel_button.clicked.connect(
- self.cancel_installation)
-
- # Show installation widget
- self.setup()
-
- def reject(self):
- """Reimplemented Qt method."""
- on_installation_widget = self._installation_widget.isVisible()
- if on_installation_widget:
- self.close_installer()
+ # ---- General
+
+ def set_status(self, status=NO_STATUS):
+ """Set the update manager status."""
+ self.sig_set_status.emit(status, self.latest_release)
+
+ def cleanup_threads(self):
+ """Clean up QThreads"""
+ if self.update_timer is not None:
+ self.update_timer.stop()
+ if self.update_thread is not None:
+ self.update_thread.quit()
+ self.update_thread.wait()
+ if self.download_thread is not None:
+ self.download_worker.cancelled = True
+ self.download_thread.quit()
+ self.download_thread.wait()
+
+ # ---- Check Update
+
+ def start_check_update(self, startup=False):
+ """
+ Check for spyder updates using a QThread.
+
+ Update actions are disabled in the menubar and statusbar while
+ checking for updates
+
+ If startup is True, then checking for updates is delayed 1 min;
+ actions are disabled during this time as well.
+ """
+ logger.debug(f"Checking for updates. startup = {startup}.")
+
+ # Disable check_update_action while the thread is working
+ self.sig_disable_actions.emit(True)
+
+ self.startup = startup
+ self.cleanup_threads()
+
+ self.update_thread = QThread(None)
+ self.update_worker = WorkerUpdate(self.get_conf('check_stable_only'))
+ self.update_worker.sig_ready.connect(self._process_check_update)
+ self.update_worker.sig_ready.connect(self.update_thread.quit)
+ self.update_worker.sig_ready.connect(
+ lambda: self.sig_disable_actions.emit(False)
+ )
+ self.update_worker.moveToThread(self.update_thread)
+ self.update_thread.started.connect(lambda: self.set_status(CHECKING))
+ self.update_thread.started.connect(self.update_worker.start)
+
+ # Delay starting this check to avoid blocking the main window
+ # while loading.
+ # Fixes spyder-ide/spyder#15839
+ if self.startup:
+ self.update_timer = QTimer(self)
+ self.update_timer.setInterval(60000)
+ self.update_timer.setSingleShot(True)
+ self.sig_block_status_signals.emit(True)
+ self.update_timer.timeout.connect(
+ lambda: self.sig_block_status_signals.emit(False))
+ self.update_timer.timeout.connect(self.update_thread.start)
+ self.update_timer.start()
+ else:
+ # Otherwise, start immediately
+ self.update_thread.start()
+
+ def _process_check_update(self):
+ """Process the results of check update."""
+ # Get results from worker
+ update_available = self.update_worker.update_available
+ error_msg = self.update_worker.error
+ self.latest_release = self.update_worker.latest_release
+ self.major_update = (
+ parse(__version__).major < parse(self.latest_release).major
+ )
+ self._set_installer_path()
+
+ # Always set status, regardless of error, DEV, or startup
+ self.set_status(PENDING if update_available else NO_STATUS)
+
+ # self.startup = True is used on startup, so only positive feedback is
+ # given. self.startup = False is used after startup when using the menu
+ # action, and gives feeback if updates are, or are not found.
+ if (
+ self.startup and # startup and...
+ ('dev' in __version__ # current version is dev
+ or error_msg is not None # or there is an error
+ or not update_available) # or no updates available
+ ):
+ # Do not alert the user to anything
+ pass
+ elif error_msg is not None:
+ error_messagebox(self, error_msg)
+ elif update_available:
+ self.start_update()
+ else:
+ info_messagebox(self, _("Spyder is up to date."), checkbox=True)
+
+ def _set_installer_path(self):
+ """Set the emp file path for the downloaded installer"""
+ if os.name == 'nt':
+ plat, ext = 'Windows', 'exe'
+ if sys.platform == 'darwin':
+ plat, ext = 'macOS', 'pkg'
+ if sys.platform.startswith('linux'):
+ plat, ext = 'Linux', 'sh'
+ mach = platform.machine().lower().replace("amd64", "x86_64")
+ fname = f'Spyder-{self.latest_release}-{plat}-{mach}.{ext}'
+
+ dirname = osp.join(TMPDIR, 'updates', self.latest_release)
+ self.installer_path = osp.join(dirname, fname)
+ self.installer_size_path = osp.join(dirname, "size")
+
+ # ---- Download Update
+
+ def _verify_installer_path(self):
+ if (
+ osp.exists(self.installer_path)
+ and osp.exists(self.installer_size_path)
+ ):
+ with open(self.installer_size_path, "r") as f:
+ size = f.read().strip()
+ return size == osp.getsize(self.installer_path)
else:
- super().reject()
+ return False
- def setup(self):
- """Setup visibility of widgets."""
- self._installation_widget.setVisible(True)
- self.adjustSize()
+ def start_update(self):
+ """
+ Start the update process
+
+ Request input from user whether to download the installer; upon
+ affirmation, proceed with download then to confirm install.
- def save_latest_release(self, latest_release_version):
- self.latest_release_version = latest_release_version
+ If the installer is already downloaded, proceed to confirm install.
+ """
+ if self._verify_installer_path():
+ self.set_status(DOWNLOAD_FINISHED)
+ self._confirm_install()
+ elif not is_conda_based_app():
+ msg = _(
+ "Would you like to automatically download and "
+ "install it using Spyder's installer?"
+ "
"
+ "We recommend our own installer "
+ "because it's more stable and makes updating easy. "
+ "This will leave your existing Spyder installation "
+ "untouched."
+ ).format(URL_I + "#standalone-installers")
+ box = confirm_messagebox(
+ self, msg, version=self.latest_release, checkbox=True
+ )
+ if box.result() == QMessageBox.Yes:
+ self._start_download()
+ else:
+ manual_update_messagebox(
+ self, self.latest_release, self.update_worker.channel)
+ elif self.major_update:
+ msg = _("Would you like to automatically download "
+ "and install it?")
+ box = confirm_messagebox(
+ self, msg, version=self.latest_release, checkbox=True
+ )
+ if box.result() == QMessageBox.Yes:
+ self._start_download()
+ else:
+ # Minor release for conda-based application will update with conda
+ self._confirm_install()
- def start_installation(self, latest_release_version):
- """Start downloading the update and set downloading status."""
- self.latest_release_version = latest_release_version
+ def _start_download(self):
+ """
+ Start downloading the installer in a QThread
+ and set downloading status.
+ """
self.cancelled = False
- self._change_update_installation_status(
- status=DOWNLOADING_INSTALLER)
- self.download_thread = QThread(None)
self.download_worker = WorkerDownloadInstaller(
- self, self.latest_release_version)
- self.download_worker.sig_ready.connect(self.confirm_installation)
+ self.latest_release, self.installer_path, self.installer_size_path)
+
+ self.sig_disable_actions.emit(True)
+ self.set_status(DOWNLOADING_INSTALLER)
+
+ self.progress_dialog = ProgressDialog(
+ self, _("Downloading Spyder {} ...").format(self.latest_release)
+ )
+ self.progress_dialog.cancel.clicked.connect(self._cancel_download)
+
+ self.download_thread = QThread(None)
+ self.download_worker.sig_ready.connect(self._confirm_install)
self.download_worker.sig_ready.connect(self.download_thread.quit)
+ self.download_worker.sig_ready.connect(
+ lambda: self.sig_disable_actions.emit(False)
+ )
self.download_worker.sig_download_progress.connect(
- self.sig_download_progress.emit)
+ self._update_download_progress)
self.download_worker.moveToThread(self.download_thread)
self.download_thread.started.connect(self.download_worker.start)
self.download_thread.start()
- def cancel_installation(self):
+ def show_progress_dialog(self, show=True):
+ """Show download progress if previously hidden"""
+ if self.progress_dialog is not None:
+ if show:
+ self.progress_dialog.show()
+ else:
+ self.progress_dialog.hide()
+
+ def _update_download_progress(self, progress, total):
+ """Update download progress in dialog and status bar"""
+ if self.progress_dialog is not None:
+ self.progress_dialog.update_progress(progress, total)
+ if progress == total:
+ self.progress_dialog.accept()
+
+ percent_progress = 0
+ if total > 0:
+ percent_progress = round((progress / total) * 100)
+ self.sig_download_progress.emit(percent_progress)
+
+ def _cancel_download(self):
"""Cancel the installation in progress."""
- reply = QMessageBox.critical(
- self._parent, 'Spyder',
- _('Do you really want to cancel the Spyder update installation?'),
- QMessageBox.Yes, QMessageBox.No)
- if reply == QMessageBox.Yes:
+ self.download_worker.paused = True
+ msg = _('Do you really want to cancel the download?')
+ box = confirm_messagebox(self, msg, critical=True)
+ if box.result() == QMessageBox.Yes:
self.cancelled = True
- self._cancel_download()
- self.finish_installation()
- return True
- return False
-
- def continue_installation(self):
- """
- Continue the installation in progress.
-
- Download the installer if needed or prompt to install.
- """
- reply = QMessageBox(icon=QMessageBox.Question,
- text=_("Would you like to update Spyder to "
- "the latest version?"
- "
"),
- parent=self._parent)
- reply.setWindowTitle("Spyder")
- reply.setAttribute(Qt.WA_ShowWithoutActivating)
- reply.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
- reply.exec_()
- if reply.result() == QMessageBox.Yes:
- self.start_installation(self.latest_release_version)
+ self.cleanup_threads()
+ self.set_status(PENDING)
else:
- self._change_update_installation_status(status=PENDING)
+ self.progress_dialog.show()
+ self.download_worker.paused = False
- def confirm_installation(self, installer_path):
+ def _confirm_install(self):
"""
- Ask users if they want to proceed with the installer execution.
+ Ask users if they want to proceed with the install immediately
+ or on close.
"""
if self.cancelled:
return
- self._change_update_installation_status(status=DOWNLOAD_FINISHED)
- self.installer_path = installer_path
- msg_box = QMessageBox(
- icon=QMessageBox.Question,
- text=_("Would you like to proceed with the installation?
"),
- parent=self._parent
- )
- msg_box.setWindowTitle(_("Spyder update"))
- msg_box.setAttribute(Qt.WA_ShowWithoutActivating)
- if os.name == 'nt' and is_conda_based_app():
- # Only add yes button for Windows installer
- # since it has the logic to restart Spyder
- yes_button = msg_box.addButton(QMessageBox.Yes)
- else:
- yes_button = None
- after_closing_button = msg_box.addButton(
- _("After closing"), QMessageBox.YesRole)
- msg_box.addButton(QMessageBox.No)
- msg_box.exec_()
-
- if msg_box.clickedButton() == yes_button:
- self._change_update_installation_status(status=INSTALLING)
- cmd = ('start' if os.name == 'nt' else 'open')
- if self.installer_path:
- subprocess.Popen(
- ' '.join([cmd, self.installer_path]),
- shell=True
- )
- self._change_update_installation_status(status=PENDING)
- elif msg_box.clickedButton() == after_closing_button:
- self.sig_install_on_close_requested.emit(self.installer_path)
- self._change_update_installation_status(status=PENDING)
+
+ if self.download_worker:
+ if self.download_worker.error:
+ # If download error, do not proceed with install
+ self.progress_dialog.reject()
+ self.set_status(PENDING)
+ error_messagebox(self, self.download_worker.error)
+ return
+ else:
+ self.set_status(DOWNLOAD_FINISHED)
+
+ msg = _("Would you like to install it?")
+ box = confirm_messagebox(
+ self, msg, version=self.latest_release, on_close=True)
+ if box.result() == QMessageBox.Yes:
+ self.sig_install_on_close.emit(True)
+ self.sig_quit_requested.emit()
+ elif box.result() == 0: # 0 is result of 3rd push-button
+ self.sig_install_on_close.emit(True)
+ self.set_status(INSTALL_ON_CLOSE)
+
+ def start_install(self):
+ """Install from downloaded installer or update through conda."""
+
+ # Install script
+ script = osp.abspath(__file__ + '/../../scripts/install.' +
+ ('bat' if os.name == 'nt' else 'sh'))
+
+ # Sub command
+ sub_cmd = [script, '-p', sys.prefix]
+ if osp.exists(self.installer_path):
+ # Run downloaded installer
+ sub_cmd.extend(['-i', self.installer_path])
+ elif self.latest_release is not None:
+ # Update with conda
+ sub_cmd.extend(['-c', find_conda(), '-v', self.latest_release])
+
+ # Final command assembly
+ if os.name == 'nt':
+ cmd = ['start', '"Update Spyder"'] + sub_cmd
+ elif sys.platform == 'darwin':
+ # Terminal cannot accept a command with arguments therefore
+ # create a temporary script
+ tmpdir = osp.join(gettempdir(), 'spyder')
+ tmpscript = osp.join(tmpdir, 'tmp_install.sh')
+ os.makedirs(tmpdir, exist_ok=True)
+ with open(tmpscript, 'w') as f:
+ f.write(' '.join(sub_cmd))
+ os.chmod(tmpscript, 0o711) # set executable permissions
+
+ cmd = ['open', '-b', 'com.apple.terminal', tmpscript]
else:
- self._change_update_installation_status(status=PENDING)
+ cmd = ['gnome-terminal', '--window', '--'] + sub_cmd
- def finish_installation(self):
- """Handle finished installation."""
- self.setup()
- self.accept()
+ subprocess.Popen(' '.join(cmd), shell=True)
- def close_installer(self):
- """Close the installation dialog."""
- if (
- self.status == FINISHED
- or self.status == CANCELLED
- ):
- self.finish_installation()
- else:
- self.hide()
-
- def _change_update_installation_status(self, status=NO_STATUS):
- """Set the installation status."""
- logger.debug(f"Installation status: {status}")
- self.status = status
- if status == DOWNLOAD_FINISHED:
- self.close_installer()
- elif status == FINISHED or status == PENDING:
- self.finish_installation()
- self.sig_installation_status.emit(
- self.status, self.latest_release_version)
- def _cancel_download(self):
- self._change_update_installation_status(status=CANCELLED)
- self.download_worker.cancelled = True
- self.download_thread.quit()
- self.download_thread.wait()
- self._change_update_installation_status(status=PENDING)
+class UpdateMessageBox(QMessageBox):
+ def __init__(self, icon=None, text=None, parent=None):
+ super().__init__(icon=icon, text=text, parent=parent)
+ self.setWindowModality(Qt.NonModal)
+ self.setWindowTitle(_("Spyder Update Manager"))
+ self.setTextFormat(Qt.RichText)
+
+
+class UpdateMessageCheckBox(MessageCheckBox):
+ def __init__(self, icon=None, text=None, parent=None):
+ super().__init__(icon=icon, text=text, parent=parent)
+ self.setWindowTitle(_("Spyder Update Manager"))
+ self.setTextFormat(Qt.RichText)
+ self._parent = parent
+ self.set_checkbox_text(_("Check for updates at startup"))
+ self.option = 'check_updates_on_startup'
+ self.accepted.connect(self.accept) # ??? Why is the signal necessary?
+ if self._parent is not None:
+ self.set_checked(parent.get_conf(self.option))
+
+ def accept(self):
+ if self._parent is not None:
+ self._parent.set_conf(self.option, self.is_checked())
+
+
+class ProgressDialog(UpdateMessageBox):
+ """Update progress installation dialog."""
+
+ def __init__(self, parent, text):
+ super().__init__(icon=QMessageBox.NoIcon, text=text, parent=parent)
+
+ self._progress_bar = QProgressBar(self)
+ self._progress_bar.setMinimumWidth(250)
+ self._progress_bar.setFixedHeight(15)
+
+ layout = self.layout()
+ layout.addWidget(self._progress_bar, 1, 1)
+
+ self.cancel = QPushButton(_("Cancel"))
+ self.okay = QPushButton(_("OK"))
+ self.addButton(self.okay, QMessageBox.YesRole)
+ self.addButton(self.cancel, QMessageBox.NoRole)
+ self.setDefaultButton(self.okay)
+
+ self.show()
+
+ def update_progress(self, progress, total):
+ """Update installation progress bar."""
+ self._progress_bar.setMaximum(total)
+ self._progress_bar.setValue(progress)
+
+
+def error_messagebox(parent, error_msg):
+ box = UpdateMessageBox(
+ icon=QMessageBox.Warning, text=error_msg, parent=parent)
+ box.setStandardButtons(QMessageBox.Ok)
+ box.setDefaultButton(QMessageBox.Ok)
+ box.show()
+ return box
+
+
+def info_messagebox(parent, message, version=None, checkbox=False):
+ box_class = UpdateMessageCheckBox if checkbox else UpdateMessageBox
+ message = HEADER.format(version) + message if version else message
+ box = box_class(icon=QMessageBox.Information, text=message, parent=parent)
+ box.setStandardButtons(QMessageBox.Ok)
+ box.setDefaultButton(QMessageBox.Ok)
+ box.show()
+ return box
+
+
+def confirm_messagebox(parent, message, version=None, critical=False,
+ checkbox=False, on_close=False):
+ box_class = UpdateMessageCheckBox if checkbox else UpdateMessageBox
+ message = HEADER.format(version) + message if version else message
+ box = box_class(
+ icon=QMessageBox.Critical if critical else QMessageBox.Question,
+ text=message, parent=parent)
+ box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
+ box.setDefaultButton(QMessageBox.Yes)
+ if on_close:
+ box.addButton(_("After closing"), QMessageBox.YesRole)
+ box.exec()
+ return box
+
+
+def manual_update_messagebox(parent, latest_release, channel):
+ msg = ""
+ if os.name == "nt":
+ if is_anaconda():
+ msg += _("Run the following command or commands in "
+ "the Anaconda prompt to update manually:"
+ "
")
+ else:
+ msg += _("Run the following command in a cmd prompt "
+ "to update manually:
")
+ else:
+ if is_anaconda():
+ msg += _("Run the following command or commands in a "
+ "terminal to update manually:
")
+ else:
+ msg += _("Run the following command in a terminal to "
+ "update manually:
")
+
+ if is_anaconda():
+ is_pypi = channel == 'pypi'
+
+ if is_anaconda_pkg() and not is_pypi:
+ msg += "conda update anaconda
"
+
+ if is_pypi:
+ dont_mix_pip_conda_video = (
+ "https://youtu.be/Ul79ihg41Rs"
+ )
+
+ msg += (
+ "pip install --upgrade spyder
"
+ "
"
+ )
+
+ msg += _(
+ "Important note: You installed Spyder with "
+ "pip in a Conda environment, which is not a good "
+ "idea. See our video for more "
+ "details about it."
+ ).format(dont_mix_pip_conda_video)
+ else:
+ if channel == 'pkgs/main':
+ channel = ''
+ else:
+ channel = f'-c {channel}'
+
+ msg += (
+ f"conda install {channel} "
+ f"spyder={latest_release}"
+ f"
"
+ )
+
+ msg += _(
+ "Important note: Since you installed "
+ "Spyder with Anaconda, please don't use pip "
+ "to update it as that will break your "
+ "installation."
+ )
+ else:
+ msg += "pip install --upgrade spyder
"
+
+ msg += _(
+ "
For more information, visit our "
+ "installation guide."
+ ).format(URL_I)
+
+ info_messagebox(parent, msg)
diff --git a/spyder/plugins/updatemanager/workers.py b/spyder/plugins/updatemanager/workers.py
index a3643bcae25..9e059c07ddb 100644
--- a/spyder/plugins/updatemanager/workers.py
+++ b/spyder/plugins/updatemanager/workers.py
@@ -8,7 +8,7 @@
import logging
import os
import os.path as osp
-import tempfile
+from time import sleep
import traceback
# Third party imports
@@ -18,10 +18,10 @@
# Local imports
from spyder import __version__
-from spyder.config.base import _, is_conda_based_app, is_stable_version
+from spyder.config.base import _, is_stable_version, is_conda_based_app
from spyder.config.utils import is_anaconda
from spyder.utils.conda import get_spyder_conda_channel
-from spyder.utils.programs import check_version, is_module_installed
+from spyder.utils.programs import check_version
# Logger setup
logger = logging.getLogger(__name__)
@@ -53,7 +53,7 @@ class UpdateDownloadIncompleteError(Exception):
pass
-class WorkerUpdates(QObject):
+class WorkerUpdate(QObject):
"""
Worker that checks for releases using either the Anaconda
default channels or the Github Releases page without
@@ -62,102 +62,84 @@ class WorkerUpdates(QObject):
"""
sig_ready = Signal()
- def __init__(self, parent, startup, version="", releases=None):
- QObject.__init__(self)
- self._parent = parent
- self.error = None
+ def __init__(self, stable_only):
+ super().__init__()
+ self.stable_only = stable_only
self.latest_release = None
- self.startup = startup
- self.releases = releases
-
- if not version:
- self.version = __version__
- else:
- self.version = version
-
- def check_update_available(self):
- """
- Check if there is an update available.
-
- It takes as parameters the current version of Spyder and a list of
- valid cleaned releases in chronological order.
- Example: ['2.3.2', '2.3.3' ...] or with github ['2.3.4', '2.3.3' ...]
- """
- # Don't perform any check for development versions or we were unable to
- # detect releases.
- if 'dev' in self.version or not self.releases:
- return (False, self.latest_release)
+ self.releases = None
+ self.update_available = None
+ self.error = None
+ self.channel = None
+ def _check_update_available(self):
+ """Checks if there is an update available from releases."""
# Filter releases
- if is_stable_version(self.version):
- releases = [r for r in self.releases if is_stable_version(r)]
- else:
- releases = [r for r in self.releases
- if not is_stable_version(r) or r in self.version]
+ releases = self.releases.copy()
+ if self.stable_only:
+ # Only use stable releases
+ releases = [r for r in releases if is_stable_version(r)]
+ logger.debug(f"Available versions: {self.releases}")
- latest_release = releases[-1]
+ self.latest_release = releases[-1] if releases else __version__
+ self.update_available = check_version(__version__,
+ self.latest_release, '<')
- return (check_version(self.version, latest_release, '<'),
- latest_release)
+ logger.debug(f"Update available: {self.update_available}")
+ logger.debug(f"Latest release: {self.latest_release}")
def start(self):
"""Main method of the worker."""
+ self.error = None
+ self.latest_release = None
self.update_available = False
- self.latest_release = __version__
-
error_msg = None
pypi_url = "https://pypi.org/pypi/spyder/json"
if is_conda_based_app():
- self.url = ('https://api.github.com/repos/'
- 'spyder-ide/spyder/releases')
+ url = ('https://api.github.com/repos/spyder-ide/spyder/releases')
elif is_anaconda():
- channel, channel_url = get_spyder_conda_channel()
+ self.channel, channel_url = get_spyder_conda_channel()
- if channel_url is None:
- return
- elif channel == "pypi":
- self.url = pypi_url
+ if channel_url is None or self.channel == "pypi":
+ url = pypi_url
else:
- self.url = channel_url + '/channeldata.json'
+ url = channel_url + '/channeldata.json'
else:
- self.url = pypi_url
+ url = pypi_url
+ logger.info(f"Checking for updates from {url}")
try:
- logger.debug(f"Checking for updates from {self.url}")
- page = requests.get(self.url)
+ page = requests.get(url)
page.raise_for_status()
data = page.json()
- if is_conda_based_app():
- if self.releases is None:
+ if self.releases is None:
+ if is_conda_based_app():
self.releases = [
item['tag_name'].replace('v', '') for item in data
]
self.releases = list(reversed(self.releases))
- elif is_anaconda() and self.url != pypi_url:
- if self.releases is None:
+ elif is_anaconda() and url != pypi_url:
spyder_data = data['packages'].get('spyder')
if spyder_data:
self.releases = [spyder_data["version"]]
- else:
- if self.releases is None:
+ else:
self.releases = [data['info']['version']]
- result = self.check_update_available()
- self.update_available, self.latest_release = result
+ self._check_update_available()
except SSLError as err:
error_msg = SSL_ERROR_MSG
- logger.debug(err, stack_info=True)
+ logger.exception(err)
except ConnectionError as err:
error_msg = CONNECT_ERROR_MSG
- logger.debug(err, stack_info=True)
+ logger.exception(err)
except HTTPError as err:
- error_msg = HTTP_ERROR_MSG.format(status_code=page.status_code)
- logger.debug(err, stack_info=True)
+ error_msg = HTTP_ERROR_MSG.format(page.status_code)
+ logger.exception(err)
except Exception as err:
error = traceback.format_exc()
- formatted_error = error.replace('\n', '
').replace(' ', ' ')
+ formatted_error = (error.replace('\n', '
')
+ .replace(' ', ' '))
error_msg = _(
'It was not possible to check for Spyder updates due to the '
@@ -165,36 +147,24 @@ def start(self):
'
'
'{}'
).format(formatted_error)
- logger.debug(err, stack_info=True)
-
- # Don't show dialog when starting up spyder and an error occur
- if not (self.startup and error_msg is not None):
+ logger.exception(err)
+ finally:
self.error = error_msg
- try:
- self.sig_ready.emit()
- except RuntimeError:
- pass
+ self.sig_ready.emit()
class WorkerDownloadInstaller(QObject):
"""
- Worker that donwloads standalone installers for Windows
- and MacOS without blocking the Spyder user interface.
- """
-
- sig_ready = Signal(str)
+ Worker that donwloads standalone installers for Windows, macOS,
+ and Linux without blocking the Spyder user interface.
"""
- Signal to inform that the worker has finished successfully.
- Parameters
- ----------
- installer_path: str
- Path where the downloaded installer is located.
- """
+ sig_ready = Signal()
+ """Signal to inform that the worker has finished successfully."""
sig_download_progress = Signal(int, int)
"""
- Signal to get the download progress.
+ Signal to send the download progress.
Parameters
----------
@@ -204,98 +174,87 @@ class WorkerDownloadInstaller(QObject):
Total size of the file expected to be downloaded.
"""
- def __init__(self, parent, latest_release_version):
- QObject.__init__(self)
- self._parent = parent
- self.latest_release_version = latest_release_version
+ def __init__(self, latest_release, installer_path, installer_size_path):
+ super().__init__()
+ self.latest_release = latest_release
+ self.installer_path = installer_path
+ self.installer_size_path = installer_size_path
self.error = None
self.cancelled = False
- self.installer_path = None
+ self.paused = False
- def _progress_reporter(self, block_number, read_size, total_size):
+ def _progress_reporter(self, progress, total_size):
"""Calculate download progress and notify."""
- progress = 0
- if total_size > 0:
- progress = block_number * read_size
+ self.sig_download_progress.emit(progress, total_size)
+
+ while self.paused and not self.cancelled:
+ sleep(0.5)
+
if self.cancelled:
raise UpdateDownloadCancelledException()
- self.sig_download_progress.emit(progress, total_size)
def _download_installer(self):
- """Donwload latest Spyder standalone installer executable."""
- tmpdir = tempfile.gettempdir()
- is_full_installer = (is_module_installed('numpy') or
- is_module_installed('pandas'))
- if os.name == 'nt':
- name = 'Spyder_64bit_{}.exe'.format('full' if is_full_installer
- else 'lite')
- else:
- name = 'Spyder{}.dmg'.format('' if is_full_installer else '-Lite')
-
- url = ('https://github.com/spyder-ide/spyder/releases/latest/'
- f'download/{name}')
- dir_path = osp.join(tmpdir, 'spyder', 'updates')
- os.makedirs(dir_path, exist_ok=True)
- installer_dir_path = osp.join(
- dir_path, self.latest_release_version)
- os.makedirs(installer_dir_path, exist_ok=True)
- for file in os.listdir(dir_path):
- if file not in [__version__, self.latest_release_version]:
- remove = osp.join(dir_path, file)
- os.remove(remove)
-
- installer_path = osp.join(installer_dir_path, name)
- self.installer_path = installer_path
+ """Donwload Spyder installer."""
+ url = (
+ 'https://github.com/spyder-ide/spyder/releases/download/'
+ f'v{self.latest_release}/{osp.basename(self.installer_path)}'
+ )
+ logger.info(f"Downloading installer from {url} "
+ f"to {self.installer_path}")
- if osp.isfile(installer_path):
- # Installer already downloaded
- logger.info(f"{installer_path} already downloaded")
- self._progress_reporter(1, 1, 1)
- return
+ dirname = osp.dirname(self.installer_path)
+ os.makedirs(dirname, exist_ok=True)
- logger.debug(f"Downloading installer from {url} to {installer_path}")
with requests.get(url, stream=True) as r:
- with open(installer_path, 'wb') as f:
+ r.raise_for_status()
+ size = -1
+ if "content-length" in r.headers:
+ size = int(r.headers["content-length"])
+ self._progress_reporter(0, size)
+
+ with open(self.installer_path, 'wb') as f:
chunk_size = 8 * 1024
- size = -1
size_read = 0
- chunk_num = 0
-
- if "content-length" in r.headers:
- size = int(r.headers["content-length"])
-
- self._progress_reporter(chunk_num, chunk_size, size)
-
for chunk in r.iter_content(chunk_size=chunk_size):
size_read += len(chunk)
f.write(chunk)
- chunk_num += 1
- self._progress_reporter(chunk_num, chunk_size, size)
+ self._progress_reporter(size_read, size)
- if size >= 0 and size_read < size:
- raise UpdateDownloadIncompleteError(
- "Download incomplete: retrieved only "
- f"{size_read} out of {size} bytes."
- )
+ if size_read == size:
+ logger.info('Download successfully completed.')
+ with open(self.installer_size_path, "w") as f:
+ f.write(str(size))
+ else:
+ raise UpdateDownloadIncompleteError(
+ "Download incomplete: retrieved only "
+ f"{size_read} out of {size} bytes."
+ )
+
+ def _clean_installer_path(self):
+ """Remove downloaded file"""
+ if osp.exists(self.installer_path):
+ os.remove(self.installer_path)
+ if osp.exists(self.installer_size_path):
+ os.remove(self.installer_size_path)
def start(self):
- """Main method of the WorkerDownloadInstaller worker."""
+ """Main method of the worker."""
error_msg = None
try:
self._download_installer()
except UpdateDownloadCancelledException:
- if self.installer_path:
- os.remove(self.installer_path)
- return
+ logger.info("Download cancelled")
+ self._clean_installer_path()
except SSLError as err:
error_msg = SSL_ERROR_MSG
- logger.debug(err, stack_info=True)
+ logger.exception(err)
except ConnectionError as err:
error_msg = CONNECT_ERROR_MSG
- logger.debug(err, stack_info=True)
+ logger.exception(err)
except Exception as err:
error = traceback.format_exc()
- formatted_error = error.replace('\n', '
').replace(' ', ' ')
+ formatted_error = (error.replace('\n', '
')
+ .replace(' ', ' '))
error_msg = _(
'It was not possible to download the installer due to the '
@@ -303,9 +262,8 @@ def start(self):
'
'
'{}'
).format(formatted_error)
- logger.debug(err, stack_info=True)
- self.error = error_msg
- try:
- self.sig_ready.emit(self.installer_path)
- except RuntimeError:
- pass
+ logger.exception(err)
+ self._clean_installer_path()
+ finally:
+ self.error = error_msg
+ self.sig_ready.emit()
From 4f25a57978d7e003eadd10ce87cd3f2045e06664 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 13 Nov 2023 21:41:12 -0800
Subject: [PATCH 04/22] Do not check for updates on startup if development
version
---
spyder/plugins/updatemanager/plugin.py | 7 ++++++-
spyder/plugins/updatemanager/widgets/update.py | 3 +--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/spyder/plugins/updatemanager/plugin.py b/spyder/plugins/updatemanager/plugin.py
index 23ed346abeb..d3ad933fa8e 100644
--- a/spyder/plugins/updatemanager/plugin.py
+++ b/spyder/plugins/updatemanager/plugin.py
@@ -9,6 +9,7 @@
"""
# Local imports
+from spyder import __version__
from spyder.api.plugins import Plugins, SpyderPluginV2
from spyder.api.translations import _
from spyder.api.plugin_registration.decorators import (
@@ -94,7 +95,11 @@ def on_mainwindow_visible(self):
container = self.get_container()
# Check for updates on startup
- if DEV is None and self.get_conf('check_updates_on_startup'):
+ if (
+ DEV is None # Not bootstrap
+ and 'dev' not in __version__ # Not dev version
+ and self.get_conf('check_updates_on_startup')
+ ):
container.start_check_updates(startup=True)
# ---- Private API
diff --git a/spyder/plugins/updatemanager/widgets/update.py b/spyder/plugins/updatemanager/widgets/update.py
index a3ecc3d4bc5..8d2d87b394f 100644
--- a/spyder/plugins/updatemanager/widgets/update.py
+++ b/spyder/plugins/updatemanager/widgets/update.py
@@ -224,8 +224,7 @@ def _process_check_update(self):
# action, and gives feeback if updates are, or are not found.
if (
self.startup and # startup and...
- ('dev' in __version__ # current version is dev
- or error_msg is not None # or there is an error
+ (error_msg is not None # there is an error
or not update_available) # or no updates available
):
# Do not alert the user to anything
From e10fea167ec8350b0df35caa900b7ca688d7156d Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 13 Nov 2023 21:42:54 -0800
Subject: [PATCH 05/22] Fix issue where versions were not sorted in release
order
---
spyder/plugins/updatemanager/workers.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/spyder/plugins/updatemanager/workers.py b/spyder/plugins/updatemanager/workers.py
index 9e059c07ddb..2f44d625e30 100644
--- a/spyder/plugins/updatemanager/workers.py
+++ b/spyder/plugins/updatemanager/workers.py
@@ -12,6 +12,7 @@
import traceback
# Third party imports
+from packaging.version import parse
from qtpy.QtCore import QObject, Signal
import requests
from requests.exceptions import ConnectionError, HTTPError, SSLError
@@ -96,7 +97,7 @@ def start(self):
pypi_url = "https://pypi.org/pypi/spyder/json"
if is_conda_based_app():
- url = ('https://api.github.com/repos/spyder-ide/spyder/releases')
+ url = 'https://api.github.com/repos/spyder-ide/spyder/releases'
elif is_anaconda():
self.channel, channel_url = get_spyder_conda_channel()
@@ -118,13 +119,13 @@ def start(self):
self.releases = [
item['tag_name'].replace('v', '') for item in data
]
- self.releases = list(reversed(self.releases))
elif is_anaconda() and url != pypi_url:
spyder_data = data['packages'].get('spyder')
if spyder_data:
self.releases = [spyder_data["version"]]
else:
self.releases = [data['info']['version']]
+ self.releases.sort(key=parse)
self._check_update_available()
except SSLError as err:
From 55f7960587570598fb1c84e2746665cdbcc7f8eb Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 13 Nov 2023 21:47:29 -0800
Subject: [PATCH 06/22] Set latest_release, installer_path, and compute
major_release in start_update. This ensures that they are assigned only if
there is an update available and after error is processed. Otherwise, compute
major_release can raise error before error_msg is processed.
---
spyder/plugins/updatemanager/widgets/update.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/spyder/plugins/updatemanager/widgets/update.py b/spyder/plugins/updatemanager/widgets/update.py
index 8d2d87b394f..13809c9fc82 100644
--- a/spyder/plugins/updatemanager/widgets/update.py
+++ b/spyder/plugins/updatemanager/widgets/update.py
@@ -131,7 +131,6 @@ def __init__(self, parent):
self.update_worker = None
self.update_timer = None
self.latest_release = None
- self.major_update = None
self.cancelled = False
self.download_thread = None
@@ -210,11 +209,6 @@ def _process_check_update(self):
# Get results from worker
update_available = self.update_worker.update_available
error_msg = self.update_worker.error
- self.latest_release = self.update_worker.latest_release
- self.major_update = (
- parse(__version__).major < parse(self.latest_release).major
- )
- self._set_installer_path()
# Always set status, regardless of error, DEV, or startup
self.set_status(PENDING if update_available else NO_STATUS)
@@ -273,6 +267,12 @@ def start_update(self):
If the installer is already downloaded, proceed to confirm install.
"""
+ self.latest_release = self.update_worker.latest_release
+ self._set_installer_path()
+ major_update = (
+ parse(__version__).major < parse(self.latest_release).major
+ )
+
if self._verify_installer_path():
self.set_status(DOWNLOAD_FINISHED)
self._confirm_install()
@@ -294,7 +294,7 @@ def start_update(self):
else:
manual_update_messagebox(
self, self.latest_release, self.update_worker.channel)
- elif self.major_update:
+ elif major_update:
msg = _("Would you like to automatically download "
"and install it?")
box = confirm_messagebox(
From 8dfe4fa255c27a25d2a1b5d22a2e102dbea162f3 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 15 Nov 2023 19:47:45 -0800
Subject: [PATCH 07/22] Fix typo that prevented checking for updates at
startup.
---
spyder/plugins/updatemanager/plugin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/plugins/updatemanager/plugin.py b/spyder/plugins/updatemanager/plugin.py
index d3ad933fa8e..01931fa0489 100644
--- a/spyder/plugins/updatemanager/plugin.py
+++ b/spyder/plugins/updatemanager/plugin.py
@@ -100,7 +100,7 @@ def on_mainwindow_visible(self):
and 'dev' not in __version__ # Not dev version
and self.get_conf('check_updates_on_startup')
):
- container.start_check_updates(startup=True)
+ container.start_check_update(startup=True)
# ---- Private API
# ------------------------------------------------------------------------
From 08a859482394c9c6f1c87916d516201a0d455ca8 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 16 Nov 2023 15:57:43 -0800
Subject: [PATCH 08/22] Use warning instead of exception or error logging
level. exception and error logging levels trigger Spyder's Issue reporter.
---
spyder/plugins/updatemanager/workers.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/spyder/plugins/updatemanager/workers.py b/spyder/plugins/updatemanager/workers.py
index 2f44d625e30..a9c7bf6c3b5 100644
--- a/spyder/plugins/updatemanager/workers.py
+++ b/spyder/plugins/updatemanager/workers.py
@@ -130,13 +130,13 @@ def start(self):
self._check_update_available()
except SSLError as err:
error_msg = SSL_ERROR_MSG
- logger.exception(err)
+ logger.warning(err, exc_info=err)
except ConnectionError as err:
error_msg = CONNECT_ERROR_MSG
- logger.exception(err)
+ logger.warning(err, exc_info=err)
except HTTPError as err:
error_msg = HTTP_ERROR_MSG.format(page.status_code)
- logger.exception(err)
+ logger.warning(err, exc_info=err)
except Exception as err:
error = traceback.format_exc()
formatted_error = (error.replace('\n', '
')
@@ -148,7 +148,7 @@ def start(self):
'
'
'{}'
).format(formatted_error)
- logger.exception(err)
+ logger.warning(err, exc_info=err)
finally:
self.error = error_msg
self.sig_ready.emit()
@@ -248,10 +248,10 @@ def start(self):
self._clean_installer_path()
except SSLError as err:
error_msg = SSL_ERROR_MSG
- logger.exception(err)
+ logger.warning(err, exc_info=err)
except ConnectionError as err:
error_msg = CONNECT_ERROR_MSG
- logger.exception(err)
+ logger.warning(err, exc_info=err)
except Exception as err:
error = traceback.format_exc()
formatted_error = (error.replace('\n', '
')
@@ -263,7 +263,7 @@ def start(self):
'
'
'{}'
).format(formatted_error)
- logger.exception(err)
+ logger.warning(err, exc_info=err)
self._clean_installer_path()
finally:
self.error = error_msg
From d2b875ac36ba963cf3473dfc22c1114bbf6a0b83 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 16 Nov 2023 16:34:11 -0800
Subject: [PATCH 09/22] Fix issue where download size verification was not int
type
---
spyder/plugins/updatemanager/widgets/update.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/plugins/updatemanager/widgets/update.py b/spyder/plugins/updatemanager/widgets/update.py
index 13809c9fc82..25ba6734fc8 100644
--- a/spyder/plugins/updatemanager/widgets/update.py
+++ b/spyder/plugins/updatemanager/widgets/update.py
@@ -253,7 +253,7 @@ def _verify_installer_path(self):
and osp.exists(self.installer_size_path)
):
with open(self.installer_size_path, "r") as f:
- size = f.read().strip()
+ size = int(f.read().strip())
return size == osp.getsize(self.installer_path)
else:
return False
From b9958f12ffd86c359eec237d0ea8b545c50cb4fc Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sun, 26 Nov 2023 07:52:09 -0800
Subject: [PATCH 10/22] Fix KeyError when applying string format
---
spyder/plugins/updatemanager/workers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/plugins/updatemanager/workers.py b/spyder/plugins/updatemanager/workers.py
index a9c7bf6c3b5..011e7951f0c 100644
--- a/spyder/plugins/updatemanager/workers.py
+++ b/spyder/plugins/updatemanager/workers.py
@@ -135,7 +135,7 @@ def start(self):
error_msg = CONNECT_ERROR_MSG
logger.warning(err, exc_info=err)
except HTTPError as err:
- error_msg = HTTP_ERROR_MSG.format(page.status_code)
+ error_msg = HTTP_ERROR_MSG.format(status_code=page.status_code)
logger.warning(err, exc_info=err)
except Exception as err:
error = traceback.format_exc()
From 5bb28e3166d2aa8f3c6d8fa59c8111dc590a2a6e Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 28 Nov 2023 11:37:04 -0800
Subject: [PATCH 11/22] Do not check for custom_widget after initialization.
UpdateManagerStatus will always have custom_widget and spinner attributes
after initialization.
---
spyder/plugins/updatemanager/widgets/status.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/spyder/plugins/updatemanager/widgets/status.py b/spyder/plugins/updatemanager/widgets/status.py
index 35fff755a53..67080dbbecc 100644
--- a/spyder/plugins/updatemanager/widgets/status.py
+++ b/spyder/plugins/updatemanager/widgets/status.py
@@ -81,8 +81,7 @@ def set_value(self, value):
self.custom_widget.show()
elif value == CHECKING:
self.tooltip = self.BASE_TOOLTIP
- if self.custom_widget:
- self.custom_widget.hide()
+ self.custom_widget.hide()
self.spinner.show()
self.spinner.start()
elif value == PENDING:
From 28ff22fcda5f13ca88ae1bd1def327798949cd99 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 28 Nov 2023 11:52:00 -0800
Subject: [PATCH 12/22] Use cb suffix for checkbox object names
---
spyder/plugins/updatemanager/confpage.py | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/spyder/plugins/updatemanager/confpage.py b/spyder/plugins/updatemanager/confpage.py
index e1b73d6f5dc..10faf6e77a7 100644
--- a/spyder/plugins/updatemanager/confpage.py
+++ b/spyder/plugins/updatemanager/confpage.py
@@ -5,11 +5,7 @@
# (see spyder/__init__.py for details)
"""
-General entry in Preferences.
-
-For historical reasons (dating back to Spyder 2) the main class here is called
-`MainConfigPage` and its associated entry in our config system is called
-`main`.
+Update manager Preferences configuration page.
"""
from qtpy.QtWidgets import QGroupBox, QVBoxLayout
@@ -22,18 +18,18 @@ class UpdateManagerConfigPage(PluginConfigPage):
def setup_page(self):
"""Setup config page widgets and options."""
updates_group = QGroupBox(_("Updates"))
- check_updates = self.create_checkbox(
+ check_update_cb = self.create_checkbox(
_("Check for updates on startup"),
'check_updates_on_startup'
)
- stable_only = self.create_checkbox(
+ stable_only_cb = self.create_checkbox(
_("Check for stable releases only"),
'check_stable_only'
)
updates_layout = QVBoxLayout()
- updates_layout.addWidget(check_updates)
- updates_layout.addWidget(stable_only)
+ updates_layout.addWidget(check_update_cb)
+ updates_layout.addWidget(stable_only_cb)
updates_group.setLayout(updates_layout)
vlayout = QVBoxLayout()
From dd7376f67992baf57856e87138ec716f61f8efe6 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 28 Nov 2023 11:52:53 -0800
Subject: [PATCH 13/22] Set status to NO_STATUS upon applying change to
check_stable_only
---
spyder/plugins/updatemanager/confpage.py | 6 ++++++
spyder/plugins/updatemanager/plugin.py | 1 +
spyder/plugins/updatemanager/widgets/status.py | 4 ++++
3 files changed, 11 insertions(+)
diff --git a/spyder/plugins/updatemanager/confpage.py b/spyder/plugins/updatemanager/confpage.py
index 10faf6e77a7..280cde428c1 100644
--- a/spyder/plugins/updatemanager/confpage.py
+++ b/spyder/plugins/updatemanager/confpage.py
@@ -36,3 +36,9 @@ def setup_page(self):
vlayout.addWidget(updates_group)
vlayout.addStretch(1)
self.setLayout(vlayout)
+
+ def apply_settings(self):
+ if 'check_stable_only' in self.changed_options:
+ self.plugin.update_manager_status.set_no_status()
+
+ return set(self.changed_options)
diff --git a/spyder/plugins/updatemanager/plugin.py b/spyder/plugins/updatemanager/plugin.py
index 01931fa0489..e96f8c40af7 100644
--- a/spyder/plugins/updatemanager/plugin.py
+++ b/spyder/plugins/updatemanager/plugin.py
@@ -133,4 +133,5 @@ def check_update_action(self):
@property
def update_manager_status(self):
+ """Get Update manager statusbar widget"""
return self.get_container().update_manager_status
diff --git a/spyder/plugins/updatemanager/widgets/status.py b/spyder/plugins/updatemanager/widgets/status.py
index 67080dbbecc..0773c2ad703 100644
--- a/spyder/plugins/updatemanager/widgets/status.py
+++ b/spyder/plugins/updatemanager/widgets/status.py
@@ -102,6 +102,10 @@ def set_value(self, value):
logger.debug(f"Update manager status: {value}")
super().set_value(value)
+ def set_no_status(self):
+ """Convenience method to set status to NO_STATUS"""
+ self.set_value(NO_STATUS)
+
def get_tooltip(self):
"""Reimplementation to get a dynamic tooltip."""
return self.tooltip
From 623e67d5854df81bf7f2a57a8a6f31eb4a2618f5 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 28 Nov 2023 17:56:20 -0800
Subject: [PATCH 14/22] Bump major version of configuration because an option
is removed from main section.
---
spyder/config/main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/config/main.py b/spyder/config/main.py
index c4cf64d951a..98dcc64bc35 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -667,4 +667,4 @@
# or if you want to *rename* options, then you need to do a MAJOR update in
# version, e.g. from 3.0.0 to 4.0.0
# 3. You don't need to touch this value if you're just adding a new option
-CONF_VERSION = '81.0.0'
+CONF_VERSION = '82.0.0'
From 3f89181d0b0c52000dbc32cdc9bfdd2e4862453b Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 28 Nov 2023 17:58:27 -0800
Subject: [PATCH 15/22] Apply suggestion from code review.
---
spyder/plugins/updatemanager/tests/test_update_manager.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/spyder/plugins/updatemanager/tests/test_update_manager.py b/spyder/plugins/updatemanager/tests/test_update_manager.py
index 0e57ed91c7e..647120abca6 100644
--- a/spyder/plugins/updatemanager/tests/test_update_manager.py
+++ b/spyder/plugins/updatemanager/tests/test_update_manager.py
@@ -55,7 +55,6 @@ def test_updates_appenv(qtbot, mocker, version):
_update = um.update_worker.update_available
assert _update if version.split('.')[0] == '1' else not _update
- # assert len(worker.releases) > 1
assert len(um.update_worker.releases) > 1
From 4c14e30c17f12698158890412d6067000791b7f0 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 12 Dec 2023 09:49:01 -0800
Subject: [PATCH 16/22] Apply suggestions from code review
---
spyder/plugins/updatemanager/container.py | 27 +++--
spyder/plugins/updatemanager/plugin.py | 21 ++--
.../plugins/updatemanager/scripts/install.bat | 6 +
.../plugins/updatemanager/scripts/install.sh | 10 ++
.../tests/test_update_manager.py | 48 +++++---
.../plugins/updatemanager/widgets/status.py | 24 ++--
.../plugins/updatemanager/widgets/update.py | 103 ++++++++++--------
spyder/plugins/updatemanager/workers.py | 35 ++++--
8 files changed, 177 insertions(+), 97 deletions(-)
diff --git a/spyder/plugins/updatemanager/container.py b/spyder/plugins/updatemanager/container.py
index dc94cd5fa8f..33086ec073b 100644
--- a/spyder/plugins/updatemanager/container.py
+++ b/spyder/plugins/updatemanager/container.py
@@ -21,7 +21,8 @@
from spyder.api.widgets.main_container import PluginMainContainer
from spyder.plugins.updatemanager.widgets.status import UpdateManagerStatus
from spyder.plugins.updatemanager.widgets.update import (
- UpdateManagerWidget, NO_STATUS
+ UpdateManagerWidget,
+ NO_STATUS
)
from spyder.utils.qthelpers import DialogManager
@@ -41,6 +42,8 @@ def __init__(self, name, plugin, parent=None):
self.install_on_close = False
+ # ---- PluginMainContainer API
+ # -------------------------------------------------------------------------
def setup(self):
self.dialog_manager = DialogManager()
self.update_manager = UpdateManagerWidget(parent=self)
@@ -76,6 +79,18 @@ def setup(self):
def update_actions(self):
pass
+ def on_close(self):
+ """To call from Spyder when the plugin is closed."""
+ self.update_manager.cleanup_threads()
+
+ # Run installer after Spyder is closed
+ if self.install_on_close:
+ self.update_manager.start_install()
+
+ self.dialog_manager.close_all()
+
+ # --- Public API
+ # -------------------------------------------------------------------------
def set_status(self, status, latest_version=None):
"""Set Update Manager status"""
self.update_manager_status.set_value(status)
@@ -93,13 +108,3 @@ def start_update(self):
def set_install_on_close(self, install_on_close):
"""Set whether start install on close."""
self.install_on_close = install_on_close
-
- def on_close(self):
- """To call from Spyder when the plugin is closed."""
- self.update_manager.cleanup_threads()
-
- # Run installer after Spyder is closed
- if self.install_on_close:
- self.update_manager.start_install()
-
- self.dialog_manager.close_all()
diff --git a/spyder/plugins/updatemanager/plugin.py b/spyder/plugins/updatemanager/plugin.py
index e96f8c40af7..fb299de9c09 100644
--- a/spyder/plugins/updatemanager/plugin.py
+++ b/spyder/plugins/updatemanager/plugin.py
@@ -13,25 +13,30 @@
from spyder.api.plugins import Plugins, SpyderPluginV2
from spyder.api.translations import _
from spyder.api.plugin_registration.decorators import (
- on_plugin_available, on_plugin_teardown)
+ on_plugin_available,
+ on_plugin_teardown
+)
from spyder.config.base import DEV
from spyder.plugins.updatemanager.confpage import UpdateManagerConfigPage
-from spyder.plugins.updatemanager.container import (UpdateManagerActions,
- UpdateManagerContainer)
+from spyder.plugins.updatemanager.container import (
+ UpdateManagerActions,
+ UpdateManagerContainer
+)
from spyder.plugins.mainmenu.api import ApplicationMenus, HelpMenuSections
class UpdateManager(SpyderPluginV2):
NAME = 'update_manager'
REQUIRES = [Plugins.Preferences]
- OPTIONAL = [Plugins.Help, Plugins.MainMenu, Plugins.Shortcuts,
- Plugins.StatusBar]
+ OPTIONAL = [Plugins.MainMenu, Plugins.StatusBar]
CONTAINER_CLASS = UpdateManagerContainer
CONF_SECTION = 'update_manager'
CONF_FILE = False
CONF_WIDGET_CLASS = UpdateManagerConfigPage
CAN_BE_DISABLED = False
+ # ---- SpyderPluginV2 API
+ # -------------------------------------------------------------------------
@staticmethod
def get_name():
return _('Update Manager')
@@ -44,8 +49,7 @@ def get_icon(cls):
def get_description():
return _('Manage application updates.')
- # --------------------- PLUGIN INITIALIZATION -----------------------------
-
+ # ---- Plugin initialization
def on_initialize(self):
pass
@@ -69,8 +73,7 @@ def on_statusbar_available(self):
statusbar = self.get_plugin(Plugins.StatusBar)
statusbar.add_status_widget(self.update_manager_status)
- # -------------------------- PLUGIN TEARDOWN ------------------------------
-
+ # ---- Plugin teardown
@on_plugin_teardown(plugin=Plugins.StatusBar)
def on_statusbar_teardown(self):
# Remove status widget if created
diff --git a/spyder/plugins/updatemanager/scripts/install.bat b/spyder/plugins/updatemanager/scripts/install.bat
index ad26ae98f08..e0c8297cddd 100644
--- a/spyder/plugins/updatemanager/scripts/install.bat
+++ b/spyder/plugins/updatemanager/scripts/install.bat
@@ -59,7 +59,13 @@ exit %ERRORLEVEL%
goto :EOF
:update_subroutine
+ echo =========================================================
echo Updating Spyder
+ echo ---------------
+ echo
+ echo IMPORTANT: Do not close this window until it has finished
+ echo =========================================================
+ echo
call :wait_for_spyder_quit
diff --git a/spyder/plugins/updatemanager/scripts/install.sh b/spyder/plugins/updatemanager/scripts/install.sh
index 381da1cc44e..4d21ed7351e 100755
--- a/spyder/plugins/updatemanager/scripts/install.sh
+++ b/spyder/plugins/updatemanager/scripts/install.sh
@@ -13,6 +13,15 @@ done
shift $(($OPTIND - 1))
update_spyder(){
+ cat < 1
@@ -68,8 +73,8 @@ def test_updates_appenv(qtbot, mocker, version):
)
def test_updates_condaenv(qtbot, worker, mocker, version, channel):
"""
- Test whether or not we offer updates for our installers according to the
- current Spyder version.
+ Test whether or not we offer updates for conda installed Spyder according
+ to the current version.
"""
mocker.patch.object(workers, "__version__", new=version)
mocker.patch.object(workers, "is_anaconda", return_value=True)
@@ -81,16 +86,17 @@ def test_updates_condaenv(qtbot, worker, mocker, version, channel):
with qtbot.waitSignal(worker.sig_ready, timeout=5000):
worker.start()
- _update = worker.update_available
- assert _update if version.split('.')[0] == '1' else not _update
+ update_available = worker.update_available
+ if version.split('.')[0] == '1':
+ assert update_available
+ else:
+ assert not update_available
assert len(worker.releases) == 1
@pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"])
def test_updates_pipenv(qtbot, worker, mocker, version):
- """
- Test updates for pip installed Spyder
- """
+ """Test updates for pip installed Spyder."""
mocker.patch.object(workers, "__version__", new=version)
mocker.patch.object(workers, "is_anaconda", return_value=False)
mocker.patch.object(workers, "is_conda_based_app", return_value=False)
@@ -102,8 +108,11 @@ def test_updates_pipenv(qtbot, worker, mocker, version):
with qtbot.waitSignal(worker.sig_ready, timeout=5000):
worker.start()
- _update = worker.update_available
- assert _update if version.split('.')[0] == '1' else not _update
+ update_available = worker.update_available
+ if version.split('.')[0] == '1':
+ assert update_available
+ else:
+ assert not update_available
assert len(worker.releases) == 1
@@ -118,24 +127,31 @@ def test_update_non_stable(qtbot, mocker, version, release, stable_only):
worker.releases = [release]
worker._check_update_available()
- _update = worker.update_available
- assert not _update if "a" in release and stable_only else _update
+ update_available = worker.update_available
+ if "a" in release and stable_only:
+ assert not update_available
+ else:
+ assert update_available
# ---- Test WorkerDownloadInstaller
+@pytest.mark.skip(reason="Re-enable when alternate repo is available")
@pytest.mark.skipif(not running_in_ci(), reason="Download only in CI")
def test_download(qtbot, mocker):
"""
Test download spyder installer.
+
Uses UpdateManagerWidget in order to also test QThread.
"""
um = UpdateManagerWidget(None)
um.latest_release = "6.0.0a2"
um._set_installer_path()
+
# Do not execute _start_install after download completes.
mocker.patch.object(
- UpdateManagerWidget, "_confirm_install", new=lambda x: None)
+ UpdateManagerWidget, "_confirm_install", new=lambda x: None
+ )
um._start_download()
qtbot.waitUntil(um.download_thread.isFinished, timeout=60000)
diff --git a/spyder/plugins/updatemanager/widgets/status.py b/spyder/plugins/updatemanager/widgets/status.py
index 0773c2ad703..7d7468635cd 100644
--- a/spyder/plugins/updatemanager/widgets/status.py
+++ b/spyder/plugins/updatemanager/widgets/status.py
@@ -21,8 +21,13 @@
from spyder.api.widgets.menus import SpyderMenu
from spyder.api.widgets.status import StatusBarWidget
from spyder.plugins.updatemanager.widgets.update import (
- NO_STATUS, DOWNLOADING_INSTALLER, PENDING,
- CHECKING, DOWNLOAD_FINISHED, INSTALL_ON_CLOSE)
+ CHECKING,
+ DOWNLOAD_FINISHED,
+ DOWNLOADING_INSTALLER,
+ INSTALL_ON_CLOSE,
+ NO_STATUS,
+ PENDING
+)
from spyder.utils.icon_manager import ima
from spyder.utils.qthelpers import add_actions, create_action
@@ -33,23 +38,23 @@
class UpdateManagerStatus(StatusBarWidget):
"""Status bar widget for update manager."""
- BASE_TOOLTIP = _("Update manager status")
+ BASE_TOOLTIP = _("Application update status")
ID = 'update_manager_status'
sig_check_update = Signal()
"""Signal to request checking for updates."""
sig_start_update = Signal()
- """Signal to start update process"""
+ """Signal to start the update process"""
sig_show_progress_dialog = Signal(bool)
"""
- Signal to show progress dialog
+ Signal to show the progress dialog.
Parameters
----------
show: bool
- True to show, False to hide
+ True to show, False to hide.
"""
CUSTOM_WIDGET_CLASS = QLabel
@@ -73,7 +78,7 @@ def set_value(self, value):
"""Set update manager status."""
if value == DOWNLOADING_INSTALLER:
self.tooltip = _(
- "Downloading update will continue in the background.\n"
+ "Downloading the update will continue in the background.\n"
"Click here to show the download dialog again."
)
self.spinner.hide()
@@ -96,6 +101,7 @@ def set_value(self, value):
if self.spinner:
self.spinner.hide()
self.spinner.stop()
+
self.setVisible(True)
self.update_tooltip()
value = f"Spyder: {value}"
@@ -132,9 +138,11 @@ def show_dialog_or_menu(self):
text=_("Check for updates..."),
triggered=self.sig_check_update.emit
)
+
add_actions(self.menu, [check_for_updates_action])
rect = self.contentsRect()
os_height = 7 if os.name == 'nt' else 12
pos = self.mapToGlobal(
- rect.topLeft() + QPoint(-10, -rect.height() - os_height))
+ rect.topLeft() + QPoint(-10, -rect.height() - os_height)
+ )
self.menu.popup(pos)
diff --git a/spyder/plugins/updatemanager/widgets/update.py b/spyder/plugins/updatemanager/widgets/update.py
index 25ba6734fc8..7287af3dee6 100644
--- a/spyder/plugins/updatemanager/widgets/update.py
+++ b/spyder/plugins/updatemanager/widgets/update.py
@@ -12,7 +12,6 @@
import os.path as osp
import sys
import subprocess
-from tempfile import gettempdir
import platform
# Third-party imports
@@ -26,9 +25,12 @@
from spyder.api.translations import _
from spyder.config.base import is_conda_based_app
from spyder.config.utils import is_anaconda
-from spyder.plugins.updatemanager.workers import (WorkerUpdate,
- WorkerDownloadInstaller)
+from spyder.plugins.updatemanager.workers import (
+ WorkerUpdate,
+ WorkerDownloadInstaller
+)
from spyder.utils.conda import find_conda, is_anaconda_pkg
+from spyder.utils.programs import get_temp_dir, is_program_installed
from spyder.widgets.helperwidgets import MessageCheckBox
# Logger setup
@@ -45,44 +47,34 @@
CANCELLED = _("Cancelled update")
INSTALL_ON_CLOSE = _("Install on close")
-INSTALL_INFO_MESSAGES = {
- DOWNLOADING_INSTALLER: _("Downloading Spyder {version}"),
- DOWNLOAD_FINISHED: _("Finished downloading Spyder {version}"),
- INSTALLING: _("Installing Spyder {version}"),
- FINISHED: _("Finished installing Spyder {version}"),
- PENDING: _("Spyder {version} available to download"),
- CHECKING: _("Checking for new Spyder version"),
- CANCELLED: _("Spyder update cancelled"),
- INSTALL_ON_CLOSE: _("Install Spyder {version} on close")
-}
-
HEADER = _("Spyder {} is available!
")
URL_I = 'https://docs.spyder-ide.org/current/installation.html'
-TMPDIR = osp.join(gettempdir(), 'spyder')
class UpdateManagerWidget(QWidget, SpyderConfigurationAccessor):
- """Check for updates widget"""
+ """Check for updates widget."""
+
+ CONF_SECTION = "update_manager"
sig_disable_actions = Signal(bool)
"""
- Signal to disable plugin actions during check for update
+ Signal to disable plugin actions during check for update.
Parameters
----------
disable: bool
- True to disable, False to re-enable
+ True to disable, False to re-enable.
"""
sig_block_status_signals = Signal(bool)
"""
Signal to block signals from update manager status during
- check for update
+ check for update.
Parameters
----------
block: bool
- True to block, False to unblock
+ True to block, False to unblock.
"""
sig_download_progress = Signal(int)
@@ -124,7 +116,6 @@ class UpdateManagerWidget(QWidget, SpyderConfigurationAccessor):
def __init__(self, parent):
super().__init__(parent)
- self.CONF_SECTION = parent.CONF_SECTION if parent else 'update_manager'
self.startup = None
self.update_thread = None
@@ -161,10 +152,10 @@ def cleanup_threads(self):
def start_check_update(self, startup=False):
"""
- Check for spyder updates using a QThread.
+ Check for Spyder updates using a QThread.
Update actions are disabled in the menubar and statusbar while
- checking for updates
+ checking for updates.
If startup is True, then checking for updates is delayed 1 min;
actions are disabled during this time as well.
@@ -197,7 +188,8 @@ def start_check_update(self, startup=False):
self.update_timer.setSingleShot(True)
self.sig_block_status_signals.emit(True)
self.update_timer.timeout.connect(
- lambda: self.sig_block_status_signals.emit(False))
+ lambda: self.sig_block_status_signals.emit(False)
+ )
self.update_timer.timeout.connect(self.update_thread.start)
self.update_timer.start()
else:
@@ -215,7 +207,7 @@ def _process_check_update(self):
# self.startup = True is used on startup, so only positive feedback is
# given. self.startup = False is used after startup when using the menu
- # action, and gives feeback if updates are, or are not found.
+ # action, and gives feeback if updates are or are not found.
if (
self.startup and # startup and...
(error_msg is not None # there is an error
@@ -231,17 +223,18 @@ def _process_check_update(self):
info_messagebox(self, _("Spyder is up to date."), checkbox=True)
def _set_installer_path(self):
- """Set the emp file path for the downloaded installer"""
+ """Set the temp file path for the downloaded installer."""
if os.name == 'nt':
plat, ext = 'Windows', 'exe'
if sys.platform == 'darwin':
plat, ext = 'macOS', 'pkg'
if sys.platform.startswith('linux'):
plat, ext = 'Linux', 'sh'
+
mach = platform.machine().lower().replace("amd64", "x86_64")
fname = f'Spyder-{self.latest_release}-{plat}-{mach}.{ext}'
- dirname = osp.join(TMPDIR, 'updates', self.latest_release)
+ dirname = osp.join(get_temp_dir(), 'updates', self.latest_release)
self.installer_path = osp.join(dirname, fname)
self.installer_size_path = osp.join(dirname, "size")
@@ -286,19 +279,23 @@ def start_update(self):
"This will leave your existing Spyder installation "
"untouched."
).format(URL_I + "#standalone-installers")
+
box = confirm_messagebox(
- self, msg, version=self.latest_release, checkbox=True
+ self, msg, _('Spyder update'),
+ version=self.latest_release, checkbox=True
)
if box.result() == QMessageBox.Yes:
self._start_download()
else:
manual_update_messagebox(
- self, self.latest_release, self.update_worker.channel)
+ self, self.latest_release, self.update_worker.channel
+ )
elif major_update:
msg = _("Would you like to automatically download "
"and install it?")
box = confirm_messagebox(
- self, msg, version=self.latest_release, checkbox=True
+ self, msg, _('Spyder update'),
+ version=self.latest_release, checkbox=True
)
if box.result() == QMessageBox.Yes:
self._start_download()
@@ -313,7 +310,8 @@ def _start_download(self):
"""
self.cancelled = False
self.download_worker = WorkerDownloadInstaller(
- self.latest_release, self.installer_path, self.installer_size_path)
+ self.latest_release, self.installer_path, self.installer_size_path
+ )
self.sig_disable_actions.emit(True)
self.set_status(DOWNLOADING_INSTALLER)
@@ -330,7 +328,8 @@ def _start_download(self):
lambda: self.sig_disable_actions.emit(False)
)
self.download_worker.sig_download_progress.connect(
- self._update_download_progress)
+ self._update_download_progress
+ )
self.download_worker.moveToThread(self.download_thread)
self.download_thread.started.connect(self.download_worker.start)
self.download_thread.start()
@@ -359,7 +358,9 @@ def _cancel_download(self):
"""Cancel the installation in progress."""
self.download_worker.paused = True
msg = _('Do you really want to cancel the download?')
- box = confirm_messagebox(self, msg, critical=True)
+ box = confirm_messagebox(
+ self, msg, _('Spyder download'), critical=True
+ )
if box.result() == QMessageBox.Yes:
self.cancelled = True
self.cleanup_threads()
@@ -388,7 +389,12 @@ def _confirm_install(self):
msg = _("Would you like to install it?")
box = confirm_messagebox(
- self, msg, version=self.latest_release, on_close=True)
+ self,
+ msg,
+ _('Spyder install'),
+ version=self.latest_release,
+ on_close=True
+ )
if box.result() == QMessageBox.Yes:
self.sig_install_on_close.emit(True)
self.sig_quit_requested.emit()
@@ -418,16 +424,23 @@ def start_install(self):
elif sys.platform == 'darwin':
# Terminal cannot accept a command with arguments therefore
# create a temporary script
- tmpdir = osp.join(gettempdir(), 'spyder')
- tmpscript = osp.join(tmpdir, 'tmp_install.sh')
- os.makedirs(tmpdir, exist_ok=True)
+ tmpscript = osp.join(get_temp_dir(), 'tmp_install.sh')
with open(tmpscript, 'w') as f:
f.write(' '.join(sub_cmd))
os.chmod(tmpscript, 0o711) # set executable permissions
cmd = ['open', '-b', 'com.apple.terminal', tmpscript]
else:
- cmd = ['gnome-terminal', '--window', '--'] + sub_cmd
+ programs = [
+ {'cmd': 'gnome-terminal', 'exe-opt': '--window --'},
+ {'cmd': 'konsole', 'exe-opt': '-e'},
+ {'cmd': 'xfce4-terminal', 'exe-opt': '-x'},
+ {'cmd': 'xterm', 'exe-opt': '-e'}
+ ]
+ for program in programs:
+ if is_program_installed(program['cmd']):
+ cmd = [program['cmd'], program['exe-opt']] + sub_cmd
+ break
subprocess.Popen(' '.join(cmd), shell=True)
@@ -436,14 +449,12 @@ class UpdateMessageBox(QMessageBox):
def __init__(self, icon=None, text=None, parent=None):
super().__init__(icon=icon, text=text, parent=parent)
self.setWindowModality(Qt.NonModal)
- self.setWindowTitle(_("Spyder Update Manager"))
self.setTextFormat(Qt.RichText)
class UpdateMessageCheckBox(MessageCheckBox):
def __init__(self, icon=None, text=None, parent=None):
super().__init__(icon=icon, text=text, parent=parent)
- self.setWindowTitle(_("Spyder Update Manager"))
self.setTextFormat(Qt.RichText)
self._parent = parent
self.set_checkbox_text(_("Check for updates at startup"))
@@ -462,6 +473,7 @@ class ProgressDialog(UpdateMessageBox):
def __init__(self, parent, text):
super().__init__(icon=QMessageBox.NoIcon, text=text, parent=parent)
+ self.setWindowTitle(_("Spyder update"))
self._progress_bar = QProgressBar(self)
self._progress_bar.setMinimumWidth(250)
@@ -486,7 +498,9 @@ def update_progress(self, progress, total):
def error_messagebox(parent, error_msg):
box = UpdateMessageBox(
- icon=QMessageBox.Warning, text=error_msg, parent=parent)
+ icon=QMessageBox.Warning, text=error_msg, parent=parent
+ )
+ box.setWindowTitle(_("Spyder update error"))
box.setStandardButtons(QMessageBox.Ok)
box.setDefaultButton(QMessageBox.Ok)
box.show()
@@ -497,19 +511,22 @@ def info_messagebox(parent, message, version=None, checkbox=False):
box_class = UpdateMessageCheckBox if checkbox else UpdateMessageBox
message = HEADER.format(version) + message if version else message
box = box_class(icon=QMessageBox.Information, text=message, parent=parent)
+ box.setWindowTitle(_("New Spyder version"))
box.setStandardButtons(QMessageBox.Ok)
box.setDefaultButton(QMessageBox.Ok)
box.show()
return box
-def confirm_messagebox(parent, message, version=None, critical=False,
+def confirm_messagebox(parent, message, title, version=None, critical=False,
checkbox=False, on_close=False):
box_class = UpdateMessageCheckBox if checkbox else UpdateMessageBox
message = HEADER.format(version) + message if version else message
box = box_class(
icon=QMessageBox.Critical if critical else QMessageBox.Question,
- text=message, parent=parent)
+ text=message, parent=parent
+ )
+ box.setWindowTitle(title)
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
box.setDefaultButton(QMessageBox.Yes)
if on_close:
diff --git a/spyder/plugins/updatemanager/workers.py b/spyder/plugins/updatemanager/workers.py
index 011e7951f0c..773eef09e41 100644
--- a/spyder/plugins/updatemanager/workers.py
+++ b/spyder/plugins/updatemanager/workers.py
@@ -68,7 +68,7 @@ def __init__(self, stable_only):
self.stable_only = stable_only
self.latest_release = None
self.releases = None
- self.update_available = None
+ self.update_available = False
self.error = None
self.channel = None
@@ -82,8 +82,11 @@ def _check_update_available(self):
logger.debug(f"Available versions: {self.releases}")
self.latest_release = releases[-1] if releases else __version__
- self.update_available = check_version(__version__,
- self.latest_release, '<')
+ self.update_available = check_version(
+ __version__,
+ self.latest_release,
+ '<'
+ )
logger.debug(f"Update available: {self.update_available}")
logger.debug(f"Latest release: {self.latest_release}")
@@ -101,7 +104,9 @@ def start(self):
elif is_anaconda():
self.channel, channel_url = get_spyder_conda_channel()
- if channel_url is None or self.channel == "pypi":
+ if channel_url is None:
+ return
+ elif self.channel == "pypi":
url = pypi_url
else:
url = channel_url + '/channeldata.json'
@@ -139,8 +144,10 @@ def start(self):
logger.warning(err, exc_info=err)
except Exception as err:
error = traceback.format_exc()
- formatted_error = (error.replace('\n', '
')
- .replace(' ', ' '))
+ formatted_error = (
+ error.replace('\n', '
')
+ .replace(' ', ' ')
+ )
error_msg = _(
'It was not possible to check for Spyder updates due to the '
@@ -151,7 +158,10 @@ def start(self):
logger.warning(err, exc_info=err)
finally:
self.error = error_msg
- self.sig_ready.emit()
+ try:
+ self.sig_ready.emit()
+ except RuntimeError:
+ pass
class WorkerDownloadInstaller(QObject):
@@ -254,8 +264,10 @@ def start(self):
logger.warning(err, exc_info=err)
except Exception as err:
error = traceback.format_exc()
- formatted_error = (error.replace('\n', '
')
- .replace(' ', ' '))
+ formatted_error = (
+ error.replace('\n', '
')
+ .replace(' ', ' ')
+ )
error_msg = _(
'It was not possible to download the installer due to the '
@@ -267,4 +279,7 @@ def start(self):
self._clean_installer_path()
finally:
self.error = error_msg
- self.sig_ready.emit()
+ try:
+ self.sig_ready.emit()
+ except RuntimeError:
+ pass
From 7b6b1a93afb211c398dc448fa3de362cb5de45e1 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 13 Dec 2023 08:57:40 -0800
Subject: [PATCH 17/22] Restore check for update preference to Application
preference pane * Restore "Check for updates on startup" check box to
Application plugin preference page. * Add "Check for stable releases only"
check box to Application plugin preference page.
---
spyder/plugins/application/confpage.py | 18 ++++++++++
spyder/plugins/updatemanager/confpage.py | 44 ------------------------
spyder/plugins/updatemanager/plugin.py | 2 --
3 files changed, 18 insertions(+), 46 deletions(-)
delete mode 100644 spyder/plugins/updatemanager/confpage.py
diff --git a/spyder/plugins/application/confpage.py b/spyder/plugins/application/confpage.py
index 0d7907b373a..e014e147859 100644
--- a/spyder/plugins/application/confpage.py
+++ b/spyder/plugins/application/confpage.py
@@ -23,6 +23,7 @@
from spyder.config.base import (_, DISABLED_LANGUAGES, LANGUAGE_CODES,
is_conda_based_app, save_lang_conf)
+from spyder.api.plugins import Plugins
from spyder.api.preferences import PluginConfigPage
from spyder.py3compat import to_text_string
@@ -65,6 +66,16 @@ def setup_page(self):
prompt_box = newcb(_("Prompt when exiting"), 'prompt_on_exit')
popup_console_box = newcb(_("Show internal Spyder errors to report "
"them to Github"), 'show_internal_errors')
+ check_update_cb = newcb(
+ _("Check for updates on startup"),
+ 'check_updates_on_startup',
+ section='update_manager'
+ )
+ stable_only_cb = newcb(
+ _("Check for stable releases only"),
+ 'check_stable_only',
+ section='update_manager'
+ )
# Decide if it's possible to activate or not single instance mode
# ??? Should we allow multiple instances for macOS?
@@ -86,6 +97,8 @@ def setup_page(self):
advanced_layout.addWidget(single_instance_box)
advanced_layout.addWidget(prompt_box)
advanced_layout.addWidget(popup_console_box)
+ advanced_layout.addWidget(check_update_cb)
+ advanced_layout.addWidget(stable_only_cb)
advanced_widget = QWidget()
advanced_widget.setLayout(advanced_layout)
@@ -269,6 +282,11 @@ def apply_settings(self, options):
self.set_option(
'high_dpi_custom_scale_factors', scale_factors_text)
self.changed_options.add('high_dpi_custom_scale_factors')
+
+ um = self.plugin.get_plugin(Plugins.UpdateManager, error=False)
+ if um and 'check_stable_only' in self.changed_options:
+ um.update_manager_status.set_no_status()
+
self.plugin.apply_settings()
def _save_lang(self):
diff --git a/spyder/plugins/updatemanager/confpage.py b/spyder/plugins/updatemanager/confpage.py
deleted file mode 100644
index 280cde428c1..00000000000
--- a/spyder/plugins/updatemanager/confpage.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""
-Update manager Preferences configuration page.
-"""
-
-from qtpy.QtWidgets import QGroupBox, QVBoxLayout
-
-from spyder.config.base import _
-from spyder.api.preferences import PluginConfigPage
-
-
-class UpdateManagerConfigPage(PluginConfigPage):
- def setup_page(self):
- """Setup config page widgets and options."""
- updates_group = QGroupBox(_("Updates"))
- check_update_cb = self.create_checkbox(
- _("Check for updates on startup"),
- 'check_updates_on_startup'
- )
- stable_only_cb = self.create_checkbox(
- _("Check for stable releases only"),
- 'check_stable_only'
- )
-
- updates_layout = QVBoxLayout()
- updates_layout.addWidget(check_update_cb)
- updates_layout.addWidget(stable_only_cb)
- updates_group.setLayout(updates_layout)
-
- vlayout = QVBoxLayout()
- vlayout.addWidget(updates_group)
- vlayout.addStretch(1)
- self.setLayout(vlayout)
-
- def apply_settings(self):
- if 'check_stable_only' in self.changed_options:
- self.plugin.update_manager_status.set_no_status()
-
- return set(self.changed_options)
diff --git a/spyder/plugins/updatemanager/plugin.py b/spyder/plugins/updatemanager/plugin.py
index fb299de9c09..39e6c4cb8e7 100644
--- a/spyder/plugins/updatemanager/plugin.py
+++ b/spyder/plugins/updatemanager/plugin.py
@@ -17,7 +17,6 @@
on_plugin_teardown
)
from spyder.config.base import DEV
-from spyder.plugins.updatemanager.confpage import UpdateManagerConfigPage
from spyder.plugins.updatemanager.container import (
UpdateManagerActions,
UpdateManagerContainer
@@ -32,7 +31,6 @@ class UpdateManager(SpyderPluginV2):
CONTAINER_CLASS = UpdateManagerContainer
CONF_SECTION = 'update_manager'
CONF_FILE = False
- CONF_WIDGET_CLASS = UpdateManagerConfigPage
CAN_BE_DISABLED = False
# ---- SpyderPluginV2 API
From 38f861e69907298760fba1bfaf445223a6845f63 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 14 Dec 2023 12:55:38 -0800
Subject: [PATCH 18/22] Use GITHUB_TOKEN with test steps in testing workflows
to avoid error: "HTTPError: 403 Client Error: rate limit exceeded for url:
https://api.github.com/repos/spyder-ide/spyder/releases"
---
.github/workflows/test-linux.yml | 4 ++++
.github/workflows/test-mac.yml | 2 ++
.github/workflows/test-win.yml | 2 ++
3 files changed, 8 insertions(+)
diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml
index 974ffd672b5..a38ccf72750 100644
--- a/.github/workflows/test-linux.yml
+++ b/.github/workflows/test-linux.yml
@@ -140,9 +140,13 @@ jobs:
- name: Run tests with gdb
if: env.USE_GDB == 'true'
shell: bash -l {0}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: xvfb-run --auto-servernum gdb -return-child-result -batch -ex r -ex py-bt --args python runtests.py -s
- name: Run tests
shell: bash -l {0}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -f pytest_log.txt # Must remove any log file from a previous run
.github/scripts/run_tests.sh || \
diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml
index d7d292139b8..66d4f5a7a79 100644
--- a/.github/workflows/test-mac.yml
+++ b/.github/workflows/test-mac.yml
@@ -110,6 +110,8 @@ jobs:
run: check-manifest
- name: Run tests
shell: bash -l {0}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -f pytest_log.txt # Must remove any log file from a previous run
.github/scripts/run_tests.sh || \
diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml
index 95e4f0fcb85..6602ebde184 100644
--- a/.github/workflows/test-win.yml
+++ b/.github/workflows/test-win.yml
@@ -134,6 +134,8 @@ jobs:
run: check-manifest
- name: Run tests
shell: bash -l {0}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -f pytest_log.txt # Must remove any log file from a previous run
.github/scripts/run_tests.sh || \
From a5300bebd0a90a6845d517f7224720f6266e85d8 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 14 Dec 2023 18:35:28 -0800
Subject: [PATCH 19/22] Add UpdateManager to Application.OPTIONAL attribute
---
spyder/plugins/application/plugin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py
index 503631f9620..d6915189ab1 100644
--- a/spyder/plugins/application/plugin.py
+++ b/spyder/plugins/application/plugin.py
@@ -38,7 +38,7 @@ class Application(SpyderPluginV2):
NAME = 'application'
REQUIRES = [Plugins.Console, Plugins.Preferences]
OPTIONAL = [Plugins.Help, Plugins.MainMenu, Plugins.Shortcuts,
- Plugins.Editor, Plugins.StatusBar]
+ Plugins.Editor, Plugins.StatusBar, Plugins.UpdateManager]
CONTAINER_CLASS = ApplicationContainer
CONF_SECTION = 'main'
CONF_FILE = False
From 2e27a02f3b7269ca1b0b60ff68b9563353cc719e Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 14 Dec 2023 21:38:04 -0800
Subject: [PATCH 20/22] Fix issue where 'check_stable_only' was incorrectly
compared to self.changed_options
---
spyder/plugins/application/confpage.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/spyder/plugins/application/confpage.py b/spyder/plugins/application/confpage.py
index e014e147859..74017b0dc1a 100644
--- a/spyder/plugins/application/confpage.py
+++ b/spyder/plugins/application/confpage.py
@@ -284,7 +284,10 @@ def apply_settings(self, options):
self.changed_options.add('high_dpi_custom_scale_factors')
um = self.plugin.get_plugin(Plugins.UpdateManager, error=False)
- if um and 'check_stable_only' in self.changed_options:
+ if (
+ um
+ and ('update_manager', 'check_stable_only') in self.changed_options
+ ):
um.update_manager_status.set_no_status()
self.plugin.apply_settings()
From f5ca4a0b3fb820bf86d1cec61e4c8c10da20dd01 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 14 Dec 2023 21:42:02 -0800
Subject: [PATCH 21/22] Display CLI notice when updating or installing, not
just when updating. For macOS and Linux, do not run installer if uninstaller
is cancelled or fails.
---
.../plugins/updatemanager/scripts/install.bat | 14 +++++++-----
.../plugins/updatemanager/scripts/install.sh | 22 ++++++++++---------
2 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/spyder/plugins/updatemanager/scripts/install.bat b/spyder/plugins/updatemanager/scripts/install.bat
index e0c8297cddd..9d80d90ee92 100644
--- a/spyder/plugins/updatemanager/scripts/install.bat
+++ b/spyder/plugins/updatemanager/scripts/install.bat
@@ -15,6 +15,14 @@ GOTO parse
:: Enforce encoding
chcp 65001>nul
+echo =========================================================
+echo Updating Spyder
+echo ---------------
+echo
+echo IMPORTANT: Do not close this window until it has finished
+echo =========================================================
+echo
+
IF not "%conda%"=="" IF not "%spy_ver%"=="" (
call :update_subroutine
call :launch_spyder
@@ -59,13 +67,7 @@ exit %ERRORLEVEL%
goto :EOF
:update_subroutine
- echo =========================================================
echo Updating Spyder
- echo ---------------
- echo
- echo IMPORTANT: Do not close this window until it has finished
- echo =========================================================
- echo
call :wait_for_spyder_quit
diff --git a/spyder/plugins/updatemanager/scripts/install.sh b/spyder/plugins/updatemanager/scripts/install.sh
index 4d21ed7351e..9b7b42c37c3 100755
--- a/spyder/plugins/updatemanager/scripts/install.sh
+++ b/spyder/plugins/updatemanager/scripts/install.sh
@@ -1,5 +1,5 @@
#!/bin/bash -i
-set -e
+
unset HISTFILE # Do not write to history with interactive shell
while getopts "i:c:p:v:" option; do
@@ -13,15 +13,6 @@ done
shift $(($OPTIND - 1))
update_spyder(){
- cat < 0 ]] && return
fi
# Run installer
[[ "$OSTYPE" = "darwin"* ]] && open $install_exe || sh $install_exe
}
+cat < /dev/null) ]]; do
echo "Waiting for Spyder to quit..."
sleep 1
From 2d9d5dec17470069d5c27888a36884960560f924 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Fri, 15 Dec 2023 13:11:23 -0800
Subject: [PATCH 22/22] Fix issue in installer batch script where echo reported
echo state rather than a blank line.
---
spyder/plugins/updatemanager/scripts/install.bat | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/spyder/plugins/updatemanager/scripts/install.bat b/spyder/plugins/updatemanager/scripts/install.bat
index 9d80d90ee92..e87c86d43af 100644
--- a/spyder/plugins/updatemanager/scripts/install.bat
+++ b/spyder/plugins/updatemanager/scripts/install.bat
@@ -18,10 +18,10 @@ chcp 65001>nul
echo =========================================================
echo Updating Spyder
echo ---------------
-echo
+echo.
echo IMPORTANT: Do not close this window until it has finished
echo =========================================================
-echo
+echo.
IF not "%conda%"=="" IF not "%spy_ver%"=="" (
call :update_subroutine