diff -r 4e8b98454baa -r 800c432b34c8 eric7/PluginManager/PluginManager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/PluginManager/PluginManager.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,1449 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Plugin Manager. +""" + +import os +import sys +import zipfile +import types +import importlib +import contextlib + +from PyQt5.QtCore import ( + pyqtSignal, QObject, QDate, QFile, QFileInfo, QUrl, QIODevice +) +from PyQt5.QtGui import QPixmap +from PyQt5.QtNetwork import ( + QNetworkAccessManager, QNetworkRequest, QNetworkReply +) + +from E5Gui import E5MessageBox +from E5Gui.E5Application import e5App + +from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired +try: + from E5Network.E5SslErrorHandler import E5SslErrorHandler, E5SslErrorState + SSL_AVAILABLE = True +except ImportError: + SSL_AVAILABLE = False + +from .PluginExceptions import ( + PluginPathError, PluginModulesError, PluginLoadError, + PluginActivationError, PluginModuleFormatError, PluginClassFormatError +) + +import UI.PixmapCache + +import Globals +import Utilities +import Preferences + +from eric6config import getConfig + + +class PluginManager(QObject): + """ + Class implementing the Plugin Manager. + + @signal shutdown() emitted at shutdown of the IDE + @signal pluginAboutToBeActivated(modulName, pluginObject) emitted just + before a plugin is activated + @signal pluginActivated(moduleName, pluginObject) emitted just after + a plugin was activated + @signal allPlugginsActivated() emitted at startup after all plugins have + been activated + @signal pluginAboutToBeDeactivated(moduleName, pluginObject) emitted just + before a plugin is deactivated + @signal pluginDeactivated(moduleName, pluginObject) emitted just after + a plugin was deactivated + """ + shutdown = pyqtSignal() + pluginAboutToBeActivated = pyqtSignal(str, object) + pluginActivated = pyqtSignal(str, object) + allPlugginsActivated = pyqtSignal() + pluginAboutToBeDeactivated = pyqtSignal(str, object) + pluginDeactivated = pyqtSignal(str, object) + + def __init__(self, parent=None, disabledPlugins=None, doLoadPlugins=True, + develPlugin=None): + """ + Constructor + + The Plugin Manager deals with three different plugin directories. + The first is the one, that is part of eric6 (eric6/Plugins). The + second one is the global plugin directory called 'eric6plugins', + which is located inside the site-packages directory. The last one + is the user plugin directory located inside the .eric6 directory + of the users home directory. + + @param parent reference to the parent object + @type QObject + @param disabledPlugins list of plug-ins that have been disabled via + the command line parameters '--disable-plugin=' + @type list of str + @param doLoadPlugins flag indicating, that plug-ins should + be loaded + @type bool + @param develPlugin filename of a plug-in to be loaded for + development + @type str + @exception PluginPathError raised to indicate an invalid plug-in path + @exception PluginModulesError raised to indicate the absence of + plug-in modules + """ + super().__init__(parent) + + self.__ui = parent + self.__develPluginFile = develPlugin + self.__develPluginName = None + if disabledPlugins is not None: + self.__disabledPlugins = disabledPlugins[:] + else: + self.__disabledPlugins = [] + + self.__inactivePluginsKey = "PluginManager/InactivePlugins" + + self.pluginDirs = { + "eric6": os.path.join(getConfig('ericDir'), "Plugins"), + "global": os.path.join(Utilities.getPythonLibraryDirectory(), + "eric6plugins"), + "user": os.path.join(Utilities.getConfigDir(), "eric6plugins"), + } + self.__priorityOrder = ["eric6", "global", "user"] + + self.__defaultDownloadDir = os.path.join( + Utilities.getConfigDir(), "Downloads") + + self.__activePlugins = {} + self.__inactivePlugins = {} + self.__onDemandActivePlugins = {} + self.__onDemandInactivePlugins = {} + self.__activeModules = {} + self.__inactiveModules = {} + self.__onDemandActiveModules = {} + self.__onDemandInactiveModules = {} + self.__failedModules = {} + + self.__foundCoreModules = [] + self.__foundGlobalModules = [] + self.__foundUserModules = [] + + self.__modulesCount = 0 + + pdirsExist, msg = self.__pluginDirectoriesExist() + if not pdirsExist: + raise PluginPathError(msg) + + if doLoadPlugins: + if not self.__pluginModulesExist(): + raise PluginModulesError + + self.__insertPluginsPaths() + + self.__loadPlugins() + + self.__checkPluginsDownloadDirectory() + + self.pluginRepositoryFile = os.path.join(Utilities.getConfigDir(), + "PluginRepository") + + # attributes for the network objects + self.__networkManager = QNetworkAccessManager(self) + self.__networkManager.proxyAuthenticationRequired.connect( + proxyAuthenticationRequired) + if SSL_AVAILABLE: + self.__sslErrorHandler = E5SslErrorHandler(self) + self.__networkManager.sslErrors.connect(self.__sslErrors) + self.__replies = [] + + with contextlib.suppress(AttributeError): + self.__ui.onlineStateChanged.connect(self.__onlineStateChanged) + + def finalizeSetup(self): + """ + Public method to finalize the setup of the plugin manager. + """ + for module in ( + list(self.__onDemandInactiveModules.values()) + + list(self.__onDemandActiveModules.values()) + ): + if hasattr(module, "moduleSetup"): + module.moduleSetup() + + def getPluginDir(self, key): + """ + Public method to get the path of a plugin directory. + + @param key key of the plug-in directory (string) + @return path of the requested plugin directory (string) + """ + if key not in ["global", "user"]: + return None + else: + try: + return self.pluginDirs[key] + except KeyError: + return None + + def __pluginDirectoriesExist(self): + """ + Private method to check, if the plugin folders exist. + + If the plugin folders don't exist, they are created (if possible). + + @return tuple of a flag indicating existence of any of the plugin + directories (boolean) and a message (string) + """ + if self.__develPluginFile: + path = Utilities.splitPath(self.__develPluginFile)[0] + fname = os.path.join(path, "__init__.py") + if not os.path.exists(fname): + try: + with open(fname, "w"): + pass + except OSError: + return ( + False, + self.tr("Could not create a package for {0}.") + .format(self.__develPluginFile)) + + fname = os.path.join(self.pluginDirs["user"], "__init__.py") + if not os.path.exists(fname): + if not os.path.exists(self.pluginDirs["user"]): + os.mkdir(self.pluginDirs["user"], 0o755) + try: + with open(fname, "w"): + pass + except OSError: + del self.pluginDirs["user"] + + if not os.path.exists(self.pluginDirs["global"]): + try: + # create the global plugins directory + os.mkdir(self.pluginDirs["global"], 0o755) + fname = os.path.join(self.pluginDirs["global"], "__init__.py") + with open(fname, "w", encoding="utf-8") as f: + f.write('# -*- coding: utf-8 -*-' + "\n") + f.write("\n") + f.write('"""' + "\n") + f.write('Package containing the global plugins.' + "\n") + f.write('"""' + "\n") + except OSError: + del self.pluginDirs["global"] + + if not os.path.exists(self.pluginDirs["eric6"]): + return ( + False, + self.tr( + "The internal plugin directory <b>{0}</b>" + " does not exits.").format(self.pluginDirs["eric6"])) + + return (True, "") + + def __pluginModulesExist(self): + """ + Private method to check, if there are plugins available. + + @return flag indicating the availability of plugins (boolean) + """ + if ( + self.__develPluginFile and + not os.path.exists(self.__develPluginFile) + ): + return False + + self.__foundCoreModules = self.getPluginModules( + self.pluginDirs["eric6"]) + if Preferences.getPluginManager("ActivateExternal"): + if "global" in self.pluginDirs: + self.__foundGlobalModules = self.getPluginModules( + self.pluginDirs["global"]) + if "user" in self.pluginDirs: + self.__foundUserModules = self.getPluginModules( + self.pluginDirs["user"]) + + return len(self.__foundCoreModules + self.__foundGlobalModules + + self.__foundUserModules) > 0 + + def getPluginModules(self, pluginPath): + """ + Public method to get a list of plugin modules. + + @param pluginPath name of the path to search (string) + @return list of plugin module names (list of string) + """ + pluginFiles = [f[:-3] for f in os.listdir(pluginPath) + if self.isValidPluginName(f)] + return pluginFiles[:] + + def isValidPluginName(self, pluginName): + """ + Public methode to check, if a file name is a valid plugin name. + + Plugin modules must start with "Plugin" and have the extension ".py". + + @param pluginName name of the file to be checked (string) + @return flag indicating a valid plugin name (boolean) + """ + return pluginName.startswith("Plugin") and pluginName.endswith(".py") + + def __insertPluginsPaths(self): + """ + Private method to insert the valid plugin paths intos the search path. + """ + for key in self.__priorityOrder: + if key in self.pluginDirs: + if self.pluginDirs[key] not in sys.path: + sys.path.insert(2, self.pluginDirs[key]) + UI.PixmapCache.addSearchPath(self.pluginDirs[key]) + + if self.__develPluginFile: + path = Utilities.splitPath(self.__develPluginFile)[0] + if path not in sys.path: + sys.path.insert(2, path) + UI.PixmapCache.addSearchPath(path) + + def __loadPlugins(self): + """ + Private method to load the plugins found. + """ + develPluginName = "" + if self.__develPluginFile: + develPluginPath, develPluginName = Utilities.splitPath( + self.__develPluginFile) + if self.isValidPluginName(develPluginName): + develPluginName = develPluginName[:-3] + + for pluginName in self.__foundGlobalModules: + # user and core plug-ins have priority + if ( + pluginName not in self.__foundUserModules and + pluginName not in self.__foundCoreModules and + pluginName != develPluginName + ): + self.loadPlugin(pluginName, self.pluginDirs["global"]) + + for pluginName in self.__foundUserModules: + # core plug-ins have priority + if ( + pluginName not in self.__foundCoreModules and + pluginName != develPluginName + ): + self.loadPlugin(pluginName, self.pluginDirs["user"]) + + for pluginName in self.__foundCoreModules: + # plug-in under development has priority + if pluginName != develPluginName: + self.loadPlugin(pluginName, self.pluginDirs["eric6"]) + + if develPluginName: + self.loadPlugin(develPluginName, develPluginPath) + self.__develPluginName = develPluginName + + def loadDocumentationSetPlugins(self): + """ + Public method to load just the documentation sets plugins. + + @exception PluginModulesError raised to indicate the absence of + plug-in modules + """ + if not self.__pluginModulesExist(): + raise PluginModulesError + + self.__insertPluginsPaths() + + for pluginName in self.__foundGlobalModules: + # user and core plug-ins have priority + if ( + pluginName not in self.__foundUserModules and + pluginName not in self.__foundCoreModules and + pluginName.startswith("PluginDocumentationSets") + ): + self.loadPlugin(pluginName, self.pluginDirs["global"]) + + for pluginName in self.__foundUserModules: + # core plug-ins have priority + if ( + pluginName not in self.__foundCoreModules and + pluginName.startswith("PluginDocumentationSets") + ): + self.loadPlugin(pluginName, self.pluginDirs["user"]) + + for pluginName in self.__foundCoreModules: + # plug-in under development has priority + if pluginName.startswith("PluginDocumentationSets"): + self.loadPlugin(pluginName, self.pluginDirs["eric6"]) + + def loadPlugin(self, name, directory, reload_=False, install=False): + """ + Public method to load a plugin module. + + Initially all modules are inactive. Modules that are requested on + demand are sorted out and are added to the on demand list. Some + basic validity checks are performed as well. Modules failing these + checks are added to the failed modules list. + + @param name name of the module to be loaded + @type str + @param directory name of the plugin directory + @type str + @param reload_ flag indicating to reload the module + @type bool + @param install flag indicating a load operation as part of an + installation process + @type bool + @exception PluginLoadError raised to indicate an issue loading + the plug-in + """ + try: + fname = "{0}.py".format(os.path.join(directory, name)) + spec = importlib.util.spec_from_file_location(name, fname) + module = importlib.util.module_from_spec(spec) + sys.modules[module.__name__] = module + spec.loader.exec_module(module) + if not hasattr(module, "autoactivate"): + module.error = self.tr( + "Module is missing the 'autoactivate' attribute.") + self.__failedModules[name] = module + raise PluginLoadError(name) + if getattr(module, "autoactivate", False): + self.__inactiveModules[name] = module + else: + if ( + not hasattr(module, "pluginType") or + not hasattr(module, "pluginTypename") + ): + module.error = self.tr( + "Module is missing the 'pluginType' " + "and/or 'pluginTypename' attributes." + ) + self.__failedModules[name] = module + raise PluginLoadError(name) + else: + self.__onDemandInactiveModules[name] = module + module.eric6PluginModuleName = name + module.eric6PluginModuleFilename = fname + if install and hasattr(module, "installDependencies"): + # ask the module to install its dependencies + module.installDependencies(self.pipInstall) + self.__modulesCount += 1 + if reload_: + importlib.reload(module) + self.initOnDemandPlugin(name) + with contextlib.suppress(KeyError, AttributeError): + pluginObject = self.__onDemandInactivePlugins[name] + pluginObject.initToolbar( + self.__ui, e5App().getObject("ToolbarManager")) + except PluginLoadError: + print("Error loading plug-in module:", name) + except Exception as err: + module = types.ModuleType(name) + module.error = self.tr( + "Module failed to load. Error: {0}").format(str(err)) + self.__failedModules[name] = module + print("Error loading plug-in module:", name) + print(str(err)) + + def unloadPlugin(self, name): + """ + Public method to unload a plugin module. + + @param name name of the module to be unloaded (string) + @return flag indicating success (boolean) + """ + if name in self.__onDemandActiveModules: + # cannot unload an ondemand plugin, that is in use + return False + + if name in self.__activeModules: + self.deactivatePlugin(name) + + if name in self.__inactiveModules: + with contextlib.suppress(KeyError): + pluginObject = self.__inactivePlugins[name] + with contextlib.suppress(AttributeError): + pluginObject.prepareUnload() + del self.__inactivePlugins[name] + del self.__inactiveModules[name] + elif name in self.__onDemandInactiveModules: + with contextlib.suppress(KeyError): + pluginObject = self.__onDemandInactivePlugins[name] + with contextlib.suppress(AttributeError): + pluginObject.prepareUnload() + del self.__onDemandInactivePlugins[name] + del self.__onDemandInactiveModules[name] + elif name in self.__failedModules: + del self.__failedModules[name] + + self.__modulesCount -= 1 + return True + + def removePluginFromSysModules(self, pluginName, package, + internalPackages): + """ + Public method to remove a plugin and all related modules from + sys.modules. + + @param pluginName name of the plugin module (string) + @param package name of the plugin package (string) + @param internalPackages list of intenal packages (list of string) + @return flag indicating the plugin module was found in sys.modules + (boolean) + """ + packages = [package] + internalPackages + found = False + if not package: + package = "__None__" + for moduleName in list(sys.modules.keys())[:]: + if ( + moduleName == pluginName or + moduleName.split(".")[0] in packages + ): + found = True + del sys.modules[moduleName] + return found + + def initOnDemandPlugins(self): + """ + Public method to create plugin objects for all on demand plugins. + + Note: The plugins are not activated. + """ + names = sorted(self.__onDemandInactiveModules.keys()) + for name in names: + self.initOnDemandPlugin(name) + + def initOnDemandPlugin(self, name): + """ + Public method to create a plugin object for the named on demand plugin. + + Note: The plug-in is not activated. + + @param name name of the plug-in (string) + @exception PluginActivationError raised to indicate an issue during the + plug-in activation + """ + try: + try: + module = self.__onDemandInactiveModules[name] + except KeyError: + return + + if not self.__canActivatePlugin(module): + raise PluginActivationError(module.eric6PluginModuleName) + version = getattr(module, "version", "0.0.0") + className = getattr(module, "className", "") + pluginClass = getattr(module, className) + pluginObject = None + if name not in self.__onDemandInactivePlugins: + pluginObject = pluginClass(self.__ui) + pluginObject.eric6PluginModule = module + pluginObject.eric6PluginName = className + pluginObject.eric6PluginVersion = version + self.__onDemandInactivePlugins[name] = pluginObject + except PluginActivationError: + return + + def initPluginToolbars(self, toolbarManager): + """ + Public method to initialize plug-in toolbars. + + @param toolbarManager reference to the toolbar manager object + (E5ToolBarManager) + """ + self.initOnDemandPlugins() + for pluginObject in self.__onDemandInactivePlugins.values(): + with contextlib.suppress(AttributeError): + pluginObject.initToolbar(self.__ui, toolbarManager) + + def activatePlugins(self): + """ + Public method to activate all plugins having the "autoactivate" + attribute set to True. + """ + savedInactiveList = Preferences.Prefs.settings.value( + self.__inactivePluginsKey) + inactiveList = self.__disabledPlugins[:] + if savedInactiveList is not None: + inactiveList += [p for p in savedInactiveList + if p not in self.__disabledPlugins] + if ( + self.__develPluginName is not None and + self.__develPluginName in inactiveList + ): + inactiveList.remove(self.__develPluginName) + names = sorted(self.__inactiveModules.keys()) + for name in names: + if name not in inactiveList: + self.activatePlugin(name) + self.allPlugginsActivated.emit() + + def activatePlugin(self, name, onDemand=False): + """ + Public method to activate a plugin. + + @param name name of the module to be activated + @param onDemand flag indicating activation of an + on demand plugin (boolean) + @return reference to the initialized plugin object + @exception PluginActivationError raised to indicate an issue during the + plug-in activation + """ + try: + try: + module = ( + self.__onDemandInactiveModules[name] + if onDemand else + self.__inactiveModules[name] + ) + except KeyError: + return None + + if not self.__canActivatePlugin(module): + raise PluginActivationError(module.eric6PluginModuleName) + version = getattr(module, "version", "0.0.0") + className = getattr(module, "className", "") + pluginClass = getattr(module, className) + pluginObject = None + if onDemand and name in self.__onDemandInactivePlugins: + pluginObject = self.__onDemandInactivePlugins[name] + elif not onDemand and name in self.__inactivePlugins: + pluginObject = self.__inactivePlugins[name] + else: + pluginObject = pluginClass(self.__ui) + self.pluginAboutToBeActivated.emit(name, pluginObject) + try: + obj, ok = pluginObject.activate() + except TypeError: + module.error = self.tr( + "Incompatible plugin activation method.") + obj = None + ok = True + except Exception as err: + module.error = str(err) + obj = None + ok = False + if not ok: + return None + + self.pluginActivated.emit(name, pluginObject) + pluginObject.eric6PluginModule = module + pluginObject.eric6PluginName = className + pluginObject.eric6PluginVersion = version + + if onDemand: + self.__onDemandInactiveModules.pop(name) + with contextlib.suppress(KeyError): + self.__onDemandInactivePlugins.pop(name) + self.__onDemandActivePlugins[name] = pluginObject + self.__onDemandActiveModules[name] = module + else: + self.__inactiveModules.pop(name) + with contextlib.suppress(KeyError): + self.__inactivePlugins.pop(name) + self.__activePlugins[name] = pluginObject + self.__activeModules[name] = module + return obj + except PluginActivationError: + return None + + def __canActivatePlugin(self, module): + """ + Private method to check, if a plugin can be activated. + + @param module reference to the module to be activated + @return flag indicating, if the module satisfies all requirements + for being activated (boolean) + @exception PluginModuleFormatError raised to indicate an invalid + plug-in module format + @exception PluginClassFormatError raised to indicate an invalid + plug-in class format + """ + try: + if not hasattr(module, "version"): + raise PluginModuleFormatError( + module.eric6PluginModuleName, "version") + if not hasattr(module, "className"): + raise PluginModuleFormatError( + module.eric6PluginModuleName, "className") + className = getattr(module, "className", "") + if not className or not hasattr(module, className): + raise PluginModuleFormatError( + module.eric6PluginModuleName, className) + pluginClass = getattr(module, className) + if not hasattr(pluginClass, "__init__"): + raise PluginClassFormatError( + module.eric6PluginModuleName, + className, "__init__") + if not hasattr(pluginClass, "activate"): + raise PluginClassFormatError( + module.eric6PluginModuleName, + className, "activate") + if not hasattr(pluginClass, "deactivate"): + raise PluginClassFormatError( + module.eric6PluginModuleName, + className, "deactivate") + return True + except PluginModuleFormatError as e: + print(repr(e)) + return False + except PluginClassFormatError as e: + print(repr(e)) + return False + + def deactivatePlugin(self, name, onDemand=False): + """ + Public method to deactivate a plugin. + + @param name name of the module to be deactivated + @param onDemand flag indicating deactivation of an + on demand plugin (boolean) + """ + try: + module = ( + self.__onDemandActiveModules[name] + if onDemand else + self.__activeModules[name] + ) + except KeyError: + return + + if self.__canDeactivatePlugin(module): + pluginObject = None + if onDemand and name in self.__onDemandActivePlugins: + pluginObject = self.__onDemandActivePlugins[name] + elif not onDemand and name in self.__activePlugins: + pluginObject = self.__activePlugins[name] + if pluginObject: + self.pluginAboutToBeDeactivated.emit(name, pluginObject) + pluginObject.deactivate() + self.pluginDeactivated.emit(name, pluginObject) + + if onDemand: + self.__onDemandActiveModules.pop(name) + self.__onDemandActivePlugins.pop(name) + self.__onDemandInactivePlugins[name] = pluginObject + self.__onDemandInactiveModules[name] = module + else: + self.__activeModules.pop(name) + with contextlib.suppress(KeyError): + self.__activePlugins.pop(name) + self.__inactivePlugins[name] = pluginObject + self.__inactiveModules[name] = module + + def __canDeactivatePlugin(self, module): + """ + Private method to check, if a plugin can be deactivated. + + @param module reference to the module to be deactivated + @return flag indicating, if the module satisfies all requirements + for being deactivated (boolean) + """ + return getattr(module, "deactivateable", True) + + def getPluginObject(self, type_, typename, maybeActive=False): + """ + Public method to activate an ondemand plugin given by type and + typename. + + @param type_ type of the plugin to be activated (string) + @param typename name of the plugin within the type category (string) + @param maybeActive flag indicating, that the plugin may be active + already (boolean) + @return reference to the initialized plugin object + """ + for name, module in list(self.__onDemandInactiveModules.items()): + if ( + getattr(module, "pluginType", "") == type_ and + getattr(module, "pluginTypename", "") == typename + ): + return self.activatePlugin(name, onDemand=True) + + if maybeActive: + for name, module in list(self.__onDemandActiveModules.items()): + if ( + getattr(module, "pluginType", "") == type_ and + getattr(module, "pluginTypename", "") == typename + ): + self.deactivatePlugin(name, onDemand=True) + return self.activatePlugin(name, onDemand=True) + + return None + + def getPluginInfos(self): + """ + Public method to get infos about all loaded plug-ins. + + @return list of dictionaries with keys "module_name", "plugin_name", + "version", "auto_activate", "active", "short_desc", "error" + @rtype list of dict ("module_name": str, "plugin_name": str, + "version": str, "auto_activate": bool, "active": bool, + "short_desc": str, "error": bool) + """ + infos = [] + + # 1. active, non-on-demand modules + for name in list(self.__activeModules.keys()): + info = self.__getShortInfo(self.__activeModules[name]) + info.update({ + "module_name": name, + "auto_activate": True, + "active": True, + }) + infos.append(info) + + # 2. inactive, non-on-demand modules + for name in list(self.__inactiveModules.keys()): + info = self.__getShortInfo(self.__inactiveModules[name]) + info.update({ + "module_name": name, + "auto_activate": True, + "active": False, + }) + infos.append(info) + + # 3. active, on-demand modules + for name in list(self.__onDemandActiveModules.keys()): + info = self.__getShortInfo(self.__onDemandActiveModules[name]) + info.update({ + "module_name": name, + "auto_activate": False, + "active": True, + }) + infos.append(info) + + # 4. inactive, non-on-demand modules + for name in list(self.__onDemandInactiveModules.keys()): + info = self.__getShortInfo(self.__onDemandInactiveModules[name]) + info.update({ + "module_name": name, + "auto_activate": False, + "active": False, + }) + infos.append(info) + + # 5. failed modules + for name in list(self.__failedModules.keys()): + info = self.__getShortInfo(self.__failedModules[name]) + info.update({ + "module_name": name, + "auto_activate": False, + "active": False, + }) + infos.append(info) + + return infos + + def __getShortInfo(self, module): + """ + Private method to extract the short info from a module. + + @param module module to extract short info from + @return dictionay containing plug-in data + @rtype dict ("plugin_name": str, "version": str, "short_desc": str, + "error": bool) + """ + return { + "plugin_name": getattr(module, "name", ""), + "version": getattr(module, "version", ""), + "short_desc": getattr(module, "shortDescription", ""), + "error": bool(getattr(module, "error", "")), + } + + def getPluginDetails(self, name): + """ + Public method to get detailed information about a plugin. + + @param name name of the module to get detailed infos about (string) + @return details of the plugin as a dictionary + """ + details = {} + + autoactivate = True + active = True + + if name in self.__activeModules: + module = self.__activeModules[name] + elif name in self.__inactiveModules: + module = self.__inactiveModules[name] + active = False + elif name in self.__onDemandActiveModules: + module = self.__onDemandActiveModules[name] + autoactivate = False + elif name in self.__onDemandInactiveModules: + module = self.__onDemandInactiveModules[name] + autoactivate = False + active = False + elif name in self.__failedModules: + module = self.__failedModules[name] + autoactivate = False + active = False + elif "_" in name: + # try stripping of a postfix + return self.getPluginDetails(name.rsplit("_", 1)[0]) + else: + # should not happen + return None + + details["moduleName"] = name + details["moduleFileName"] = getattr( + module, "eric6PluginModuleFilename", "") + details["pluginName"] = getattr(module, "name", "") + details["version"] = getattr(module, "version", "") + details["author"] = getattr(module, "author", "") + details["description"] = getattr(module, "longDescription", "") + details["autoactivate"] = autoactivate + details["active"] = active + details["error"] = getattr(module, "error", "") + + return details + + def doShutdown(self): + """ + Public method called to perform actions upon shutdown of the IDE. + """ + names = [] + for name in list(self.__inactiveModules.keys()): + names.append(name) + Preferences.Prefs.settings.setValue(self.__inactivePluginsKey, names) + + self.shutdown.emit() + + def getPluginDisplayStrings(self, type_): + """ + Public method to get the display strings of all plugins of a specific + type. + + @param type_ type of the plugins (string) + @return dictionary with name as key and display string as value + (dictionary of string) + """ + pluginDict = {} + + for module in ( + list(self.__onDemandActiveModules.values()) + + list(self.__onDemandInactiveModules.values()) + ): + if ( + getattr(module, "pluginType", "") == type_ and + getattr(module, "error", "") == "" + ): + plugin_name = getattr(module, "pluginTypename", "") + if plugin_name: + if hasattr(module, "displayString"): + try: + disp = module.displayString() + except TypeError: + disp = getattr(module, "displayString", "") + if disp != "": + pluginDict[plugin_name] = disp + else: + pluginDict[plugin_name] = plugin_name + + return pluginDict + + def getPluginPreviewPixmap(self, type_, name): + """ + Public method to get a preview pixmap of a plugin of a specific type. + + @param type_ type of the plugin (string) + @param name name of the plugin type (string) + @return preview pixmap (QPixmap) + """ + for module in ( + list(self.__onDemandActiveModules.values()) + + list(self.__onDemandInactiveModules.values()) + ): + if ( + getattr(module, "pluginType", "") == type_ and + getattr(module, "pluginTypename", "") == name + ): + if hasattr(module, "previewPix"): + return module.previewPix() + else: + return QPixmap() + + return QPixmap() + + def getPluginApiFiles(self, language): + """ + Public method to get the list of API files installed by a plugin. + + @param language language of the requested API files (string) + @return list of API filenames (list of string) + """ + apis = [] + + for module in ( + list(self.__activeModules.values()) + + list(self.__onDemandActiveModules.values()) + ): + if hasattr(module, "apiFiles"): + apis.extend(module.apiFiles(language)) + + return apis + + def getPluginQtHelpFiles(self): + """ + Public method to get the list of QtHelp documentation files provided + by a plug-in. + + @return dictionary with documentation type as key and list of files + as value + @rtype dict (key: str, value: list of str) + """ + helpFiles = {} + for module in ( + list(self.__activeModules.values()) + + list(self.__onDemandActiveModules.values()) + ): + if hasattr(module, "helpFiles"): + helpFiles.update(module.helpFiles()) + + return helpFiles + + def getPluginExeDisplayData(self): + """ + Public method to get data to display information about a plugins + external tool. + + @return list of dictionaries containing the data. Each dictionary must + either contain data for the determination or the data to be + displayed.<br /> + A dictionary of the first form must have the following entries: + <ul> + <li>programEntry - indicator for this dictionary form + (boolean), always True</li> + <li>header - string to be diplayed as a header (string)</li> + <li>exe - the executable (string)</li> + <li>versionCommand - commandline parameter for the exe + (string)</li> + <li>versionStartsWith - indicator for the output line + containing the version (string)</li> + <li>versionPosition - number of element containing the + version (integer)</li> + <li>version - version to be used as default (string)</li> + <li>versionCleanup - tuple of two integers giving string + positions start and stop for the version string + (tuple of integers)</li> + </ul> + A dictionary of the second form must have the following entries: + <ul> + <li>programEntry - indicator for this dictionary form + (boolean), always False</li> + <li>header - string to be diplayed as a header (string)</li> + <li>text - entry text to be shown (string)</li> + <li>version - version text to be shown (string)</li> + </ul> + """ + infos = [] + + for module in ( + list(self.__activeModules.values()) + + list(self.__inactiveModules.values()) + ): + if hasattr(module, "exeDisplayDataList"): + infos.extend(module.exeDisplayDataList()) + elif hasattr(module, "exeDisplayData"): + infos.append(module.exeDisplayData()) + for module in ( + list(self.__onDemandActiveModules.values()) + + list(self.__onDemandInactiveModules.values()) + ): + if hasattr(module, "exeDisplayDataList"): + infos.extend(module.exeDisplayDataList()) + elif hasattr(module, "exeDisplayData"): + infos.append(module.exeDisplayData()) + + return infos + + def getPluginConfigData(self): + """ + Public method to get the config data of all active, non on-demand + plugins used by the configuration dialog. + + Plugins supporting this functionality must provide the plugin module + function 'getConfigData' returning a dictionary with unique keys + of lists with the following list contents: + <dl> + <dt>display string</dt> + <dd>string shown in the selection area of the configuration page. + This should be a localized string</dd> + <dt>pixmap name</dt> + <dd>filename of the pixmap to be shown next to the display + string</dd> + <dt>page creation function</dt> + <dd>plugin module function to be called to create the configuration + page. The page must be subclasses from + Preferences.ConfigurationPages.ConfigurationPageBase and must + implement a method called 'save' to save the settings. A parent + entry will be created in the selection list, if this value is + None.</dd> + <dt>parent key</dt> + <dd>dictionary key of the parent entry or None, if this defines a + toplevel entry.</dd> + <dt>reference to configuration page</dt> + <dd>This will be used by the configuration dialog and must always + be None</dd> + </dl> + + @return plug-in configuration data + """ + configData = {} + for module in ( + list(self.__activeModules.values()) + + list(self.__onDemandActiveModules.values()) + + list(self.__onDemandInactiveModules.values()) + ): + if hasattr(module, 'getConfigData'): + configData.update(module.getConfigData()) + return configData + + def isPluginLoaded(self, pluginName): + """ + Public method to check, if a certain plugin is loaded. + + @param pluginName name of the plugin to check for (string) + @return flag indicating, if the plugin is loaded (boolean) + """ + return ( + pluginName in self.__activeModules or + pluginName in self.__inactiveModules or + pluginName in self.__onDemandActiveModules or + pluginName in self.__onDemandInactiveModules + ) + + def isPluginActive(self, pluginName): + """ + Public method to check, if a certain plugin is active. + + @param pluginName name of the plugin to check for (string) + @return flag indicating, if the plugin is active (boolean) + """ + return ( + pluginName in self.__activeModules or + pluginName in self.__onDemandActiveModules + ) + + ########################################################################### + ## Specialized plug-in module handling methods below + ########################################################################### + + ########################################################################### + ## VCS related methods below + ########################################################################### + + def getVcsSystemIndicators(self): + """ + Public method to get the Vcs System indicators. + + Plugins supporting this functionality must support the module function + getVcsSystemIndicator returning a dictionary with indicator as key and + a tuple with the vcs name (string) and vcs display string (string). + + @return dictionary with indicator as key and a list of tuples as + values. Each tuple contains the vcs name (string) and vcs display + string (string). + """ + vcsDict = {} + + for module in ( + list(self.__onDemandActiveModules.values()) + + list(self.__onDemandInactiveModules.values()) + ): + if ( + getattr(module, "pluginType", "") == "version_control" and + hasattr(module, "getVcsSystemIndicator") + ): + res = module.getVcsSystemIndicator() + for indicator, vcsData in list(res.items()): + if indicator in vcsDict: + vcsDict[indicator].append(vcsData) + else: + vcsDict[indicator] = [vcsData] + + return vcsDict + + def deactivateVcsPlugins(self): + """ + Public method to deactivated all activated VCS plugins. + """ + for name, module in list(self.__onDemandActiveModules.items()): + if getattr(module, "pluginType", "") == "version_control": + self.deactivatePlugin(name, True) + + ######################################################################## + ## Methods for the creation of the plug-ins download directory + ######################################################################## + + def __checkPluginsDownloadDirectory(self): + """ + Private slot to check for the existence of the plugins download + directory. + """ + downloadDir = Preferences.getPluginManager("DownloadPath") + if not downloadDir: + downloadDir = self.__defaultDownloadDir + + if not os.path.exists(downloadDir): + try: + os.mkdir(downloadDir, 0o755) + except OSError: + # try again with (possibly) new default + downloadDir = self.__defaultDownloadDir + if not os.path.exists(downloadDir): + try: + os.mkdir(downloadDir, 0o755) + except OSError as err: + E5MessageBox.critical( + self.__ui, + self.tr("Plugin Manager Error"), + self.tr( + """<p>The plugin download directory""" + """ <b>{0}</b> could not be created. Please""" + """ configure it via the configuration""" + """ dialog.</p><p>Reason: {1}</p>""") + .format(downloadDir, str(err))) + downloadDir = "" + + Preferences.setPluginManager("DownloadPath", downloadDir) + + def preferencesChanged(self): + """ + Public slot to react to changes in configuration. + """ + self.__checkPluginsDownloadDirectory() + + ######################################################################## + ## Methods for automatic plug-in update check below + ######################################################################## + + def __onlineStateChanged(self, online): + """ + Private slot handling changes in online state. + + @param online flag indicating the online state + @type bool + """ + if online: + self.checkPluginUpdatesAvailable() + + def checkPluginUpdatesAvailable(self): + """ + Public method to check the availability of updates of plug-ins. + """ + period = Preferences.getPluginManager("UpdatesCheckInterval") + if period == 0: + return + elif period in [1, 2, 3]: + lastModified = QFileInfo(self.pluginRepositoryFile).lastModified() + if lastModified.isValid() and lastModified.date().isValid(): + lastModifiedDate = lastModified.date() + now = QDate.currentDate() + if ( + (period == 1 and lastModifiedDate.day() == now.day()) or + (period == 2 and lastModifiedDate.daysTo(now) < 7) or + (period == 3 and (lastModifiedDate.daysTo(now) < + lastModifiedDate.daysInMonth())) + ): + # daily, weekly, monthly + return + + self.__updateAvailable = False + + request = QNetworkRequest( + QUrl(Preferences.getUI("PluginRepositoryUrl6"))) + request.setAttribute( + QNetworkRequest.Attribute.CacheLoadControlAttribute, + QNetworkRequest.CacheLoadControl.AlwaysNetwork) + reply = self.__networkManager.get(request) + reply.finished.connect( + lambda: self.__downloadRepositoryFileDone(reply)) + self.__replies.append(reply) + + def __downloadRepositoryFileDone(self, reply): + """ + Private method called after the repository file was downloaded. + + @param reply reference to the reply object of the download + @type QNetworkReply + """ + if reply in self.__replies: + self.__replies.remove(reply) + + if reply.error() != QNetworkReply.NetworkError.NoError: + E5MessageBox.warning( + None, + self.tr("Error downloading file"), + self.tr( + """<p>Could not download the requested file""" + """ from {0}.</p><p>Error: {1}</p>""" + ).format(Preferences.getUI("PluginRepositoryUrl6"), + reply.errorString()) + ) + reply.deleteLater() + return + + ioDevice = QFile(self.pluginRepositoryFile + ".tmp") + ioDevice.open(QIODevice.OpenModeFlag.WriteOnly) + ioDevice.write(reply.readAll()) + ioDevice.close() + if QFile.exists(self.pluginRepositoryFile): + QFile.remove(self.pluginRepositoryFile) + ioDevice.rename(self.pluginRepositoryFile) + reply.deleteLater() + + if os.path.exists(self.pluginRepositoryFile): + f = QFile(self.pluginRepositoryFile) + if f.open(QIODevice.OpenModeFlag.ReadOnly): + # save current URL + url = Preferences.getUI("PluginRepositoryUrl6") + + # read the repository file + from E5XML.PluginRepositoryReader import PluginRepositoryReader + reader = PluginRepositoryReader(f, self.checkPluginEntry) + reader.readXML() + if url != Preferences.getUI("PluginRepositoryUrl6"): + # redo if it is a redirect + self.checkPluginUpdatesAvailable() + return + + if self.__updateAvailable: + res = E5MessageBox.information( + None, + self.tr("New plugin versions available"), + self.tr("<p>There are new plug-ins or plug-in" + " updates available. Use the plug-in" + " repository dialog to get them.</p>"), + E5MessageBox.StandardButtons( + E5MessageBox.Ignore | + E5MessageBox.Open), + E5MessageBox.Open) + if res == E5MessageBox.Open: + self.__ui.showPluginsAvailable() + + def checkPluginEntry(self, name, short, description, url, author, version, + filename, status): + """ + Public method to check a plug-in's data for an update. + + @param name data for the name field (string) + @param short data for the short field (string) + @param description data for the description field (list of strings) + @param url data for the url field (string) + @param author data for the author field (string) + @param version data for the version field (string) + @param filename data for the filename field (string) + @param status status of the plugin (string [stable, unstable, unknown]) + """ + # ignore hidden plug-ins + pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0] + if pluginName in Preferences.getPluginManager("HiddenPlugins"): + return + + archive = os.path.join(Preferences.getPluginManager("DownloadPath"), + filename) + + # Check against installed/loaded plug-ins + pluginDetails = self.getPluginDetails(pluginName) + if pluginDetails is None: + if not Preferences.getPluginManager("CheckInstalledOnly"): + self.__updateAvailable = True + return + + versionTuple = Globals.versionToTuple(version)[:3] + pluginVersionTuple = Globals.versionToTuple( + pluginDetails["version"])[:3] + + if pluginVersionTuple < versionTuple: + self.__updateAvailable = True + return + + if not Preferences.getPluginManager("CheckInstalledOnly"): + # Check against downloaded plugin archives + # 1. Check, if the archive file exists + if not os.path.exists(archive): + if pluginDetails["moduleName"] != pluginName: + self.__updateAvailable = True + return + + # 2. Check, if the archive is a valid zip file + if not zipfile.is_zipfile(archive): + self.__updateAvailable = True + return + + # 3. Check the version of the archive file + zipFile = zipfile.ZipFile(archive, "r") + try: + aversion = zipFile.read("VERSION").decode("utf-8") + except KeyError: + aversion = "0.0.0" + zipFile.close() + + aversionTuple = Globals.versionToTuple(aversion)[:3] + if aversionTuple != versionTuple: + self.__updateAvailable = True + + def __sslErrors(self, reply, errors): + """ + Private slot to handle SSL errors. + + @param reply reference to the reply object (QNetworkReply) + @param errors list of SSL errors (list of QSslError) + """ + ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] + if ignored == E5SslErrorState.NOT_IGNORED: + self.__downloadCancelled = True + + ######################################################################## + ## Methods to clear private data of plug-ins below + ######################################################################## + + def clearPluginsPrivateData(self, type_): + """ + Public method to clear the private data of plug-ins of a specified + type. + + Plugins supporting this functionality must support the module function + clearPrivateData() and have the module level attribute pluginType. + + @param type_ type of the plugin to clear private data for (string) + """ + for module in ( + list(self.__onDemandActiveModules.values()) + + list(self.__onDemandInactiveModules.values()) + + list(self.__activeModules.values()) + + list(self.__inactiveModules.values()) + ): + if ( + getattr(module, "pluginType", "") == type_ and + hasattr(module, "clearPrivateData") + ): + module.clearPrivateData() + + ######################################################################## + ## Methods to install a plug-in module dependency via pip + ######################################################################## + + def pipInstall(self, packages): + """ + Public method to install the given package via pip. + + @param packages list of packages to install + @type list of str + """ + try: + pip = e5App().getObject("Pip") + except KeyError: + # Installation is performed via the plug-in installation script. + from PipInterface.Pip import Pip + pip = Pip(self) + pip.installPackages(packages, interpreter=sys.executable) + +# +# eflag: noqa = M801