|
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 |