eric6/PipInterface/Pip.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
child 6975
3325bf3e7b2c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/PipInterface/Pip.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,658 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the pip GUI logic.
+"""
+
+from __future__ import unicode_literals
+try:
+    str = unicode       # __IGNORE_EXCEPTION__
+except NameError:
+    pass
+
+import os
+import sys
+import json
+
+from PyQt5.QtCore import pyqtSlot, QObject, QProcess, QUrl, QCoreApplication
+from PyQt5.QtWidgets import QDialog
+from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, \
+    QNetworkReply
+
+from E5Gui import E5MessageBox
+from E5Gui.E5Application import e5App
+
+from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired
+try:
+    from E5Network.E5SslErrorHandler import E5SslErrorHandler
+    SSL_AVAILABLE = True
+except ImportError:
+    SSL_AVAILABLE = False
+
+from .PipDialog import PipDialog
+
+import Preferences
+import Globals
+
+
+class Pip(QObject):
+    """
+    Class implementing the pip GUI logic.
+    """
+    DefaultPyPiUrl = "https://pypi.org"
+    DefaultIndexUrlXml = DefaultPyPiUrl + "/pypi"
+    DefaultIndexUrlPip = DefaultPyPiUrl + "/simple"
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent parent
+        @type QObject
+        """
+        super(Pip, self).__init__(parent)
+        
+        # attributes for the network objects
+        self.__networkManager = QNetworkAccessManager(self)
+        self.__networkManager.proxyAuthenticationRequired.connect(
+            proxyAuthenticationRequired)
+        if SSL_AVAILABLE:
+            self.__sslErrorHandler = E5SslErrorHandler(self)
+            self.__networkManager.sslErrors.connect(
+                self.__sslErrorHandler.sslErrorsReply)
+        self.__replies = []
+    
+    ##########################################################################
+    ## 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
+        
+        try:
+            return os.environ["PIP_CONFIG_FILE"]
+        except KeyError:
+            pass
+        
+        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
+        
+        if Globals.isWindowsPlatform():
+            pip = "pip.ini"
+        else:
+            pip = "pip.conf"
+        
+        venvManager = e5App().getObject("VirtualEnvManager")
+        if venvManager.isGlobalEnvironment(venvName):
+            venvDirectory = os.path.dirname(self.getUserConfig())
+        else:
+            venvDirectory = venvManager.getVirtualenvDirectory(venvName)
+        
+        if venvDirectory:
+            config = os.path.join(venvDirectory, pip)
+        else:
+            config = ""
+        
+        return config
+    
+    def getProjectEnvironmentString(self):
+        """
+        Public method to get the string for the project environment.
+        
+        @return string for the project environment
+        @rtype str
+        """
+        if e5App().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
+        """
+        if venvName == self.getProjectEnvironmentString():
+            venvName = \
+                e5App().getObject("Project").getDebugProperty("VIRTUALENV")
+            if not venvName:
+                # fall back to interpreter used to run eric6
+                return sys.executable
+        
+        interpreter = \
+            e5App().getObject("VirtualEnvManager").getVirtualenvInterpreter(
+                venvName)
+        if not interpreter:
+            E5MessageBox.critical(
+                None,
+                self.tr("Interpreter for Virtual Environment"),
+                self.tr("""No interpreter configured for the selected"""
+                        """ virtual environment."""))
+        
+        return interpreter
+    
+    def getVirtualenvNames(self):
+        """
+        Public method to get a sorted list of virtual environment names.
+        
+        @return sorted list of virtual environment names
+        @rtype list of str
+        """
+        return sorted(
+            e5App().getObject("VirtualEnvManager").getVirtualenvNames())
+    
+    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'))
+        if userSite:
+            commands = [(interpreter, ["-m", "ensurepip", "--user"])]
+        else:
+            commands = [(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 to abort the upgrade attempt
+        @rtype bool
+        """
+        pyqtPackages = [p for p in packages
+                        if p.lower() in ["pyqt5", "pyqt5-sip", "pyqtwebengine",
+                                         "qscintilla", "sip"]]
+        
+        if bool(pyqtPackages):
+            abort = not E5MessageBox.yesNo(
+                None,
+                self.tr("Upgrade Packages"),
+                self.tr(
+                    """You are trying to upgrade PyQt packages. This might"""
+                    """ not work for the current instance of Python ({0})."""
+                    """ Do you want to continue?""").format(sys.executable),
+                icon=E5MessageBox.Critical)
+        else:
+            abort = False
+        
+        return abort
+    
+    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 self.__checkUpgradePyQt(packages):
+            return False
+        
+        if not venvName:
+            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=""):
+        """
+        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
+        """
+        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")
+            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.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.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.Accepted:
+                requirements, _user = dlg.getData()
+                if requirements and os.path.exists(requirements):
+                    try:
+                        f = open(requirements, "r")
+                        reqs = f.read().splitlines()
+                        f.close()
+                    except (OSError, IOError):
+                        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.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
+        """
+        if Preferences.getPip("PipSearchIndex"):
+            indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
+        else:
+            indexUrl = Pip.DefaultIndexUrlPip
+        
+        return indexUrl
+    
+    def getIndexUrlXml(self):
+        """
+        Public method to get the index URL for XML RPC calls.
+        
+        @return index URL for XML RPC calls
+        @rtype str
+        """
+        if Preferences.getPip("PipSearchIndex"):
+            indexUrl = Preferences.getPip("PipSearchIndex") + "/pypi"
+        else:
+            indexUrl = Pip.DefaultIndexUrlXml
+        
+        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")
+                
+                proc = QProcess()
+                proc.start(interpreter, args)
+                if proc.waitForStarted(15000):
+                    if 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")
+                
+                proc = QProcess()
+                proc.start(interpreter, args)
+                if proc.waitForStarted(15000):
+                    if 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 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 = "https://pypi.org/pypi/{0}/{1}/json".format(name, version)
+            request = QNetworkRequest(QUrl(url))
+            reply = self.__networkManager.get(request)
+            while not reply.isFinished():
+                QCoreApplication.processEvents()
+            
+            reply.deleteLater()
+            if reply.error() == QNetworkReply.NoError:
+                data = str(reply.readAll(),
+                           Preferences.getSystem("IOEncoding"),
+                           'replace')
+                try:
+                    result = json.loads(data)
+                except Exception:
+                    # ignore JSON exceptions
+                    pass
+        
+        return result

eric ide

mercurial