fix: catch update notification exceptions (#1278)

* feat: add test for failing GitHub API call.

* fix: don't raise exceptions in get_most_recent_release_version.

* style: apply ruff.

* That not should not have been there.
This commit is contained in:
Sola-ris
2026-05-09 08:57:07 +02:00
committed by GitHub
parent cece7920a8
commit 7af7420167
6 changed files with 41 additions and 10 deletions

View File

@@ -46,6 +46,7 @@ pyinstaller = ["Pyinstaller~=6.13"]
pytest = [
"pytest==8.3.5",
"pytest-cov==6.1.1",
"pytest-mock==3.15.1",
"pytest-qt==4.4.0",
"syrupy==4.9.1",
]

View File

@@ -193,18 +193,31 @@ class TagStudioCore:
@staticmethod
@lru_cache(maxsize=1)
def get_most_recent_release_version() -> str:
"""Get the version of the most recent Github release."""
resp = requests.get("https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest")
assert resp.status_code == 200, "Could not fetch information on latest release."
def get_most_recent_release_version() -> str | None:
"""Get the version of the most recent GitHub release."""
try:
resp = requests.get(
"https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest"
)
except Exception as e:
logger.error("Error getting most recent GitHub release.", error=e)
return None
if resp.status_code != 200:
logger.error("Error getting most recent GitHub release.", status_code=resp.status_code)
return None
data = resp.json()
tag: str = data["tag_name"]
assert tag.startswith("v")
if not tag.startswith("v"):
logger.error("Unexpected tag format.", tag=tag)
return None
version = tag[1:]
# the assert does not allow for prerelease/build,
# the assertion does not allow for prerelease/build,
# because the latest release should never have them
assert re.match(r"^\d+\.\d+\.\d+$", version) is not None, "Invalid version format."
if re.match(r"^\d+\.\d+\.\d+$", version) is None:
logger.error("Invalid version format.", version=version)
return None
return version

View File

@@ -4,6 +4,7 @@ from PySide6.QtWidgets import QMessageBox
from tagstudio.core.constants import VERSION
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
@@ -30,7 +31,7 @@ class OutOfDateMessageBox(QMessageBox):
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
latest_release_version = TagStudioCore.get_most_recent_release_version()
latest_release_version = unwrap(TagStudioCore.get_most_recent_release_version())
status = Translations.format(
"version_modal.status",
installed_version=f"<span style='color:{red}'>{VERSION}</span>",

View File

@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (
from tagstudio.core.constants import VERSION, VERSION_BRANCH
from tagstudio.core.enums import Theme
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored import ffmpeg
from tagstudio.qt.resource_manager import ResourceManager
@@ -106,7 +107,7 @@ class AboutModal(QWidget):
# Version
version_title = QLabel("Version")
most_recent_release = TagStudioCore.get_most_recent_release_version()
most_recent_release = unwrap(TagStudioCore.get_most_recent_release_version(), "UNKNOWN")
version_content_style = self.form_content_style
if most_recent_release == VERSION:
version_content = QLabel(f"{VERSION}")

View File

@@ -613,7 +613,8 @@ class QtDriver(DriverMixin, QObject):
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
FfmpegMissingMessageBox().show()
if is_version_outdated(VERSION, TagStudioCore.get_most_recent_release_version()):
latest_version = TagStudioCore.get_most_recent_release_version()
if latest_version and is_version_outdated(VERSION, latest_version):
OutOfDateMessageBox().exec()
self.app.exec()

View File

@@ -0,0 +1,14 @@
from pytestqt.qtbot import QtBot
from tagstudio.qt.mixed.about_modal import AboutModal
def test_github_api_unavailable(qtbot: QtBot, mocker) -> None:
mocker.patch(
"requests.get",
side_effect=ConnectionError(
"Failed to resolve 'api.github.com' ([Errno -3] Temporary failure in name resolution)"
),
)
modal = AboutModal("/tmp")
qtbot.addWidget(modal)