PipInterface/Pip.py

branch
maintenance
changeset 6826
c6dda2cbe081
parent 6819
6c49d4ed077d
child 6828
bb6667ea9ae7
equal deleted inserted replaced
6764:d14ddbfbbd36 6826:c6dda2cbe081
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 """
348 Public method to install the given list of packages.
349
350 @param packages list of packages to install
351 @type list of str
352 @param venvName name of the virtual environment to be used
353 @type str
354 @param userSite flag indicating an install to the user install
355 directory
356 @type bool
357 """
358 if venvName:
359 interpreter = self.getVirtualenvInterpreter(venvName)
360 if not interpreter:
361 return
362
363 if Preferences.getPip("PipSearchIndex"):
364 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
365 args = ["-m", "pip", "install", "--index-url", indexUrl]
366 else:
367 args = ["-m", "pip", "install"]
368 if userSite:
369 args.append("--user")
370 args += packages
371 dia = PipDialog(self.tr('Install Packages'))
372 res = dia.startProcess(interpreter, args)
373 if res:
374 dia.exec_()
375
376 def installRequirements(self, venvName):
377 """
378 Public method to install packages as given in a requirements file.
379
380 @param venvName name of the virtual environment to be used
381 @type str
382 """
383 from .PipFileSelectionDialog import PipFileSelectionDialog
384 dlg = PipFileSelectionDialog(self, "requirements")
385 if dlg.exec_() == QDialog.Accepted:
386 requirements, user = dlg.getData()
387 if requirements and os.path.exists(requirements):
388 interpreter = self.getVirtualenvInterpreter(venvName)
389 if not interpreter:
390 return
391
392 if Preferences.getPip("PipSearchIndex"):
393 indexUrl = Preferences.getPip("PipSearchIndex") + \
394 "/simple"
395 args = ["-m", "pip", "install", "--index-url", indexUrl]
396 else:
397 args = ["-m", "pip", "install"]
398 if user:
399 args.append("--user")
400 args += ["--requirement", requirements]
401 dia = PipDialog(self.tr('Install Packages from Requirements'))
402 res = dia.startProcess(interpreter, args)
403 if res:
404 dia.exec_()
405
406 def uninstallPackages(self, packages, venvName):
407 """
408 Public method to uninstall the given list of packages.
409
410 @param packages list of packages to uninstall
411 @type list of str
412 @param venvName name of the virtual environment to be used
413 @type str
414 @return flag indicating a successful execution
415 @rtype bool
416 """
417 res = False
418 if packages and venvName:
419 from UI.DeleteFilesConfirmationDialog import \
420 DeleteFilesConfirmationDialog
421 dlg = DeleteFilesConfirmationDialog(
422 self.parent(),
423 self.tr("Uninstall Packages"),
424 self.tr(
425 "Do you really want to uninstall these packages?"),
426 packages)
427 if dlg.exec_() == QDialog.Accepted:
428 interpreter = self.getVirtualenvInterpreter(venvName)
429 if not interpreter:
430 return
431 args = ["-m", "pip", "uninstall", "--yes"] + packages
432 dia = PipDialog(self.tr('Uninstall Packages'))
433 res = dia.startProcess(interpreter, args)
434 if res:
435 dia.exec_()
436 return res
437
438 def uninstallRequirements(self, venvName):
439 """
440 Public method to uninstall packages as given in a requirements file.
441
442 @param venvName name of the virtual environment to be used
443 @type str
444 """
445 if venvName:
446 from .PipFileSelectionDialog import PipFileSelectionDialog
447 dlg = PipFileSelectionDialog(self, "requirements",
448 install=False)
449 if dlg.exec_() == QDialog.Accepted:
450 requirements, _user = dlg.getData()
451 if requirements and os.path.exists(requirements):
452 try:
453 f = open(requirements, "r")
454 reqs = f.read().splitlines()
455 f.close()
456 except (OSError, IOError):
457 return
458
459 from UI.DeleteFilesConfirmationDialog import \
460 DeleteFilesConfirmationDialog
461 dlg = DeleteFilesConfirmationDialog(
462 self.parent(),
463 self.tr("Uninstall Packages"),
464 self.tr(
465 "Do you really want to uninstall these packages?"),
466 reqs)
467 if dlg.exec_() == QDialog.Accepted:
468 interpreter = self.getVirtualenvInterpreter(venvName)
469 if not interpreter:
470 return
471
472 args = ["-m", "pip", "uninstall", "--requirement",
473 requirements]
474 dia = PipDialog(
475 self.tr('Uninstall Packages from Requirements'))
476 res = dia.startProcess(interpreter, args)
477 if res:
478 dia.exec_()
479
480 def getIndexUrl(self):
481 """
482 Public method to get the index URL for PyPI.
483
484 @return index URL for PyPI
485 @rtype str
486 """
487 if Preferences.getPip("PipSearchIndex"):
488 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
489 else:
490 indexUrl = Pip.DefaultIndexUrlPip
491
492 return indexUrl
493
494 def getIndexUrlXml(self):
495 """
496 Public method to get the index URL for XML RPC calls.
497
498 @return index URL for XML RPC calls
499 @rtype str
500 """
501 if Preferences.getPip("PipSearchIndex"):
502 indexUrl = Preferences.getPip("PipSearchIndex") + "/pypi"
503 else:
504 indexUrl = Pip.DefaultIndexUrlXml
505
506 return indexUrl
507
508 def getInstalledPackages(self, envName, localPackages=True,
509 notRequired=False, usersite=False):
510 """
511 Public method to get the list of installed packages.
512
513 @param envName name of the environment to get the packages for
514 @type str
515 @param localPackages flag indicating to get local packages only
516 @type bool
517 @param notRequired flag indicating to list packages that are not
518 dependencies of installed packages as well
519 @type bool
520 @param usersite flag indicating to only list packages installed
521 in user-site
522 @type bool
523 @return list of tuples containing the package name and version
524 @rtype list of tuple of (str, str)
525 """
526 packages = []
527
528 if envName:
529 interpreter = self.getVirtualenvInterpreter(envName)
530 if interpreter:
531 args = [
532 "-m", "pip",
533 "list",
534 "--format=json",
535 ]
536 if localPackages:
537 args.append("--local")
538 if notRequired:
539 args.append("--not-required")
540 if usersite:
541 args.append("--user")
542
543 proc = QProcess()
544 proc.start(interpreter, args)
545 if proc.waitForStarted(15000):
546 if proc.waitForFinished(30000):
547 output = str(proc.readAllStandardOutput(),
548 Preferences.getSystem("IOEncoding"),
549 'replace').strip()
550 try:
551 jsonList = json.loads(output)
552 except Exception:
553 jsonList = []
554
555 for package in jsonList:
556 if isinstance(package, dict):
557 packages.append((
558 package["name"],
559 package["version"],
560 ))
561
562 return packages
563
564 def getOutdatedPackages(self, envName, localPackages=True,
565 notRequired=False, usersite=False):
566 """
567 Public method to get the list of outdated packages.
568
569 @param envName name of the environment to get the packages for
570 @type str
571 @param localPackages flag indicating to get local packages only
572 @type bool
573 @param notRequired flag indicating to list packages that are not
574 dependencies of installed packages as well
575 @type bool
576 @param usersite flag indicating to only list packages installed
577 in user-site
578 @type bool
579 @return list of tuples containing the package name, installed version
580 and available version
581 @rtype list of tuple of (str, str, str)
582 """
583 packages = []
584
585 if envName:
586 interpreter = self.getVirtualenvInterpreter(envName)
587 if interpreter:
588 args = [
589 "-m", "pip",
590 "list",
591 "--outdated",
592 "--format=json",
593 ]
594 if localPackages:
595 args.append("--local")
596 if notRequired:
597 args.append("--not-required")
598 if usersite:
599 args.append("--user")
600
601 proc = QProcess()
602 proc.start(interpreter, args)
603 if proc.waitForStarted(15000):
604 if proc.waitForFinished(30000):
605 output = str(proc.readAllStandardOutput(),
606 Preferences.getSystem("IOEncoding"),
607 'replace').strip()
608 try:
609 jsonList = json.loads(output)
610 except Exception:
611 jsonList = []
612
613 for package in jsonList:
614 if isinstance(package, dict):
615 packages.append((
616 package["name"],
617 package["version"],
618 package["latest_version"],
619 ))
620
621 return packages
622
623 def getPackageDetails(self, name, version):
624 """
625 Public method to get package details using the PyPI JSON interface.
626
627 @param name package name
628 @type str
629 @param version package version
630 @type str
631 @return dictionary containing PyPI package data
632 @rtype dict
633 """
634 result = {}
635
636 if name and version:
637 url = "https://pypi.org/pypi/{0}/{1}/json".format(name, version)
638 request = QNetworkRequest(QUrl(url))
639 reply = self.__networkManager.get(request)
640 while not reply.isFinished():
641 QCoreApplication.processEvents()
642
643 reply.deleteLater()
644 if reply.error() == QNetworkReply.NoError:
645 data = str(reply.readAll(),
646 Preferences.getSystem("IOEncoding"),
647 'replace')
648 try:
649 result = json.loads(data)
650 except Exception:
651 # ignore JSON exceptions
652 pass
653
654 return result

eric ide

mercurial