Commit 9301122e by 周田

fix: 修复 PyQt 更新对话框并发布 261030.0.0

parent cacd1900
Program Updater 更新日志 Program Updater 更新日志
========================= =========================
## 261030.0.0 [WHEEL]
发布日期:2026-10-30
发行包:program-updater
制品:program_updater-261030.0.0-py3-none-any.whl
- `run_startup_update()` 改为用户确认更新后才创建并显示下载进度框,修复首次
更新时确认框和进度框同时弹出的问题。
- 确认框和下载进度框移除标题栏帮助按钮;进度框不再显示取消/空白按钮。
## 261028.4.0 [WHEEL] ## 261028.4.0 [WHEEL]
发布日期:2026-10-28 发布日期:2026-10-28
......
[project] [project]
name = "program-updater" name = "program-updater"
version = "261028.4.0" version = "261030.0.0"
description = "Lightweight updater for Windows PyInstaller applications" description = "Lightweight updater for Windows PyInstaller applications"
readme = "updater/README.md" readme = "updater/README.md"
authors = [ authors = [
......
...@@ -41,14 +41,16 @@ def run_startup_update( ...@@ -41,14 +41,16 @@ def run_startup_update(
if QApplication.instance() is None: if QApplication.instance() is None:
raise RuntimeError("create QApplication before running the updater.") raise RuntimeError("create QApplication before running the updater.")
progress = QProgressDialog("正在下载更新…", "", 0, 0, parent) def remove_context_help(dialog: Any) -> None:
progress.setWindowTitle("程序更新") dialog.setWindowFlags(
progress.setWindowModality(Qt.WindowModal) dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint
progress.setMinimumDuration(0) )
progress.setAutoClose(False)
progress.setAutoReset(False) progress: QProgressDialog | None = None
def on_progress(downloaded: int, total: int) -> None: def on_progress(downloaded: int, total: int) -> None:
if progress is None:
return
progress.setLabelText("正在下载更新…") progress.setLabelText("正在下载更新…")
progress.setRange(0, 1000) progress.setRange(0, 1000)
progress.setValue(int(downloaded * 1000 / total) if total else 0) progress.setValue(int(downloaded * 1000 / total) if total else 0)
...@@ -57,20 +59,29 @@ def run_startup_update( ...@@ -57,20 +59,29 @@ def run_startup_update(
QApplication.processEvents() QApplication.processEvents()
def confirm_update(update) -> bool: def confirm_update(update) -> bool:
progress.hide() nonlocal progress
answer = QMessageBox.question( message_box = QMessageBox(parent)
parent, remove_context_help(message_box)
"发现新版本", message_box.setWindowTitle("发现新版本")
f"发现版本 {update.version},现在更新吗?选择“否”可稍后更新。", message_box.setIcon(QMessageBox.Question)
QMessageBox.Yes | QMessageBox.No, message_box.setText(
QMessageBox.Yes, f"发现版本 {update.version},现在更新吗?选择“否”可稍后更新。"
) )
if answer == QMessageBox.Yes: message_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
progress.setLabelText("准备下载更新…") message_box.setDefaultButton(QMessageBox.Yes)
progress.setRange(0, 0) if message_box.exec_() != QMessageBox.Yes:
return False
progress = QProgressDialog("准备下载更新…", None, 0, 0, parent)
remove_context_help(progress)
progress.setWindowTitle("程序更新")
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setAutoClose(False)
progress.setAutoReset(False)
progress.show() progress.show()
QApplication.processEvents() QApplication.processEvents()
return answer == QMessageBox.Yes return True
try: try:
return update_on_startup( return update_on_startup(
...@@ -92,4 +103,5 @@ def run_startup_update( ...@@ -92,4 +103,5 @@ def run_startup_update(
_logger.exception("自动更新失败,将启动当前版本") _logger.exception("自动更新失败,将启动当前版本")
return False return False
finally: finally:
if progress is not None:
progress.close() progress.close()
"""Tests for the optional PyQt5 integration."""
import sys
import types
import unittest
from unittest.mock import patch
from updater.integrations.pyqt import run_startup_update
HELP_BUTTON_HINT = 1 << 16
class FakeProgressDialog:
instances: list["FakeProgressDialog"] = []
def __init__(self, label, cancel_button, minimum, maximum, parent=None):
self.label = label
self.cancel_button = cancel_button
self.window_flags = HELP_BUTTON_HINT
self.visible = False
self.shown = False
self.closed = False
self.range = None
self.value = None
FakeProgressDialog.instances.append(self)
def setWindowTitle(self, title):
self.title = title
def setWindowModality(self, modality):
self.modality = modality
def setMinimumDuration(self, milliseconds):
self.minimum_duration = milliseconds
def setAutoClose(self, enabled):
self.auto_close = enabled
def setAutoReset(self, enabled):
self.auto_reset = enabled
def setWindowFlags(self, flags):
self.window_flags = flags
def windowFlags(self):
return self.window_flags
def setLabelText(self, text):
self.label = text
def setRange(self, minimum, maximum):
self.range = (minimum, maximum)
def setValue(self, value):
self.value = value
def isVisible(self):
return self.visible
def show(self):
self.visible = True
self.shown = True
def hide(self):
self.visible = False
def close(self):
self.visible = False
self.closed = True
class FakeApplication:
@staticmethod
def instance():
return object()
@staticmethod
def processEvents():
pass
class FakeMessageBox:
Yes = 1
No = 0
Question = 2
answer = No
instances: list["FakeMessageBox"] = []
def __init__(self, parent=None):
self.parent = parent
self.window_flags = HELP_BUTTON_HINT
self.title = None
self.text = None
FakeMessageBox.instances.append(self)
def setWindowTitle(self, title):
self.title = title
def setIcon(self, icon):
self.icon = icon
def setText(self, text):
self.text = text
def setStandardButtons(self, buttons):
self.buttons = buttons
def setDefaultButton(self, button):
self.default_button = button
def setWindowFlags(self, flags):
self.window_flags = flags
def windowFlags(self):
return self.window_flags
def exec_(self):
return FakeMessageBox.answer
class FakeUpdate:
version = "1.1.0"
def fake_modules():
pyqt = types.ModuleType("PyQt5")
qtcore = types.ModuleType("PyQt5.QtCore")
qtwidgets = types.ModuleType("PyQt5.QtWidgets")
qtcore.Qt = types.SimpleNamespace(
WindowModal=2,
WindowContextHelpButtonHint=HELP_BUTTON_HINT,
)
qtwidgets.QApplication = FakeApplication
qtwidgets.QMessageBox = FakeMessageBox
qtwidgets.QProgressDialog = FakeProgressDialog
pyqt.QtCore = qtcore
pyqt.QtWidgets = qtwidgets
return {
"PyQt5": pyqt,
"PyQt5.QtCore": qtcore,
"PyQt5.QtWidgets": qtwidgets,
}
class PyQtIntegrationTests(unittest.TestCase):
def setUp(self):
FakeProgressDialog.instances = []
FakeMessageBox.instances = []
FakeMessageBox.answer = FakeMessageBox.No
def run_updater(self, updater):
with patch.dict(sys.modules, fake_modules()), patch(
"updater.integrations.pyqt.update_on_startup",
side_effect=updater,
):
return run_startup_update(
"my-program",
"1.0.0",
"http://127.0.0.1:8000/latest.json",
)
def test_declining_does_not_create_progress_dialog(self):
def update_on_startup(*args, **kwargs):
self.assertFalse(kwargs["confirm_update"](FakeUpdate()))
return False
self.assertFalse(self.run_updater(update_on_startup))
self.assertEqual(FakeProgressDialog.instances, [])
self.assertEqual(len(FakeMessageBox.instances), 1)
def test_confirmation_has_no_help_button(self):
def update_on_startup(*args, **kwargs):
kwargs["confirm_update"](FakeUpdate())
return False
self.run_updater(update_on_startup)
message_box = FakeMessageBox.instances[0]
self.assertFalse(message_box.window_flags & HELP_BUTTON_HINT)
def test_progress_dialog_is_created_only_after_confirmation(self):
FakeMessageBox.answer = FakeMessageBox.Yes
def update_on_startup(*args, **kwargs):
on_progress = kwargs["on_progress"]
on_progress(1, 10)
self.assertEqual(FakeProgressDialog.instances, [])
self.assertTrue(kwargs["confirm_update"](FakeUpdate()))
on_progress(5, 10)
return True
self.assertTrue(self.run_updater(update_on_startup))
self.assertEqual(len(FakeProgressDialog.instances), 1)
progress = FakeProgressDialog.instances[0]
self.assertTrue(progress.shown)
self.assertTrue(progress.closed)
self.assertEqual(progress.range, (0, 1000))
self.assertEqual(progress.value, 500)
def test_progress_dialog_has_no_help_or_cancel_button(self):
FakeMessageBox.answer = FakeMessageBox.Yes
def update_on_startup(*args, **kwargs):
kwargs["confirm_update"](FakeUpdate())
return False
self.assertFalse(self.run_updater(update_on_startup))
progress = FakeProgressDialog.instances[0]
self.assertFalse(progress.window_flags & HELP_BUTTON_HINT)
self.assertIsNone(progress.cancel_button)
def test_confirmation_uses_discovered_version(self):
def update_on_startup(*args, **kwargs):
kwargs["confirm_update"](FakeUpdate())
return False
self.run_updater(update_on_startup)
message_box = FakeMessageBox.instances[0]
self.assertEqual(message_box.title, "发现新版本")
self.assertIn(FakeUpdate.version, message_box.text)
if __name__ == "__main__":
unittest.main()
...@@ -130,7 +130,7 @@ wheels = [ ...@@ -130,7 +130,7 @@ wheels = [
[[package]] [[package]]
name = "program-updater" name = "program-updater"
version = "261028.4.0" version = "261030.0.0"
source = { editable = "." } source = { editable = "." }
[package.optional-dependencies] [package.optional-dependencies]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment