Replace .config rather than reconstructing plugins, drivers, and UIs.
[hooke.git] / hooke / util / pluggable.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """``pluggable`` provides utility functions for extensible plugin modules.
20 """
21
22 import logging
23
24 from ..compat.odict import odict
25 from .graph import Node, Graph
26
27
28 class IsSubclass (object):
29     """A safe subclass comparator.
30     
31     Examples
32     --------
33
34     >>> class A (object):
35     ...     pass
36     >>> class B (A):
37     ...     pass
38     >>> C = 5
39     >>> is_subclass = IsSubclass(A)
40     >>> is_subclass(A)
41     True
42     >>> is_subclass = IsSubclass(A, blacklist=[A])
43     >>> is_subclass(A)
44     False
45     >>> is_subclass(B)
46     True
47     >>> is_subclass(C)
48     False
49     """
50     def __init__(self, base_class, blacklist=None):
51         self.base_class = base_class
52         if blacklist == None:
53             blacklist = []
54         self.blacklist = blacklist
55     def __call__(self, other):
56         try:
57             subclass = issubclass(other, self.base_class)
58         except TypeError:
59             return False
60         if other in self.blacklist:
61             return False
62         return subclass
63
64
65 def submods(this_modname, submodnames):
66     """Iterate through (submodname, submod) pairs.
67     """
68     for submodname in submodnames:
69         count = len([s for s in submodnames if s == submodname])
70         assert count > 0, 'No %s entries: %s' % (submodname, submodnames)
71         assert count == 1, 'Multiple (%d) %s entries: %s' \
72             % (count, submodname, submodnames)
73         try:
74             this_mod = __import__(this_modname, fromlist=[submodname])
75         except ImportError, e:
76             # Use the root logger because the 'hooke' logger is
77             # configured by a Hooke instance after module imports.
78             logging.warn('could not import %s from %s: %s'
79                          % (submodname, this_modname, e))
80             continue
81         submod = getattr(this_mod, submodname)
82         yield (submodname, submod)
83
84
85 def construct_odict(this_modname, submodnames, class_selector,
86                     instantiate=True):
87     """Search the submodules `submodnames` of a module `this_modname`
88     for class objects for which `class_selector(class)` returns
89     `True`.  If `instantiate == True` these classes are instantiated
90     and stored in the returned :class:`hooke.compat.odict.odict` in
91     the order in which they were discovered.  Otherwise, the class
92     itself is stored.
93     """
94     objs = odict()
95     for submodname,submod in submods(this_modname, submodnames):
96         for objname in dir(submod):
97             obj = getattr(submod, objname)
98             if class_selector(obj):
99                 if instantiate == True:
100                     obj = obj()
101                 name = getattr(obj, 'name', submodname)
102                 objs[name] = obj
103     return objs
104
105
106 def construct_graph(this_modname, submodnames, class_selector,
107                     assert_name_match=True):
108     """Search the submodules `submodnames` of a module `this_modname`
109     for class objects for which `class_selector(class)` returns
110     `True`.  These classes are instantiated, and the `instance.name`
111     is compared to the `submodname` (if `assert_name_match` is
112     `True`).
113
114     The instances are further arranged into a dependency
115     :class:`hooke.util.graph.Graph` according to their
116     `instance.dependencies()` values.  The topologically sorted graph
117     is returned.
118     """
119     instances = {}
120     for submodname,submod in submods(this_modname, submodnames):
121         for objname in dir(submod):
122             obj = getattr(submod, objname)
123             if class_selector(obj):
124                 instance = obj()
125                 if assert_name_match == True and instance.name != submodname:
126                     raise Exception(
127                         'Instance name %s does not match module name %s'
128                         % (instance.name, submodname))
129                 instances[instance.name] = instance
130     nodes = {}
131     for i in instances.values():     # make nodes for each instance
132         nodes[i.name] = Node(data=i)
133     for n in nodes.values():         # fill in dependencies for each node
134         n.extend([nodes[name] for name in n.data.dependencies()])
135     graph = Graph(nodes.values())
136     graph.topological_sort()
137     return graph