8eb87477c1638c45b550c072279410973f8d3471
[hooke.git] / hooke / util / pluggable.py
1 # Copyright (C) 2010-2012 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                     try:
101                         obj = obj()
102                     except Exception, e:
103                         logging.error('could not instantiate %s from %s: %s'
104                                       % (obj, submodname, e))
105                         raise
106                 name = getattr(obj, 'name', submodname)
107                 objs[name] = obj
108     return objs
109
110
111 def construct_graph(this_modname, submodnames, class_selector,
112                     assert_name_match=True):
113     """Search the submodules `submodnames` of a module `this_modname`
114     for class objects for which `class_selector(class)` returns
115     `True`.  These classes are instantiated, and the `instance.name`
116     is compared to the `submodname` (if `assert_name_match` is
117     `True`).
118
119     The instances are further arranged into a dependency
120     :class:`hooke.util.graph.Graph` according to their
121     `instance.dependencies()` values.  The topologically sorted graph
122     is returned.
123     """
124     instances = {}
125     for submodname,submod in submods(this_modname, submodnames):
126         for objname in dir(submod):
127             obj = getattr(submod, objname)
128             if class_selector(obj):
129                 try:
130                     instance = obj()
131                 except Exception, e:
132                     logging.error('could not instantiate %s from %s: %s'
133                                   % (obj, submodname, e))
134                     raise
135                 if assert_name_match == True and instance.name != submodname:
136                     raise Exception(
137                         'Instance name %s does not match module name %s'
138                         % (instance.name, submodname))
139                 instances[instance.name] = instance
140     nodes = {}
141     for i in instances.values():     # make nodes for each instance
142         nodes[i.name] = Node(data=i)
143     for n in nodes.values():         # fill in dependencies for each node
144         n.extend([nodes[name] for name in n.data.dependencies()])
145     graph = Graph(nodes.values())
146     graph.topological_sort()
147     return graph