diff -r 3fc8dfeb6ebe -r b99e7fd55fd3 src/eric7/PipInterface/Pip.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/PipInterface/Pip.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,1047 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the pip GUI logic. +""" + +import os +import sys +import json +import contextlib + +from PyQt6.QtCore import ( + pyqtSlot, QObject, QProcess, QUrl, QCoreApplication, QThread +) +from PyQt6.QtWidgets import QDialog, QInputDialog, QLineEdit +from PyQt6.QtNetwork import ( + QNetworkAccessManager, QNetworkRequest, QNetworkReply +) + +from EricWidgets import EricMessageBox +from EricWidgets.EricApplication import ericApp + +from EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired +try: + from EricNetwork.EricSslErrorHandler import EricSslErrorHandler + SSL_AVAILABLE = True +except ImportError: + SSL_AVAILABLE = False + +from .PipDialog import PipDialog +from .PipVulnerabilityChecker import PipVulnerabilityChecker + +import Preferences +import Globals + + +class Pip(QObject): + """ + Class implementing the pip GUI logic. + """ + DefaultPyPiUrl = "https://pypi.org" + DefaultIndexUrlPypi = DefaultPyPiUrl + "/pypi" + DefaultIndexUrlSimple = DefaultPyPiUrl + "/simple" + DefaultIndexUrlSearch = DefaultPyPiUrl + "/search/" + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the user interface object + @type QObject + """ + super().__init__(parent) + + self.__ui = parent + + # attributes for the network objects + self.__networkManager = QNetworkAccessManager(self) + self.__networkManager.proxyAuthenticationRequired.connect( + proxyAuthenticationRequired) + if SSL_AVAILABLE: + self.__sslErrorHandler = EricSslErrorHandler(self) + self.__networkManager.sslErrors.connect( + self.__sslErrorHandler.sslErrorsReply) + self.__replies = [] + + self.__vulnerabilityChecker = PipVulnerabilityChecker(self, self) + + def getNetworkAccessManager(self): + """ + Public method to get a reference to the network access manager object. + + @return reference to the network access manager object + @rtype QNetworkAccessManager + """ + return self.__networkManager + + def getVulnerabilityChecker(self): + """ + Public method to get a reference to the vulnerability checker object. + + @return reference to the vulnerability checker object + @rtype PipVulnerabilityChecker + """ + return self.__vulnerabilityChecker + + ########################################################################## + ## Methods below implement some utility functions + ########################################################################## + + def runProcess(self, args, interpreter): + """ + Public method to execute the current pip with the given arguments. + + The selected pip executable is called with the given arguments and + waited for its end. + + @param args list of command line arguments + @type list of str + @param interpreter path of the Python interpreter to be used + @type str + @return tuple containing a flag indicating success and the output + of the process + @rtype tuple of (bool, str) + """ + ioEncoding = Preferences.getSystem("IOEncoding") + + process = QProcess() + process.start(interpreter, args) + procStarted = process.waitForStarted() + if procStarted: + finished = process.waitForFinished(30000) + if finished: + if process.exitCode() == 0: + output = str(process.readAllStandardOutput(), ioEncoding, + 'replace') + return True, output + else: + return (False, + self.tr("python exited with an error ({0}).") + .format(process.exitCode())) + else: + process.terminate() + process.waitForFinished(2000) + process.kill() + process.waitForFinished(3000) + return False, self.tr("python did not finish within" + " 30 seconds.") + + return False, self.tr("python could not be started.") + + def getUserConfig(self): + """ + Public method to get the name of the user configuration file. + + @return path of the user configuration file + @rtype str + """ + # Unix: ~/.config/pip/pip.conf + # OS X: ~/Library/Application Support/pip/pip.conf + # Windows: %APPDATA%\pip\pip.ini + # Environment: $PIP_CONFIG_FILE + + with contextlib.suppress(KeyError): + return os.environ["PIP_CONFIG_FILE"] + + if Globals.isWindowsPlatform(): + config = os.path.join(os.environ["APPDATA"], "pip", "pip.ini") + elif Globals.isMacPlatform(): + config = os.path.expanduser( + "~/Library/Application Support/pip/pip.conf") + else: + config = os.path.expanduser("~/.config/pip/pip.conf") + + return config + + def getVirtualenvConfig(self, venvName): + """ + Public method to get the name of the virtualenv configuration file. + + @param venvName name of the environment to get config file path for + @type str + @return path of the virtualenv configuration file + @rtype str + """ + # Unix, OS X: $VIRTUAL_ENV/pip.conf + # Windows: %VIRTUAL_ENV%\pip.ini + + pip = "pip.ini" if Globals.isWindowsPlatform() else "pip.conf" + + venvManager = ericApp().getObject("VirtualEnvManager") + venvDirectory = ( + os.path.dirname(self.getUserConfig()) + if venvManager.isGlobalEnvironment(venvName) else + venvManager.getVirtualenvDirectory(venvName) + ) + + config = os.path.join(venvDirectory, pip) if venvDirectory else "" + + return config + + def getProjectEnvironmentString(self): + """ + Public method to get the string for the project environment. + + @return string for the project environment + @rtype str + """ + if ericApp().getObject("Project").isOpen(): + return self.tr("<project>") + else: + return "" + + def getVirtualenvInterpreter(self, venvName): + """ + Public method to get the interpreter for a virtual environment. + + @param venvName logical name for the virtual environment + @type str + @return interpreter path + @rtype str + """ + interpreter = ( + ericApp().getObject("Project").getProjectInterpreter() + if venvName == self.getProjectEnvironmentString() else + ericApp().getObject("VirtualEnvManager") + .getVirtualenvInterpreter(venvName) + ) + if not interpreter: + EricMessageBox.critical( + None, + self.tr("Interpreter for Virtual Environment"), + self.tr("""No interpreter configured for the selected""" + """ virtual environment.""")) + + return interpreter + + def getVirtualenvNames(self, noRemote=False, noConda=False): + """ + Public method to get a sorted list of virtual environment names. + + @param noRemote flag indicating to exclude environments for remote + debugging + @type bool + @param noConda flag indicating to exclude Conda environments + @type bool + @return sorted list of virtual environment names + @rtype list of str + """ + return sorted( + ericApp().getObject("VirtualEnvManager").getVirtualenvNames( + noRemote=noRemote, noConda=noConda)) + + def installPip(self, venvName, userSite=False): + """ + Public method to install pip. + + @param venvName name of the environment to install pip into + @type str + @param userSite flag indicating an install to the user install + directory + @type bool + """ + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return + + dia = PipDialog(self.tr('Install PIP')) + commands = ( + [(interpreter, ["-m", "ensurepip", "--user"])] + if userSite else + [(interpreter, ["-m", "ensurepip"])] + ) + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args = ["-m", "pip", "install", "--index-url", indexUrl, + "--upgrade"] + else: + args = ["-m", "pip", "install", "--upgrade"] + if userSite: + args.append("--user") + args.append("pip") + commands.append((interpreter, args[:])) + + res = dia.startProcesses(commands) + if res: + dia.exec() + + @pyqtSlot() + def repairPip(self, venvName): + """ + Public method to repair the pip installation. + + @param venvName name of the environment to install pip into + @type str + """ + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return + + # python -m pip install --ignore-installed pip + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args = ["-m", "pip", "install", "--index-url", indexUrl, + "--ignore-installed"] + else: + args = ["-m", "pip", "install", "--ignore-installed"] + args.append("pip") + + dia = PipDialog(self.tr('Repair PIP')) + res = dia.startProcess(interpreter, args) + if res: + dia.exec() + + def __checkUpgradePyQt(self, packages): + """ + Private method to check, if an upgrade of PyQt packages is attempted. + + @param packages list of packages to upgrade + @type list of str + @return flag indicating a PyQt upgrade + @rtype bool + """ + pyqtPackages = [ + p for p in packages if p.lower() in [ + "pyqt6", "pyqt6-sip", "pyqt6-webengine", "pyqt6-charts", + "pyqt6-qscintilla", "pyqt6-qt6", "pyqt6-webengine-qt6", + "pyqt6-charts-qt6" + ] + ] + return bool(pyqtPackages) + + def __checkUpgradeEric(self, packages): + """ + Private method to check, if an upgrade of the eric-ide package is + attempted. + + @param packages list of packages to upgrade + @type list of str + @return flag indicating an eric-ide upgrade + @rtype bool + """ + ericPackages = [ + p for p in packages if p.lower() == "eric-ide" + ] + return bool(ericPackages) + + def upgradePackages(self, packages, venvName, userSite=False): + """ + Public method to upgrade the given list of packages. + + @param packages list of packages to upgrade + @type list of str + @param venvName name of the virtual environment to be used + @type str + @param userSite flag indicating an install to the user install + directory + @type bool + @return flag indicating a successful execution + @rtype bool + """ + if not venvName: + return False + + if self.getVirtualenvInterpreter(venvName) in ( + sys.executable, Globals.getPythonExecutable() + ): + upgradePyQt = self.__checkUpgradePyQt(packages) + upgradeEric = self.__checkUpgradeEric(packages) + if upgradeEric or upgradePyQt: + try: + if upgradeEric and upgradePyQt: + self.__ui.upgradeEricPyQt() + elif upgradeEric: + self.__ui.upgradeEric() + elif upgradePyQt: + self.__ui.upgradePyQt() + return None # should not be reached; play it safe + except AttributeError: + return False + + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return False + + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args = ["-m", "pip", "install", "--index-url", indexUrl, + "--upgrade"] + else: + args = ["-m", "pip", "install", "--upgrade"] + if userSite: + args.append("--user") + args += packages + dia = PipDialog(self.tr('Upgrade Packages')) + res = dia.startProcess(interpreter, args) + if res: + dia.exec() + return res + + def installPackages(self, packages, venvName="", userSite=False, + interpreter="", forceReinstall=False): + """ + Public method to install the given list of packages. + + @param packages list of packages to install + @type list of str + @param venvName name of the virtual environment to be used + @type str + @param userSite flag indicating an install to the user install + directory + @type bool + @param interpreter interpreter to be used for execution + @type str + @param forceReinstall flag indicating to force a reinstall of + the packages + @type bool + """ + if venvName: + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return + + if interpreter: + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args = ["-m", "pip", "install", "--index-url", indexUrl] + else: + args = ["-m", "pip", "install"] + if userSite: + args.append("--user") + if forceReinstall: + args.append("--force-reinstall") + args += packages + dia = PipDialog(self.tr('Install Packages')) + res = dia.startProcess(interpreter, args) + if res: + dia.exec() + + def installRequirements(self, venvName): + """ + Public method to install packages as given in a requirements file. + + @param venvName name of the virtual environment to be used + @type str + """ + from .PipFileSelectionDialog import PipFileSelectionDialog + dlg = PipFileSelectionDialog(self, "requirements") + if dlg.exec() == QDialog.DialogCode.Accepted: + requirements, user = dlg.getData() + if requirements and os.path.exists(requirements): + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return + + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args = ["-m", "pip", "install", "--index-url", indexUrl] + else: + args = ["-m", "pip", "install"] + if user: + args.append("--user") + args += ["--requirement", requirements] + dia = PipDialog(self.tr('Install Packages from Requirements')) + res = dia.startProcess(interpreter, args) + if res: + dia.exec() + + def uninstallPackages(self, packages, venvName): + """ + Public method to uninstall the given list of packages. + + @param packages list of packages to uninstall + @type list of str + @param venvName name of the virtual environment to be used + @type str + @return flag indicating a successful execution + @rtype bool + """ + res = False + if packages and venvName: + from UI.DeleteFilesConfirmationDialog import ( + DeleteFilesConfirmationDialog + ) + dlg = DeleteFilesConfirmationDialog( + self.parent(), + self.tr("Uninstall Packages"), + self.tr( + "Do you really want to uninstall these packages?"), + packages) + if dlg.exec() == QDialog.DialogCode.Accepted: + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return False + args = ["-m", "pip", "uninstall", "--yes"] + packages + dia = PipDialog(self.tr('Uninstall Packages')) + res = dia.startProcess(interpreter, args) + if res: + dia.exec() + return res + + def uninstallRequirements(self, venvName): + """ + Public method to uninstall packages as given in a requirements file. + + @param venvName name of the virtual environment to be used + @type str + """ + if venvName: + from .PipFileSelectionDialog import PipFileSelectionDialog + dlg = PipFileSelectionDialog(self, "requirements", + install=False) + if dlg.exec() == QDialog.DialogCode.Accepted: + requirements, _user = dlg.getData() + if requirements and os.path.exists(requirements): + try: + with open(requirements, "r") as f: + reqs = f.read().splitlines() + except OSError: + return + + from UI.DeleteFilesConfirmationDialog import ( + DeleteFilesConfirmationDialog + ) + dlg = DeleteFilesConfirmationDialog( + self.parent(), + self.tr("Uninstall Packages"), + self.tr( + "Do you really want to uninstall these packages?"), + reqs) + if dlg.exec() == QDialog.DialogCode.Accepted: + interpreter = self.getVirtualenvInterpreter(venvName) + if not interpreter: + return + + args = ["-m", "pip", "uninstall", "--requirement", + requirements] + dia = PipDialog( + self.tr('Uninstall Packages from Requirements')) + res = dia.startProcess(interpreter, args) + if res: + dia.exec() + + def getIndexUrl(self): + """ + Public method to get the index URL for PyPI. + + @return index URL for PyPI + @rtype str + """ + indexUrl = ( + Preferences.getPip("PipSearchIndex") + "/simple" + if Preferences.getPip("PipSearchIndex") else + Pip.DefaultIndexUrlSimple + ) + + return indexUrl + + def getIndexUrlPypi(self): + """ + Public method to get the index URL for PyPI API calls. + + @return index URL for XML RPC calls + @rtype str + """ + indexUrl = ( + Preferences.getPip("PipSearchIndex") + "/pypi" + if Preferences.getPip("PipSearchIndex") else + Pip.DefaultIndexUrlPypi + ) + + return indexUrl + + def getIndexUrlSearch(self): + """ + Public method to get the index URL for PyPI API calls. + + @return index URL for XML RPC calls + @rtype str + """ + indexUrl = ( + Preferences.getPip("PipSearchIndex") + "/search/" + if Preferences.getPip("PipSearchIndex") else + Pip.DefaultIndexUrlSearch + ) + + return indexUrl + + def getInstalledPackages(self, envName, localPackages=True, + notRequired=False, usersite=False): + """ + Public method to get the list of installed packages. + + @param envName name of the environment to get the packages for + @type str + @param localPackages flag indicating to get local packages only + @type bool + @param notRequired flag indicating to list packages that are not + dependencies of installed packages as well + @type bool + @param usersite flag indicating to only list packages installed + in user-site + @type bool + @return list of tuples containing the package name and version + @rtype list of tuple of (str, str) + """ + packages = [] + + if envName: + interpreter = self.getVirtualenvInterpreter(envName) + if interpreter: + args = [ + "-m", "pip", + "list", + "--format=json", + ] + if localPackages: + args.append("--local") + if notRequired: + args.append("--not-required") + if usersite: + args.append("--user") + + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args += ["--index-url", indexUrl] + + proc = QProcess() + proc.start(interpreter, args) + if proc.waitForStarted(15000) and proc.waitForFinished(30000): + output = str(proc.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace').strip() + try: + jsonList = json.loads(output) + except Exception: + jsonList = [] + + for package in jsonList: + if isinstance(package, dict): + packages.append(( + package["name"], + package["version"], + )) + + return packages + + def getOutdatedPackages(self, envName, localPackages=True, + notRequired=False, usersite=False): + """ + Public method to get the list of outdated packages. + + @param envName name of the environment to get the packages for + @type str + @param localPackages flag indicating to get local packages only + @type bool + @param notRequired flag indicating to list packages that are not + dependencies of installed packages as well + @type bool + @param usersite flag indicating to only list packages installed + in user-site + @type bool + @return list of tuples containing the package name, installed version + and available version + @rtype list of tuple of (str, str, str) + """ + packages = [] + + if envName: + interpreter = self.getVirtualenvInterpreter(envName) + if interpreter: + args = [ + "-m", "pip", + "list", + "--outdated", + "--format=json", + ] + if localPackages: + args.append("--local") + if notRequired: + args.append("--not-required") + if usersite: + args.append("--user") + + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + args += ["--index-url", indexUrl] + + proc = QProcess() + proc.start(interpreter, args) + if proc.waitForStarted(15000) and proc.waitForFinished(30000): + output = str(proc.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace').strip() + try: + jsonList = json.loads(output) + except Exception: + jsonList = [] + + for package in jsonList: + if isinstance(package, dict): + packages.append(( + package["name"], + package["version"], + package["latest_version"], + )) + + return packages + + def checkPackageOutdated(self, packageStart, envName): + """ + Public method to check, if a group of packages is outdated. + + @param packageStart start string for package names to be checked + (case insensitive) + @type str + @param envName name of the environment to get the packages for + @type str + @return tuple containing a flag indicating outdated packages and the + list of tuples containing the package name, installed version + and available version + @rtype tuple of (bool, (str, str, str)) + """ + filteredPackages = [] + + if bool(envName) and bool(packageStart): + packages = self.getOutdatedPackages(envName) + filterStr = packageStart.lower() + filteredPackages = [ + p for p in packages + if p[0].lower().startswith(filterStr)] + + return bool(filteredPackages), filteredPackages + + def getPackageDetails(self, name, version): + """ + Public method to get package details using the PyPI JSON interface. + + @param name package name + @type str + @param version package version + @type str + @return dictionary containing PyPI package data + @rtype dict + """ + result = {} + + if name and version: + url = "{0}/{1}/{2}/json".format( + self.getIndexUrlPypi(), name, version) + request = QNetworkRequest(QUrl(url)) + reply = self.__networkManager.get(request) + while not reply.isFinished(): + QCoreApplication.processEvents() + QThread.msleep(100) + + reply.deleteLater() + if reply.error() == QNetworkReply.NetworkError.NoError: + data = str(reply.readAll(), + Preferences.getSystem("IOEncoding"), + 'replace') + with contextlib.suppress(Exception): + result = json.loads(data) + + return result + + def getPackageVersions(self, name): + """ + Public method to get a list of versions available for the given + package. + + @param name package name + @type str + @return list of available versions + @rtype list of str + """ + result = [] + + if name: + url = "{0}/{1}/json".format(self.getIndexUrlPypi(), name) + request = QNetworkRequest(QUrl(url)) + reply = self.__networkManager.get(request) + while not reply.isFinished(): + QCoreApplication.processEvents() + QThread.msleep(100) + + reply.deleteLater() + if reply.error() == QNetworkReply.NetworkError.NoError: + dataStr = str(reply.readAll(), + Preferences.getSystem("IOEncoding"), + 'replace') + with contextlib.suppress(Exception): + data = json.loads(dataStr) + result = list(data["releases"].keys()) + + return result + + def getFrozenPackages(self, envName, localPackages=True, usersite=False, + requirement=None): + """ + Public method to get the list of package specifiers to freeze them. + + @param envName name of the environment to get the package specifiers + for + @type str + @param localPackages flag indicating to get package specifiers for + local packages only + @type bool + @param usersite flag indicating to get package specifiers for packages + installed in user-site only + @type bool + @param requirement name of a requirements file + @type str + @return list of package specifiers + @rtype list of str + """ + specifiers = [] + + if envName: + interpreter = self.getVirtualenvInterpreter(envName) + if interpreter: + args = [ + "-m", "pip", + "freeze", + ] + if localPackages: + args.append("--local") + if usersite: + args.append("--user") + if requirement and os.path.exists(requirement): + args.append("--requirement") + args.append(requirement) + + success, output = self.runProcess(args, interpreter) + if success and output: + specifiers = [spec.strip() for spec in output.splitlines() + if spec.strip()] + + return specifiers + + ####################################################################### + ## Cache handling methods below + ####################################################################### + + def showCacheInfo(self, venvName): + """ + Public method to show some information about the pip cache. + + @param venvName name of the virtual environment to be used + @type str + """ + if venvName: + interpreter = self.getVirtualenvInterpreter(venvName) + if interpreter: + args = ["-m", "pip", "cache", "info"] + dia = PipDialog(self.tr("Cache Info")) + res = dia.startProcess(interpreter, args, showArgs=False) + if res: + dia.exec() + + def cacheList(self, venvName): + """ + Public method to list files contained in the pip cache. + + @param venvName name of the virtual environment to be used + @type str + """ + if venvName: + interpreter = self.getVirtualenvInterpreter(venvName) + if interpreter: + pattern, ok = QInputDialog.getText( + None, + self.tr("List Cached Files"), + self.tr("Enter a file pattern (empty for all):"), + QLineEdit.EchoMode.Normal) + + if ok: + args = ["-m", "pip", "cache", "list"] + if pattern.strip(): + args.append(pattern.strip()) + dia = PipDialog(self.tr("List Cached Files")) + res = dia.startProcess(interpreter, args, + showArgs=False) + if res: + dia.exec() + + def cacheRemove(self, venvName): + """ + Public method to remove files from the pip cache. + + @param venvName name of the virtual environment to be used + @type str + """ + if venvName: + interpreter = self.getVirtualenvInterpreter(venvName) + if interpreter: + pattern, ok = QInputDialog.getText( + None, + self.tr("Remove Cached Files"), + self.tr("Enter a file pattern:"), + QLineEdit.EchoMode.Normal) + + if ok and pattern.strip(): + args = ["-m", "pip", "cache", "remove", pattern.strip()] + dia = PipDialog(self.tr("Remove Cached Files")) + res = dia.startProcess(interpreter, args, + showArgs=False) + if res: + dia.exec() + + def cachePurge(self, venvName): + """ + Public method to remove all files from the pip cache. + + @param venvName name of the virtual environment to be used + @type str + """ + if venvName: + interpreter = self.getVirtualenvInterpreter(venvName) + if interpreter: + ok = EricMessageBox.yesNo( + None, + self.tr("Purge Cache"), + self.tr("Do you really want to purge the pip cache? All" + " files need to be downloaded again.")) + if ok: + args = ["-m", "pip", "cache", "purge"] + dia = PipDialog(self.tr("Purge Cache")) + res = dia.startProcess(interpreter, args, + showArgs=False) + if res: + dia.exec() + + ####################################################################### + ## Dependency tree handling methods below + ####################################################################### + + def getDependencyTree(self, envName, localPackages=True, usersite=False, + reverse=False): + """ + Public method to get the dependency tree of installed packages. + + @param envName name of the environment to get the packages for + @type str + @param localPackages flag indicating to get the tree for local + packages only + @type bool + @param usersite flag indicating to get the tree for packages + installed in user-site directory only + @type bool + @param reverse flag indicating to get the dependency tree in + reverse order (i.e. list packages needed by other) + @type bool + @return list of nested dictionaries resembling the requested + dependency tree + @rtype list of dict + """ + dependencies = [] + + if envName: + interpreter = self.getVirtualenvInterpreter(envName) + if interpreter: + args = [ + "-m", "pipdeptree", + "--json-tree", + "--python", interpreter, + ] + if localPackages: + args.append("--local-only") + if usersite: + args.append("--user-only") + if reverse: + args.append("--reverse") + + proc = QProcess() + proc.start(Globals.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) + + return dependencies + + ####################################################################### + ## License handling methods below + ####################################################################### + + def getLicenses(self, envName, localPackages=True, usersite=False, + summary=False): + """ + Public method to get the licenses per package for a given environment. + + @param envName name of the environment to get the licenses for + @type str + @param localPackages flag indicating to get the licenses for local + packages only + @type bool + @param usersite flag indicating to get the licenses for packages + installed in user-site directory only + @type bool + @param summary flag indicating to get a summary listing (defaults to + False) + @type bool (optional) + @return list of dictionaries containing the license and version per + package + @rtype dict + """ + licenses = [] + + if envName: + interpreter = self.getVirtualenvInterpreter(envName) + if interpreter: + from . import piplicenses + with open(piplicenses.__file__, "r") as f: + content = f.read() + args = [ + "-c", + content, + "--from", + "mixed", + "--with-system", + "--with-authors", + "--with-urls", + "--with-description", + ] + if localPackages: + args.append("--local-only") + if usersite: + args.append("--user-only") + if summary: + args.append("--summary") + + proc = QProcess() + proc.start(interpreter, args) + if proc.waitForStarted(15000) and proc.waitForFinished(30000): + output = str(proc.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace').strip() + with contextlib.suppress(json.JSONDecodeError): + licenses = json.loads(output) + + return licenses + + def getLicensesSummary(self, envName, localPackages=True, usersite=False): + """ + Public method to get a summary of licenses found in a given + environment. + + @param envName name of the environment to get the licenses summary for + @type str + @param localPackages flag indicating to get the licenses summary for + local packages only + @type bool + @param usersite flag indicating to get the licenses summary for + packages installed in user-site directory only + @type bool + @return list of dictionaries containing the license and the count of + packages + @rtype dict + """ + return self.getLicenses(envName, localPackages=localPackages, + usersite=usersite, summary=True)