Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
program-updater
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
周田
program-updater
Commits
9301122e
Commit
9301122e
authored
Oct 30, 2026
by
周田
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 修复 PyQt 更新对话框并发布 261030.0.0
parent
cacd1900
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
264 additions
and
19 deletions
+264
-19
CHANGELOG.txt
dist/CHANGELOG.txt
+10
-0
program_updater-261030.0.0-py3-none-any.whl
dist/program_updater-261030.0.0-py3-none-any.whl
+0
-0
pyproject.toml
pyproject.toml
+1
-1
pyqt.py
updater/integrations/pyqt.py
+29
-17
test_pyqt.py
updater/tests/test_pyqt.py
+223
-0
uv.lock
uv.lock
+1
-1
No files found.
dist/CHANGELOG.txt
View file @
9301122e
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]
发布日期:2026-10-28
...
...
dist/program_updater-261030.0.0-py3-none-any.whl
0 → 100644
View file @
9301122e
File added
pyproject.toml
View file @
9301122e
[project]
name
=
"program-updater"
version
=
"2610
28.4
.0"
version
=
"2610
30.0
.0"
description
=
"Lightweight updater for Windows PyInstaller applications"
readme
=
"updater/README.md"
authors
=
[
...
...
updater/integrations/pyqt.py
View file @
9301122e
...
...
@@ -41,14 +41,16 @@ def run_startup_update(
if
QApplication
.
instance
()
is
None
:
raise
RuntimeError
(
"create QApplication before running the updater."
)
progress
=
QProgressDialog
(
"正在下载更新…"
,
""
,
0
,
0
,
parent
)
progress
.
setWindowTitle
(
"程序更新"
)
progress
.
setWindowModality
(
Qt
.
WindowModal
)
progress
.
setMinimumDuration
(
0
)
progress
.
setAutoClose
(
False
)
progress
.
setAutoReset
(
False
)
def
remove_context_help
(
dialog
:
Any
)
->
None
:
dialog
.
setWindowFlags
(
dialog
.
windowFlags
()
&
~
Qt
.
WindowContextHelpButtonHint
)
progress
:
QProgressDialog
|
None
=
None
def
on_progress
(
downloaded
:
int
,
total
:
int
)
->
None
:
if
progress
is
None
:
return
progress
.
setLabelText
(
"正在下载更新…"
)
progress
.
setRange
(
0
,
1000
)
progress
.
setValue
(
int
(
downloaded
*
1000
/
total
)
if
total
else
0
)
...
...
@@ -57,20 +59,29 @@ def run_startup_update(
QApplication
.
processEvents
()
def
confirm_update
(
update
)
->
bool
:
progress
.
hide
()
answer
=
QMessageBox
.
question
(
parent
,
"发现新版本"
,
f
"发现版本 {update.version},现在更新吗?选择“否”可稍后更新。"
,
QMessageBox
.
Yes
|
QMessageBox
.
No
,
QMessageBox
.
Yes
,
nonlocal
progress
message_box
=
QMessageBox
(
parent
)
remove_context_help
(
message_box
)
message_box
.
setWindowTitle
(
"发现新版本"
)
message_box
.
setIcon
(
QMessageBox
.
Question
)
message_box
.
setText
(
f
"发现版本 {update.version},现在更新吗?选择“否”可稍后更新。"
)
if
answer
==
QMessageBox
.
Yes
:
progress
.
setLabelText
(
"准备下载更新…"
)
progress
.
setRange
(
0
,
0
)
message_box
.
setStandardButtons
(
QMessageBox
.
Yes
|
QMessageBox
.
No
)
message_box
.
setDefaultButton
(
QMessageBox
.
Yes
)
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
()
QApplication
.
processEvents
()
return
answer
==
QMessageBox
.
Yes
return
True
try
:
return
update_on_startup
(
...
...
@@ -92,4 +103,5 @@ def run_startup_update(
_logger
.
exception
(
"自动更新失败,将启动当前版本"
)
return
False
finally
:
if
progress
is
not
None
:
progress
.
close
()
updater/tests/test_pyqt.py
0 → 100644
View file @
9301122e
"""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
()
uv.lock
View file @
9301122e
...
...
@@ -130,7 +130,7 @@ wheels = [
[[package]]
name = "program-updater"
version = "2610
28.4
.0"
version = "2610
30.0
.0"
source = { editable = "." }
[package.optional-dependencies]
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment