def test_topological_sort(self): dep_list = [('myplugin4', {'myplugin2', 'myplugin1'}), ('myplugin2', {'myplugin1'}), ('myplugin3', {}), ('myplugin1', {}), ('myplugin5', {'myplugin1', 'myplugin3', 'myplugin4'})] sorted_list = [x for x in functions.topological_sort(dep_list)] self.assertListEqual(sorted_list, ['myplugin3', 'myplugin1', 'myplugin2', 'myplugin4', 'myplugin5'])
def test_topological_sort(self): dep_list = [('myplugin4', {'myplugin2', 'myplugin1'}), ('myplugin2', {'myplugin1'}), ('myplugin3', {}), ('myplugin1', {}), ('myplugin5', {'myplugin1', 'myplugin3', 'myplugin4'})] sorted_list = [x for x in functions.topological_sort(dep_list)] self.assertListEqual( sorted_list, ['myplugin3', 'myplugin1', 'myplugin2', 'myplugin4', 'myplugin5'])
def test_topological_sort(self): dep_list = [ ("myplugin4", {"myplugin2", "myplugin1"}), ("myplugin2", {"myplugin1"}), ("myplugin3", {}), ("myplugin1", {}), ("myplugin5", {"myplugin1", "myplugin3", "myplugin4"}), ] sorted_list = [x for x in functions.topological_sort(dep_list)] self.assertListEqual(sorted_list, ["myplugin3", "myplugin1", "myplugin2", "myplugin4", "myplugin5"])
def do_load(self, client, name=None): """ Load a new plugin :param client: The client who launched the command :param name: The name of the plugin to load """ def _get_plugin_config(p_name, p_clazz): """ Helper that load and return a configuration file for the given Plugin :param p_name: The plugin name :param p_clazz: The class implementing the plugin """ def _search_config_file(match): """ Helper that returns a list of available configuration file paths for the given plugin. :param match: The plugin name """ # first look in the built-in plugins directory search = '%s%s*%s*' % (b3.getAbsolutePath('@b3\\conf'), os.path.sep, match) self.debug('searching for configuration file(s) matching: %s' % search) collection = glob.glob(search) if len(collection) > 0: return collection # if none is found, then search in the extplugins directory extplugins_dir = self.console.config.get_external_plugins_dir() search = '%s%s*%s*' % (os.path.join( b3.getAbsolutePath(extplugins_dir), match, 'conf'), os.path.sep, match) self.debug('searching for configuration file(s) matching: %s' % search) collection = glob.glob(search) return collection search_path = _search_config_file(p_name) if len(search_path) == 0: if p_clazz.requiresConfigFile: raise b3.config.ConfigFileNotFound( 'could not find any configuration file') self.debug( 'no configuration file found for plugin %s: is not required either...' % p_name) return None if len(search_path) > 1: self.warning( 'multiple configuration files found for plugin %s: %s', p_name, ', '.join(search_path)) self.debug('using %s as configuration file for plugin %s', search_path[0], p_name) self.bot('loading configuration file %s for plugin %s', search_path[0], p_name) return b3.config.load(search_path[0]) name = name.lower() if name in self._protected: client.message('^7Plugin ^1%s ^7is protected' % name) return plugin = self.console.getPlugin(name) if plugin: client.message('^7Plugin ^2%s ^7is already loaded' % name) return try: mod = self.console.pluginImport(name) clz = getattr(mod, '%sPlugin' % name.title()) cfg = _get_plugin_config(p_name=name, p_clazz=clz) plugin_data = PluginData(name=name, module=mod, clazz=clz, conf=cfg) except ImportError: client.message('^7Missing ^1%s ^7plugin python module' % name) client.message( '^7Please put the plugin module in ^3@b3/extplugins/') except AttributeError: client.message( '^7Plugin ^1%s ^7has an invalid structure: can\'t load' % name) client.message( '^7Please inspect your b3 log file for more information') self.error('could not create plugin %s instance: %s' % (name, extract_tb(sys.exc_info()[2]))) except b3.config.ConfigFileNotFound: client.message('^7Missing ^1%s ^7plugin configuration file' % name) client.message( '^7Please put the plugin configuration file in ^3@b3/conf ^7or ^3@b3/extplugins/%s/conf' % name) except b3.config.ConfigFileNotValid: client.message( '^7invalid configuration file found for plugin ^1%s' % name) client.message( '^7Please inspect your b3 log file for more information') self.error( 'plugin %s has an invalid configuration file and can\'t be loaded: %s' % (name, extract_tb(sys.exc_info()[2]))) else: plugin_required = [] def _get_plugin_data(p_data): """ Return a list of PluginData of plugins needed by the current one :param p_data: A PluginData containing plugin information :return: list[PluginData] a list of PluginData of plugins needed by the current one """ # check for correct B3 version if p_data.clazz.requiresVersion and B3version( p_data.clazz.requiresVersion) > B3version( currentVersion): raise MissingRequirement( 'plugin %s requires B3 version %s (you have version %s) : please update your ' 'B3 if you want to run this plugin' % (p_data.name, p_data.clazz.requiresVersion, currentVersion)) # check if the current game support this plugin (this may actually exclude more than one plugin # in case a plugin is built on top of an incompatible one, due to plugin dependencies) if p_data.clazz.requiresParsers and self.console.gameName not in p_data.clazz.requiresParsers: raise MissingRequirement( 'plugin %s is not compatible with %s parser : supported games are : %s' % (p_data.name, self.console.gameName, ', '.join( p_data.clazz.requiresParsers))) # check if the plugin needs a particular storage protocol to work if p_data.clazz.requiresStorage and self.console.storage.protocol not in p_data.clazz.requiresStorage: raise MissingRequirement( 'plugin %s is not compatible with the storage protocol being used (%s) : ' 'supported protocols are : %s' % (p_data.name, self.console.storage.protocol, ', '.join( p_data.clazz.requiresStorage))) if p_data.clazz.requiresPlugins: collection = [] for r in p_data.requiresPlugins: if r not in self.console._plugins and r not in plugin_required: try: # missing requirement, try to load it self.warning( 'plugin %s has unmet dependency : %s : trying to load plugin %s...' % (p_data.name, r, r)) collection += _get_plugin_data( PluginData(name=r)) self.debug( 'plugin %s dependency satisfied: %s' % r) except Exception as err: raise MissingRequirement( 'missing required plugin: %s' % r, err) return collection # plugin has not been loaded manually nor a previous automatic load attempt has been done if p_data.name not in self.console._plugins and p_data.name not in plugin_required: # we are at the bottom step where we load a new requirement by importing the # plugin module, class and configuration file. If the following generate an exception, recursion # will catch it here above and raise it back so we can exclude the first plugin in the list from load self.debug( 'looking for plugin %s module and configuration file...' % p_data.name) p_data.module = self.console.pluginImport(p_data.name) p_data.clazz = getattr(p_data.module, '%sPlugin' % p_data.name.title()) p_data.conf = _get_plugin_config(p_data.name, p_data.clazz) plugin_required.append(p_data.name) # load just once return [p_data] rollback = [] try: plugin_list = _get_plugin_data( plugin_data ) # generate a list of PluginData (also requirements) plugin_dict = {x.name: x for x in plugin_list} # dict(str, PluginData) sorted_list = [ y for y in topological_sort([(x.name, set(x.clazz.requiresPlugins)) for x in plugin_list]) ] if len(sorted_list) > 1: client.message( '^7Plugin ^3%s ^7relies on other plugins to work: they will be automatically loaded' % name) for s in sorted_list: p = plugin_dict[s] self.bot('loading plugin %s [%s]', p.name, '--' if p.conf is None else p.conf.fileName) plugin_instance = p.clazz(self.console, p.conf) plugin_instance.onLoadConfig() plugin_instance.onStartup() self.console._plugins[p.name] = plugin_instance v = getattr(p.module, '__version__', 'unknown') a = getattr(p.module, '__author__', 'unknown') self.bot('plugin %s (%s - %s) loaded', p.name, v, a) client.message( '^7Plugin ^2%s ^7(^3%s ^7- ^3%s^7) loaded' % p.name, v, a) # queue an event so other plugins may react on this change (for example if 2 plugins provide the # same functionalities, one of them can be disabled not to do duplicated work) self.console.queueEvent( self.console.getEvent('EVT_PLUGIN_LOADED', data=p.name)) # track down all the plugins that we are enabling so we can rollback # changes if a plugin in the dependency tree fails to load/start rollback.append(p.name) except b3.exceptions.MissingRequirement: # here we do not have to rollback client.message( '^7Plugin ^1%s can\'t be loaded due to unmet dependencies' % name) client.message( '^7Please inspect your b3 log file for more information') except Exception as e: # here we rollback all the plugins loaded which are not needed anymore client.message('^7Could not load plugin ^1%s^7: %s' % (name, e)) client.message( '^7Please inspect your b3 log file for more information') self.error('plugin %s could not be loaded: %s' % (name, extract_tb(sys.exc_info()[2]))) if rollback: for name in rollback: self.do_unload(client=client, name=name)
def do_load(self, client, name=None): """ Load a new plugin :param client: The client who launched the command :param name: The name of the plugin to load """ def _get_plugin_config(p_name, p_clazz): """ Helper that load and return a configuration file for the given Plugin :param p_name: The plugin name :param p_clazz: The class implementing the plugin """ def _search_config_file(match): """ Helper that returns a list of available configuration file paths for the given plugin. :param match: The plugin name """ # first look in the built-in plugins directory search = '%s%s*%s*' % (b3.getAbsolutePath('@b3\\conf'), os.path.sep, match) self.debug('searching for configuration file(s) matching: %s' % search) collection = glob.glob(search) if len(collection) > 0: return collection # if none is found, then search in the extplugins directory extplugins_dir = self.console.config.get_external_plugins_dir() search = '%s%s*%s*' % (os.path.join(b3.getAbsolutePath(extplugins_dir), match, 'conf'), os.path.sep, match) self.debug('searching for configuration file(s) matching: %s' % search) collection = glob.glob(search) return collection search_path = _search_config_file(p_name) if len(search_path) == 0: if p_clazz.requiresConfigFile: raise b3.config.ConfigFileNotFound('could not find any configuration file') self.debug('no configuration file found for plugin %s: is not required either...' % p_name) return None if len(search_path) > 1: self.warning('multiple configuration files found for plugin %s: %s', p_name, ', '.join(search_path)) self.debug('using %s as configuration file for plugin %s', search_path[0], p_name) self.bot('loading configuration file %s for plugin %s', search_path[0], p_name) return b3.config.load(search_path[0]) name = name.lower() if name in self._protected: client.message('^7Plugin ^1%s ^7is protected' % name) return plugin = self.console.getPlugin(name) if plugin: client.message('^7Plugin ^2%s ^7is already loaded' % name) return try: mod = self.console.pluginImport(name) clz = getattr(mod, '%sPlugin' % name.title()) cfg = _get_plugin_config(p_name=name, p_clazz=clz) plugin_data = PluginData(name=name, module=mod, clazz=clz, conf=cfg) except ImportError: client.message('^7Missing ^1%s ^7plugin python module' % name) client.message('^7Please put the plugin module in ^3@b3/extplugins/') except AttributeError: client.message('^7Plugin ^1%s ^7has an invalid structure: can\'t load' % name) client.message('^7Please inspect your b3 log file for more information') self.error('could not create plugin %s instance: %s' % (name, extract_tb(sys.exc_info()[2]))) except b3.config.ConfigFileNotFound: client.message('^7Missing ^1%s ^7plugin configuration file' % name) client.message('^7Please put the plugin configuration file in ^3@b3/conf ^7or ^3@b3/extplugins/%s/conf' % name) except b3.config.ConfigFileNotValid: client.message('^7invalid configuration file found for plugin ^1%s' % name) client.message('^7Please inspect your b3 log file for more information') self.error('plugin %s has an invalid configuration file and can\'t be loaded: %s' % (name, extract_tb(sys.exc_info()[2]))) else: plugin_required = [] def _get_plugin_data(p_data): """ Return a list of PluginData of plugins needed by the current one :param p_data: A PluginData containing plugin information :return: list[PluginData] a list of PluginData of plugins needed by the current one """ # check for correct B3 version if p_data.clazz.requiresVersion and B3version(p_data.clazz.requiresVersion) > B3version(currentVersion): raise MissingRequirement('plugin %s requires B3 version %s (you have version %s) : please update your ' 'B3 if you want to run this plugin' % (p_data.name, p_data.clazz.requiresVersion, currentVersion)) # check if the current game support this plugin (this may actually exclude more than one plugin # in case a plugin is built on top of an incompatible one, due to plugin dependencies) if p_data.clazz.requiresParsers and self.console.gameName not in p_data.clazz.requiresParsers: raise MissingRequirement('plugin %s is not compatible with %s parser : supported games are : %s' % ( p_data.name, self.console.gameName, ', '.join(p_data.clazz.requiresParsers))) # check if the plugin needs a particular storage protocol to work if p_data.clazz.requiresStorage and self.console.storage.protocol not in p_data.clazz.requiresStorage: raise MissingRequirement('plugin %s is not compatible with the storage protocol being used (%s) : ' 'supported protocols are : %s' % (p_data.name, self.console.storage.protocol, ', '.join(p_data.clazz.requiresStorage))) if p_data.clazz.requiresPlugins: collection = [] for r in p_data.requiresPlugins: if r not in self.console._plugins and r not in plugin_required: try: # missing requirement, try to load it self.warning('plugin %s has unmet dependency : %s : trying to load plugin %s...' % (p_data.name, r, r)) collection += _get_plugin_data(PluginData(name=r)) self.debug('plugin %s dependency satisfied: %s' % r) except Exception, err: raise MissingRequirement('missing required plugin: %s' % r, err) return collection # plugin has not been loaded manually nor a previous automatic load attempt has been done if p_data.name not in self.console._plugins and p_data.name not in plugin_required: # we are at the bottom step where we load a new requirement by importing the # plugin module, class and configuration file. If the following generate an exception, recursion # will catch it here above and raise it back so we can exclude the first plugin in the list from load self.debug('looking for plugin %s module and configuration file...' % p_data.name) p_data.module = self.console.pluginImport(p_data.name) p_data.clazz = getattr(p_data.module, '%sPlugin' % p_data.name.title()) p_data.conf = _get_plugin_config(p_data.name, p_data.clazz) plugin_required.append(p_data.name) # load just once return [p_data] rollback = [] try: plugin_list = _get_plugin_data(plugin_data) # generate a list of PluginData (also requirements) plugin_dict = {x.name: x for x in plugin_list} # dict(str, PluginData) sorted_list = [y for y in topological_sort([(x.name, set(x.clazz.requiresPlugins)) for x in plugin_list])] if len(sorted_list) > 1: client.message('^7Plugin ^3%s ^7relies on other plugins to work: they will be automatically loaded' % name) for s in sorted_list: p = plugin_dict[s] self.bot('loading plugin %s [%s]', p.name, '--' if p.conf is None else p.conf.fileName) plugin_instance = p.clazz(self.console, p.conf) plugin_instance.onLoadConfig() plugin_instance.onStartup() self.console._plugins[p.name] = plugin_instance v = getattr(p.module, '__version__', 'unknown') a = getattr(p.module, '__author__', 'unknown') self.bot('plugin %s (%s - %s) loaded', p.name, v, a) client.message('^7Plugin ^2%s ^7(^3%s ^7- ^3%s^7) loaded' % p.name, v, a) # queue an event so other plugins may react on this change (for example if 2 plugins provide the # same functionalities, one of them can be disabled not to do duplicated work) self.console.queueEvent(self.console.getEvent('EVT_PLUGIN_LOADED', data=p.name)) # track down all the plugins that we are enabling so we can rollback # changes if a plugin in the dependency tree fails to load/start rollback.append(p.name) except b3.exceptions.MissingRequirement: # here we do not have to rollback client.message('^7Plugin ^1%s can\'t be loaded due to unmet dependencies' % name) client.message('^7Please inspect your b3 log file for more information') except Exception, e: # here we rollback all the plugins loaded which are not needed anymore client.message('^7Could not load plugin ^1%s^7: %s' % (name, e)) client.message('^7Please inspect your b3 log file for more information') self.error('plugin %s could not be loaded: %s' % (name, extract_tb(sys.exc_info()[2]))) if rollback: for name in rollback: self.do_unload(client=client, name=name)