def test_unimportable_package(self): self._create_pkg("_foobar").close() s = ModuleScanner([self.d]) self.failIf(s.modules) removed, added = s.rescan() self.failIf(added) self.failIf(removed)
def test_scanner_error(self): h = self._create_mod("q4.py") h.write(b"1syntaxerror\n") h.close() s = ModuleScanner([self.d]) removed, added = s.rescan() self.failIf(added) self.failIf(removed) self.failUnlessEqual(len(s.failures), 1) self.failUnless("q4" in s.failures)
def test_scanner_add(self): self._create_mod("q1.py").close() self._create_mod("q2.py").close() s = ModuleScanner([self.d]) self.failIf(s.modules) removed, added = s.rescan() self.failIf(removed) self.failUnlessEqual(set(added), {"q1", "q2"}) self.failUnlessEqual(len(s.modules), 2) self.failUnlessEqual(len(s.failures), 0)
def test_scanner_add_package(self): h = self._create_pkg("somepkg") h2 = self._create_mod("sub.py", "somepkg") h2.write(b"test=123\n") h2.close() h.write(b"from .sub import *\nmain=321\n") h.close() s = ModuleScanner([self.d]) removed, added = s.rescan() self.failUnlessEqual(added, ["somepkg"]) self.failUnlessEqual(s.modules["somepkg"].module.main, 321) self.failUnlessEqual(s.modules["somepkg"].module.test, 123)
def test_scanner_remove(self): h = self._create_mod("q3.py") h.close() s = ModuleScanner([self.d]) s.rescan() os.remove(h.name) try: os.remove(h.name + "c") except OSError: pass removed, added = s.rescan() self.failIf(added) self.failUnlessEqual(removed, ["q3"]) self.failUnlessEqual(len(s.modules), 0) self.failUnlessEqual(len(s.failures), 0)
def __init__(self, folders=None): """folders is a list of paths that will be scanned for plugins. Plugins in later paths will be preferred if they share a name. """ super(PluginManager, self).__init__() if folders is None: folders = [] self.__scanner = ModuleScanner(folders) self.__modules = {} # name: PluginModule self.__handlers = [] # handler list self.__enabled = set() # (possibly) enabled plugin IDs self.__restore()
class PluginManager(object): """ The manager takes care of plugin loading/reloading. Interested plugin handlers can register them self to get called when plugins get enabled or disabled. Plugins get exposed when at least one handler shows interest in them (by returning True in the handle method). Plugins need to define PLUGIN_ID, PLUGIN_NAME attributes to get loaded. Plugins that have a true PLUGIN_INSTANCE attribute get instantiated on enable and the enabled/disabled methods get called. If plugin handlers want a plugin instance, they have to call get_instance to get a singleton. handlers need to implement the following methods: handler.plugin_handle(plugin) Needs to return True if the handler should be called whenever the plugin's enabled status changes. handler.plugin_enable(plugin, instance) Gets called if the plugin gets enabled. 2nd parameter is an instance of the plugin or None handler.plugin_disable(plugin) Gets called if the plugin gets disabled. Should remove all references. """ CONFIG_SECTION = "plugins" CONFIG_OPTION = "active_plugins" instance = None # default instance def __init__(self, folders=None): """folders is a list of paths that will be scanned for plugins. Plugins in later paths will be preferred if they share a name. """ super(PluginManager, self).__init__() if folders is None: folders = [] self.__scanner = ModuleScanner(folders) self.__modules = {} # name: module self.__plugins = {} # module: [plugin, ..] self.__handlers = {} # plugins: [handler, ..] self.__instance = {} # plugin: instance self.__list = [] # handler list self.__enabled = set() # (possibly) enabled plugin IDs self.__restore() def rescan(self): """Scan for plugin changes or to initially load all plugins""" print_d("Rescanning..") removed, added = self.__scanner.rescan() modules = self.__scanner.modules # remember IDs of enabled plugin that get reloaded, so we can enable # them again reload_ids = [] for name in removed: if name not in added: continue mod = self.__modules[name] for plugin in self.__plugins[mod]: if self.enabled(plugin): reload_ids.append(plugin.PLUGIN_ID) for name in removed: # share the namespace with ModuleScanner for now self.__remove_module(name) # restore enabled state self.__enabled.update(reload_ids) for name in added: module = modules[name] self.__add_module(name, module) print_d("Rescanning done.") @property def _modules(self): return self.__scanner.modules.itervalues() @property def plugins(self): """Returns a list of plugin classes or instances""" items = self.__handlers.items() return [self.get_instance(p) or p for (p,h) in items if h] def get_instance(self, plugin): """"Returns a possibly shared instance of the plugin class""" if not getattr(plugin, "PLUGIN_INSTANCE", False): return if plugin not in self.__instance: try: obj = plugin() except: util.print_exc() return self.__instance[plugin] = obj return self.__instance[plugin] def register_handler(self, handler): print_d("Registering handler: %r" % type(handler).__name__) self.__list.append(handler) for plugins in self.__plugins.itervalues(): for plugin in plugins: if not handler.plugin_handle(plugin): continue if self.__handlers.get(plugin): self.__handlers[plugin].append(handler) if self.enabled(plugin): obj = self.get_instance(plugin) handler.plugin_enable(plugin, obj) else: self.__handlers[plugin] = [handler] if self.enabled(plugin): self.enable(plugin, True, force=True) def save(self): print_d("Saving plugins: %d active" % len(self.__enabled)) config.set(self.CONFIG_SECTION, self.CONFIG_OPTION, "\n".join(self.__enabled)) def enabled(self, plugin): """Returns if the plugin is enabled. Also takes an instance.""" if type(plugin) in self.__handlers: plugin = type(plugin) return plugin.PLUGIN_ID in self.__enabled def enable(self, plugin, status, force=False): """Enable or disable a plugin. Also takes an instance.""" if type(plugin) in self.__handlers: plugin = type(plugin) if not force and self.enabled(plugin) == bool(status): return if not status: print_d("Disable %r" % plugin.PLUGIN_ID) for handler in self.__handlers[plugin]: handler.plugin_disable(plugin) self.__enabled.discard(plugin.PLUGIN_ID) instance = self.__instance.get(plugin) if instance and hasattr(instance, "disabled"): try: instance.disabled() except Exception: util.print_exc() else: print_d("Enable %r" % plugin.PLUGIN_ID) obj = self.get_instance(plugin) if obj and hasattr(obj, "enabled"): try: obj.enabled() except Exception: util.print_exc() for handler in self.__handlers[plugin]: handler.plugin_enable(plugin, obj) self.__enabled.add(plugin.PLUGIN_ID) @property def failures(self): errors = {} for (name, (exc, text)) in self.__scanner.failures.iteritems(): if isinstance(exc, PluginImportException): if not exc.should_show(): continue errors[name] = [exc.desc] else: errors[name] = text return errors def quit(self): """Disable plugins and tell all handlers to clean up""" for name in self.__modules.keys(): self.__remove_module(name) def __remove_module(self, name): module = self.__modules.pop(name) plugins = self.__plugins.pop(module) for plugin in plugins: if self.__handlers.get(plugin): self.enable(plugin, False) self.__handlers.pop(plugin, None) def __add_module(self, name, module): self.__modules[name] = module plugins = list_plugins(module) self.__plugins[module] = plugins for plugin in plugins: handlers = [] for handler in self.__list: if handler.plugin_handle(plugin): handlers.append(handler) if handlers: self.__handlers[plugin] = handlers if self.enabled(plugin): self.enable(plugin, True, force=True) def __restore(self): migrate_old_config() active = config.get(self.CONFIG_SECTION, self.CONFIG_OPTION, "").splitlines() self.__enabled.update(active) print_d("Restoring plugins: %d" % len(self.__enabled)) for plugin, handlers in self.__handlers.iteritems(): if self.enabled(plugin): self.enable(plugin, True, force=True)
class PluginManager(object): """ The manager takes care of plugin loading/reloading. Interested plugin handlers can register them self to get called when plugins get enabled or disabled. Plugins get exposed when at least one handler shows interest in them (by returning True in the handle method). Plugins have to be a class which defines PLUGIN_ID, PLUGIN_NAME. Plugins that have a true PLUGIN_INSTANCE attribute get instantiated on enable and the enabled/disabled methods get called. If plugin handlers want a plugin instance, they have to call Plugin.get_instance() to get a singleton. handlers need to implement the following methods: handler.plugin_handle(plugin) Needs to return True if the handler should be called whenever the plugin's enabled status changes. handler.plugin_enable(plugin) Gets called if the plugin gets enabled. handler.plugin_disable(plugin) Gets called if the plugin gets disabled. Should remove all references. """ CONFIG_SECTION = "plugins" CONFIG_OPTION = "active_plugins" instance = None # default instance def __init__(self, folders=None): """folders is a list of paths that will be scanned for plugins. Plugins in later paths will be preferred if they share a name. """ super(PluginManager, self).__init__() if folders is None: folders = [] self.__scanner = ModuleScanner(folders) self.__modules = {} # name: PluginModule self.__handlers = [] # handler list self.__enabled = set() # (possibly) enabled plugin IDs self.__restore() def rescan(self): """Scan for plugin changes or to initially load all plugins""" print_d("Rescanning..") removed, added = self.__scanner.rescan() # remember IDs of enabled plugin that get reloaded, so we can enable # them again reload_ids = [] for name in removed: if name not in added: continue mod = self.__modules[name] for plugin in mod.plugins: if self.enabled(plugin): reload_ids.append(plugin.id) for name in removed: # share the namespace with ModuleScanner for now self.__remove_module(name) # restore enabled state self.__enabled.update(reload_ids) for name in added: new_module = self.__scanner.modules[name] self.__add_module(name, new_module.module) print_d("Rescanning done.") @property def _modules(self): return itervalues(self.__scanner.modules) @property def _plugins(self): """All registered plugins""" plugins = [] for module in itervalues(self.__modules): for plugin in module.plugins: plugins.append(plugin) return plugins @property def plugins(self): """Returns a list of plugins with active handlers""" return [p for p in self._plugins if p.handlers] def register_handler(self, handler): """ Registers a handler, attaching it to any current plugins it advertises that it can handle `handler` should probably be a `PluginHandler` """ print_d("Registering handler: %r" % type(handler).__name__) self.__handlers.append(handler) for plugin in self._plugins: if not handler.plugin_handle(plugin): continue if plugin.handlers: plugin.handlers.append(handler) if self.enabled(plugin): handler.plugin_enable(plugin) else: plugin.handlers.append(handler) if self.enabled(plugin): self.enable(plugin, True, force=True) def save(self): print_d("Saving plugins: %d active" % len(self.__enabled)) config.set(self.CONFIG_SECTION, self.CONFIG_OPTION, "\n".join(self.__enabled)) def enabled(self, plugin): """Returns if the plugin is enabled.""" if not plugin.handlers: return False return plugin.id in self.__enabled def enable(self, plugin, status, force=False): """Enable or disable a plugin.""" if not force and self.enabled(plugin) == bool(status): return if not status: print_d("Disable %r" % plugin.id) for handler in plugin.handlers: handler.plugin_disable(plugin) self.__enabled.discard(plugin.id) instance = plugin.instance if instance and hasattr(instance, "disabled"): try: instance.disabled() except Exception: util.print_exc() else: print_d("Enable %r" % plugin.id) obj = plugin.get_instance() if obj and hasattr(obj, "enabled"): try: obj.enabled() except Exception: util.print_exc() for handler in plugin.handlers: handler.plugin_enable(plugin) self.__enabled.add(plugin.id) @property def failures(self): """module name: list of error message text lines""" errors = {} for name, error in iteritems(self.__scanner.failures): exception = error.exception if isinstance(exception, PluginImportException): if not exception.should_show(): continue errors[name] = [exception.desc] else: errors[name] = error.traceback return errors def quit(self): """Disable plugins and tell all handlers to clean up""" for name in self.__modules.keys(): self.__remove_module(name) def __remove_module(self, name): plugin_module = self.__modules.pop(name) for plugin in plugin_module.plugins: if plugin.handlers: self.enable(plugin, False) def __add_module(self, name, module): plugin_mod = PluginModule(name, module) self.__modules[name] = plugin_mod for plugin in plugin_mod.plugins: handlers = [] for handler in self.__handlers: if handler.plugin_handle(plugin): handlers.append(handler) if handlers: plugin.handlers = handlers if self.enabled(plugin): self.enable(plugin, True, force=True) def __restore(self): migrate_old_config() active = config.get(self.CONFIG_SECTION, self.CONFIG_OPTION, "").splitlines() self.__enabled.update(active) print_d("Restoring plugins: %d" % len(self.__enabled)) for plugin in self._plugins: if self.enabled(plugin): self.enable(plugin, True, force=True)
class PluginManager(object): """ The manager takes care of plugin loading/reloading. Interested plugin handlers can register them self to get called when plugins get enabled or disabled. Plugins get exposed when at least one handler shows interest in them (by returning True in the handle method). Plugins have to be a class which defines PLUGIN_ID, PLUGIN_NAME. Plugins that have a true PLUGIN_INSTANCE attribute get instantiated on enable and the enabled/disabled methods get called. If plugin handlers want a plugin instance, they have to call Plugin.get_instance() to get a singleton. handlers need to implement the following methods: handler.plugin_handle(plugin) Needs to return True if the handler should be called whenever the plugin's enabled status changes. handler.plugin_enable(plugin) Gets called if the plugin gets enabled. handler.plugin_disable(plugin) Gets called if the plugin gets disabled. Should remove all references. """ CONFIG_SECTION = "plugins" CONFIG_OPTION = "active_plugins" instance = None # default instance def __init__(self, folders=None): """folders is a list of paths that will be scanned for plugins. Plugins in later paths will be preferred if they share a name. """ super(PluginManager, self).__init__() if folders is None: folders = [] self.__scanner = ModuleScanner(folders) self.__modules = {} # name: PluginModule self.__handlers = [] # handler list self.__enabled = set() # (possibly) enabled plugin IDs self.__restore() def rescan(self): """Scan for plugin changes or to initially load all plugins""" print_d("Rescanning..") removed, added = self.__scanner.rescan() # remember IDs of enabled plugin that get reloaded, so we can enable # them again reload_ids = [] for name in removed: if name not in added: continue mod = self.__modules[name] for plugin in mod.plugins: if self.enabled(plugin): reload_ids.append(plugin.id) for name in removed: # share the namespace with ModuleScanner for now self.__remove_module(name) # restore enabled state self.__enabled.update(reload_ids) for name in added: new_module = self.__scanner.modules[name] self.__add_module(name, new_module.module) print_d("Rescanning done.") @property def _modules(self): return self.__scanner.modules.values() @property def _plugins(self): """All registered plugins""" plugins = [] for module in self.__modules.values(): for plugin in module.plugins: plugins.append(plugin) return plugins @property def plugins(self): """Returns a list of plugins with active handlers""" return [p for p in self._plugins if p.handlers] def register_handler(self, handler): """ Registers a handler, attaching it to any current plugins it advertises that it can handle `handler` should probably be a `PluginHandler` """ print_d("Registering handler: %r" % type(handler).__name__) self.__handlers.append(handler) for plugin in self._plugins: if not handler.plugin_handle(plugin): continue if plugin.handlers: plugin.handlers.append(handler) if self.enabled(plugin): handler.plugin_enable(plugin) else: plugin.handlers.append(handler) if self.enabled(plugin): self.enable(plugin, True, force=True) def save(self): print_d("Saving plugins: %d active" % len(self.__enabled)) config.set(self.CONFIG_SECTION, self.CONFIG_OPTION, "\n".join(self.__enabled)) def enabled(self, plugin): """Returns if the plugin is enabled.""" if not plugin.handlers: return False return plugin.id in self.__enabled def enable(self, plugin, status, force=False): """Enable or disable a plugin.""" if not force and self.enabled(plugin) == bool(status): return if not status: print_d("Disable %r" % plugin.id) for handler in plugin.handlers: handler.plugin_disable(plugin) self.__enabled.discard(plugin.id) instance = plugin.instance if instance and hasattr(instance, "disabled"): try: instance.disabled() except Exception: util.print_exc() else: print_d("Enable %r" % plugin.id) obj = plugin.get_instance() if obj and hasattr(obj, "enabled"): try: obj.enabled() except Exception: util.print_exc() for handler in plugin.handlers: handler.plugin_enable(plugin) self.__enabled.add(plugin.id) @property def failures(self): """module name: list of error message text lines""" errors = {} for name, error in self.__scanner.failures.items(): exception = error.exception if isinstance(exception, PluginImportException): if not exception.should_show(): continue errors[name] = [exception.desc] else: errors[name] = error.traceback return errors def quit(self): """Disable plugins and tell all handlers to clean up""" for name in list(self.__modules.keys()): self.__remove_module(name) def __remove_module(self, name): plugin_module = self.__modules.pop(name) for plugin in plugin_module.plugins: if plugin.handlers: self.enable(plugin, False) def __add_module(self, name, module): plugin_mod = PluginModule(name, module) self.__modules[name] = plugin_mod for plugin in plugin_mod.plugins: handlers = [] for handler in self.__handlers: if handler.plugin_handle(plugin): handlers.append(handler) if handlers: plugin.handlers = handlers if self.enabled(plugin): self.enable(plugin, True, force=True) def __restore(self): migrate_old_config() active = config.get(self.CONFIG_SECTION, self.CONFIG_OPTION, "").splitlines() self.__enabled.update(active) print_d("Restoring plugins: %d" % len(self.__enabled)) for plugin in self._plugins: if self.enabled(plugin): self.enable(plugin, True, force=True)
import quodlibet import sys import os # Nasty hack to allow importing of plugins... PLUGIN_DIRS = [] root = os.path.join(quodlibet.__path__[0], "ext") for entry in os.listdir(root): path = os.path.join(root, entry) if not os.path.isdir(path): continue PLUGIN_DIRS.append(path) PLUGIN_DIRS.append(os.path.join(os.path.dirname(__file__), "test_plugins")) ms = ModuleScanner(PLUGIN_DIRS) ms.rescan() plugins = {} modules = {} for name, module in ms.modules.iteritems(): for plugin in list_plugins(module.module): plugins[plugin.PLUGIN_ID] = Plugin(plugin) modules[plugin.PLUGIN_ID] = module.module class PluginTestCase(AbstractTestCase): """Base class for all plugin tests""" plugins = plugins modules = modules
# Nasty hack to allow importing of plugins... PLUGIN_DIRS = [] root = os.path.join(get_module_dir(quodlibet), "ext") for entry in os.listdir(root): if entry.startswith("_"): continue path = os.path.join(root, entry) if not os.path.isdir(path): continue PLUGIN_DIRS.append(path) PLUGIN_DIRS.append(os.path.join(util.get_module_dir(), "test_plugins")) ms = ModuleScanner(PLUGIN_DIRS) ms.rescan() # make sure plugins only raise expected errors for name, err in ms.failures.items(): exc = err.exception assert issubclass(type(exc), (PluginImportException, ImportError)),\ "'%s' plugin shouldn't have raised a %s, but it did (%r)."\ % (name, type(exc).__name__, exc) plugins = {} modules = {} for name, module in iteritems(ms.modules): for plugin in list_plugins(module.module): plugins[plugin.PLUGIN_ID] = Plugin(plugin)