2 from collections import deque,defaultdict
3 from itertools import ifilter
6 from core.bits import Item,Group
9 '''always return the underlying function from a bound method'''
10 return getattr(func, 'im_func', func)
12 class BaseDependencyStrategy:
13 '''Base class for strategies for dependency resolution'''
15 def iterate_pruned_item_dependencies(cls, requirements, item):
16 '''return an ordered list of dependencies for an item such that an items dependencies are before it in the list
17 prunted based on predicate
18 Not used, but left here because at some point it might be needed to do
19 this in a way that does not alter the items at all (as per the below filter)'''
20 raise NotImplementedError
23 def iterate_item_dependencies(cls, item):
24 '''return an ordered list of dependencies for an item such that an items dependencies are before it in the list'''
25 raise NotImplementedError
28 def make_group_dependencies_explicit(cls, item):
29 '''applying dependencies of groups to those it contains'''
30 raise NotImplementedError
33 def filter_dependency_graph(cls, requirements, item):
34 '''filter items in the dependency graph INPLACE based on the supplied requirements
35 It's highly recommended you only do this on item instances and not classes as
36 it alters or the depends attribute on all items in the supplied DAG '''
37 raise NotImplementedError
40 def item_factory(cls, goal_item):
41 '''instantiate all items in a dependency tree from an end-goal item
43 for every instance in the dependency tree, the depends attribute on the instance overriding the class
44 returns an instance of the end-goal where all recursively all dependencies are item instances
46 raise NotImplementedError
49 def early_iter_all_items(cls, item):
50 '''get a list of all times, including all groups, contents and dependencies of same
51 before groups are unrolled into something that can be represented by a digraph we
52 need a way of getting all items, including traversing group contents
54 raise NotImplementedError
57 class SimpleDependencyStrategy(BaseDependencyStrategy):
58 '''Reasonably generic strategy for working with dependencies
59 Requires implementation of some things by other strategies
63 def iterate_pruned_item_dependencies(cls, requirements, item, seen=None):
64 '''return an ordered list of dependencies for an item such that an items dependencies are before it in the list
65 This is equivalent to treating the dependencies as a DAG and traversing while:
66 - reducing it to a tree by ignoring any nodes seen in the traversal
67 - pruning branches where the requirements do not meet an item's predicate
68 - doing a post-order traversal to maintain the invaraint that a node's
69 dependencies preceed it in the traversal.
71 if seen is None: seen = set()
72 if item in seen: raise StopIteration
74 filtereddeps = ifilter(lambda i: unbound(i.predicate)(requirements), item.depends)
75 for dep in filtereddeps:
76 for cdep in cls.iterate_pruned_item_dependencies(requirements,dep,seen):
81 def iterate_item_dependencies(cls, item, seen=None):
82 if seen is None: seen = set()
83 if item in seen: raise StopIteration
85 for dep in item.depends:
86 for cdep in cls.iterate_item_dependencies(dep,seen):
91 def early_iter_all_items(cls, item, seen=None):
92 '''get a list of all times, including all groups, contents and dependencies of same
93 before groups are unrolled into soemthing that can be represented by a digraph we
94 need a way of getting all items, including traversing group contents
96 if seen is None: seen = set()
97 if item in seen: raise StopIteration
99 for dep in item.depends:
100 for cdep in cls.early_iter_all_items(dep,seen):
102 if isinstance(item,Group) or type(item) == type and issubclass(item,Group):
103 for member in item.contains:
104 for cdep in cls.early_iter_all_items(member,seen):
109 def filter_dependency_graph(cls, requirements, item):
110 '''filter items in the dependency graph INPLACE based on the supplied requirements
111 It's highly recommended you only do this on item instances and not classes as
112 it alters or the depends attribute on all items in the supplied DAG '''
113 items = cls.iterate_item_dependencies(item)
114 keptitems = set(filter(lambda i: unbound(i.predicate)(requirements), items))
115 droppeditems = set(items).difference(keptitems)
117 for dropped in droppeditems: del(dropped.depends) # Drop possible circular refs to filtered instances
118 for survivor in keptitems:
119 # Drop references to filtered instances
120 survivor.depends = tuple(dep for dep in survivor.depends if dep in keptitems)
121 if item not in keptitems:
126 def instantiate_items(cls, items):
127 '''returns a map from classes to instances'''
128 # Pre: All items, including all deps, are in 'items'
129 instancemap = dict((item, item()) for item in items) # Only ever instantiate each item once
130 iteminstances = map(instancemap.get, items)
131 for inst in iteminstances:
132 inst.depends = tuple(map(instancemap.get, inst.depends))
133 if isinstance(inst,Group):
134 inst.contains = tuple(map(instancemap.get, inst.contains))
138 def item_factory(cls, goal_item):
139 '''instantiate all items in a dependency tree from an end-goal item
141 for every instance in the dependency tree, the depends attribute on the instance overriding the class
142 returns an instance of the end-goal where all recursively all dependencies are item instances
144 # Flatten the dep graph to a topsort, instantiate all items, then override depends attributes
145 items = set(cls.early_iter_all_items(goal_item))
146 instancemap = cls.instantiate_items(items)
147 return instancemap[goal_item]
150 def make_group_dependencies_explicit(cls, item):
151 items = set(cls.early_iter_all_items(item)) # Gotta catch them all
152 items = cls.make_group_dependencies_explicit_for_items(items)
157 def make_group_dependencies_explicit_for_items(cls, items):
159 items = set(k for k in allitems if isinstance(k,Item) or type(k) == type and issubclass(k,Item))
160 groups = set(k for k in allitems if isinstance(k,Group) or type(k) == type and issubclass(k,Group))
161 origitems,origgroups = set(items),set(groups) # For later testing
162 assert allitems == items.union(groups)
163 assert items.isdisjoint(groups)
165 contained_by = defaultdict(set)
167 # First find out what groups an item is contained by
168 def iter_group_contents(group):
169 for k in group.contains:
171 for kk in iter_group_contents(k): yield kk
174 for k in iter_group_contents(group):
175 contained_by[k].add(group)
177 # Item dependencies are the explicit dependencies of the items themselves
178 # plus the dependencies of the groups they are contained by
179 for item,ingroups in contained_by.items():
180 assert ingroups.issubset(groups)
181 new_deps = set(item.depends)
183 new_deps.update(g.depends)
184 item.depends = tuple(new_deps)
186 # Now the dependencies of the items inside groups are unrolled, reduce any
187 # references of groups to the contents of the groups
189 # First do the group contents themselves recursively, and store as sets
191 group.contains = set(group.contains)
192 while not group.contains.isdisjoint(groups):
193 for containedgroup in group.contains.intersection(groups):
194 assert containedgroup != group
195 group.contains.update(containedgroup.contains)
196 group.contains.remove(containedgroup)
198 # Now that for group.contains has been reduced to non-group items,
199 # replace any reference to groups in the dependencies of any item with
200 # the contents of that group
201 for item in items.difference(groups):
202 assert item not in groups
203 if not groups.isdisjoint(item.depends):
204 item.depends = set(item.depends)
205 for groupdep in item.depends.intersection(groups):
206 item.depends.update(groupdep.contains)
207 item.depends.remove(groupdep)
208 item.depends = tuple(item.depends)
209 assert groups.isdisjoint(item.depends)
211 assert items == origitems
212 assert groups == origgroups
213 assert allitems == items.union(groups)
214 assert items.isdisjoint(groups)
215 assert items == allitems.difference(groups)
220 not_none = lambda n: n is not None
222 class GraphDependencyStrategy(SimpleDependencyStrategy):
223 '''deal with item dependencies by treating them as a directed acyclic graph
224 a graph is represented as a 2tuple of a node, and a list of nodes connected to it's outgoing edges
226 graphs calls are generally passed by goal node, with dependencies on the goal being
227 it's outgoing edges'''
230 def get_graph(cls, item, seen=None):
231 '''return a DAG from a base item and a set of requirements
232 items are pruned from the graph if their predicates are false for the requirements
234 if seen is None: seen = dict()
235 if item in seen: return seen[item]
237 seen[item] = [item, branches]
238 for dep in item.depends:
239 branch = cls.get_graph(dep, seen)
241 branches.append(branch)
245 def get_pruned_graph(cls, requirements, item, seen=None):
246 '''return a DAG from a base item and a set of requirements
247 items are pruned from the graph if their predicates are false for the requirements
249 if seen is None: seen = dict()
250 if item in seen: return seen[item]
252 seen[item] = [item, branches]
253 for dep in filter(lambda i: unbound(i.predicate)(requirements), item.depends):
254 branch = cls.get_pruned_graph(requirements, dep, seen)
256 branches.append(branch)
260 def tree_from_graph(cls, graph, seen=None):
261 '''convert a DAG into a arborescence by removing edges to seen nodes'''
262 node,connected = graph
263 if seen is None: seen=set()
264 if node in seen: return None
266 connected = [cls.tree_from_graph(sub,seen) for sub in connected]
267 return [ node,filter(not_none,connected) ]
271 def postorder_traversal(cls, tree):
272 '''traverse tree post-order and return a list of nodes'''
274 ret = map(cls.postorder_traversal,branches)
275 return sum(ret,[]) + [root]
278 def iterate_pruned_item_dependencies(cls, requirements, item):
279 tree = cls.tree_from_graph( cls.get_pruned_graph(requirements,item) )
280 return cls.postorder_traversal(tree)
283 def iterate_item_dependencies(cls, item):
284 tree = cls.tree_from_graph( cls.get_graph(item) )
285 return cls.postorder_traversal(tree)
288 class DigraphDependencyStrategy(SimpleDependencyStrategy):
290 def edges_from_item_deps(cls, item):
291 '''iterates over dependency edges with transiability from item'''
292 tovisit = deque([item])
295 for dep in item.depends:
297 tovisit.extendleft(item.depends)
300 def graph_from_item_deps(cls, item):
301 return digraphtools.graph_from_edges( cls.edges_from_item_deps(item) )
304 def iterate_item_dependencies(cls, item):
305 g = cls.graph_from_item_deps(item)
306 return digraphtools.dfs_topsort_traversal(g, item)
309 def graph_from_items(cls, items):
310 # pre: all items in graph are passed
311 def edges_from_items(items):
313 for d in i.depends: yield (i,d)
314 return digraphtools.graph_from_edges( edges_from_items(items) )
317 def find_goal_nodes(cls, items):
318 '''return the set of all nodes that aren't depended on'''
319 graph = cls.graph_from_items(items)
320 start_nodes,dependencies = zip(*list(digraphtools.iter_edges(graph)))
321 return set(start_nodes).difference(dependencies)
324 def needed_dependencies(cls, graph, item):
325 '''return which of an item's dependencies are incomplete'''
326 return [dep for dep in graph[item] if dep.data['state'] != dep.COMPLETE]
329 def ready_to_run(cls, items):
330 '''return items that are incomplete who have no uncompleted dependencies'''
331 graph = cls.graph_from_items(items)
332 incomplete = [item for item in items if item.data['state'] == item.INCOMPLETE]
333 return [item for item in incomplete if len(cls.needed_dependencies(graph,item)) == 0]
335 class DeprecatedDependencyMagic(object):
336 #TODO: Figure out the object structure for *Magic. Likely it shouldn't even be in this module
337 def __init__(self, strategy=DigraphDependencyStrategy):
338 self.strategy = strategy
339 def make_new_dep_graph(self, goal_node):
340 goal_node = self.strategy.item_factory(goal_node)
341 self.strategy.make_group_dependencies_explicit(goal_node)
343 def item_list_for_task(self, task):
344 # FIXME: Doesn't look at all goal nodes. If we want only 1 goal node we should enforce it
345 goal = self.make_new_dep_graph(task.will[0])
346 goal = self.strategy.filter_dependency_graph(task.wants, goal)
347 return self.strategy.iterate_item_dependencies(goal)