eric6/VirtualEnv/VirtualenvManager.py

changeset 6942
2602857055c5
parent 6896
3716c4af48bb
child 6975
3325bf3e7b2c
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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 ""

eric ide

mercurial