Moved system commands from hooke_cli to plugin.system.
[hooke.git] / hooke / plugin / __init__.py
1 """The `plugin` module provides optional submodules that add new Hooke
2 commands.
3
4 All of the science happens in here.
5 """
6
7 import ConfigParser as configparser
8
9 from ..config import Setting
10 from ..util.graph import Node, Graph
11
12
13 PLUGIN_MODULES = [
14 #    ('autopeak', True),
15 #    ('curvetools', True),
16     ('cut', True),
17 #    ('fit', True),
18 #    ('flatfilts-rolf', True),
19 #    ('flatfilts', True),
20 #    ('generalclamp', True),
21 #    ('generaltccd', True),
22 #    ('generalvclamp', True),
23 #    ('jumpstat', True),
24 #    ('macro', True),
25 #    ('massanalysis', True),
26 #    ('multidistance', True),
27 #    ('multifit', True),
28 #    ('pcluster', True),
29 #    ('procplots', True),
30 #    ('review', True),
31 #    ('showconvoluted', True),
32 #    ('superimpose', True),
33     ('system', True),
34 #    ('tutorial', True),
35 #    ('viewer', True),
36     ]
37 """List of plugin modules and whether they should be included by
38 default.  TODO: autodiscovery
39 """
40
41 BUILTIN_MODULES = [
42     'playlist',
43     ]
44 """List of builtin modules.  TODO: autodiscovery
45 """
46
47
48 # Plugins and settings
49
50 class Plugin (object):
51     """A pluggable collection of Hooke commands.
52
53     Fulfills the same role for Hooke that a software package does for
54     an operating system.
55     """
56     def __init__(self, name):
57         self.name = name
58         self.setting_section = '%s plugin' % self.name
59         self.config = {}
60
61     def dependencies(self):
62         """Return a list of :class:`Plugin`\s we require."""
63         return []
64
65     def default_settings(self):
66         """Return a list of :class:`hooke.config.Setting`\s for any
67         configurable plugin settings.
68
69         The suggested section setting is::
70
71             Setting(section=self.setting_section, help=self.__doc__)
72         """
73         return []
74
75     def commands(self):
76         """Return a list of :class:`hooke.command.Command`\s provided.
77         """
78         return []
79
80 class Builtin (Plugin):
81     """A required collection of Hooke commands.
82
83     These "core" plugins provide essential administrative commands
84     (playlist handling, etc.).
85     """
86     pass
87
88 # Construct plugin dependency graph and load plugin instances.
89
90 def construct_graph(this_modname, submodnames, class_selector,
91                     assert_name_match=True):
92     """Search the submodules `submodnames` of a module `this_modname`
93     for class objects for which `class_selector(class)` returns
94     `True`.  These classes are instantiated, and the `instance.name`
95     is compared to the `submodname` (if `assert_name_match` is
96     `True`).
97
98     The instances are further arranged into a dependency
99     :class:`hooke.util.graph.Graph` according to their
100     `instance.dependencies()` values.  The topologically sorted graph
101     is returned.
102     """
103     instances = {}
104     for submodname in submodnames:
105         count = len([s for s in submodnames if s == submodname])
106         assert count > 0, 'No %s entries: %s' % (submodname, submodnames)
107         assert count == 1, 'Multiple (%d) %s entries: %s' \
108             % (count, submodname, submodnames)
109         this_mod = __import__(this_modname, fromlist=[submodname])
110         submod = getattr(this_mod, submodname)
111         for objname in dir(submod):
112             obj = getattr(submod, objname)
113             if class_selector(obj):
114                 instance = obj()
115                 if assert_name_match == True and instance.name != submodname:
116                     raise Exception(
117                         'Instance name %s does not match module name %s'
118                         % (instance.name, submodname))
119                 instances[instance.name] = instance
120     graph = Graph([Node([instances[name] for name in i.dependencies()],
121                         data=i)
122                    for i in instances.values()])
123     graph.topological_sort()
124     return graph
125
126 class IsSubclass (object):
127     """A safe subclass comparator.
128     
129     Examples
130     --------
131
132     >>> class A (object):
133     ...     pass
134     >>> class B (A):
135     ...     pass
136     >>> C = 5
137     >>> is_subclass = IsSubclass(A)
138     >>> is_subclass(A)
139     True
140     >>> is_subclass = IsSubclass(A, blacklist=[A])
141     >>> is_subclass(A)
142     False
143     >>> is_subclass(B)
144     True
145     >>> is_subclass(C)
146     False
147     """
148     def __init__(self, base_class, blacklist=None):
149         self.base_class = base_class
150         if blacklist == None:
151             blacklist = []
152         self.blacklist = blacklist
153     def __call__(self, other):
154         try:
155             subclass = issubclass(other, self.base_class)
156         except TypeError:
157             return False
158         if other in self.blacklist:
159             return False
160         return subclass
161
162 PLUGIN_GRAPH = construct_graph(
163     this_modname=__name__,
164     submodnames=[name for name,include in PLUGIN_MODULES] + BUILTIN_MODULES,
165     class_selector=IsSubclass(Plugin, blacklist=[Plugin, Builtin]))
166 """Topologically sorted list of all possible :class:`Plugin`\s and
167 :class:`Builtin`\s.
168 """
169
170 def default_settings():
171     settings = [Setting(
172             'plugins', help='Enable/disable default plugins.')]
173     for pnode in PLUGIN_GRAPH:
174         if pnode.data.name in BUILTIN_MODULES:
175             continue # builtin inclusion is not optional
176         plugin = pnode.data
177         default_include = [di for mod_name,di in PLUGIN_MODULES
178                            if mod_name == plugin.name][0]
179         help = 'Commands: ' + ', '.join([c.name for c in plugin.commands()])
180         settings.append(Setting(
181                 section='plugins',
182                 option=plugin.name,
183                 value=str(default_include),
184                 help=help,
185                 ))
186     for pnode in PLUGIN_GRAPH:
187         plugin = pnode.data
188         settings.extend(plugin.default_settings())
189     return settings
190
191 def load_graph(graph, config, include_section):
192     items = []
193     for node in graph:
194         item = node.data
195         try:
196             include = config.getboolean(include_section, item.name)
197         except configparser.NoOptionError:
198             include = True # non-optional include (e.g. a Builtin)
199         if include == True:
200             try:
201                 item.config = dict(config.items(item.setting_section))
202             except configparser.NoSectionError:
203                 pass
204             items.append(item)
205     return items