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