New feature: browse Python environment

初步实现用户浏览与选择 Python 解释器环境功能;
This commit is contained in:
muzing 2023-12-31 00:35:33 +08:00
parent e75204a282
commit fc7dfd7dfb
5 changed files with 127 additions and 30 deletions

View File

@ -86,7 +86,7 @@ class InterpreterValidator:
except subprocess.SubprocessError:
return False
except OSError as e:
print(f"无法启动Python解释器有效性验证子进程{e}")
print(f"{path} 启动 Python 解释器有效性验证子进程失败{e}")
return False
else:
return True

View File

@ -45,6 +45,9 @@ class PyEnv:
else:
self.type = type_
def __repr__(self) -> str:
return f"PyEnv object (executable_path={self.exe_path}, type={self.type})"
@staticmethod
def get_py_version(executable_path: Union[str, Path]) -> str:
"""获取Python解释器的版本以形如 "3.11.7" 的字符串形式返回

View File

@ -28,9 +28,11 @@ from PySide6.QtWidgets import (
)
from ..Constants import PyInstOpt
from ..Core import InterpreterValidator
from ..Utilities import PyEnv
from .add_data_widget import AddDataWindow
from .arguments_browser import ArgumentsBrowser
from .dialog_widgets import IconFileDlg, ScriptFileDlg
from .dialog_widgets import IconFileDlg, InterpreterFileDlg, ScriptFileDlg
from .multi_item_edit_widget import MultiPkgEditWindow
from .pyenv_combobox import PyEnvComboBox
from .pyinstaller_option_widget import load_pyinst_options
@ -41,6 +43,7 @@ class CenterWidget(QWidget):
# 自定义信号
option_selected = QtCore.Signal(tuple) # 用户通过界面控件选择选项后发射此信号
# option_selected 实际类型为 tuple[PyinstallerArgs, str]
def __init__(self, parent: QMainWindow) -> None:
@ -61,8 +64,10 @@ class CenterWidget(QWidget):
self.script_browse_btn = QPushButton()
self.script_path_le = QLineEdit()
# Python 解释器选择下拉框
# Python 解释器选择
self.pyenv_combobox = PyEnvComboBox()
self.pyenv_browse_btn = QPushButton()
self.itp_dlg = InterpreterFileDlg()
# 打包后输出的项目名称
self.project_name_label = QLabel()
@ -107,6 +112,10 @@ class CenterWidget(QWidget):
self.script_path_le.setReadOnly(True)
self.script_path_le.setPlaceholderText("Python入口文件路径")
self.script_browse_btn.setText("浏览")
self.script_browse_btn.setFixedWidth(80)
self.pyenv_browse_btn.setText("浏览")
self.pyenv_browse_btn.setFixedWidth(80)
self.project_name_label.setText("项目名称:")
self.project_name_le.setPlaceholderText("打包的应用程序名称")
@ -147,6 +156,7 @@ class CenterWidget(QWidget):
)
self.clean_checkbox.setToolTip(opt["--clean"])
# noinspection DuplicatedCode
def _connect_slots(self) -> None:
"""定义、连接信号与槽"""
@ -245,7 +255,6 @@ class CenterWidget(QWidget):
self.script_browse_btn.clicked.connect(self.script_file_dlg.open)
self.script_file_dlg.fileSelected.connect(script_file_selected)
self.project_name_le.editingFinished.connect(project_name_selected)
# noinspection DuplicatedCode
self.fd_group.idClicked.connect(one_fd_selected)
self.add_data_btn.clicked.connect(handle_add_data_btn_clicked)
self.add_data_dlg.data_selected.connect(add_data_selected)
@ -255,15 +264,27 @@ class CenterWidget(QWidget):
self.hidden_import_dlg.items_selected.connect(hidden_import_selected)
self.clean_checkbox.toggled.connect(clean_selected)
# 添加 Python 解释器
self.pyenv_browse_btn.clicked.connect(self.itp_dlg.open)
self.itp_dlg.fileSelected.connect(self._handle_itp_file_selected)
# noinspection DuplicatedCode
def _set_layout(self) -> None:
"""设置布局管理器"""
self.main_layout = QVBoxLayout()
script_layout = QGridLayout()
script_layout.addWidget(self.script_path_label, 0, 0, 1, 2)
script_layout.addWidget(self.script_path_le, 1, 0)
script_layout.addWidget(self.script_browse_btn, 1, 1)
pyenv_layout = QHBoxLayout()
pyenv_layout.addWidget(self.pyenv_combobox)
pyenv_layout.addWidget(self.pyenv_browse_btn)
pyenv_layout.setStretchFactor(self.pyenv_combobox, 3)
pyenv_layout.setStretchFactor(self.pyenv_browse_btn, 1)
name_layout = QVBoxLayout()
name_layout.addWidget(self.project_name_label)
name_layout.addWidget(self.project_name_le)
@ -277,26 +298,24 @@ class CenterWidget(QWidget):
add_btn_layout.addWidget(self.add_data_btn)
add_btn_layout.addWidget(self.add_binary_btn)
main_layout = QVBoxLayout()
self.main_layout = main_layout
main_layout.addSpacing(10)
main_layout.addLayout(script_layout)
main_layout.addWidget(self.pyenv_combobox)
main_layout.addStretch(10)
main_layout.addLayout(name_layout)
main_layout.addStretch(10)
main_layout.addLayout(fd_layout)
main_layout.addStretch(10)
main_layout.addLayout(add_btn_layout)
main_layout.addStretch(10)
main_layout.addWidget(self.hidden_import_btn)
main_layout.addStretch(10)
main_layout.addWidget(self.clean_checkbox)
main_layout.addStretch(10)
main_layout.addWidget(self.pyinstaller_args_browser)
main_layout.addWidget(self.run_packaging_btn)
self.main_layout.addSpacing(10)
self.main_layout.addLayout(script_layout)
self.main_layout.addLayout(pyenv_layout)
self.main_layout.addStretch(10)
self.main_layout.addLayout(name_layout)
self.main_layout.addStretch(10)
self.main_layout.addLayout(fd_layout)
self.main_layout.addStretch(10)
self.main_layout.addLayout(add_btn_layout)
self.main_layout.addStretch(10)
self.main_layout.addWidget(self.hidden_import_btn)
self.main_layout.addStretch(10)
self.main_layout.addWidget(self.clean_checkbox)
self.main_layout.addStretch(10)
self.main_layout.addWidget(self.pyinstaller_args_browser)
self.main_layout.addWidget(self.run_packaging_btn)
self.setLayout(main_layout)
self.setLayout(self.main_layout)
@QtCore.Slot(tuple)
def handle_option_set(self, option: tuple[PyInstOpt, str]) -> None:
@ -346,13 +365,67 @@ class CenterWidget(QWidget):
@QtCore.Slot(bool)
def handle_ready_to_pack(self, ready: bool) -> None:
"""
处理 ready_to_pack 信号的槽 \n
"""处理 ready_to_pack 信号的槽
:param ready: 是否可以进行打包
"""
self.run_packaging_btn.setEnabled(ready)
@QtCore.Slot(str)
def _handle_itp_file_selected(self, file_path: str) -> None:
"""Python解释器文件完成选择的槽函数
首先对用户选择的文件进行有效性判断
若有效则以此创建新的 PyEnv 对象设置到下拉框中并选中
若无效则弹出错误警告对话框要求重新选择文件
:param file_path: 选择的解释器文件路径
"""
itp_path = Path(file_path).absolute()
# 首先判断用户选择的路径是否已在当前存储的列表中,如在则不添加直接返回
# FIXME 此段代码似乎无效,需要解决
for index in range(self.pyenv_combobox.count()):
exist_env: PyEnv = self.pyenv_combobox.itemData(index)
print(itp_path)
print(exist_env.exe_path)
if itp_path == exist_env.exe_path:
return
if InterpreterValidator.validate(itp_path):
# 用户选择的解释器有效
new_pyenv = PyEnv(itp_path, type_=None)
if not new_pyenv.pkg_installed("pyinstaller"):
# 但在该环境中没有安装 PyInstaller询问用户是否继续操作
result = QMessageBox.warning(
self.parent_widget,
"警告",
"在该 Python 环境中似乎没有安装 Pyinstaller" "是否仍要继续?",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Cancel,
)
if result == QMessageBox.StandardButton.Cancel:
return
self.pyenv_combobox.addItem(*self.pyenv_combobox.gen_item(new_pyenv))
self.pyenv_combobox.setCurrentIndex(self.pyenv_combobox.count() - 1)
else:
self.itp_dlg.close()
# 警告对话框
result = QMessageBox.critical(
self.parent_widget,
"错误",
"选择的不是有效的Python解释器请重新选择",
QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Ok,
)
if result == QMessageBox.StandardButton.Ok:
self.itp_dlg.exec()
class WinMacCenterWidget(CenterWidget):
"""
@ -386,6 +459,8 @@ class WinMacCenterWidget(CenterWidget):
self.icon_path_le.setReadOnly(True)
self.icon_path_le.setPlaceholderText("图标文件路径")
self.icon_browse_btn.setText("浏览")
self.icon_browse_btn.setFixedWidth(80)
self.console_checkbox.setText("为标准I/O启用终端")
self.console_checkbox.setChecked(True) # 默认值
@ -441,12 +516,13 @@ class WinMacCenterWidget(CenterWidget):
super()._set_layout()
self.main_layout.insertWidget(7, self.console_checkbox)
self.main_layout.addStretch(10)
icon_layout = QGridLayout()
icon_layout.addWidget(self.icon_path_label, 0, 0, 1, 2)
icon_layout.addWidget(self.icon_path_le, 1, 0)
icon_layout.addWidget(self.icon_browse_btn, 1, 1)
self.main_layout.insertWidget(7, self.console_checkbox)
self.main_layout.addStretch(10)
self.main_layout.insertLayout(8, icon_layout)
@QtCore.Slot(tuple)

View File

@ -63,6 +63,24 @@ class IconFileDlg(QFileDialog):
self.setLabelText(QFileDialog.DialogLabel.Reject, "取消")
class InterpreterFileDlg(QFileDialog):
"""用于获取 Python 解释器可执行文件的对话框"""
def __init__(self, parent: Optional[QWidget] = None) -> None:
"""
:param parent: 父控件对象
"""
super().__init__(parent)
self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
self.setViewMode(QFileDialog.ViewMode.Detail)
self.setFileMode(QFileDialog.FileMode.ExistingFile)
self.setLabelText(QFileDialog.DialogLabel.FileName, "Python解释器")
self.setLabelText(QFileDialog.DialogLabel.FileType, "可执行文件")
# TODO 增加一定筛选,如 Windows 平台自动设置 .exe setNameFilters
class AboutDlg(QMessageBox):
"""用于显示关于信息的对话框"""

View File

@ -30,14 +30,14 @@ class PyEnvComboBox(QComboBox):
if not RUNTIME_INFO.is_bundled:
# 在非 PyInstaller 捆绑环境中,第一项为当前用于运行 Py2exe-GUI 的 Python 环境
current_pyenv = PyEnv(sys.executable, None)
self.addItem(*self._gen_item(current_pyenv))
self.addItem(*self.gen_item(current_pyenv))
else:
# 若已由 PyInstaller 捆绑成冻结应用程序,则第一项为系统 Python 环境
sys_pyenv = PyEnv(get_sys_python(), PyEnvType.system)
self.addItem(*self._gen_item(sys_pyenv))
self.addItem(*self.gen_item(sys_pyenv))
@staticmethod
def _gen_item(pyenv: PyEnv) -> tuple:
def gen_item(pyenv: PyEnv) -> tuple:
"""根据传入的 Python 环境,生成一个适用于 QComboBox.addItem() 参数的三元素元组
:param pyenv: Python 解释器环境