src/eric7/VirtualEnv/VirtualenvManager.py

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

eric ide

mercurial