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