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