eric7/PipInterface/Pip.py

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

eric ide

mercurial