--- a/PipxInterface/Pipx.py Tue Dec 10 17:53:34 2024 +0100 +++ b/PipxInterface/Pipx.py Fri Dec 13 15:40:08 2024 +0100 @@ -14,11 +14,12 @@ import pathlib import sysconfig +from packaging.specifiers import InvalidSpecifier, SpecifierSet from PyQt6.QtCore import QObject, QProcess, pyqtSignal from eric7 import Preferences from eric7.EricWidgets import EricMessageBox -from eric7.SystemUtilities import OSUtilities +from eric7.SystemUtilities import OSUtilities, PythonUtilities try: from eric7.EricCore.EricProcess import EricProcess @@ -818,3 +819,148 @@ self.tr("Upgrade Dependencies"), self.tr("""All dependencies are already up-to-date."""), ) + + def __getPackageInterpreter(self, package): + """ + Private method to determine the executable path of the python interpreter + of a package. + + @param package name of the package + @type str + @return Python interpreter path + @rtype str + """ + from pipx.paths import ctx # noqa: I102 + from pipx.venv import Venv # noqa: I102 + + packagePath = ctx.venvs / package + _venv = Venv(packagePath) + return str(_venv.python_path) +## + ##def __addDependency(self, dependency, parent): + ##""" + ##Private method to add a dependency branch to a given parent. +## + ##@param dependency dependency to be added + ##@type dict + ##@param parent reference to the parent item + ##@type QTreeWidget or QTreeWidgetItem + ##""" + ##itm = QTreeWidgetItem( + ##parent, + ##[ + ##dependency["package_name"], + ##dependency["installed_version"], + ##dependency["required_version"], + ##], + ##) + ##itm.setExpanded(True) +## + ##if dependency["installed_version"] == "?": + ##itm.setText(PipPackagesWidget.DepInstalledVersionColumn, self.tr("unknown")) +## + ##if dependency["required_version"].lower() not in ("any", "?"): + ##spec = ( + ##"=={0}".format(dependency["required_version"]) + ##if dependency["required_version"][0] in "0123456789" + ##else dependency["required_version"] + ##) + ##try: + ##specifierSet = SpecifierSet(specifiers=spec) + ##if not specifierSet.contains(dependency["installed_version"]): + ##itm.setIcon( + ##PipPackagesWidget.DepRequiredVersionColumn, + ##EricPixmapCache.getIcon("warning"), + ##) + ##except InvalidSpecifier: + ##itm.setText( + ##PipPackagesWidget.DepRequiredVersionColumn, + ##dependency["required_version"], + ##) +## + ##elif dependency["required_version"].lower() == "any": + ##itm.setText(PipPackagesWidget.DepRequiredVersionColumn, self.tr("any")) +## + ##elif dependency["required_version"] == "?": + ##itm.setText(PipPackagesWidget.DepRequiredVersionColumn, self.tr("unknown")) +## + ### recursively add sub-dependencies + ##for dep in dependency["dependencies"]: + ##self.__addDependency(dep, itm) + + def __getBrokenDependencies(self, dependencies): + brokenDependecies = [] + + for dependency in dependencies: + if dependency["required_version"].lower() not in ("any", "?"): + spec = ( + "=={0}".format(dependency["required_version"]) + if dependency["required_version"][0] in "0123456789" + else dependency["required_version"] + ) + with contextlib.suppress(InvalidSpecifier): + specifierSet = SpecifierSet(specifiers=spec) + if not specifierSet.contains(dependency["installed_version"]): + brokenDependecies.append(f"{dependency["package_name"]}{spec}") + + # recursively add sub-dependencies + brokenDependecies.extend( + self.__getBrokenDependencies(dependency["dependencies"]) + ) + + return brokenDependecies + + def repairBrokenDependencies(self, package): + """ + Public method to get repair broken or unmet package dependencies. + + @param package name of the package + @type str + """ + dependencies = [] + + interpreter = self.__getPackageInterpreter(package=package) + if interpreter: + args = ["-m", "pipdeptree", "--python", interpreter, "--json-tree"] + + proc = QProcess() + proc.start(PythonUtilities.getPythonExecutable(), args) + if proc.waitForStarted(15000) and proc.waitForFinished(30000): + output = str( + proc.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + "replace", + ).strip() + with contextlib.suppress(json.JSONDecodeError): + dependencies = json.loads(output) + + brokenDependecies = self.__getBrokenDependencies(dependencies) + if brokenDependecies: + args = [ + "runpip", + package, + "install", + "--prefer-binary", + ] + brokenDependecies + + dia = PipxExecDialog( + self.tr("Repair Broken Dependencies"), parent=self.__ui + ) + res = dia.startProcess(self.__getPipxExecutable(), args) + if res: + dia.exec() + else: + EricMessageBox.information( + self.__ui, + self.tr("Repair Broken Dependencies"), + self.tr("There are no broken dependencies."), + ) + else: + EricMessageBox.critical( + self.__ui, + self.tr("Repair Broken Dependencies"), + self.tr( + "<p>The interpreter for package <b>{0}</b> could not be determined." + " Aborting...</p>" + ).format(package), + )