Wed, 29 Nov 2023 14:27:25 +0100
Virtual Environments
- Added the capability to search for unregistered Python interpreters in order to create an environment entry for those selected by the user.
--- a/src/eric7/SystemUtilities/PythonUtilities.py Wed Nov 29 11:33:32 2023 +0100 +++ b/src/eric7/SystemUtilities/PythonUtilities.py Wed Nov 29 14:27:25 2023 +0100 @@ -7,10 +7,14 @@ Module implementing Python related utility functions. """ +import contextlib import os +import platform import sys import sysconfig +from .OSUtilities import isWindowsPlatform + def getPythonExecutable(): """ @@ -135,3 +139,135 @@ pyVer = 3 return pyVer + + +def searchInterpreters(environments=None): + """ + Function to determine a list of all Python interpreters available via the + executable search path (i.e. PATH) (Windows variant). + + @param environments list of environment directories to scan for Python interpreters + (defaults to None) + @type list of str (optional) + @return list of found interpreter executables + @rtype list of str + """ + if isWindowsPlatform(): + return __searchInterpreters_Windows(environments=environments) + else: + return __searchInterpreters_Linux(environments=environments) + + +def __searchInterpreters_Windows(environments=None): + """ + Function to determine a list of all Python interpreters available via the + executable search path (i.e. PATH) (Windows variant). + + @param environments list of environment directories to scan for Python interpreters + (defaults to None) + @type list of str (optional) + @return list of found interpreter executables + @rtype list of str + """ + try: + import winreg + except ImportError: + import _winreg as winreg # __IGNORE_WARNING__ + + def getExePath(branch, access, versionStr): + with contextlib.suppress(WindowsError, OSError): + software = winreg.OpenKey(branch, "Software", 0, access) + python = winreg.OpenKey(software, "Python", 0, access) + pcore = winreg.OpenKey(python, "PythonCore", 0, access) + version = winreg.OpenKey(pcore, versionStr, 0, access) + installpath = winreg.QueryValue(version, "InstallPath") + exe = os.path.join(installpath, "python.exe") + if os.access(exe, os.X_OK): + return exe + + return None + + minorVersions = range(8, 16) # Py 3.8 until Py 3.15 + interpreters = set() + + if environments: + for directory in [os.path.join(d, "Scripts") for d in environments]: + exe = os.path.join(directory, "python.exe") + if os.access(exe, os.X_OK): + interpreters.add(exe) + + else: + versionSuffixes = ["", "-32", "-64"] + for minorVersion in minorVersions: + for versionSuffix in versionSuffixes: + versionStr = "{0}.{1}{2}".format( + "3", minorVersion, versionSuffix + ) + exePath = getExePath( + winreg.HKEY_CURRENT_USER, + winreg.KEY_WOW64_32KEY | winreg.KEY_READ, + versionStr, + ) + if exePath: + interpreters.add(exePath) + + exePath = getExePath( + winreg.HKEY_LOCAL_MACHINE, + winreg.KEY_WOW64_32KEY | winreg.KEY_READ, + versionStr, + ) + if exePath: + interpreters.add(exePath) + + # Even on Intel 64-bit machines it's 'AMD64' + if platform.machine() == "AMD64": + exePath = getExePath( + winreg.HKEY_CURRENT_USER, + winreg.KEY_WOW64_64KEY | winreg.KEY_READ, + versionStr, + ) + if exePath: + interpreters.add(exePath) + + exePath = getExePath( + winreg.HKEY_LOCAL_MACHINE, + winreg.KEY_WOW64_64KEY | winreg.KEY_READ, + versionStr, + ) + if exePath: + interpreters.add(exePath) + + return list(interpreters) + + +def __searchInterpreters_Linux(environments=None): + """ + Function to determine a list of all Python interpreters available via the + executable search path (i.e. PATH) (non Windows variant). + + @param environments list of environment directories to scan for Python interpreters + (defaults to None) + @type list of str (optional) + @return list of found interpreter executables + @rtype list of str + """ + from eric7.SystemUtilities import OSUtilities + + minorVersions = range(8, 16) # Py 3.8 until Py 3.15 + interpreters = [] + + if environments: + directories = [os.path.join(d, "bin") for d in environments] + else: + searchpath = OSUtilities.getEnvironmentEntry("PATH") + directories = searchpath.split(os.pathsep) if searchpath else [] + + if directories: + pythonNames = ["python3.{0}".format(v) for v in minorVersions] + for directory in directories: + for interpreter in pythonNames: + exe = os.path.join(directory, interpreter) + if os.access(exe, os.X_OK): + interpreters.append(exe) + + return interpreters
--- a/src/eric7/VirtualEnv/VirtualenvManager.py Wed Nov 29 11:33:32 2023 +0100 +++ b/src/eric7/VirtualEnv/VirtualenvManager.py Wed Nov 29 14:27:25 2023 +0100 @@ -20,7 +20,7 @@ from eric7 import Preferences from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp -from eric7.SystemUtilities import FileSystemUtilities, PythonUtilities +from eric7.SystemUtilities import FileSystemUtilities, OSUtilities, PythonUtilities from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog from .VirtualenvMeta import VirtualenvMetaData @@ -479,6 +479,33 @@ self.virtualEnvironmentRemoved.emit() self.virtualEnvironmentsListChanged.emit() + def searchUnregisteredInterpreters(self): + """ + Public method to search for unregistered Python interpreters. + + @return list of unregistered interpreters + @rtype list of str + """ + interpreters = [] + baseDir = self.getVirtualEnvironmentsBaseDir() + if not baseDir: + # search in home directory, if no environments base directory is defined + baseDir = OSUtilities.getHomeDir() + environments = [ + os.path.join(baseDir, d) + for d in os.listdir(baseDir) + if os.path.isdir(os.path.join(baseDir, d)) + ] + + interpreters = PythonUtilities.searchInterpreters() + if environments: + interpreters += PythonUtilities.searchInterpreters(environments) + + interpreters = { + i for i in interpreters if not self.environmentForInterpreter(i)[0] + } # filter the list into a set to make the remaining ones unique + return list(interpreters) + def getEnvironmentEntries(self): """ Public method to get a list of the defined virtual environment entries.
--- a/src/eric7/VirtualEnv/VirtualenvManagerWidget.ui Wed Nov 29 11:33:32 2023 +0100 +++ b/src/eric7/VirtualEnv/VirtualenvManagerWidget.ui Wed Nov 29 14:27:25 2023 +0100 @@ -70,6 +70,13 @@ </widget> </item> <item> + <widget class="QToolButton" name="searchNewButton"> + <property name="toolTip"> + <string>Search the execution path for all Python interpreters not configured in an environment yet.</string> + </property> + </widget> + </item> + <item> <widget class="Line" name="line_6"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -265,6 +272,7 @@ <tabstop>refreshButton</tabstop> <tabstop>addButton</tabstop> <tabstop>newButton</tabstop> + <tabstop>searchNewButton</tabstop> <tabstop>editButton</tabstop> <tabstop>upgradeButton</tabstop> <tabstop>removeButton</tabstop>
--- a/src/eric7/VirtualEnv/VirtualenvManagerWidgets.py Wed Nov 29 11:33:32 2023 +0100 +++ b/src/eric7/VirtualEnv/VirtualenvManagerWidgets.py Wed Nov 29 14:27:25 2023 +0100 @@ -8,6 +8,7 @@ environments. """ +import datetime import os from PyQt6.QtCore import Qt, pyqtSlot @@ -21,11 +22,14 @@ ) from eric7.EricGui import EricPixmapCache +from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricMainWindow import EricMainWindow from eric7.EricWidgets.EricPathPicker import EricPathPickerModes +from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog from eric7.SystemUtilities import OSUtilities from .Ui_VirtualenvManagerWidget import Ui_VirtualenvManagerWidget +from .VirtualenvMeta import VirtualenvMetaData class VirtualenvManagerWidget(QWidget, Ui_VirtualenvManagerWidget): @@ -55,6 +59,7 @@ self.refreshButton.setIcon(EricPixmapCache.getIcon("reload")) self.addButton.setIcon(EricPixmapCache.getIcon("plus")) self.newButton.setIcon(EricPixmapCache.getIcon("new")) + self.searchNewButton.setIcon(EricPixmapCache.getIcon("question")) self.editButton.setIcon(EricPixmapCache.getIcon("edit")) self.upgradeButton.setIcon(EricPixmapCache.getIcon("upgrade")) self.removeButton.setIcon(EricPixmapCache.getIcon("minus")) @@ -161,6 +166,73 @@ self.__manager.createVirtualEnv(baseDir=self.envBaseDirectoryPicker.text()) @pyqtSlot() + def on_searchNewButton_clicked(self): + """ + Public slot to search for new (not yet registered) Python interpreters. + """ + potentialInterpreters = self.__manager.searchUnregisteredInterpreters() + + if not bool(potentialInterpreters): + EricMessageBox.information( + self, + self.tr("Search Virtual Environments"), + self.tr("""No unregistered virtual environments were found."""), + ) + return + + baseDir = self.__manager.getVirtualEnvironmentsBaseDir() + if not baseDir: + baseDir = OSUtilities.getHomeDir() + + selectionList = [] + for interpreter in potentialInterpreters: + if not interpreter.startswith(baseDir): + realpath = os.path.realpath(interpreter) + if realpath != interpreter: + # interpreter is a link + selectionList.append( + ( + self.tr("{0}\n(=> {1})").format(interpreter, realpath), + interpreter, + ) + ) + continue + + selectionList.append((interpreter, interpreter)) + + dlg = EricListSelectionDialog( + sorted(selectionList), + title=self.tr("Search Virtual Environments"), + message=self.tr( + "Select the interpreters to create environment entries for:" + ), + checkBoxSelection=True, + emptySelectionOk=True, + showSelectAll=True, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + selectedInterpreters = [env[1] for env in dlg.getSelection()] + + nameTemplate = ( + "Environment #{0} added " + + datetime.datetime.now().strftime("%Y-%m-%d %H:%M") + ) + for interpreter in selectedInterpreters: + metadata = VirtualenvMetaData( + name=nameTemplate.format( + selectedInterpreters.index(interpreter) + 1 + ), + path=( + os.path.abspath(os.path.join(interpreter, "..", "..")) + if interpreter.startswith(baseDir) + else "" + ), + interpreter=interpreter, + is_global=not interpreter.startswith(baseDir), + ) + self.__manager.addVirtualEnv(metadata) + + @pyqtSlot() def on_editButton_clicked(self): """ Private slot to edit the selected entry.