eric7/VirtualEnv/VirtualenvManager.py

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

eric ide

mercurial