Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions .github/workflows/installers-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,10 @@ jobs:

EXIT /B %ERRORLEVEL%

- name: Notarize or Compute Checksum
if: env.NOTARIZE == 'true'
- name: Notarize
if: runner.os == 'macOS' && env.NOTARIZE == 'true'
run: |
if [[ $RUNNER_OS == "macOS" ]]; then
./notarize.sh -p $APPLICATION_PWD $PKG_PATH
else
cd $DISTDIR
echo $(sha256sum $PKG_NAME) > "${ARTIFACT_NAME}-sha256sum.txt"
fi
./notarize.sh -p $APPLICATION_PWD $PKG_PATH

- name: Upload Artifact
uses: actions/upload-artifact@v4
Expand All @@ -390,11 +385,21 @@ jobs:
- name: Zip Lock Files
run: zip -mT spyder-conda-lock *.lock

- name: Upload Artifact
- name: Create Checksums
run: |
sha256sum *.zip *.pkg *.sh *.exe > Spyder-checksums.txt

- name: Upload Lock Files
uses: actions/upload-artifact@v4
with:
path: spyder-conda-lock.zip
name: spyder-conda-lock
name: spyder-conda-lock-artifact

- name: Upload Checksums
uses: actions/upload-artifact@v4
with:
path: Spyder-checksums.txt
name: Spyder-checksums

- name: Get Release
if: env.IS_RELEASE == 'true'
Expand Down
93 changes: 48 additions & 45 deletions spyder/plugins/updatemanager/tests/test_update_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# (see spyder/__init__.py for details)

import os
from functools import lru_cache
import logging
from packaging.version import parse

Expand All @@ -13,24 +14,32 @@
from spyder.config.base import running_in_ci
from spyder.plugins.updatemanager import workers
from spyder.plugins.updatemanager.workers import (
UpdateType, get_asset_info, WorkerUpdate, HTTP_ERROR_MSG
UpdateType, get_asset_info, WorkerUpdate
)
from spyder.plugins.updatemanager.widgets import update
from spyder.plugins.updatemanager.widgets.update import UpdateManagerWidget

logging.basicConfig()

workers.get_github_releases = lru_cache(workers.get_github_releases)
workers.get_asset_checksum = lru_cache(workers.get_asset_checksum)
_tags = (
"v6.0.5", "v6.0.5rc1", "v6.1.0a1", "v6.0.4",
"v6.0.4rc1", "v6.0.3", "v6.0.3rc2", "v6.0.3rc1",
"v6.0.2", "v6.0.2rc1", "v6.0.1", "v6.0.0",
"v5.5.6", "v6.0.0rc2", "v6.0.0rc1", "v6.0.0b3",
"v6.0.0b2", "v5.5.5", "v6.0.0b1", "v6.0.0a5"
)
workers.get_github_releases(_tags) # Run once to cache result for tests


@pytest.fixture(autouse=True)
def capture_logging(caplog):
# Capture >=DEBUG logging messages for spyder.plugins.updatemanager.
# Messages will be reported at the end of the pytest run for failed tests.
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"])
Expand All @@ -57,78 +66,69 @@ def test_updates(qtbot, mocker, caplog, version, channel):
workers, "get_spyder_conda_channel", return_value=channel
)

with caplog.at_level(logging.DEBUG, logger='spyder.plugins.updatemanager'):
# Capture >=DEBUG logging messages for spyder.plugins.updatemanager
# while checking for updates. Messages will be reported at the end
# of the pytest run, and only if this test fails.
um = UpdateManagerWidget(None)
um.start_check_update()
qtbot.waitUntil(um.update_thread.isFinished)

if um.update_worker.error:
# Possible 403 error - rate limit error, was encountered while doing
# the tests
# Check error message corresponds to the status code and exit early to
# prevent failing the test
assert um.update_worker.error == HTTP_ERROR_MSG.format(status_code="403")
return

assert not um.update_worker.error

update_available = um.update_worker.update_available
um = UpdateManagerWidget(None)
um.start_check_update()
qtbot.waitUntil(um.update_thread.isFinished, timeout=10000)

if version.split('.')[0] == '1':
assert update_available
assert um.update_worker.asset_info is not None
else:
assert not update_available
assert um.update_worker.asset_info is None


@pytest.mark.parametrize("release", ["6.0.0", "6.0.0b3"])
@pytest.mark.parametrize("version", ["4.0.0a1", "4.0.0"])
@pytest.mark.parametrize("release", ["6.0.0", "6.0.0rc1"])
@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, "CURRENT_VERSION", new=parse(version))

release = parse(release)
worker = WorkerUpdate(stable_only)
worker._check_update_available([release])
worker._check_update_available(release)

if release.is_prerelease and stable_only:
assert not worker.update_available
assert worker.asset_info is None
else:
assert worker.update_available
assert worker.asset_info is not None


@pytest.mark.parametrize("version", ["4.0.0", "6.0.0"])
def test_update_no_asset(qtbot, mocker, version):
@pytest.mark.parametrize("release", [None, "6.0.1", "6.100.0"])
def test_update_no_asset(qtbot, mocker, version, release):
"""Test update availability when asset is not available"""
mocker.patch.object(workers, "CURRENT_VERSION", new=parse(version))

releases = [parse("6.0.1"), parse("6.100.0")]
release = parse(release) if release else None
worker = WorkerUpdate(True)
worker._check_update_available(releases)
worker._check_update_available(release)

# For both values of version, there should be an update available
# However, the available version should be 6.0.1, since there is
# no asset for 6.100.0
assert worker.update_available
assert worker.latest_release == releases[0]
assert worker.asset_info is not None
assert worker.asset_info["version"] >= parse("6.0.1")


@pytest.mark.parametrize(
"release,update_type",
"app,version,release,update_type",
[
("6.0.1", UpdateType.Micro),
("6.1.0", UpdateType.Minor),
("7.0.0", UpdateType.Major)
(True, "6.0.0", "6.0.1", UpdateType.Micro),
(True, "6.0.0", "6.1.0a1", UpdateType.Minor),
(True, "5.0.0", "6.0.0", UpdateType.Major),
(False, "6.0.0", "6.0.1", UpdateType.Major),
(False, "6.0.0", "6.1.0a1", UpdateType.Major),
(False, "5.0.0", "6.0.0", UpdateType.Major)
]
)
@pytest.mark.parametrize("app", [True, False])
def test_get_asset_info(qtbot, mocker, release, update_type, app):
mocker.patch.object(workers, "CURRENT_VERSION", new=parse("6.0.0"))
def test_get_asset_info(qtbot, mocker, app, version, release, update_type):
mocker.patch.object(workers, "CURRENT_VERSION", new=parse(version))
mocker.patch.object(workers, "is_conda_based_app", return_value=app)

info = get_asset_info(release)
worker = WorkerUpdate(False)
worker._check_update_available(parse(release))
info = worker.asset_info

assert info['update_type'] == update_type

if update_type == "major" or not app:
Expand All @@ -149,8 +149,11 @@ def test_download(qtbot, mocker):

Uses UpdateManagerWidget in order to also test QThread.
"""
releases = workers.get_github_releases()
release_info = releases[parse("6.0.0a2")]

um = UpdateManagerWidget(None)
um.latest_release = "6.0.0a2"
um.asset_info = get_asset_info(release_info)
um._set_installer_path()

# Do not execute _start_install after download completes.
Expand Down
75 changes: 34 additions & 41 deletions spyder/plugins/updatemanager/widgets/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from spyder.api.translations import _
from spyder.config.base import is_conda_based_app
from spyder.plugins.updatemanager.workers import (
get_asset_info,
validate_download,
WorkerUpdate,
WorkerDownloadInstaller
)
Expand Down Expand Up @@ -133,23 +133,22 @@ def __init__(self, parent):
self.update_thread = None
self.update_worker = None
self.update_timer = None
self.latest_release = None
self.asset_info = None

self.cancelled = False
self.download_thread = None
self.download_worker = None
self.progress_dialog = None
self.installer_path = None
self.installer_size_path = None

# Type of Spyder update. It can be "major", "minor" or "micro"
self.update_type = None

# ---- General

def set_status(self, status=NO_STATUS):
"""Set the update manager status."""
self.sig_set_status.emit(status, str(self.latest_release))
version = None
if self.asset_info is not None:
version = self.asset_info["version"]
self.sig_set_status.emit(status, str(version))

def cleanup_threads(self):
"""Clean up QThreads"""
Expand Down Expand Up @@ -226,7 +225,7 @@ def start_check_update(self, startup=False):
def _process_check_update(self):
"""Process the results of check update."""
# Get results from worker
update_available = self.update_worker.update_available
update_available = self.update_worker.asset_info is not None
error_msg = self.update_worker.error
checkbox = self.update_worker.checkbox

Expand All @@ -250,30 +249,22 @@ def _process_check_update(self):
else:
info_messagebox(self, _("Spyder is up to date."), checkbox=True)

def _set_installer_path(self):
"""Set the temp file path for the downloaded installer."""
asset_info = get_asset_info(self.latest_release)
self.update_type = asset_info['update_type']

dirname = osp.join(get_temp_dir(), 'updates', str(self.latest_release))
self.installer_path = osp.join(dirname, asset_info['filename'])
self.installer_size_path = osp.join(dirname, "size")

logger.info(f"Update type: {self.update_type}")

# ---- 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 = int(f.read().strip())
def _set_installer_path(self):
dirname = osp.join(
get_temp_dir(), 'updates', str(self.asset_info["version"])
)
self.installer_path = osp.join(dirname, self.asset_info['filename'])

update_downloaded = size == osp.getsize(self.installer_path)
else:
update_downloaded = False
logger.info(f"Update type: {self.asset_info['update_type']}")

def _validate_download(self):
update_downloaded = False
if osp.exists(self.installer_path):
update_downloaded = validate_download(
self.installer_path, self.asset_info["checksum"]
)

logger.debug(f"Update already downloaded: {update_downloaded}")

Expand All @@ -288,10 +279,11 @@ def start_update(self):

If the installer is already downloaded, proceed to confirm install.
"""
self.latest_release = self.update_worker.latest_release
self.asset_info = self.update_worker.asset_info
self._set_installer_path()
version = self.asset_info["version"]

if self._verify_installer_path():
if self._validate_download():
self.set_status(DOWNLOAD_FINISHED)
self._confirm_install()
elif not is_conda_based_app():
Expand All @@ -306,21 +298,19 @@ def start_update(self):
).format(URL_I + "#standalone-installers")

box = confirm_messagebox(
self, msg, _('Spyder update'),
version=self.latest_release, checkbox=True
self, msg, _('Spyder update'), version=version, checkbox=True
)
if box.result() == QMessageBox.Yes:
self._start_download()
else:
manual_update_messagebox(
self, self.latest_release, self.update_worker.channel
self, version, self.update_worker.channel
)
else:
msg = _("Would you like to automatically download "
"and install it?")
box = confirm_messagebox(
self, msg, _('Spyder update'),
version=self.latest_release, checkbox=True
self, msg, _('Spyder update'), version=version, checkbox=True
)
if box.result() == QMessageBox.Yes:
self._start_download()
Expand All @@ -334,7 +324,7 @@ def _start_download(self):
self.progress_dialog = None

self.download_worker = WorkerDownloadInstaller(
self.latest_release, self.installer_path, self.installer_size_path
self.asset_info, self.installer_path
)

self.sig_disable_actions.emit(True)
Expand All @@ -343,7 +333,10 @@ def _start_download(self):
# Only show progress bar for installers
if not self.installer_path.endswith('zip'):
self.progress_dialog = ProgressDialog(
self, _("Downloading Spyder {} ...").format(self.latest_release)
self,
_("Downloading Spyder {} ...").format(
self.asset_info["version"]
)
)
self.progress_dialog.cancel.clicked.connect(self._cancel_download)

Expand Down Expand Up @@ -422,7 +415,7 @@ def _confirm_install(self):
self,
msg,
_('Spyder install'),
version=self.latest_release,
version=self.asset_info["version"],
on_close=True
)
if box.result() == QMessageBox.Yes:
Expand All @@ -444,11 +437,11 @@ def start_install(self):

# Sub command
sub_cmd = [tmpscript_path, '-i', self.installer_path]
if self.update_type != 'major':
if self.asset_info["update_type"] != 'major':
# Update with conda
sub_cmd.extend(['-c', find_conda(), '-p', sys.prefix])

if self.update_type == 'minor':
if self.asset_info["update_type"] == 'minor':
# Rebuild runtime environment
sub_cmd.append('-r')

Expand Down
Loading
Loading