v2.8.5 - fix: resolve DuplicateOptionError on config load and replace webbrowser with QDesktopServices

fix: resolve config DuplicateOptionError and replace webbrowser with QDesktopServices

- Config: Initialize ConfigParser with strict=False and normalize DEFAULT_MAPPINGS keys to lowercase to prevent DuplicateOptionError.
- Settings: Ensure package mapping keys are lowercased before persisting to config.ini.
- UI: Replace dynamic `__import__('webbrowser')` calls with `QDesktopServices.openUrl` to fix ModuleNotFoundError in standalone builds.
- Bump version to 2.8.5.
This commit is contained in:
QwejayHuang committed 2026-09-10 22:44:27 +08:00
1 parent 31cd5345ed
commit 12d7ef32d3
4 files changed
+39 -11

No files matched your search

+12
View File
@@ -4,6 +4,18 @@ QPyPack 项目的所有重要变更都将记录在此文件中。
[English Version](./CHANGELOG_EN.md) | **中文**
--
## [2.8.5] - 2026-09-10
### 修复
- 修复配置文件读取时因键名大小写映射导致 `configparser.DuplicateOptionError` 崩溃的问题
- 修复独立打包(Standalone)环境下点击关于页面链接及 Python 下载链接时抛出 `ModuleNotFoundError: No module named 'webbrowser'` 的问题
### 优化
- 配置解析器开启宽松容错模式(`strict=False`),并在加载与保存时统一做小写预去重,提升配置读写健壮性
- 移除对 Python 标准库 `webbrowser` 的隐式动态依赖,全面改用 Qt 原生 `QDesktopServices.openUrl` 打开系统默认浏览器,提升跨平台兼容性并减少打包产物体积
---
## [2.8.4] - 2026-09-08
+12
View File
@@ -6,6 +6,18 @@ All notable changes to the **QPyPack** project will be documented in this file.
---
## [2.8.5] - 2026-09-10
### Fixed
- Fixed crash caused by `configparser.DuplicateOptionError` when loading configurations with case-insensitive mapping keys.
- Fixed `ModuleNotFoundError: No module named 'webbrowser'` when clicking About links or the Python download button in standalone frozen builds.
### Improved
- Enabled non-strict parsing (`strict=False`) in `configparser` and normalized mapping keys to lowercase to ensure robust configuration I/O.
- Replaced dynamic `webbrowser` calls with native Qt `QDesktopServices.openUrl` for better cross-platform reliability and reduced binary bundle size.
---
## [2.8.4] - 2026-09-08
### Fixed
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "qpypack"
version = "2.8.4"
version = "2.8.5"
description = "A cross-platform GUI packaging and compilation suite based on PyInstaller and Nuitka."
readme = "README_EN.md"
authors = [
+14 -10
View File
@@ -125,7 +125,7 @@ except ImportError:
HAS_QT_AUDIO = False
__app_name__ = "QPyPack"
__version__ = "2.8.4"
__version__ = "2.8.5"
__author__ = "QwejayHuang"
__company__ = "Qwesoft"
__description__ = "Modern Cross-Platform Python Packaging GUI Powered by PyInstaller & Nuitka"
@@ -889,7 +889,7 @@ MATERIAL_ICONS = {
def load_config(retry=True):
config = configparser.ConfigParser()
config = configparser.ConfigParser(strict=False)
default_mirror = "https://pypi.org/simple"
default_backup = "https://test.pypi.org/simple"
@@ -897,8 +897,12 @@ def load_config(retry=True):
default_mirror = "https://pypi.tuna.tsinghua.edu.cn/simple"
default_backup = "https://mirrors.aliyun.com/pypi/simple/"
clean_default_mappings = {}
for k, v in DEFAULT_MAPPINGS.items():
clean_default_mappings[k.lower()] = v
if not os.path.exists(CONFIG_FILE):
config["Mappings"] = {k.lower(): v for k, v in DEFAULT_MAPPINGS.items()}
config["Mappings"] = clean_default_mappings
config["Settings"] = {
"language": "auto",
"engine": "PyInstaller",
@@ -948,19 +952,19 @@ def load_config(retry=True):
return load_config(retry=False)
else:
if "Mappings" not in config:
config["Mappings"] = {k.lower(): v for k, v in DEFAULT_MAPPINGS.items()}
config["Mappings"] = clean_default_mappings
if "Settings" not in config:
config["Settings"] = {}
if "BackportRules" not in config:
config["BackportRules"] = DEFAULT_BACKPORT_RULES
if "Mappings" not in config:
config["Mappings"] = {k.lower(): v for k, v in DEFAULT_MAPPINGS.items()}
config["Mappings"] = clean_default_mappings
else:
updated_map = False
for k, v in DEFAULT_MAPPINGS.items():
for k, v in clean_default_mappings.items():
if k not in config["Mappings"]:
config["Mappings"][k.lower()] = v
config["Mappings"][k] = v
updated_map = True
if updated_map:
try:
@@ -3717,7 +3721,7 @@ class SettingsPanel(QWidget):
QPushButton:hover {{ background-color: {hover_bg}; color: {icon_color}; }}
QPushButton:pressed {{ background-color: {pressed_bg}; }}
""")
btn.clicked.connect(lambda: __import__("webbrowser").open(url))
btn.clicked.connect(lambda checked=False, target_url=url: QDesktopServices.openUrl(QUrl(target_url)))
return btn
p_github = "M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.379.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z"
@@ -4509,7 +4513,7 @@ class SettingsPanel(QWidget):
for r in range(self.mapping_table.rowCount()):
item_k = self.mapping_table.item(r, 0)
item_v = self.mapping_table.item(r, 1)
k = item_k.text().strip() if item_k else ""
k = item_k.text().strip().lower() if item_k else ""
v = item_v.text().strip() if item_v else ""
if k and v:
config["Mappings"][k] = v
@@ -7935,7 +7939,7 @@ class MainWindow(QMainWindow):
msg.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole)
msg.exec()
if msg.clickedButton() == btn_down:
__import__("webbrowser").open("https://www.python.org/downloads/")
QDesktopServices.openUrl(QUrl("https://www.python.org/downloads/"))
return
validation_errors = []