"""The `plugin` module provides optional submodules that add new Hooke commands. All of the science happens in here. """ import ConfigParser as configparser from ..config import Setting from ..util.graph import Node, Graph PLUGIN_MODULES = [ # ('autopeak', True), # ('curvetools', True), ('cut', True), ('debug', True), # ('fit', True), # ('flatfilts-rolf', True), # ('flatfilts', True), # ('generalclamp', True), # ('generaltccd', True), # ('generalvclamp', True), # ('jumpstat', True), # ('macro', True), # ('massanalysis', True), # ('multidistance', True), # ('multifit', True), # ('pcluster', True), # ('procplots', True), # ('review', True), # ('showconvoluted', True), # ('superimpose', True), ('system', True), # ('tutorial', True), ] """List of plugin modules and whether they should be included by default. TODO: autodiscovery """ BUILTIN_MODULES = [ 'playlist', ] """List of builtin modules. TODO: autodiscovery """ PLUGIN_SETTING_SECTION = 'plugins' """Name of the config section which controls plugin selection. """ # Plugins and settings class Plugin (object): """A pluggable collection of Hooke commands. Fulfills the same role for Hooke that a software package does for an operating system. """ def __init__(self, name): self.name = name self.setting_section = '%s plugin' % self.name self.config = {} def dependencies(self): """Return a list of :class:`Plugin`\s we require.""" return [] def default_settings(self): """Return a list of :class:`hooke.config.Setting`\s for any configurable plugin settings. The suggested section setting is:: Setting(section=self.setting_section, help=self.__doc__) """ return [] def commands(self): """Return a list of :class:`hooke.command.Command`\s provided. """ return [] class Builtin (Plugin): """A required collection of Hooke commands. These "core" plugins provide essential administrative commands (playlist handling, etc.). """ pass # Construct plugin dependency graph and load plugin instances. def construct_graph(this_modname, submodnames, class_selector, assert_name_match=True): """Search the submodules `submodnames` of a module `this_modname` for class objects for which `class_selector(class)` returns `True`. These classes are instantiated, and the `instance.name` is compared to the `submodname` (if `assert_name_match` is `True`). The instances are further arranged into a dependency :class:`hooke.util.graph.Graph` according to their `instance.dependencies()` values. The topologically sorted graph is returned. """ instances = {} for submodname in submodnames: count = len([s for s in submodnames if s == submodname]) assert count > 0, 'No %s entries: %s' % (submodname, submodnames) assert count == 1, 'Multiple (%d) %s entries: %s' \ % (count, submodname, submodnames) this_mod = __import__(this_modname, fromlist=[submodname]) submod = getattr(this_mod, submodname) for objname in dir(submod): obj = getattr(submod, objname) if class_selector(obj): instance = obj() if assert_name_match == True and instance.name != submodname: raise Exception( 'Instance name %s does not match module name %s' % (instance.name, submodname)) instances[instance.name] = instance graph = Graph([Node([instances[name] for name in i.dependencies()], data=i) for i in instances.values()]) graph.topological_sort() return graph class IsSubclass (object): """A safe subclass comparator. Examples -------- >>> class A (object): ... pass >>> class B (A): ... pass >>> C = 5 >>> is_subclass = IsSubclass(A) >>> is_subclass(A) True >>> is_subclass = IsSubclass(A, blacklist=[A]) >>> is_subclass(A) False >>> is_subclass(B) True >>> is_subclass(C) False """ def __init__(self, base_class, blacklist=None): self.base_class = base_class if blacklist == None: blacklist = [] self.blacklist = blacklist def __call__(self, other): try: subclass = issubclass(other, self.base_class) except TypeError: return False if other in self.blacklist: return False return subclass PLUGIN_GRAPH = construct_graph( this_modname=__name__, submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES, class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin])) """Topologically sorted list of all possible :class:`Plugin`\s and :class:`Builtin`\s. """ def default_settings(): settings = [Setting(PLUGIN_SETTING_SECTION, help='Enable/disable default plugins.')] for pnode in PLUGIN_GRAPH: if pnode.data.name in BUILTIN_MODULES: continue # builtin inclusion is not optional plugin = pnode.data default_include = [di for mod_name,di in PLUGIN_MODULES if mod_name == plugin.name][0] help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()]) settings.append(Setting( section=PLUGIN_SETTING_SECTION, option=plugin.name, value=str(default_include), help=help, )) for pnode in PLUGIN_GRAPH: plugin = pnode.data settings.extend(plugin.default_settings()) return settings def load_graph(graph, config, include_section): items = [] for node in graph: item = node.data try: include = config.getboolean(include_section, item.name) except configparser.NoOptionError: include = True # non-optional include (e.g. a Builtin) if include == True: try: item.config = dict(config.items(item.setting_section)) except configparser.NoSectionError: pass items.append(item) return items