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