|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2022 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 PyQt6.QtCore import pyqtSlot, QObject, QProcess, QUrl, QCoreApplication, QThread |
|
16 from PyQt6.QtWidgets import QDialog, QInputDialog, QLineEdit |
|
17 from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply |
|
18 |
|
19 from EricWidgets import EricMessageBox |
|
20 from EricWidgets.EricApplication import ericApp |
|
21 |
|
22 from EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired |
|
23 |
|
24 try: |
|
25 from EricNetwork.EricSslErrorHandler import EricSslErrorHandler |
|
26 |
|
27 SSL_AVAILABLE = True |
|
28 except ImportError: |
|
29 SSL_AVAILABLE = False |
|
30 |
|
31 from .PipDialog import PipDialog |
|
32 from .PipVulnerabilityChecker import PipVulnerabilityChecker |
|
33 |
|
34 import Preferences |
|
35 import Globals |
|
36 |
|
37 |
|
38 class Pip(QObject): |
|
39 """ |
|
40 Class implementing the pip GUI logic. |
|
41 """ |
|
42 |
|
43 DefaultPyPiUrl = "https://pypi.org" |
|
44 DefaultIndexUrlPypi = DefaultPyPiUrl + "/pypi" |
|
45 DefaultIndexUrlSimple = DefaultPyPiUrl + "/simple" |
|
46 DefaultIndexUrlSearch = DefaultPyPiUrl + "/search/" |
|
47 |
|
48 def __init__(self, parent=None): |
|
49 """ |
|
50 Constructor |
|
51 |
|
52 @param parent reference to the user interface object |
|
53 @type QObject |
|
54 """ |
|
55 super().__init__(parent) |
|
56 |
|
57 self.__ui = parent |
|
58 |
|
59 # attributes for the network objects |
|
60 self.__networkManager = QNetworkAccessManager(self) |
|
61 self.__networkManager.proxyAuthenticationRequired.connect( |
|
62 proxyAuthenticationRequired |
|
63 ) |
|
64 if SSL_AVAILABLE: |
|
65 self.__sslErrorHandler = EricSslErrorHandler(self) |
|
66 self.__networkManager.sslErrors.connect( |
|
67 self.__sslErrorHandler.sslErrorsReply |
|
68 ) |
|
69 self.__replies = [] |
|
70 |
|
71 self.__vulnerabilityChecker = PipVulnerabilityChecker(self, self) |
|
72 |
|
73 def getNetworkAccessManager(self): |
|
74 """ |
|
75 Public method to get a reference to the network access manager object. |
|
76 |
|
77 @return reference to the network access manager object |
|
78 @rtype QNetworkAccessManager |
|
79 """ |
|
80 return self.__networkManager |
|
81 |
|
82 def getVulnerabilityChecker(self): |
|
83 """ |
|
84 Public method to get a reference to the vulnerability checker object. |
|
85 |
|
86 @return reference to the vulnerability checker object |
|
87 @rtype PipVulnerabilityChecker |
|
88 """ |
|
89 return self.__vulnerabilityChecker |
|
90 |
|
91 ########################################################################## |
|
92 ## Methods below implement some utility functions |
|
93 ########################################################################## |
|
94 |
|
95 def runProcess(self, args, interpreter): |
|
96 """ |
|
97 Public method to execute the current pip with the given arguments. |
|
98 |
|
99 The selected pip executable is called with the given arguments and |
|
100 waited for its end. |
|
101 |
|
102 @param args list of command line arguments |
|
103 @type list of str |
|
104 @param interpreter path of the Python interpreter to be used |
|
105 @type str |
|
106 @return tuple containing a flag indicating success and the output |
|
107 of the process |
|
108 @rtype tuple of (bool, str) |
|
109 """ |
|
110 ioEncoding = Preferences.getSystem("IOEncoding") |
|
111 |
|
112 process = QProcess() |
|
113 process.start(interpreter, args) |
|
114 procStarted = process.waitForStarted() |
|
115 if procStarted: |
|
116 finished = process.waitForFinished(30000) |
|
117 if finished: |
|
118 if process.exitCode() == 0: |
|
119 output = str(process.readAllStandardOutput(), ioEncoding, "replace") |
|
120 return True, output |
|
121 else: |
|
122 return ( |
|
123 False, |
|
124 self.tr("python exited with an error ({0}).").format( |
|
125 process.exitCode() |
|
126 ), |
|
127 ) |
|
128 else: |
|
129 process.terminate() |
|
130 process.waitForFinished(2000) |
|
131 process.kill() |
|
132 process.waitForFinished(3000) |
|
133 return False, self.tr("python did not finish within" " 30 seconds.") |
|
134 |
|
135 return False, self.tr("python could not be started.") |
|
136 |
|
137 def getUserConfig(self): |
|
138 """ |
|
139 Public method to get the name of the user configuration file. |
|
140 |
|
141 @return path of the user configuration file |
|
142 @rtype str |
|
143 """ |
|
144 # Unix: ~/.config/pip/pip.conf |
|
145 # OS X: ~/Library/Application Support/pip/pip.conf |
|
146 # Windows: %APPDATA%\pip\pip.ini |
|
147 # Environment: $PIP_CONFIG_FILE |
|
148 |
|
149 with contextlib.suppress(KeyError): |
|
150 return os.environ["PIP_CONFIG_FILE"] |
|
151 |
|
152 if Globals.isWindowsPlatform(): |
|
153 config = os.path.join(os.environ["APPDATA"], "pip", "pip.ini") |
|
154 elif Globals.isMacPlatform(): |
|
155 config = os.path.expanduser("~/Library/Application Support/pip/pip.conf") |
|
156 else: |
|
157 config = os.path.expanduser("~/.config/pip/pip.conf") |
|
158 |
|
159 return config |
|
160 |
|
161 def getVirtualenvConfig(self, venvName): |
|
162 """ |
|
163 Public method to get the name of the virtualenv configuration file. |
|
164 |
|
165 @param venvName name of the environment to get config file path for |
|
166 @type str |
|
167 @return path of the virtualenv configuration file |
|
168 @rtype str |
|
169 """ |
|
170 # Unix, OS X: $VIRTUAL_ENV/pip.conf |
|
171 # Windows: %VIRTUAL_ENV%\pip.ini |
|
172 |
|
173 pip = "pip.ini" if Globals.isWindowsPlatform() else "pip.conf" |
|
174 |
|
175 venvManager = ericApp().getObject("VirtualEnvManager") |
|
176 venvDirectory = ( |
|
177 os.path.dirname(self.getUserConfig()) |
|
178 if venvManager.isGlobalEnvironment(venvName) |
|
179 else venvManager.getVirtualenvDirectory(venvName) |
|
180 ) |
|
181 |
|
182 config = os.path.join(venvDirectory, pip) if venvDirectory else "" |
|
183 |
|
184 return config |
|
185 |
|
186 def getProjectEnvironmentString(self): |
|
187 """ |
|
188 Public method to get the string for the project environment. |
|
189 |
|
190 @return string for the project environment |
|
191 @rtype str |
|
192 """ |
|
193 if ericApp().getObject("Project").isOpen(): |
|
194 return self.tr("<project>") |
|
195 else: |
|
196 return "" |
|
197 |
|
198 def getVirtualenvInterpreter(self, venvName): |
|
199 """ |
|
200 Public method to get the interpreter for a virtual environment. |
|
201 |
|
202 @param venvName logical name for the virtual environment |
|
203 @type str |
|
204 @return interpreter path |
|
205 @rtype str |
|
206 """ |
|
207 interpreter = ( |
|
208 ericApp().getObject("Project").getProjectInterpreter() |
|
209 if venvName == self.getProjectEnvironmentString() |
|
210 else ericApp() |
|
211 .getObject("VirtualEnvManager") |
|
212 .getVirtualenvInterpreter(venvName) |
|
213 ) |
|
214 if not interpreter: |
|
215 EricMessageBox.critical( |
|
216 None, |
|
217 self.tr("Interpreter for Virtual Environment"), |
|
218 self.tr( |
|
219 """No interpreter configured for the selected""" |
|
220 """ virtual environment.""" |
|
221 ), |
|
222 ) |
|
223 |
|
224 return interpreter |
|
225 |
|
226 def getVirtualenvNames(self, noRemote=False, noConda=False): |
|
227 """ |
|
228 Public method to get a sorted list of virtual environment names. |
|
229 |
|
230 @param noRemote flag indicating to exclude environments for remote |
|
231 debugging |
|
232 @type bool |
|
233 @param noConda flag indicating to exclude Conda environments |
|
234 @type bool |
|
235 @return sorted list of virtual environment names |
|
236 @rtype list of str |
|
237 """ |
|
238 return sorted( |
|
239 ericApp() |
|
240 .getObject("VirtualEnvManager") |
|
241 .getVirtualenvNames(noRemote=noRemote, noConda=noConda) |
|
242 ) |
|
243 |
|
244 def installPip(self, venvName, userSite=False): |
|
245 """ |
|
246 Public method to install pip. |
|
247 |
|
248 @param venvName name of the environment to install pip into |
|
249 @type str |
|
250 @param userSite flag indicating an install to the user install |
|
251 directory |
|
252 @type bool |
|
253 """ |
|
254 interpreter = self.getVirtualenvInterpreter(venvName) |
|
255 if not interpreter: |
|
256 return |
|
257 |
|
258 dia = PipDialog(self.tr("Install PIP")) |
|
259 commands = ( |
|
260 [(interpreter, ["-m", "ensurepip", "--user"])] |
|
261 if userSite |
|
262 else [(interpreter, ["-m", "ensurepip"])] |
|
263 ) |
|
264 if Preferences.getPip("PipSearchIndex"): |
|
265 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
266 args = ["-m", "pip", "install", "--index-url", indexUrl, "--upgrade"] |
|
267 else: |
|
268 args = ["-m", "pip", "install", "--upgrade"] |
|
269 if userSite: |
|
270 args.append("--user") |
|
271 args.append("pip") |
|
272 commands.append((interpreter, args[:])) |
|
273 |
|
274 res = dia.startProcesses(commands) |
|
275 if res: |
|
276 dia.exec() |
|
277 |
|
278 @pyqtSlot() |
|
279 def repairPip(self, venvName): |
|
280 """ |
|
281 Public method to repair the pip installation. |
|
282 |
|
283 @param venvName name of the environment to install pip into |
|
284 @type str |
|
285 """ |
|
286 interpreter = self.getVirtualenvInterpreter(venvName) |
|
287 if not interpreter: |
|
288 return |
|
289 |
|
290 # python -m pip install --ignore-installed pip |
|
291 if Preferences.getPip("PipSearchIndex"): |
|
292 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
293 args = [ |
|
294 "-m", |
|
295 "pip", |
|
296 "install", |
|
297 "--index-url", |
|
298 indexUrl, |
|
299 "--ignore-installed", |
|
300 ] |
|
301 else: |
|
302 args = ["-m", "pip", "install", "--ignore-installed"] |
|
303 args.append("pip") |
|
304 |
|
305 dia = PipDialog(self.tr("Repair PIP")) |
|
306 res = dia.startProcess(interpreter, args) |
|
307 if res: |
|
308 dia.exec() |
|
309 |
|
310 def __checkUpgradePyQt(self, packages): |
|
311 """ |
|
312 Private method to check, if an upgrade of PyQt packages is attempted. |
|
313 |
|
314 @param packages list of packages to upgrade |
|
315 @type list of str |
|
316 @return flag indicating a PyQt upgrade |
|
317 @rtype bool |
|
318 """ |
|
319 pyqtPackages = [ |
|
320 p |
|
321 for p in packages |
|
322 if p.lower() |
|
323 in [ |
|
324 "pyqt6", |
|
325 "pyqt6-sip", |
|
326 "pyqt6-webengine", |
|
327 "pyqt6-charts", |
|
328 "pyqt6-qscintilla", |
|
329 "pyqt6-qt6", |
|
330 "pyqt6-webengine-qt6", |
|
331 "pyqt6-charts-qt6", |
|
332 ] |
|
333 ] |
|
334 return bool(pyqtPackages) |
|
335 |
|
336 def __checkUpgradeEric(self, packages): |
|
337 """ |
|
338 Private method to check, if an upgrade of the eric-ide package is |
|
339 attempted. |
|
340 |
|
341 @param packages list of packages to upgrade |
|
342 @type list of str |
|
343 @return flag indicating an eric-ide upgrade |
|
344 @rtype bool |
|
345 """ |
|
346 ericPackages = [p for p in packages if p.lower() == "eric-ide"] |
|
347 return bool(ericPackages) |
|
348 |
|
349 def upgradePackages(self, packages, venvName, userSite=False): |
|
350 """ |
|
351 Public method to upgrade the given list of packages. |
|
352 |
|
353 @param packages list of packages to upgrade |
|
354 @type list of str |
|
355 @param venvName name of the virtual environment to be used |
|
356 @type str |
|
357 @param userSite flag indicating an install to the user install |
|
358 directory |
|
359 @type bool |
|
360 @return flag indicating a successful execution |
|
361 @rtype bool |
|
362 """ |
|
363 if not venvName: |
|
364 return False |
|
365 |
|
366 if self.getVirtualenvInterpreter(venvName) in ( |
|
367 sys.executable, |
|
368 Globals.getPythonExecutable(), |
|
369 ): |
|
370 upgradePyQt = self.__checkUpgradePyQt(packages) |
|
371 upgradeEric = self.__checkUpgradeEric(packages) |
|
372 if upgradeEric or upgradePyQt: |
|
373 try: |
|
374 if upgradeEric and upgradePyQt: |
|
375 self.__ui.upgradeEricPyQt() |
|
376 elif upgradeEric: |
|
377 self.__ui.upgradeEric() |
|
378 elif upgradePyQt: |
|
379 self.__ui.upgradePyQt() |
|
380 return None # should not be reached; play it safe |
|
381 except AttributeError: |
|
382 return False |
|
383 |
|
384 interpreter = self.getVirtualenvInterpreter(venvName) |
|
385 if not interpreter: |
|
386 return False |
|
387 |
|
388 if Preferences.getPip("PipSearchIndex"): |
|
389 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
390 args = ["-m", "pip", "install", "--index-url", indexUrl, "--upgrade"] |
|
391 else: |
|
392 args = ["-m", "pip", "install", "--upgrade"] |
|
393 if userSite: |
|
394 args.append("--user") |
|
395 args += packages |
|
396 dia = PipDialog(self.tr("Upgrade Packages")) |
|
397 res = dia.startProcess(interpreter, args) |
|
398 if res: |
|
399 dia.exec() |
|
400 return res |
|
401 |
|
402 def installPackages( |
|
403 self, |
|
404 packages, |
|
405 venvName="", |
|
406 userSite=False, |
|
407 interpreter="", |
|
408 forceReinstall=False, |
|
409 ): |
|
410 """ |
|
411 Public method to install the given list of packages. |
|
412 |
|
413 @param packages list of packages to install |
|
414 @type list of str |
|
415 @param venvName name of the virtual environment to be used |
|
416 @type str |
|
417 @param userSite flag indicating an install to the user install |
|
418 directory |
|
419 @type bool |
|
420 @param interpreter interpreter to be used for execution |
|
421 @type str |
|
422 @param forceReinstall flag indicating to force a reinstall of |
|
423 the packages |
|
424 @type bool |
|
425 """ |
|
426 if venvName: |
|
427 interpreter = self.getVirtualenvInterpreter(venvName) |
|
428 if not interpreter: |
|
429 return |
|
430 |
|
431 if interpreter: |
|
432 if Preferences.getPip("PipSearchIndex"): |
|
433 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
434 args = ["-m", "pip", "install", "--index-url", indexUrl] |
|
435 else: |
|
436 args = ["-m", "pip", "install"] |
|
437 if userSite: |
|
438 args.append("--user") |
|
439 if forceReinstall: |
|
440 args.append("--force-reinstall") |
|
441 args += packages |
|
442 dia = PipDialog(self.tr("Install Packages")) |
|
443 res = dia.startProcess(interpreter, args) |
|
444 if res: |
|
445 dia.exec() |
|
446 |
|
447 def installRequirements(self, venvName): |
|
448 """ |
|
449 Public method to install packages as given in a requirements file. |
|
450 |
|
451 @param venvName name of the virtual environment to be used |
|
452 @type str |
|
453 """ |
|
454 from .PipFileSelectionDialog import PipFileSelectionDialog |
|
455 |
|
456 dlg = PipFileSelectionDialog(self, "requirements") |
|
457 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
458 requirements, user = dlg.getData() |
|
459 if requirements and os.path.exists(requirements): |
|
460 interpreter = self.getVirtualenvInterpreter(venvName) |
|
461 if not interpreter: |
|
462 return |
|
463 |
|
464 if Preferences.getPip("PipSearchIndex"): |
|
465 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
466 args = ["-m", "pip", "install", "--index-url", indexUrl] |
|
467 else: |
|
468 args = ["-m", "pip", "install"] |
|
469 if user: |
|
470 args.append("--user") |
|
471 args += ["--requirement", requirements] |
|
472 dia = PipDialog(self.tr("Install Packages from Requirements")) |
|
473 res = dia.startProcess(interpreter, args) |
|
474 if res: |
|
475 dia.exec() |
|
476 |
|
477 def uninstallPackages(self, packages, venvName): |
|
478 """ |
|
479 Public method to uninstall the given list of packages. |
|
480 |
|
481 @param packages list of packages to uninstall |
|
482 @type list of str |
|
483 @param venvName name of the virtual environment to be used |
|
484 @type str |
|
485 @return flag indicating a successful execution |
|
486 @rtype bool |
|
487 """ |
|
488 res = False |
|
489 if packages and venvName: |
|
490 from UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog |
|
491 |
|
492 dlg = DeleteFilesConfirmationDialog( |
|
493 self.parent(), |
|
494 self.tr("Uninstall Packages"), |
|
495 self.tr("Do you really want to uninstall these packages?"), |
|
496 packages, |
|
497 ) |
|
498 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
499 interpreter = self.getVirtualenvInterpreter(venvName) |
|
500 if not interpreter: |
|
501 return False |
|
502 args = ["-m", "pip", "uninstall", "--yes"] + packages |
|
503 dia = PipDialog(self.tr("Uninstall Packages")) |
|
504 res = dia.startProcess(interpreter, args) |
|
505 if res: |
|
506 dia.exec() |
|
507 return res |
|
508 |
|
509 def uninstallRequirements(self, venvName): |
|
510 """ |
|
511 Public method to uninstall packages as given in a requirements file. |
|
512 |
|
513 @param venvName name of the virtual environment to be used |
|
514 @type str |
|
515 """ |
|
516 if venvName: |
|
517 from .PipFileSelectionDialog import PipFileSelectionDialog |
|
518 |
|
519 dlg = PipFileSelectionDialog(self, "requirements", install=False) |
|
520 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
521 requirements, _user = dlg.getData() |
|
522 if requirements and os.path.exists(requirements): |
|
523 try: |
|
524 with open(requirements, "r") as f: |
|
525 reqs = f.read().splitlines() |
|
526 except OSError: |
|
527 return |
|
528 |
|
529 from UI.DeleteFilesConfirmationDialog import ( |
|
530 DeleteFilesConfirmationDialog, |
|
531 ) |
|
532 |
|
533 dlg = DeleteFilesConfirmationDialog( |
|
534 self.parent(), |
|
535 self.tr("Uninstall Packages"), |
|
536 self.tr("Do you really want to uninstall these packages?"), |
|
537 reqs, |
|
538 ) |
|
539 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
540 interpreter = self.getVirtualenvInterpreter(venvName) |
|
541 if not interpreter: |
|
542 return |
|
543 |
|
544 args = ["-m", "pip", "uninstall", "--requirement", requirements] |
|
545 dia = PipDialog(self.tr("Uninstall Packages from Requirements")) |
|
546 res = dia.startProcess(interpreter, args) |
|
547 if res: |
|
548 dia.exec() |
|
549 |
|
550 def getIndexUrl(self): |
|
551 """ |
|
552 Public method to get the index URL for PyPI. |
|
553 |
|
554 @return index URL for PyPI |
|
555 @rtype str |
|
556 """ |
|
557 indexUrl = ( |
|
558 Preferences.getPip("PipSearchIndex") + "/simple" |
|
559 if Preferences.getPip("PipSearchIndex") |
|
560 else Pip.DefaultIndexUrlSimple |
|
561 ) |
|
562 |
|
563 return indexUrl |
|
564 |
|
565 def getIndexUrlPypi(self): |
|
566 """ |
|
567 Public method to get the index URL for PyPI API calls. |
|
568 |
|
569 @return index URL for XML RPC calls |
|
570 @rtype str |
|
571 """ |
|
572 indexUrl = ( |
|
573 Preferences.getPip("PipSearchIndex") + "/pypi" |
|
574 if Preferences.getPip("PipSearchIndex") |
|
575 else Pip.DefaultIndexUrlPypi |
|
576 ) |
|
577 |
|
578 return indexUrl |
|
579 |
|
580 def getIndexUrlSearch(self): |
|
581 """ |
|
582 Public method to get the index URL for PyPI API calls. |
|
583 |
|
584 @return index URL for XML RPC calls |
|
585 @rtype str |
|
586 """ |
|
587 indexUrl = ( |
|
588 Preferences.getPip("PipSearchIndex") + "/search/" |
|
589 if Preferences.getPip("PipSearchIndex") |
|
590 else Pip.DefaultIndexUrlSearch |
|
591 ) |
|
592 |
|
593 return indexUrl |
|
594 |
|
595 def getInstalledPackages( |
|
596 self, envName, localPackages=True, notRequired=False, usersite=False |
|
597 ): |
|
598 """ |
|
599 Public method to get the list of installed packages. |
|
600 |
|
601 @param envName name of the environment to get the packages for |
|
602 @type str |
|
603 @param localPackages flag indicating to get local packages only |
|
604 @type bool |
|
605 @param notRequired flag indicating to list packages that are not |
|
606 dependencies of installed packages as well |
|
607 @type bool |
|
608 @param usersite flag indicating to only list packages installed |
|
609 in user-site |
|
610 @type bool |
|
611 @return list of tuples containing the package name and version |
|
612 @rtype list of tuple of (str, str) |
|
613 """ |
|
614 packages = [] |
|
615 |
|
616 if envName: |
|
617 interpreter = self.getVirtualenvInterpreter(envName) |
|
618 if interpreter: |
|
619 args = [ |
|
620 "-m", |
|
621 "pip", |
|
622 "list", |
|
623 "--format=json", |
|
624 ] |
|
625 if localPackages: |
|
626 args.append("--local") |
|
627 if notRequired: |
|
628 args.append("--not-required") |
|
629 if usersite: |
|
630 args.append("--user") |
|
631 |
|
632 if Preferences.getPip("PipSearchIndex"): |
|
633 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
634 args += ["--index-url", indexUrl] |
|
635 |
|
636 proc = QProcess() |
|
637 proc.start(interpreter, args) |
|
638 if proc.waitForStarted(15000) and proc.waitForFinished(30000): |
|
639 output = str( |
|
640 proc.readAllStandardOutput(), |
|
641 Preferences.getSystem("IOEncoding"), |
|
642 "replace", |
|
643 ).strip().splitlines()[0] |
|
644 try: |
|
645 jsonList = json.loads(output) |
|
646 except Exception: |
|
647 jsonList = [] |
|
648 |
|
649 for package in jsonList: |
|
650 if isinstance(package, dict): |
|
651 packages.append( |
|
652 ( |
|
653 package["name"], |
|
654 package["version"], |
|
655 ) |
|
656 ) |
|
657 |
|
658 return packages |
|
659 |
|
660 def getOutdatedPackages( |
|
661 self, envName, localPackages=True, notRequired=False, usersite=False |
|
662 ): |
|
663 """ |
|
664 Public method to get the list of outdated packages. |
|
665 |
|
666 @param envName name of the environment to get the packages for |
|
667 @type str |
|
668 @param localPackages flag indicating to get local packages only |
|
669 @type bool |
|
670 @param notRequired flag indicating to list packages that are not |
|
671 dependencies of installed packages as well |
|
672 @type bool |
|
673 @param usersite flag indicating to only list packages installed |
|
674 in user-site |
|
675 @type bool |
|
676 @return list of tuples containing the package name, installed version |
|
677 and available version |
|
678 @rtype list of tuple of (str, str, str) |
|
679 """ |
|
680 packages = [] |
|
681 |
|
682 if envName: |
|
683 interpreter = self.getVirtualenvInterpreter(envName) |
|
684 if interpreter: |
|
685 args = [ |
|
686 "-m", |
|
687 "pip", |
|
688 "list", |
|
689 "--outdated", |
|
690 "--format=json", |
|
691 ] |
|
692 if localPackages: |
|
693 args.append("--local") |
|
694 if notRequired: |
|
695 args.append("--not-required") |
|
696 if usersite: |
|
697 args.append("--user") |
|
698 |
|
699 if Preferences.getPip("PipSearchIndex"): |
|
700 indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" |
|
701 args += ["--index-url", indexUrl] |
|
702 |
|
703 proc = QProcess() |
|
704 proc.start(interpreter, args) |
|
705 if proc.waitForStarted(15000) and proc.waitForFinished(30000): |
|
706 output = str( |
|
707 proc.readAllStandardOutput(), |
|
708 Preferences.getSystem("IOEncoding"), |
|
709 "replace", |
|
710 ).strip().splitlines()[0] |
|
711 try: |
|
712 jsonList = json.loads(output) |
|
713 except Exception: |
|
714 jsonList = [] |
|
715 |
|
716 for package in jsonList: |
|
717 if isinstance(package, dict): |
|
718 packages.append( |
|
719 ( |
|
720 package["name"], |
|
721 package["version"], |
|
722 package["latest_version"], |
|
723 ) |
|
724 ) |
|
725 |
|
726 return packages |
|
727 |
|
728 def checkPackageOutdated(self, packageStart, envName): |
|
729 """ |
|
730 Public method to check, if a group of packages is outdated. |
|
731 |
|
732 @param packageStart start string for package names to be checked |
|
733 (case insensitive) |
|
734 @type str |
|
735 @param envName name of the environment to get the packages for |
|
736 @type str |
|
737 @return tuple containing a flag indicating outdated packages and the |
|
738 list of tuples containing the package name, installed version |
|
739 and available version |
|
740 @rtype tuple of (bool, (str, str, str)) |
|
741 """ |
|
742 filteredPackages = [] |
|
743 |
|
744 if bool(envName) and bool(packageStart): |
|
745 packages = self.getOutdatedPackages(envName) |
|
746 filterStr = packageStart.lower() |
|
747 filteredPackages = [ |
|
748 p for p in packages if p[0].lower().startswith(filterStr) |
|
749 ] |
|
750 |
|
751 return bool(filteredPackages), filteredPackages |
|
752 |
|
753 def getPackageDetails(self, name, version): |
|
754 """ |
|
755 Public method to get package details using the PyPI JSON interface. |
|
756 |
|
757 @param name package name |
|
758 @type str |
|
759 @param version package version |
|
760 @type str |
|
761 @return dictionary containing PyPI package data |
|
762 @rtype dict |
|
763 """ |
|
764 result = {} |
|
765 |
|
766 if name and version: |
|
767 url = "{0}/{1}/{2}/json".format(self.getIndexUrlPypi(), name, version) |
|
768 request = QNetworkRequest(QUrl(url)) |
|
769 reply = self.__networkManager.get(request) |
|
770 while not reply.isFinished(): |
|
771 QCoreApplication.processEvents() |
|
772 QThread.msleep(100) |
|
773 |
|
774 reply.deleteLater() |
|
775 if reply.error() == QNetworkReply.NetworkError.NoError: |
|
776 data = str( |
|
777 reply.readAll(), Preferences.getSystem("IOEncoding"), "replace" |
|
778 ) |
|
779 with contextlib.suppress(Exception): |
|
780 result = json.loads(data) |
|
781 |
|
782 return result |
|
783 |
|
784 def getPackageVersions(self, name): |
|
785 """ |
|
786 Public method to get a list of versions available for the given |
|
787 package. |
|
788 |
|
789 @param name package name |
|
790 @type str |
|
791 @return list of available versions |
|
792 @rtype list of str |
|
793 """ |
|
794 result = [] |
|
795 |
|
796 if name: |
|
797 url = "{0}/{1}/json".format(self.getIndexUrlPypi(), name) |
|
798 request = QNetworkRequest(QUrl(url)) |
|
799 reply = self.__networkManager.get(request) |
|
800 while not reply.isFinished(): |
|
801 QCoreApplication.processEvents() |
|
802 QThread.msleep(100) |
|
803 |
|
804 reply.deleteLater() |
|
805 if reply.error() == QNetworkReply.NetworkError.NoError: |
|
806 dataStr = str( |
|
807 reply.readAll(), Preferences.getSystem("IOEncoding"), "replace" |
|
808 ) |
|
809 with contextlib.suppress(Exception): |
|
810 data = json.loads(dataStr) |
|
811 result = list(data["releases"].keys()) |
|
812 |
|
813 return result |
|
814 |
|
815 def getFrozenPackages( |
|
816 self, envName, localPackages=True, usersite=False, requirement=None |
|
817 ): |
|
818 """ |
|
819 Public method to get the list of package specifiers to freeze them. |
|
820 |
|
821 @param envName name of the environment to get the package specifiers |
|
822 for |
|
823 @type str |
|
824 @param localPackages flag indicating to get package specifiers for |
|
825 local packages only |
|
826 @type bool |
|
827 @param usersite flag indicating to get package specifiers for packages |
|
828 installed in user-site only |
|
829 @type bool |
|
830 @param requirement name of a requirements file |
|
831 @type str |
|
832 @return list of package specifiers |
|
833 @rtype list of str |
|
834 """ |
|
835 specifiers = [] |
|
836 |
|
837 if envName: |
|
838 interpreter = self.getVirtualenvInterpreter(envName) |
|
839 if interpreter: |
|
840 args = [ |
|
841 "-m", |
|
842 "pip", |
|
843 "freeze", |
|
844 ] |
|
845 if localPackages: |
|
846 args.append("--local") |
|
847 if usersite: |
|
848 args.append("--user") |
|
849 if requirement and os.path.exists(requirement): |
|
850 args.append("--requirement") |
|
851 args.append(requirement) |
|
852 |
|
853 success, output = self.runProcess(args, interpreter) |
|
854 if success and output: |
|
855 specifiers = [ |
|
856 spec.strip() for spec in output.splitlines() if spec.strip() |
|
857 ] |
|
858 |
|
859 return specifiers |
|
860 |
|
861 ####################################################################### |
|
862 ## Cache handling methods below |
|
863 ####################################################################### |
|
864 |
|
865 def showCacheInfo(self, venvName): |
|
866 """ |
|
867 Public method to show some information about the pip cache. |
|
868 |
|
869 @param venvName name of the virtual environment to be used |
|
870 @type str |
|
871 """ |
|
872 if venvName: |
|
873 interpreter = self.getVirtualenvInterpreter(venvName) |
|
874 if interpreter: |
|
875 args = ["-m", "pip", "cache", "info"] |
|
876 dia = PipDialog(self.tr("Cache Info")) |
|
877 res = dia.startProcess(interpreter, args, showArgs=False) |
|
878 if res: |
|
879 dia.exec() |
|
880 |
|
881 def cacheList(self, venvName): |
|
882 """ |
|
883 Public method to list files contained in the pip cache. |
|
884 |
|
885 @param venvName name of the virtual environment to be used |
|
886 @type str |
|
887 """ |
|
888 if venvName: |
|
889 interpreter = self.getVirtualenvInterpreter(venvName) |
|
890 if interpreter: |
|
891 pattern, ok = QInputDialog.getText( |
|
892 None, |
|
893 self.tr("List Cached Files"), |
|
894 self.tr("Enter a file pattern (empty for all):"), |
|
895 QLineEdit.EchoMode.Normal, |
|
896 ) |
|
897 |
|
898 if ok: |
|
899 args = ["-m", "pip", "cache", "list"] |
|
900 if pattern.strip(): |
|
901 args.append(pattern.strip()) |
|
902 dia = PipDialog(self.tr("List Cached Files")) |
|
903 res = dia.startProcess(interpreter, args, showArgs=False) |
|
904 if res: |
|
905 dia.exec() |
|
906 |
|
907 def cacheRemove(self, venvName): |
|
908 """ |
|
909 Public method to remove files from the pip cache. |
|
910 |
|
911 @param venvName name of the virtual environment to be used |
|
912 @type str |
|
913 """ |
|
914 if venvName: |
|
915 interpreter = self.getVirtualenvInterpreter(venvName) |
|
916 if interpreter: |
|
917 pattern, ok = QInputDialog.getText( |
|
918 None, |
|
919 self.tr("Remove Cached Files"), |
|
920 self.tr("Enter a file pattern:"), |
|
921 QLineEdit.EchoMode.Normal, |
|
922 ) |
|
923 |
|
924 if ok and pattern.strip(): |
|
925 args = ["-m", "pip", "cache", "remove", pattern.strip()] |
|
926 dia = PipDialog(self.tr("Remove Cached Files")) |
|
927 res = dia.startProcess(interpreter, args, showArgs=False) |
|
928 if res: |
|
929 dia.exec() |
|
930 |
|
931 def cachePurge(self, venvName): |
|
932 """ |
|
933 Public method to remove all files from the pip cache. |
|
934 |
|
935 @param venvName name of the virtual environment to be used |
|
936 @type str |
|
937 """ |
|
938 if venvName: |
|
939 interpreter = self.getVirtualenvInterpreter(venvName) |
|
940 if interpreter: |
|
941 ok = EricMessageBox.yesNo( |
|
942 None, |
|
943 self.tr("Purge Cache"), |
|
944 self.tr( |
|
945 "Do you really want to purge the pip cache? All" |
|
946 " files need to be downloaded again." |
|
947 ), |
|
948 ) |
|
949 if ok: |
|
950 args = ["-m", "pip", "cache", "purge"] |
|
951 dia = PipDialog(self.tr("Purge Cache")) |
|
952 res = dia.startProcess(interpreter, args, showArgs=False) |
|
953 if res: |
|
954 dia.exec() |
|
955 |
|
956 ####################################################################### |
|
957 ## Dependency tree handling methods below |
|
958 ####################################################################### |
|
959 |
|
960 def getDependencyTree( |
|
961 self, envName, localPackages=True, usersite=False, reverse=False |
|
962 ): |
|
963 """ |
|
964 Public method to get the dependency tree of installed packages. |
|
965 |
|
966 @param envName name of the environment to get the packages for |
|
967 @type str |
|
968 @param localPackages flag indicating to get the tree for local |
|
969 packages only |
|
970 @type bool |
|
971 @param usersite flag indicating to get the tree for packages |
|
972 installed in user-site directory only |
|
973 @type bool |
|
974 @param reverse flag indicating to get the dependency tree in |
|
975 reverse order (i.e. list packages needed by other) |
|
976 @type bool |
|
977 @return list of nested dictionaries resembling the requested |
|
978 dependency tree |
|
979 @rtype list of dict |
|
980 """ |
|
981 dependencies = [] |
|
982 |
|
983 if envName: |
|
984 interpreter = self.getVirtualenvInterpreter(envName) |
|
985 if interpreter: |
|
986 from . import pipdeptree |
|
987 |
|
988 with open(pipdeptree.__file__, "r") as f: |
|
989 content = f.read() |
|
990 args = [ |
|
991 "-c", |
|
992 content, |
|
993 "--json-tree", |
|
994 ] |
|
995 if localPackages: |
|
996 args.append("--local-only") |
|
997 if usersite: |
|
998 args.append("--user-only") |
|
999 if reverse: |
|
1000 args.append("--reverse") |
|
1001 |
|
1002 proc = QProcess() |
|
1003 proc.start(interpreter, args) |
|
1004 if proc.waitForStarted(15000) and proc.waitForFinished(30000): |
|
1005 output = str( |
|
1006 proc.readAllStandardOutput(), |
|
1007 Preferences.getSystem("IOEncoding"), |
|
1008 "replace", |
|
1009 ).strip() |
|
1010 with contextlib.suppress(json.JSONDecodeError): |
|
1011 dependencies = json.loads(output) |
|
1012 |
|
1013 return dependencies |
|
1014 |
|
1015 ####################################################################### |
|
1016 ## License handling methods below |
|
1017 ####################################################################### |
|
1018 |
|
1019 def getLicenses(self, envName, localPackages=True, usersite=False, summary=False): |
|
1020 """ |
|
1021 Public method to get the licenses per package for a given environment. |
|
1022 |
|
1023 @param envName name of the environment to get the licenses for |
|
1024 @type str |
|
1025 @param localPackages flag indicating to get the licenses for local |
|
1026 packages only |
|
1027 @type bool |
|
1028 @param usersite flag indicating to get the licenses for packages |
|
1029 installed in user-site directory only |
|
1030 @type bool |
|
1031 @param summary flag indicating to get a summary listing (defaults to |
|
1032 False) |
|
1033 @type bool (optional) |
|
1034 @return list of dictionaries containing the license and version per |
|
1035 package |
|
1036 @rtype dict |
|
1037 """ |
|
1038 licenses = [] |
|
1039 |
|
1040 if envName: |
|
1041 interpreter = self.getVirtualenvInterpreter(envName) |
|
1042 if interpreter: |
|
1043 from . import piplicenses |
|
1044 |
|
1045 with open(piplicenses.__file__, "r") as f: |
|
1046 content = f.read() |
|
1047 args = [ |
|
1048 "-c", |
|
1049 content, |
|
1050 "--from", |
|
1051 "mixed", |
|
1052 "--with-system", |
|
1053 "--with-authors", |
|
1054 "--with-urls", |
|
1055 "--with-description", |
|
1056 ] |
|
1057 if localPackages: |
|
1058 args.append("--local-only") |
|
1059 if usersite: |
|
1060 args.append("--user-only") |
|
1061 if summary: |
|
1062 args.append("--summary") |
|
1063 |
|
1064 proc = QProcess() |
|
1065 proc.start(interpreter, args) |
|
1066 if proc.waitForStarted(15000) and proc.waitForFinished(30000): |
|
1067 output = str( |
|
1068 proc.readAllStandardOutput(), |
|
1069 Preferences.getSystem("IOEncoding"), |
|
1070 "replace", |
|
1071 ).strip() |
|
1072 with contextlib.suppress(json.JSONDecodeError): |
|
1073 licenses = json.loads(output) |
|
1074 |
|
1075 return licenses |
|
1076 |
|
1077 def getLicensesSummary(self, envName, localPackages=True, usersite=False): |
|
1078 """ |
|
1079 Public method to get a summary of licenses found in a given |
|
1080 environment. |
|
1081 |
|
1082 @param envName name of the environment to get the licenses summary for |
|
1083 @type str |
|
1084 @param localPackages flag indicating to get the licenses summary for |
|
1085 local packages only |
|
1086 @type bool |
|
1087 @param usersite flag indicating to get the licenses summary for |
|
1088 packages installed in user-site directory only |
|
1089 @type bool |
|
1090 @return list of dictionaries containing the license and the count of |
|
1091 packages |
|
1092 @rtype dict |
|
1093 """ |
|
1094 return self.getLicenses( |
|
1095 envName, localPackages=localPackages, usersite=usersite, summary=True |
|
1096 ) |