|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2018 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a class to manage Python virtual environments. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 import os |
|
13 import sys |
|
14 import shutil |
|
15 import json |
|
16 import copy |
|
17 |
|
18 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject |
|
19 from PyQt5.QtWidgets import QDialog |
|
20 |
|
21 from E5Gui import E5MessageBox |
|
22 from E5Gui.E5Application import e5App |
|
23 |
|
24 import Preferences |
|
25 |
|
26 |
|
27 class VirtualenvManager(QObject): |
|
28 """ |
|
29 Class implementing an object to manage Python virtual environments. |
|
30 |
|
31 @signal virtualEnvironmentAdded() emitted to indicate the addition of |
|
32 a virtual environment |
|
33 @signal virtualEnvironmentRemoved() emitted to indicate the removal and |
|
34 deletion of a virtual environment |
|
35 @signal virtualEnvironmentChanged(name) emitted to indicate a change of |
|
36 a virtual environment |
|
37 """ |
|
38 DefaultKey = "<default>" |
|
39 |
|
40 virtualEnvironmentAdded = pyqtSignal() |
|
41 virtualEnvironmentRemoved = pyqtSignal() |
|
42 virtualEnvironmentChanged = pyqtSignal(str) |
|
43 |
|
44 def __init__(self, parent=None): |
|
45 """ |
|
46 Constructor |
|
47 |
|
48 @param parent reference to the parent object |
|
49 @type QWidget |
|
50 """ |
|
51 super(VirtualenvManager, self).__init__(parent) |
|
52 |
|
53 self.__ui = parent |
|
54 |
|
55 self.__loadSettings() |
|
56 |
|
57 self.__virtualenvManagerDialog = None |
|
58 |
|
59 def __loadSettings(self): |
|
60 """ |
|
61 Private slot to load the virtual environments. |
|
62 """ |
|
63 venvString = Preferences.Prefs.settings.value( |
|
64 "PyVenv/VirtualEnvironments", "{}") # __IGNORE_WARNING_M613__ |
|
65 environments = json.loads(venvString) |
|
66 |
|
67 self.__virtualEnvironments = {} |
|
68 # each environment entry is a dictionary: |
|
69 # path: the directory of the virtual environment |
|
70 # (empty for a global environment) |
|
71 # interpreter: the path of the Python interpreter |
|
72 # variant: Python variant (2 or 3) |
|
73 # is_global: a flag indicating a global environment |
|
74 # is_conda: a flag indicating an Anaconda environment |
|
75 # is_remote: a flag indicating a remotely accessed environment |
|
76 # exec_path: a string to be prefixed to the PATH environment |
|
77 # setting |
|
78 # |
|
79 for venvName in environments: |
|
80 environment = environments[venvName] |
|
81 if ("is_remote" in environment and environment["is_remote"]) or \ |
|
82 os.access(environment["interpreter"], os.X_OK): |
|
83 if "is_global" not in environment: |
|
84 environment["is_global"] = environment["path"] == "" |
|
85 if "is_conda" not in environment: |
|
86 environment["is_conda"] = False |
|
87 if "is_remote" not in environment: |
|
88 environment["is_remote"] = False |
|
89 if "exec_path" not in environment: |
|
90 environment["exec_path"] = "" |
|
91 self.__virtualEnvironments[venvName] = environment |
|
92 |
|
93 # check, if the interpreter used to run eric is in the environments |
|
94 defaultPy = sys.executable.replace("w.exe", ".exe") |
|
95 found = False |
|
96 for venvName in self.__virtualEnvironments: |
|
97 if (defaultPy == |
|
98 self.__virtualEnvironments[venvName]["interpreter"]): |
|
99 found = True |
|
100 break |
|
101 if not found: |
|
102 # add an environment entry for the default interpreter |
|
103 self.__virtualEnvironments[VirtualenvManager.DefaultKey] = { |
|
104 "path": "", |
|
105 "interpreter": defaultPy, |
|
106 "variant": sys.version_info[0], |
|
107 "is_global": True, |
|
108 "is_conda": False, |
|
109 "is_remote": False, |
|
110 "exec_path": "", |
|
111 } |
|
112 |
|
113 self.__saveSettings() |
|
114 |
|
115 def __saveSettings(self): |
|
116 """ |
|
117 Private slot to save the virtual environments. |
|
118 """ |
|
119 Preferences.Prefs.settings.setValue( |
|
120 "PyVenv/VirtualEnvironments", |
|
121 json.dumps(self.__virtualEnvironments) |
|
122 ) |
|
123 Preferences.syncPreferences() |
|
124 |
|
125 def getDefaultEnvironment(self): |
|
126 """ |
|
127 Public method to get the default virtual environment. |
|
128 |
|
129 Default is an environment with the key '<default>' or the first one |
|
130 having an interpreter matching sys.executable (i.e. the one used to |
|
131 execute eric6 with) |
|
132 |
|
133 @return tuple containing the environment name and a dictionary |
|
134 containing a copy of the default virtual environment |
|
135 @rtype tuple of (str, dict) |
|
136 """ |
|
137 if VirtualenvManager.DefaultKey in self.__virtualEnvironments: |
|
138 return ( |
|
139 VirtualenvManager.DefaultKey, |
|
140 copy.copy( |
|
141 self.__virtualEnvironments[VirtualenvManager.DefaultKey]) |
|
142 ) |
|
143 |
|
144 else: |
|
145 defaultPy = sys.executable.replace("w.exe", ".exe") |
|
146 for venvName in self.__virtualEnvironments: |
|
147 if (defaultPy == |
|
148 self.__virtualEnvironments[venvName]["interpreter"]): |
|
149 return ( |
|
150 venvName, |
|
151 copy.copy(self.__virtualEnvironments[venvName]) |
|
152 ) |
|
153 |
|
154 return ("", {}) |
|
155 |
|
156 @pyqtSlot() |
|
157 def createVirtualEnv(self): |
|
158 """ |
|
159 Public slot to create a new virtual environment. |
|
160 """ |
|
161 from .VirtualenvConfigurationDialog import \ |
|
162 VirtualenvConfigurationDialog |
|
163 |
|
164 dlg = VirtualenvConfigurationDialog() |
|
165 if dlg.exec_() == QDialog.Accepted: |
|
166 resultDict = dlg.getData() |
|
167 |
|
168 if resultDict["envType"] == "conda": |
|
169 # create the conda environment |
|
170 conda = e5App().getObject("Conda") |
|
171 ok, prefix, interpreter = conda.createCondaEnvironment( |
|
172 resultDict["arguments"]) |
|
173 if ok and "--dry-run" not in resultDict["arguments"]: |
|
174 self.addVirtualEnv(resultDict["logicalName"], |
|
175 prefix, |
|
176 venvInterpreter=interpreter, |
|
177 isConda=True) |
|
178 else: |
|
179 # now do the call |
|
180 from .VirtualenvExecDialog import VirtualenvExecDialog |
|
181 dia = VirtualenvExecDialog(resultDict, self) |
|
182 dia.show() |
|
183 dia.start(resultDict["arguments"]) |
|
184 dia.exec_() |
|
185 |
|
186 def addVirtualEnv(self, venvName, venvDirectory, venvInterpreter="", |
|
187 venvVariant=3, isGlobal=False, isConda=False, |
|
188 isRemote=False, execPath=""): |
|
189 """ |
|
190 Public method to add a virtual environment. |
|
191 |
|
192 @param venvName logical name for the virtual environment |
|
193 @type str |
|
194 @param venvDirectory directory of the virtual environment |
|
195 @type str |
|
196 @param venvInterpreter interpreter of the virtual environment |
|
197 @type str |
|
198 @param venvVariant Python variant of the virtual environment |
|
199 @type int |
|
200 @param isGlobal flag indicating a global environment |
|
201 @type bool |
|
202 @param isConda flag indicating an Anaconda virtual environment |
|
203 @type bool |
|
204 @param isRemote flag indicating a remotely accessed environment |
|
205 @type bool |
|
206 @param execPath search path string to be prepended to the PATH |
|
207 environment variable |
|
208 @type str |
|
209 """ |
|
210 if venvName in self.__virtualEnvironments: |
|
211 ok = E5MessageBox.yesNo( |
|
212 None, |
|
213 self.tr("Add Virtual Environment"), |
|
214 self.tr("""A virtual environment named <b>{0}</b> exists""" |
|
215 """ already. Shall it be replaced?""") |
|
216 .format(venvName), |
|
217 icon=E5MessageBox.Warning) |
|
218 if not ok: |
|
219 from .VirtualenvNameDialog import VirtualenvNameDialog |
|
220 dlg = VirtualenvNameDialog( |
|
221 list(self.__virtualEnvironments.keys()), |
|
222 venvName) |
|
223 if dlg.exec_() != QDialog.Accepted: |
|
224 return |
|
225 |
|
226 venvName = dlg.getName() |
|
227 |
|
228 if not venvInterpreter: |
|
229 from .VirtualenvInterpreterSelectionDialog import \ |
|
230 VirtualenvInterpreterSelectionDialog |
|
231 dlg = VirtualenvInterpreterSelectionDialog(venvName, venvDirectory) |
|
232 if dlg.exec_() == QDialog.Accepted: |
|
233 venvInterpreter, venvVariant = dlg.getData() |
|
234 |
|
235 if venvInterpreter: |
|
236 self.__virtualEnvironments[venvName] = { |
|
237 "path": venvDirectory, |
|
238 "interpreter": venvInterpreter, |
|
239 "variant": venvVariant, |
|
240 "is_global": isGlobal, |
|
241 "is_conda": isConda, |
|
242 "is_remote": isRemote, |
|
243 "exec_path": execPath, |
|
244 } |
|
245 |
|
246 self.__saveSettings() |
|
247 |
|
248 self.virtualEnvironmentAdded.emit() |
|
249 if self.__virtualenvManagerDialog: |
|
250 self.__virtualenvManagerDialog.refresh() |
|
251 |
|
252 def setVirtualEnv(self, venvName, venvDirectory, venvInterpreter, |
|
253 venvVariant, isGlobal, isConda, isRemote, |
|
254 execPath): |
|
255 """ |
|
256 Public method to change a virtual environment. |
|
257 |
|
258 @param venvName logical name of the virtual environment |
|
259 @type str |
|
260 @param venvDirectory directory of the virtual environment |
|
261 @type str |
|
262 @param venvInterpreter interpreter of the virtual environment |
|
263 @type str |
|
264 @param venvVariant Python variant of the virtual environment |
|
265 @type int |
|
266 @param isGlobal flag indicating a global environment |
|
267 @type bool |
|
268 @param isConda flag indicating an Anaconda virtual environment |
|
269 @type bool |
|
270 @param isRemote flag indicating a remotely accessed environment |
|
271 @type bool |
|
272 @param execPath search path string to be prepended to the PATH |
|
273 environment variable |
|
274 @type str |
|
275 """ |
|
276 if venvName not in self.__virtualEnvironments: |
|
277 E5MessageBox.yesNo( |
|
278 None, |
|
279 self.tr("Change Virtual Environment"), |
|
280 self.tr("""A virtual environment named <b>{0}</b> does not""" |
|
281 """ exist. Aborting!""") |
|
282 .format(venvName), |
|
283 icon=E5MessageBox.Warning) |
|
284 return |
|
285 |
|
286 self.__virtualEnvironments[venvName] = { |
|
287 "path": venvDirectory, |
|
288 "interpreter": venvInterpreter, |
|
289 "variant": venvVariant, |
|
290 "is_global": isGlobal, |
|
291 "is_conda": isConda, |
|
292 "is_remote": isRemote, |
|
293 "exec_path": execPath, |
|
294 } |
|
295 |
|
296 self.__saveSettings() |
|
297 |
|
298 self.virtualEnvironmentChanged.emit(venvName) |
|
299 if self.__virtualenvManagerDialog: |
|
300 self.__virtualenvManagerDialog.refresh() |
|
301 |
|
302 def renameVirtualEnv(self, oldVenvName, venvName, venvDirectory, |
|
303 venvInterpreter, venvVariant, isGlobal, isConda, |
|
304 isRemote, execPath): |
|
305 """ |
|
306 Public method to substitute a virtual environment entry with a new |
|
307 name. |
|
308 |
|
309 @param oldVenvName old name of the virtual environment |
|
310 @type str |
|
311 @param venvName logical name for the virtual environment |
|
312 @type str |
|
313 @param venvDirectory directory of the virtual environment |
|
314 @type str |
|
315 @param venvInterpreter interpreter of the virtual environment |
|
316 @type str |
|
317 @param venvVariant Python variant of the virtual environment |
|
318 @type int |
|
319 @param isGlobal flag indicating a global environment |
|
320 @type bool |
|
321 @param isConda flag indicating an Anaconda virtual environment |
|
322 @type bool |
|
323 @param isRemote flag indicating a remotely accessed environment |
|
324 @type bool |
|
325 @param execPath search path string to be prepended to the PATH |
|
326 environment variable |
|
327 @type str |
|
328 """ |
|
329 if oldVenvName not in self.__virtualEnvironments: |
|
330 E5MessageBox.yesNo( |
|
331 None, |
|
332 self.tr("Rename Virtual Environment"), |
|
333 self.tr("""A virtual environment named <b>{0}</b> does not""" |
|
334 """ exist. Aborting!""") |
|
335 .format(oldVenvName), |
|
336 icon=E5MessageBox.Warning) |
|
337 return |
|
338 |
|
339 del self.__virtualEnvironments[oldVenvName] |
|
340 self.addVirtualEnv(venvName, venvDirectory, venvInterpreter, |
|
341 venvVariant, isGlobal, isConda, isRemote, |
|
342 execPath) |
|
343 |
|
344 def deleteVirtualEnvs(self, venvNames): |
|
345 """ |
|
346 Public method to delete virtual environments from the list and disk. |
|
347 |
|
348 @param venvNames list of logical names for the virtual environments |
|
349 @type list of str |
|
350 """ |
|
351 venvMessages = [] |
|
352 for venvName in venvNames: |
|
353 if venvName in self.__virtualEnvironments and \ |
|
354 bool(self.__virtualEnvironments[venvName]["path"]): |
|
355 venvMessages.append(self.tr("{0} - {1}").format( |
|
356 venvName, self.__virtualEnvironments[venvName]["path"])) |
|
357 if venvMessages: |
|
358 from UI.DeleteFilesConfirmationDialog import \ |
|
359 DeleteFilesConfirmationDialog |
|
360 dlg = DeleteFilesConfirmationDialog( |
|
361 None, |
|
362 self.tr("Delete Virtual Environments"), |
|
363 self.tr("""Do you really want to delete these virtual""" |
|
364 """ environments?"""), |
|
365 venvMessages |
|
366 ) |
|
367 if dlg.exec_() == QDialog.Accepted: |
|
368 for venvName in venvNames: |
|
369 if self.__isEnvironmentDeleteable(venvName): |
|
370 if self.isCondaEnvironment(venvName): |
|
371 conda = e5App().getObject("Conda") |
|
372 path = self.__virtualEnvironments[venvName]["path"] |
|
373 res = conda.removeCondaEnvironment(prefix=path) |
|
374 if res: |
|
375 del self.__virtualEnvironments[venvName] |
|
376 else: |
|
377 shutil.rmtree( |
|
378 self.__virtualEnvironments[venvName]["path"], |
|
379 True) |
|
380 del self.__virtualEnvironments[venvName] |
|
381 |
|
382 self.__saveSettings() |
|
383 |
|
384 self.virtualEnvironmentRemoved.emit() |
|
385 if self.__virtualenvManagerDialog: |
|
386 self.__virtualenvManagerDialog.refresh() |
|
387 |
|
388 def __isEnvironmentDeleteable(self, venvName): |
|
389 """ |
|
390 Private method to check, if a virtual environment can be deleted from |
|
391 disk. |
|
392 |
|
393 @param venvName name of the virtual environment |
|
394 @type str |
|
395 @return flag indicating it can be deleted |
|
396 @rtype bool |
|
397 """ |
|
398 ok = False |
|
399 if venvName in self.__virtualEnvironments: |
|
400 ok = True |
|
401 ok &= bool(self.__virtualEnvironments[venvName]["path"]) |
|
402 ok &= not self.__virtualEnvironments[venvName]["is_global"] |
|
403 ok &= not self.__virtualEnvironments[venvName]["is_remote"] |
|
404 ok &= os.access(self.__virtualEnvironments[venvName]["path"], |
|
405 os.W_OK) |
|
406 |
|
407 return ok |
|
408 |
|
409 def removeVirtualEnvs(self, venvNames): |
|
410 """ |
|
411 Public method to delete virtual environment from the list. |
|
412 |
|
413 @param venvNames list of logical names for the virtual environments |
|
414 @type list of str |
|
415 """ |
|
416 venvMessages = [] |
|
417 for venvName in venvNames: |
|
418 if venvName in self.__virtualEnvironments: |
|
419 venvMessages.append(self.tr("{0} - {1}").format( |
|
420 venvName, self.__virtualEnvironments[venvName]["path"])) |
|
421 if venvMessages: |
|
422 from UI.DeleteFilesConfirmationDialog import \ |
|
423 DeleteFilesConfirmationDialog |
|
424 dlg = DeleteFilesConfirmationDialog( |
|
425 None, |
|
426 self.tr("Remove Virtual Environments"), |
|
427 self.tr("""Do you really want to remove these virtual""" |
|
428 """ environments?"""), |
|
429 venvMessages |
|
430 ) |
|
431 if dlg.exec_() == QDialog.Accepted: |
|
432 for venvName in venvNames: |
|
433 if venvName in self.__virtualEnvironments: |
|
434 del self.__virtualEnvironments[venvName] |
|
435 |
|
436 self.__saveSettings() |
|
437 |
|
438 self.virtualEnvironmentRemoved.emit() |
|
439 if self.__virtualenvManagerDialog: |
|
440 self.__virtualenvManagerDialog.refresh() |
|
441 |
|
442 def getEnvironmentEntries(self): |
|
443 """ |
|
444 Public method to get a dictionary containing the defined virtual |
|
445 environment entries. |
|
446 |
|
447 @return dictionary containing a copy of the defined virtual |
|
448 environments |
|
449 @rtype dict |
|
450 """ |
|
451 return copy.deepcopy(self.__virtualEnvironments) |
|
452 |
|
453 @pyqtSlot() |
|
454 def showVirtualenvManagerDialog(self, modal=False): |
|
455 """ |
|
456 Public slot to show the virtual environment manager dialog. |
|
457 |
|
458 @param modal flag indicating that the dialog should be shown in |
|
459 a blocking mode |
|
460 """ |
|
461 if self.__virtualenvManagerDialog is None: |
|
462 from .VirtualenvManagerDialog import VirtualenvManagerDialog |
|
463 self.__virtualenvManagerDialog = VirtualenvManagerDialog( |
|
464 self, self.__ui) |
|
465 |
|
466 if modal: |
|
467 self.__virtualenvManagerDialog.exec_() |
|
468 else: |
|
469 self.__virtualenvManagerDialog.show() |
|
470 |
|
471 def shutdown(self): |
|
472 """ |
|
473 Public method to shutdown the manager. |
|
474 """ |
|
475 if self.__virtualenvManagerDialog is not None: |
|
476 self.__virtualenvManagerDialog.close() |
|
477 self.__virtualenvManagerDialog = None |
|
478 |
|
479 def isUnique(self, venvName): |
|
480 """ |
|
481 Public method to check, if the give logical name is unique. |
|
482 |
|
483 @param venvName logical name for the virtual environment |
|
484 @type str |
|
485 @return flag indicating uniqueness |
|
486 @rtype bool |
|
487 """ |
|
488 return venvName not in self.__virtualEnvironments |
|
489 |
|
490 def getVirtualenvInterpreter(self, venvName): |
|
491 """ |
|
492 Public method to get the interpreter for a virtual environment. |
|
493 |
|
494 @param venvName logical name for the virtual environment |
|
495 @type str |
|
496 @return interpreter path |
|
497 @rtype str |
|
498 """ |
|
499 if venvName in self.__virtualEnvironments: |
|
500 return self.__virtualEnvironments[venvName]["interpreter"] |
|
501 else: |
|
502 return "" |
|
503 |
|
504 def getVirtualenvDirectory(self, venvName): |
|
505 """ |
|
506 Public method to get the directory of a virtual environment. |
|
507 |
|
508 @param venvName logical name for the virtual environment |
|
509 @type str |
|
510 @return directory path |
|
511 @rtype str |
|
512 """ |
|
513 if venvName in self.__virtualEnvironments: |
|
514 return self.__virtualEnvironments[venvName]["path"] |
|
515 else: |
|
516 return "" |
|
517 |
|
518 def getVirtualenvNames(self): |
|
519 """ |
|
520 Public method to get a list of defined virtual environments. |
|
521 |
|
522 @return list of defined virtual environments |
|
523 @rtype list of str |
|
524 """ |
|
525 return list(self.__virtualEnvironments.keys()) |
|
526 |
|
527 def getVirtualenvNamesForVariant(self, variant): |
|
528 """ |
|
529 Public method to get a list of virtual environments for a given |
|
530 Python variant. |
|
531 |
|
532 @param variant Python variant (2 or 3) |
|
533 @type int |
|
534 @return list of defined virtual environments |
|
535 @rtype list of str |
|
536 """ |
|
537 assert variant in (2, 3) |
|
538 |
|
539 environments = [] |
|
540 for venvName in self.__virtualEnvironments: |
|
541 if self.__virtualEnvironments[venvName]["variant"] == variant: |
|
542 environments.append(venvName) |
|
543 |
|
544 return environments |
|
545 |
|
546 def getVirtualenvVariant(self, venvName): |
|
547 """ |
|
548 Public method to get the variant of a virtual environment. |
|
549 |
|
550 @param venvName logical name for the virtual environment |
|
551 @type str |
|
552 @return Python variant of the environment |
|
553 @rtype str |
|
554 """ |
|
555 if venvName in self.__virtualEnvironments: |
|
556 return "Python{0}".format( |
|
557 self.__virtualEnvironments[venvName]["variant"]) |
|
558 else: |
|
559 return "" |
|
560 |
|
561 def isGlobalEnvironment(self, venvName): |
|
562 """ |
|
563 Public method to test, if a given environment is a global one. |
|
564 |
|
565 @param venvName logical name of the virtual environment |
|
566 @type str |
|
567 @return flag indicating a global environment |
|
568 @rtype bool |
|
569 """ |
|
570 if venvName in self.__virtualEnvironments: |
|
571 return self.__virtualEnvironments[venvName]["is_global"] |
|
572 else: |
|
573 return False |
|
574 |
|
575 def isCondaEnvironment(self, venvName): |
|
576 """ |
|
577 Public method to test, if a given environment is an Anaconda |
|
578 environment. |
|
579 |
|
580 @param venvName logical name of the virtual environment |
|
581 @type str |
|
582 @return flag indicating an Anaconda environment |
|
583 @rtype bool |
|
584 """ |
|
585 if venvName in self.__virtualEnvironments: |
|
586 return self.__virtualEnvironments[venvName]["is_conda"] |
|
587 else: |
|
588 return False |
|
589 |
|
590 def isRemoteEnvironment(self, venvName): |
|
591 """ |
|
592 Public method to test, if a given environment is a remotely accessed |
|
593 environment. |
|
594 |
|
595 @param venvName logical name of the virtual environment |
|
596 @type str |
|
597 @return flag indicating a remotely accessed environment |
|
598 @rtype bool |
|
599 """ |
|
600 if venvName in self.__virtualEnvironments: |
|
601 return self.__virtualEnvironments[venvName]["is_remote"] |
|
602 else: |
|
603 return False |
|
604 |
|
605 def getVirtualenvExecPath(self, venvName): |
|
606 """ |
|
607 Public method to get the search path prefix of a virtual environment. |
|
608 |
|
609 @param venvName logical name for the virtual environment |
|
610 @type str |
|
611 @return search path prefix |
|
612 @rtype str |
|
613 """ |
|
614 if venvName in self.__virtualEnvironments: |
|
615 return self.__virtualEnvironments[venvName]["exec_path"] |
|
616 else: |
|
617 return "" |