From 313e249bf423784d50cc5697cde467ed65ddbb8e Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Thu, 1 Jun 2023 16:50:46 +0200 Subject: [PATCH] Add update functionality - Add `Check for updates` to Help menu - Checks if latest version on pypi is more recent than running version - If so, offers to upgrade the app - Upgrade is done by calling pip in a subprocess - When user restarts the app they see the new version - Allows windows users to update the app without having to run the installer again or use pip - bump version to 0.27.0 - resolves #187 --- .pre-commit-config.yaml | 2 +- docs/developer/install.rst | 2 +- docs/quickstart/install.rst | 7 +++-- pyproject.toml | 10 ++++++- src/vstt/__init__.py | 2 +- src/vstt/gui.py | 23 +++++++++++++++ src/vstt/update.py | 59 +++++++++++++++++++++++++++++++++++++ tests/test_update.py | 31 +++++++++++++++++++ 8 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 src/vstt/update.py create mode 100644 tests/test_update.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0bf04fe..5d151a6e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: rev: v1.3.0 hooks: - id: mypy - additional_dependencies: [numpy, PyQt5-stubs] + additional_dependencies: [numpy, PyQt5-stubs, types-requests] args: [ --ignore-missing-imports, diff --git a/docs/developer/install.rst b/docs/developer/install.rst index 4be0bf33..54b4e458 100644 --- a/docs/developer/install.rst +++ b/docs/developer/install.rst @@ -30,6 +30,6 @@ Unfortunately, psychopy itself has a lot of dependencies, some of which are syst * ``sudo apt-get install swig libasound2-dev portaudio19-dev libpulse-dev libusb-1.0-0-dev libsndfile1-dev libportmidi-dev liblo-dev libgtk-3-dev`` * ``pip install wxPython`` (which took a long time to complete - alternative is to install a `pre-built wheel `_) * additionally, at runtime psychtoolbox needs permission on linux to set its priority: - * ``sudo setcap cap_sys_nice+ep \`python -c "import os; import sys; print(os.path.realpath(sys.executable))"\``` + * ``sudo setcap cap_sys_nice+ep `python -c "import os; import sys; print(os.path.realpath(sys.executable))"``` * alternatively simply remove psychtoolbox (it is an optional psychopy dependency): * ``pip uninstall psychtoolbox`` diff --git a/docs/quickstart/install.rst b/docs/quickstart/install.rst index 9b1a6b4b..8bc5dbe5 100644 --- a/docs/quickstart/install.rst +++ b/docs/quickstart/install.rst @@ -19,6 +19,9 @@ VSTT can also be installed from `PyPI `_ using pi .. note:: On linux the optional psychtoolbox dependency needs permission to set its priority. To allow this: - * ``sudo setcap cap_sys_nice+ep `python -c "import os; import sys; print(os.path.realpath(sys.executable))"`` - Alternatively you can simply remove psychtoolbox + + * ``sudo setcap cap_sys_nice+ep `python -c "import os; import sys; print(os.path.realpath(sys.executable))"``` + + Alternatively you can simply remove psychtoolbox: + * ``pip uninstall psychtoolbox`` diff --git a/pyproject.toml b/pyproject.toml index 2f3be4f3..99384844 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,15 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] -dependencies = ["psychopy", "psychopy-sounddevice", "numpy", "click", "PyQt5"] +dependencies = [ + "psychopy", + "psychopy-sounddevice", + "numpy", + "click", + "PyQt5", + "requests", + "packaging" +] dynamic = ["version"] [project.scripts] diff --git a/src/vstt/__init__.py b/src/vstt/__init__.py index fecb5950..5e7102d0 100644 --- a/src/vstt/__init__.py +++ b/src/vstt/__init__.py @@ -5,4 +5,4 @@ "__version__", ] -__version__ = "0.26.0" +__version__ = "0.27.0" diff --git a/src/vstt/gui.py b/src/vstt/gui.py index 388424f2..06eab8f5 100644 --- a/src/vstt/gui.py +++ b/src/vstt/gui.py @@ -16,6 +16,8 @@ from vstt.results_widget import ResultsWidget from vstt.task import MotorTask from vstt.trials_widget import TrialsWidget +from vstt.update import check_for_new_version +from vstt.update import do_pip_upgrade class Gui(QtWidgets.QMainWindow): @@ -225,6 +227,26 @@ def about(self) -> None: + "https://ssciwr.github.io/vstt", ) + def check_for_updates(self) -> None: + title = "Update VSTT" + QtWidgets.QApplication.setOverrideCursor(Qt.WaitCursor) + new_version_available, new_version_message = check_for_new_version() + QtWidgets.QApplication.restoreOverrideCursor() + if not new_version_available: + QtWidgets.QMessageBox.information( + self, + title, + new_version_message, + ) + return + yes_no = QtWidgets.QMessageBox.question(self, title, new_version_message) + if yes_no != QtWidgets.QMessageBox.Yes: + return + QtWidgets.QApplication.setOverrideCursor(Qt.WaitCursor) + upgrade_success, upgrade_message = do_pip_upgrade() + QtWidgets.QApplication.restoreOverrideCursor() + QtWidgets.QMessageBox.information(self, title, upgrade_message) + def _add_action( name: str, @@ -300,5 +322,6 @@ def _create_menu_and_toolbar(gui: vstt.gui.Gui) -> QtWidgets.QToolBar: ) help_menu = menu.addMenu("&Help") _add_action("&About", gui.about, help_menu) + _add_action("&Check for updates", gui.check_for_updates, help_menu) toolbar.setContextMenuPolicy(Qt.PreventContextMenu) return toolbar diff --git a/src/vstt/update.py b/src/vstt/update.py new file mode 100644 index 00000000..9b154de4 --- /dev/null +++ b/src/vstt/update.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import logging +import subprocess +import sys +from typing import Tuple + +import requests +import vstt +from packaging import version + + +def _get_latest_pypi_version() -> version.Version: + logging.info("Requesting latest version from pypi...") + r = requests.get("https://pypi.org/pypi/vstt/json", timeout=5) + version_str = r.json().get("info", {}).get("version", "") + ver = version.parse(version_str) + logging.info(f" -> {ver}") + return ver + + +def check_for_new_version() -> Tuple[bool, str]: + try: + current_version = version.parse(vstt.__version__) + logging.info(f"Current version: {current_version}") + latest_version = _get_latest_pypi_version() + logging.info(f"Latest version: {latest_version}") + if latest_version > current_version: + return ( + True, + f"Latest version of VSTT: {latest_version}.\nYou currently have version {current_version}.\nWould you like to upgrade now?", + ) + else: + return ( + False, + "You have the latest version of VSTT.", + ) + except Exception as e: + logging.exception(e) + return ( + False, + "An error occurred while checking for updates, please try again later.", + ) + + +def do_pip_upgrade() -> Tuple[bool, str]: + logging.info(f"Doing pip upgrade using {sys.executable}...") + try: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "--upgrade", "vstt"] + ) + logging.info("done.") + return ( + True, + "VSTT has been updated.\nPlease close the program and open it again to see the latest version.", + ) + except Exception as e: + logging.exception(e) + return False, f"An error occurred when trying to update VSTT: {e}" diff --git a/tests/test_update.py b/tests/test_update.py new file mode 100644 index 00000000..f182a07e --- /dev/null +++ b/tests/test_update.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import sys + +import vstt +from pytest import MonkeyPatch +from vstt.update import check_for_new_version +from vstt.update import do_pip_upgrade + + +def test_new_version_available(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(vstt, "__version__", "0.0.0") + available, message = check_for_new_version() + assert available is True + assert "0.0.0" in message + assert "upgrade" in message + monkeypatch.setattr(vstt, "__version__", "9999.0.0") + available, message = check_for_new_version() + assert available is False + assert "latest version" in message + monkeypatch.setattr(vstt, "__version__", "imnot-a-valid-version!") + available, message = check_for_new_version() + assert available is False + assert "error" in message + + +def test_do_pip_upgrade(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(sys, "executable", "abc123_i_dont_exist") + success, message = do_pip_upgrade() + assert success is False + assert "error" in message