eric6/PipInterface/Pip.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
child 6975
3325bf3e7b2c
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Package implementing the pip GUI logic.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode # __IGNORE_EXCEPTION__
13 except NameError:
14 pass
15
16 import os
17 import sys
18 import json
19
20 from PyQt5.QtCore import pyqtSlot, QObject, QProcess, QUrl, QCoreApplication
21 from PyQt5.QtWidgets import QDialog
22 from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, \
23 QNetworkReply
24
25 from E5Gui import E5MessageBox
26 from E5Gui.E5Application import e5App
27
28 from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired
29 try:
30 from E5Network.E5SslErrorHandler import E5SslErrorHandler
31 SSL_AVAILABLE = True
32 except ImportError:
33 SSL_AVAILABLE = False
34
35 from .PipDialog import PipDialog
36
37 import Preferences
38 import Globals
39
40
41 class Pip(QObject):
42 """
43 Class implementing the pip GUI logic.
44 """
45 DefaultPyPiUrl = "https://pypi.org"
46 DefaultIndexUrlXml = DefaultPyPiUrl + "/pypi"
47 DefaultIndexUrlPip = DefaultPyPiUrl + "/simple"
48
49 def __init__(self, parent=None):
50 """
51 Constructor
52
53 @param parent parent
54 @type QObject
55 """
56 super(Pip, self).__init__(parent)
57
58 # attributes for the network objects
59 self.__networkManager = QNetworkAccessManager(self)
60 self.__networkManager.proxyAuthenticationRequired.connect(
61 proxyAuthenticationRequired)
62 if SSL_AVAILABLE:
63 self.__sslErrorHandler = E5SslErrorHandler(self)
64 self.__networkManager.sslErrors.connect(
65 self.__sslErrorHandler.sslErrorsReply)
66 self.__replies = []
67
68 ##########################################################################
69 ## Methods below implement some utility functions
70 ##########################################################################
71
72 def runProcess(self, args, interpreter):
73 """
74 Public method to execute the current pip with the given arguments.
75
76 The selected pip executable is called with the given arguments and
77 waited for its end.
78
79 @param args list of command line arguments
80 @type list of str
81 @param interpreter path of the Python interpreter to be used
82 @type str
83 @return tuple containing a flag indicating success and the output
84 of the process
85 @rtype tuple of (bool, str)
86 """
87 ioEncoding = Preferences.getSystem("IOEncoding")
88
89 process = QProcess()
90 process.start(interpreter, args)
91 procStarted = process.waitForStarted()
92 if procStarted:
93 finished = process.waitForFinished(30000)
94 if finished:
95 if process.exitCode() == 0:
96 output = str(process.readAllStandardOutput(), ioEncoding,
97 'replace')
98 return True, output
99 else:
100 return (False,
101 self.tr("python exited with an error ({0}).")
102 .format(process.exitCode()))
103 else:
104 process.terminate()
105 process.waitForFinished(2000)
106 process.kill()
107 process.waitForFinished(3000)
108 return False, self.tr("python did not finish within"
109 " 30 seconds.")
110
111 return False, self.tr("python could not be started.")
112
113 def getUserConfig(self):
114 """
115 Public method to get the name of the user configuration file.
116
117 @return path of the user configuration file
118 @rtype str
119 """
120 # Unix: ~/.config/pip/pip.conf
121 # OS X: ~/Library/Application Support/pip/pip.conf
122 # Windows: %APPDATA%\pip\pip.ini
123 # Environment: $PIP_CONFIG_FILE
124
125 try:
126 return os.environ["PIP_CONFIG_FILE"]
127 except KeyError:
128 pass
129
130 if Globals.isWindowsPlatform():
131 config = os.path.join(os.environ["APPDATA"], "pip", "pip.ini")
132 elif Globals.isMacPlatform():
133 config = os.path.expanduser(
134 "~/Library/Application Support/pip/pip.conf")
135 else:
136 config = os.path.expanduser("~/.config/pip/pip.conf")
137
138 return config
139
140 def getVirtualenvConfig(self, venvName):
141 """
142 Public method to get the name of the virtualenv configuration file.
143
144 @param venvName name of the environment to get config file path for
145 @type str
146 @return path of the virtualenv configuration file
147 @rtype str
148 """
149 # Unix, OS X: $VIRTUAL_ENV/pip.conf
150 # Windows: %VIRTUAL_ENV%\pip.ini
151
152 if Globals.isWindowsPlatform():
153 pip = "pip.ini"
154 else:
155 pip = "pip.conf"
156
157 venvManager = e5App().getObject("VirtualEnvManager")
158 if venvManager.isGlobalEnvironment(venvName):
159 venvDirectory = os.path.dirname(self.getUserConfig())
160 else:
161 venvDirectory = venvManager.getVirtualenvDirectory(venvName)
162
163 if venvDirectory:
164 config = os.path.join(venvDirectory, pip)
165 else:
166 config = ""
167
168 return config
169
170 def getProjectEnvironmentString(self):
171 """
172 Public method to get the string for the project environment.
173
174 @return string for the project environment
175 @rtype str
176 """
177 if e5App().getObject("Project").isOpen():
178 return self.tr("<project>")
179 else:
180 return ""
181
182 def getVirtualenvInterpreter(self, venvName):
183 """
184 Public method to get the interpreter for a virtual environment.
185
186 @param venvName logical name for the virtual environment
187 @type str
188 @return interpreter path
189 @rtype str
190 """
191 if venvName == self.getProjectEnvironmentString():
192 venvName = \
193 e5App().getObject("Project").getDebugProperty("VIRTUALENV")
194 if not venvName:
195 # fall back to interpreter used to run eric6
196 return sys.executable
197
198 interpreter = \
199 e5App().getObject("VirtualEnvManager").getVirtualenvInterpreter(
200 venvName)
201 if not interpreter:
202 E5MessageBox.critical(
203 None,
204 self.tr("Interpreter for Virtual Environment"),
205 self.tr("""No interpreter configured for the selected"""
206 """ virtual environment."""))
207
208 return interpreter
209
210 def getVirtualenvNames(self):
211 """
212 Public method to get a sorted list of virtual environment names.
213
214 @return sorted list of virtual environment names
215 @rtype list of str
216 """
217 return sorted(
218 e5App().getObject("VirtualEnvManager").getVirtualenvNames())
219
220 def installPip(self, venvName, userSite=False):
221 """
222 Public method to install pip.
223
224 @param venvName name of the environment to install pip into
225 @type str
226 @param userSite flag indicating an install to the user install
227 directory
228 @type bool
229 """
230 interpreter = self.getVirtualenvInterpreter(venvName)
231 if not interpreter:
232 return
233
234 dia = PipDialog(self.tr('Install PIP'))
235 if userSite:
236 commands = [(interpreter, ["-m", "ensurepip", "--user"])]
237 else:
238 commands = [(interpreter, ["-m", "ensurepip"])]
239 if Preferences.getPip("PipSearchIndex"):
240 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
241 args = ["-m", "pip", "install", "--index-url", indexUrl,
242 "--upgrade"]
243 else:
244 args = ["-m", "pip", "install", "--upgrade"]
245 if userSite:
246 args.append("--user")
247 args.append("pip")
248 commands.append((interpreter, args[:]))
249
250 res = dia.startProcesses(commands)
251 if res:
252 dia.exec_()
253
254 @pyqtSlot()
255 def repairPip(self, venvName):
256 """
257 Public method to repair the pip installation.
258
259 @param venvName name of the environment to install pip into
260 @type str
261 """
262 interpreter = self.getVirtualenvInterpreter(venvName)
263 if not interpreter:
264 return
265
266 # python -m pip install --ignore-installed pip
267 if Preferences.getPip("PipSearchIndex"):
268 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
269 args = ["-m", "pip", "install", "--index-url", indexUrl,
270 "--ignore-installed"]
271 else:
272 args = ["-m", "pip", "install", "--ignore-installed"]
273 args.append("pip")
274
275 dia = PipDialog(self.tr('Repair PIP'))
276 res = dia.startProcess(interpreter, args)
277 if res:
278 dia.exec_()
279
280 def __checkUpgradePyQt(self, packages):
281 """
282 Private method to check, if an upgrade of PyQt packages is attempted.
283
284 @param packages list of packages to upgrade
285 @type list of str
286 @return flag indicating to abort the upgrade attempt
287 @rtype bool
288 """
289 pyqtPackages = [p for p in packages
290 if p.lower() in ["pyqt5", "pyqt5-sip", "pyqtwebengine",
291 "qscintilla", "sip"]]
292
293 if bool(pyqtPackages):
294 abort = not E5MessageBox.yesNo(
295 None,
296 self.tr("Upgrade Packages"),
297 self.tr(
298 """You are trying to upgrade PyQt packages. This might"""
299 """ not work for the current instance of Python ({0})."""
300 """ Do you want to continue?""").format(sys.executable),
301 icon=E5MessageBox.Critical)
302 else:
303 abort = False
304
305 return abort
306
307 def upgradePackages(self, packages, venvName, userSite=False):
308 """
309 Public method to upgrade the given list of packages.
310
311 @param packages list of packages to upgrade
312 @type list of str
313 @param venvName name of the virtual environment to be used
314 @type str
315 @param userSite flag indicating an install to the user install
316 directory
317 @type bool
318 @return flag indicating a successful execution
319 @rtype bool
320 """
321 if self.__checkUpgradePyQt(packages):
322 return False
323
324 if not venvName:
325 return False
326
327 interpreter = self.getVirtualenvInterpreter(venvName)
328 if not interpreter:
329 return False
330
331 if Preferences.getPip("PipSearchIndex"):
332 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
333 args = ["-m", "pip", "install", "--index-url", indexUrl,
334 "--upgrade"]
335 else:
336 args = ["-m", "pip", "install", "--upgrade"]
337 if userSite:
338 args.append("--user")
339 args += packages
340 dia = PipDialog(self.tr('Upgrade Packages'))
341 res = dia.startProcess(interpreter, args)
342 if res:
343 dia.exec_()
344 return res
345
346 def installPackages(self, packages, venvName="", userSite=False,
347 interpreter=""):
348 """
349 Public method to install the given list of packages.
350
351 @param packages list of packages to install
352 @type list of str
353 @param venvName name of the virtual environment to be used
354 @type str
355 @param userSite flag indicating an install to the user install
356 directory
357 @type bool
358 @param interpreter interpreter to be used for execution
359 @type str
360 """
361 if venvName:
362 interpreter = self.getVirtualenvInterpreter(venvName)
363 if not interpreter:
364 return
365
366 if interpreter:
367 if Preferences.getPip("PipSearchIndex"):
368 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
369 args = ["-m", "pip", "install", "--index-url", indexUrl]
370 else:
371 args = ["-m", "pip", "install"]
372 if userSite:
373 args.append("--user")
374 args += packages
375 dia = PipDialog(self.tr('Install Packages'))
376 res = dia.startProcess(interpreter, args)
377 if res:
378 dia.exec_()
379
380 def installRequirements(self, venvName):
381 """
382 Public method to install packages as given in a requirements file.
383
384 @param venvName name of the virtual environment to be used
385 @type str
386 """
387 from .PipFileSelectionDialog import PipFileSelectionDialog
388 dlg = PipFileSelectionDialog(self, "requirements")
389 if dlg.exec_() == QDialog.Accepted:
390 requirements, user = dlg.getData()
391 if requirements and os.path.exists(requirements):
392 interpreter = self.getVirtualenvInterpreter(venvName)
393 if not interpreter:
394 return
395
396 if Preferences.getPip("PipSearchIndex"):
397 indexUrl = Preferences.getPip("PipSearchIndex") + \
398 "/simple"
399 args = ["-m", "pip", "install", "--index-url", indexUrl]
400 else:
401 args = ["-m", "pip", "install"]
402 if user:
403 args.append("--user")
404 args += ["--requirement", requirements]
405 dia = PipDialog(self.tr('Install Packages from Requirements'))
406 res = dia.startProcess(interpreter, args)
407 if res:
408 dia.exec_()
409
410 def uninstallPackages(self, packages, venvName):
411 """
412 Public method to uninstall the given list of packages.
413
414 @param packages list of packages to uninstall
415 @type list of str
416 @param venvName name of the virtual environment to be used
417 @type str
418 @return flag indicating a successful execution
419 @rtype bool
420 """
421 res = False
422 if packages and venvName:
423 from UI.DeleteFilesConfirmationDialog import \
424 DeleteFilesConfirmationDialog
425 dlg = DeleteFilesConfirmationDialog(
426 self.parent(),
427 self.tr("Uninstall Packages"),
428 self.tr(
429 "Do you really want to uninstall these packages?"),
430 packages)
431 if dlg.exec_() == QDialog.Accepted:
432 interpreter = self.getVirtualenvInterpreter(venvName)
433 if not interpreter:
434 return False
435 args = ["-m", "pip", "uninstall", "--yes"] + packages
436 dia = PipDialog(self.tr('Uninstall Packages'))
437 res = dia.startProcess(interpreter, args)
438 if res:
439 dia.exec_()
440 return res
441
442 def uninstallRequirements(self, venvName):
443 """
444 Public method to uninstall packages as given in a requirements file.
445
446 @param venvName name of the virtual environment to be used
447 @type str
448 """
449 if venvName:
450 from .PipFileSelectionDialog import PipFileSelectionDialog
451 dlg = PipFileSelectionDialog(self, "requirements",
452 install=False)
453 if dlg.exec_() == QDialog.Accepted:
454 requirements, _user = dlg.getData()
455 if requirements and os.path.exists(requirements):
456 try:
457 f = open(requirements, "r")
458 reqs = f.read().splitlines()
459 f.close()
460 except (OSError, IOError):
461 return
462
463 from UI.DeleteFilesConfirmationDialog import \
464 DeleteFilesConfirmationDialog
465 dlg = DeleteFilesConfirmationDialog(
466 self.parent(),
467 self.tr("Uninstall Packages"),
468 self.tr(
469 "Do you really want to uninstall these packages?"),
470 reqs)
471 if dlg.exec_() == QDialog.Accepted:
472 interpreter = self.getVirtualenvInterpreter(venvName)
473 if not interpreter:
474 return
475
476 args = ["-m", "pip", "uninstall", "--requirement",
477 requirements]
478 dia = PipDialog(
479 self.tr('Uninstall Packages from Requirements'))
480 res = dia.startProcess(interpreter, args)
481 if res:
482 dia.exec_()
483
484 def getIndexUrl(self):
485 """
486 Public method to get the index URL for PyPI.
487
488 @return index URL for PyPI
489 @rtype str
490 """
491 if Preferences.getPip("PipSearchIndex"):
492 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
493 else:
494 indexUrl = Pip.DefaultIndexUrlPip
495
496 return indexUrl
497
498 def getIndexUrlXml(self):
499 """
500 Public method to get the index URL for XML RPC calls.
501
502 @return index URL for XML RPC calls
503 @rtype str
504 """
505 if Preferences.getPip("PipSearchIndex"):
506 indexUrl = Preferences.getPip("PipSearchIndex") + "/pypi"
507 else:
508 indexUrl = Pip.DefaultIndexUrlXml
509
510 return indexUrl
511
512 def getInstalledPackages(self, envName, localPackages=True,
513 notRequired=False, usersite=False):
514 """
515 Public method to get the list of installed packages.
516
517 @param envName name of the environment to get the packages for
518 @type str
519 @param localPackages flag indicating to get local packages only
520 @type bool
521 @param notRequired flag indicating to list packages that are not
522 dependencies of installed packages as well
523 @type bool
524 @param usersite flag indicating to only list packages installed
525 in user-site
526 @type bool
527 @return list of tuples containing the package name and version
528 @rtype list of tuple of (str, str)
529 """
530 packages = []
531
532 if envName:
533 interpreter = self.getVirtualenvInterpreter(envName)
534 if interpreter:
535 args = [
536 "-m", "pip",
537 "list",
538 "--format=json",
539 ]
540 if localPackages:
541 args.append("--local")
542 if notRequired:
543 args.append("--not-required")
544 if usersite:
545 args.append("--user")
546
547 proc = QProcess()
548 proc.start(interpreter, args)
549 if proc.waitForStarted(15000):
550 if proc.waitForFinished(30000):
551 output = str(proc.readAllStandardOutput(),
552 Preferences.getSystem("IOEncoding"),
553 'replace').strip()
554 try:
555 jsonList = json.loads(output)
556 except Exception:
557 jsonList = []
558
559 for package in jsonList:
560 if isinstance(package, dict):
561 packages.append((
562 package["name"],
563 package["version"],
564 ))
565
566 return packages
567
568 def getOutdatedPackages(self, envName, localPackages=True,
569 notRequired=False, usersite=False):
570 """
571 Public method to get the list of outdated packages.
572
573 @param envName name of the environment to get the packages for
574 @type str
575 @param localPackages flag indicating to get local packages only
576 @type bool
577 @param notRequired flag indicating to list packages that are not
578 dependencies of installed packages as well
579 @type bool
580 @param usersite flag indicating to only list packages installed
581 in user-site
582 @type bool
583 @return list of tuples containing the package name, installed version
584 and available version
585 @rtype list of tuple of (str, str, str)
586 """
587 packages = []
588
589 if envName:
590 interpreter = self.getVirtualenvInterpreter(envName)
591 if interpreter:
592 args = [
593 "-m", "pip",
594 "list",
595 "--outdated",
596 "--format=json",
597 ]
598 if localPackages:
599 args.append("--local")
600 if notRequired:
601 args.append("--not-required")
602 if usersite:
603 args.append("--user")
604
605 proc = QProcess()
606 proc.start(interpreter, args)
607 if proc.waitForStarted(15000):
608 if proc.waitForFinished(30000):
609 output = str(proc.readAllStandardOutput(),
610 Preferences.getSystem("IOEncoding"),
611 'replace').strip()
612 try:
613 jsonList = json.loads(output)
614 except Exception:
615 jsonList = []
616
617 for package in jsonList:
618 if isinstance(package, dict):
619 packages.append((
620 package["name"],
621 package["version"],
622 package["latest_version"],
623 ))
624
625 return packages
626
627 def getPackageDetails(self, name, version):
628 """
629 Public method to get package details using the PyPI JSON interface.
630
631 @param name package name
632 @type str
633 @param version package version
634 @type str
635 @return dictionary containing PyPI package data
636 @rtype dict
637 """
638 result = {}
639
640 if name and version:
641 url = "https://pypi.org/pypi/{0}/{1}/json".format(name, version)
642 request = QNetworkRequest(QUrl(url))
643 reply = self.__networkManager.get(request)
644 while not reply.isFinished():
645 QCoreApplication.processEvents()
646
647 reply.deleteLater()
648 if reply.error() == QNetworkReply.NoError:
649 data = str(reply.readAll(),
650 Preferences.getSystem("IOEncoding"),
651 'replace')
652 try:
653 result = json.loads(data)
654 except Exception:
655 # ignore JSON exceptions
656 pass
657
658 return result

eric ide

mercurial