Thu, 27 Jun 2024 15:42:50 +0200
Added functionality to create a spec metadata file and to use it for the 'install-all' function.
--- a/PipxInterface/Pipx.py Wed Jun 26 18:40:48 2024 +0200 +++ b/PipxInterface/Pipx.py Thu Jun 27 15:42:50 2024 +0200 @@ -10,7 +10,8 @@ import contextlib import json import os -import sys +import pathlib +##import sys import sysconfig from PyQt6.QtCore import QObject, QProcess @@ -111,7 +112,7 @@ return pipx - def runProcess(self, args): + def runPipxProcess(self, args): """ Public method to execute pipx with the given arguments. @@ -124,7 +125,7 @@ ioEncoding = Preferences.getSystem("IOEncoding") process = QProcess() - process.start(sys.executable, ["-m", "pipx"] + args) + process.start(self.__getPipxExecutable(), args) procStarted = process.waitForStarted() if procStarted: finished = process.waitForFinished(30000) @@ -150,6 +151,20 @@ return False, self.tr("pipx could not be started.") + def __metadataDecoderHook(self, jsonDict): + """ + Private method to allow the JSON decoding of Path objects of a spec metadata + file as created by 'pipx list --json'. + + @param jsonDict JSON dictionary to be decoded + @type dict + @return decoded Path object or the dictionary unaltered + @rtype dict or pathlib.Path + """ + if jsonDict.get("__type__") == "Path" and "__Path__" in jsonDict: + return pathlib.Path(jsonDict["__Path__"]) + return jsonDict + ############################################################################ ## Command methods ############################################################################ @@ -163,11 +178,11 @@ """ packages = [] - ok, output = self.runProcess(["list", "--json"]) + ok, output = self.runPipxProcess(["list", "--json"]) if ok: if output: with contextlib.suppress(json.JSONDecodeError): - data = json.loads(output) + data = json.loads(output, object_hook=self.__metadataDecoderHook) for venvName in data["venvs"]: metadata = data["venvs"][venvName]["metadata"] package = { @@ -177,8 +192,7 @@ "python": metadata["python_version"], } for appPath in metadata["main_package"]["app_paths"]: - path = appPath["__Path__"] - package["apps"].append((os.path.basename(path), path)) + package["apps"].append((appPath.name, str(appPath))) packages.append(package) return packages @@ -265,7 +279,7 @@ args.append("--force") if systemSitePackages: args.append("--system-site-packages") - args += specFile + args.append(specFile) dia = PipxExecDialog(self.tr("Install All Packages")) res = dia.startProcess(self.__getPipxExecutable(), args) if res: @@ -281,7 +295,7 @@ of failure @rtype tuple of (bool, str) """ - ok, output = self.runProcess(["list", "--json"]) + ok, output = self.runPipxProcess(["list", "--json"]) if ok: try: with open(specFile, "w") as f:
--- a/PipxInterface/PipxSpecInputDialog.py Wed Jun 26 18:40:48 2024 +0200 +++ b/PipxInterface/PipxSpecInputDialog.py Thu Jun 27 15:42:50 2024 +0200 @@ -36,6 +36,7 @@ self.setWindowTitle(title) self.specFilePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE) + self.specFilePicker.setFilters(self.tr("JSON Files (*.json);;All Files (*)")) self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
--- a/PipxInterface/PipxWidget.py Wed Jun 26 18:40:48 2024 +0200 +++ b/PipxInterface/PipxWidget.py Thu Jun 27 15:42:50 2024 +0200 @@ -7,10 +7,13 @@ Module implementing the pipx management widget. """ +import os + from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtWidgets import QDialog, QMenu, QTreeWidgetItem, QWidget from eric7.EricGui import EricPixmapCache +from eric7.EricWidgets import EricFileDialog, EricMessageBox from .Pipx import Pipx from .PipxAppStartDialog import PipxAppStartDialog @@ -55,6 +58,13 @@ # TODO: set the various icons self.pipxMenuButton.setIcon(EricPixmapCache.getIcon("superMenu")) self.refreshButton.setIcon(EricPixmapCache.getIcon("reload")) + self.installButton.setIcon(EricPixmapCache.getIcon("plus")) + self.upgradeButton.setIcon(EricPixmapCache.getIcon("upgrade")) + self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus")) + + self.installButton.clicked.connect(self.__installPackages) + self.upgradeButton.clicked.connect(self.__upgradePackages) + self.uninstallButton.clicked.connect(self.__uninstallPackages) self.pipxMenuButton.setAutoRaise(True) self.pipxMenuButton.setShowMenuInside(True) @@ -73,6 +83,7 @@ self.interpretersPathEdit.setText(pipxPaths["pythonPath"]) self.__populatePackages() + self.on_packagesList_itemSelectionChanged() ####################################################################### ## Menu related methods below @@ -154,10 +165,11 @@ """ Private slot to set the action enabled status. """ - hasPackagesSelected = bool(self.__selectedPackages()) - self.__reinstallPackagesAct.setEnabled(hasPackagesSelected) - self.__upgradePackagesAct.setEnabled(hasPackagesSelected) - self.__uninstallPackagesAct.setEnabled(hasPackagesSelected) + selectedPackages = self.__selectedPackages() + + self.__reinstallPackagesAct.setEnabled(len(selectedPackages) == 1) + self.__upgradePackagesAct.setEnabled(bool(selectedPackages)) + self.__uninstallPackagesAct.setEnabled(len(selectedPackages) == 1) @pyqtSlot() def __installPackages(self): @@ -183,13 +195,12 @@ """ Private slot to install all packages listed in a specification file. """ - # TODO: not implemented yet from .PipxSpecInputDialog import PipxSpecInputDialog dlg = PipxSpecInputDialog(self.tr("Install All Packages")) if dlg.exec() == QDialog.DialogCode.Accepted: specFile, pyVersion, fetchMissing, force, systemSitePackages = dlg.getData() - self.__pipx.installPackages( + self.__pipx.installAllPackages( specFile, interpreterVersion=pyVersion, fetchMissingInterpreter=fetchMissing, @@ -203,8 +214,51 @@ """ Private slot to create a spec metadata file needed by 'pipx install-all'. """ - # TODO: not implemented yet - pass + specFile, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + self.tr("Create Spec Metadata File"), + "", + self.tr("JSON Files (*.json);;All Files (*)"), + self.tr("JSON Files (*.json)"), + EricFileDialog.DontConfirmOverwrite, + ) + if specFile: + ext = os.path.splitext(specFile)[1] + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + specFile += ex + + if os.path.exists(specFile): + ok = EricMessageBox.yesNo( + self, + self.tr("Create Spec Metadata File"), + self.tr( + "<p>The file <b>{0}</b> exists already. Overwrite it?</p>" + ).format(specFile), + ) + if not ok: + return + + ok, message = self.__pipx.createSpecMetadataFile(specFile=specFile) + if ok: + EricMessageBox.information( + self, + self.tr("Create Spec Metadata File"), + self.tr( + "<p>The spec metadata file <b>{0}</b> was created" + " successfully.</p>" + ).format(specFile), + ) + else: + EricMessageBox.critical( + self, + self.tr("Create Spec Metadata File"), + self.tr( + "<p>The spec metadata file <b>{0}</b> could not be created.</p>" + "<p>Reason: {1}</p>" + ).format(specFile, message), + ) @pyqtSlot() def __reinstallPackages(self): @@ -336,6 +390,16 @@ dlg = PipxAppStartDialog(app, self.__plugin, self) dlg.show() + @pyqtSlot() + def on_packagesList_itemSelectionChanged(self): + """ + Private slot to handle a change of selected packages and apps. + """ + selectedPackages = self.__selectedPackages() + + self.upgradeButton.setEnabled(bool(selectedPackages)) + self.uninstallButton.setEnabled(len(selectedPackages) == 1) + def __selectedPackages(self): """ Private method to determine the list of selected packages.
--- a/PipxInterface/PipxWidget.ui Wed Jun 26 18:40:48 2024 +0200 +++ b/PipxInterface/PipxWidget.ui Thu Jun 27 15:42:50 2024 +0200 @@ -143,6 +143,27 @@ </widget> </item> <item> + <widget class="QToolButton" name="installButton"> + <property name="toolTip"> + <string>Press to install packages.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="upgradeButton"> + <property name="toolTip"> + <string>Press to upgrade the selected packages.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="uninstallButton"> + <property name="toolTip"> + <string>Press to uninstall the selected packages.</string> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer_3"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -197,6 +218,9 @@ <tabstops> <tabstop>packagesList</tabstop> <tabstop>refreshButton</tabstop> + <tabstop>installButton</tabstop> + <tabstop>upgradeButton</tabstop> + <tabstop>uninstallButton</tabstop> <tabstop>pipxMenuButton</tabstop> <tabstop>venvsPathEdit</tabstop> <tabstop>applicationsPathEdit</tabstop>
--- a/PipxInterface/Ui_PipxWidget.py Wed Jun 26 18:40:48 2024 +0200 +++ b/PipxInterface/Ui_PipxWidget.py Thu Jun 27 15:42:50 2024 +0200 @@ -70,6 +70,15 @@ self.refreshButton = QtWidgets.QToolButton(parent=PipxWidget) self.refreshButton.setObjectName("refreshButton") self.horizontalLayout_2.addWidget(self.refreshButton) + self.installButton = QtWidgets.QToolButton(parent=PipxWidget) + self.installButton.setObjectName("installButton") + self.horizontalLayout_2.addWidget(self.installButton) + self.upgradeButton = QtWidgets.QToolButton(parent=PipxWidget) + self.upgradeButton.setObjectName("upgradeButton") + self.horizontalLayout_2.addWidget(self.upgradeButton) + self.uninstallButton = QtWidgets.QToolButton(parent=PipxWidget) + self.uninstallButton.setObjectName("uninstallButton") + self.horizontalLayout_2.addWidget(self.uninstallButton) spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.horizontalLayout_2.addItem(spacerItem3) self.verticalLayout.addLayout(self.horizontalLayout_2) @@ -82,7 +91,10 @@ self.retranslateUi(PipxWidget) QtCore.QMetaObject.connectSlotsByName(PipxWidget) PipxWidget.setTabOrder(self.packagesList, self.refreshButton) - PipxWidget.setTabOrder(self.refreshButton, self.pipxMenuButton) + PipxWidget.setTabOrder(self.refreshButton, self.installButton) + PipxWidget.setTabOrder(self.installButton, self.upgradeButton) + PipxWidget.setTabOrder(self.upgradeButton, self.uninstallButton) + PipxWidget.setTabOrder(self.uninstallButton, self.pipxMenuButton) PipxWidget.setTabOrder(self.pipxMenuButton, self.venvsPathEdit) PipxWidget.setTabOrder(self.venvsPathEdit, self.applicationsPathEdit) PipxWidget.setTabOrder(self.applicationsPathEdit, self.manPagesPathEdit) @@ -96,6 +108,9 @@ self.label_4.setText(_translate("PipxWidget", "Manual Pages:")) self.label_5.setText(_translate("PipxWidget", "Standalone Interpreters:")) self.refreshButton.setToolTip(_translate("PipxWidget", "Press to refresh the packages list.")) + self.installButton.setToolTip(_translate("PipxWidget", "Press to install packages.")) + self.upgradeButton.setToolTip(_translate("PipxWidget", "Press to upgrade the selected packages.")) + self.uninstallButton.setToolTip(_translate("PipxWidget", "Press to uninstall the selected packages.")) self.packagesList.setSortingEnabled(True) self.packagesList.headerItem().setText(0, _translate("PipxWidget", "Package/Application")) self.packagesList.headerItem().setText(1, _translate("PipxWidget", "Version"))