Skip to content

Commit 37e7212

Browse files
committed
Moved graph functions to new module
1 parent 89f9fc3 commit 37e7212

File tree

2 files changed

+232
-224
lines changed

2 files changed

+232
-224
lines changed

pythonforandroid/graph.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
2+
from copy import deepcopy
3+
4+
from pythonforandroid.logger import (info, info_notify, warning)
5+
from pythonforandroid.recipe import Recipe
6+
from pythonforandroid.bootstrap import Bootstrap
7+
8+
9+
class Graph(object):
10+
# Taken from the old python-for-android/depsort
11+
# Modified to include alternative dependencies
12+
def __init__(self):
13+
# `graph`: dict that maps each package to a set of its dependencies.
14+
self.graphs = [{}]
15+
# self.graph = {}
16+
17+
def remove_redundant_graphs(self):
18+
'''Removes possible graphs if they are equivalent to others.'''
19+
graphs = self.graphs
20+
# Walk the list backwards so that popping elements doesn't
21+
# mess up indexing.
22+
23+
# n.b. no need to test graph 0 as it will have been tested against
24+
# all others by the time we get to it
25+
for i in range(len(graphs) - 1, 0, -1):
26+
graph = graphs[i]
27+
28+
# test graph i against all graphs 0 to i-1
29+
for j in range(0, i):
30+
comparison_graph = graphs[j]
31+
32+
if set(comparison_graph.keys()) == set(graph.keys()):
33+
# graph[i] == graph[j]
34+
# so remove graph[i] and continue on to testing graph[i-1]
35+
graphs.pop(i)
36+
break
37+
38+
def add(self, dependent, dependency):
39+
"""Add a dependency relationship to the graph"""
40+
if isinstance(dependency, (tuple, list)):
41+
for graph in self.graphs[:]:
42+
for dep in dependency[1:]:
43+
new_graph = deepcopy(graph)
44+
self._add(new_graph, dependent, dep)
45+
self.graphs.append(new_graph)
46+
self._add(graph, dependent, dependency[0])
47+
else:
48+
for graph in self.graphs:
49+
self._add(graph, dependent, dependency)
50+
self.remove_redundant_graphs()
51+
52+
def _add(self, graph, dependent, dependency):
53+
'''Add a dependency relationship to a specific graph, where dependency
54+
must be a single dependency, not a list or tuple.
55+
'''
56+
graph.setdefault(dependent, set())
57+
graph.setdefault(dependency, set())
58+
if dependent != dependency:
59+
graph[dependent].add(dependency)
60+
61+
def conflicts(self, conflict):
62+
graphs = self.graphs
63+
for i in range(len(graphs)):
64+
graph = graphs[len(graphs) - 1 - i]
65+
if conflict in graph:
66+
graphs.pop(len(graphs) - 1 - i)
67+
return len(graphs) == 0
68+
69+
def remove_remaining_conflicts(self, ctx):
70+
# It's unpleasant to have to pass ctx as an argument...
71+
'''Checks all possible graphs for conflicts that have arisen during
72+
the additon of alternative repice branches, as these are not checked
73+
for conflicts at the time.'''
74+
new_graphs = []
75+
for i, graph in enumerate(self.graphs):
76+
for name in graph.keys():
77+
recipe = Recipe.get_recipe(name, ctx)
78+
if any([c in graph for c in recipe.conflicts]):
79+
break
80+
else:
81+
new_graphs.append(graph)
82+
self.graphs = new_graphs
83+
84+
def add_optional(self, dependent, dependency):
85+
"""Add an optional (ordering only) dependency relationship to the graph
86+
87+
Only call this after all mandatory requirements are added
88+
"""
89+
for graph in self.graphs:
90+
if dependent in graph and dependency in graph:
91+
self._add(graph, dependent, dependency)
92+
93+
def find_order(self, index=0):
94+
"""Do a topological sort on a dependency graph
95+
96+
:Parameters:
97+
:Returns:
98+
iterator, sorted items form first to last
99+
"""
100+
graph = self.graphs[index]
101+
graph = dict((k, set(v)) for k, v in graph.items())
102+
while graph:
103+
# Find all items without a parent
104+
leftmost = [l for l, s in graph.items() if not s]
105+
if not leftmost:
106+
raise ValueError('Dependency cycle detected! %s' % graph)
107+
# If there is more than one, sort them for predictable order
108+
leftmost.sort()
109+
for result in leftmost:
110+
# Yield and remove them from the graph
111+
yield result
112+
graph.pop(result)
113+
for bset in graph.values():
114+
bset.discard(result)
115+
116+
117+
def get_recipe_order_and_bootstrap(ctx, names, bs=None):
118+
'''Takes a list of recipe names and (optionally) a bootstrap. Then
119+
works out the dependency graph (including bootstrap recipes if
120+
necessary). Finally, if no bootstrap was initially selected,
121+
chooses one that supports all the recipes.
122+
'''
123+
graph = Graph()
124+
recipes_to_load = set(names)
125+
if bs is not None and bs.recipe_depends:
126+
info_notify('Bootstrap requires recipes {}'.format(bs.recipe_depends))
127+
recipes_to_load = recipes_to_load.union(set(bs.recipe_depends))
128+
recipes_to_load = list(recipes_to_load)
129+
recipe_loaded = []
130+
python_modules = []
131+
while recipes_to_load:
132+
name = recipes_to_load.pop(0)
133+
if name in recipe_loaded or isinstance(name, (list, tuple)):
134+
continue
135+
try:
136+
recipe = Recipe.get_recipe(name, ctx)
137+
except IOError:
138+
info('No recipe named {}; will attempt to install with pip'
139+
.format(name))
140+
python_modules.append(name)
141+
continue
142+
except (KeyboardInterrupt, SystemExit):
143+
raise
144+
except:
145+
warning('Failed to import recipe named {}; the recipe exists '
146+
'but appears broken.'.format(name))
147+
warning('Exception was:')
148+
raise
149+
graph.add(name, name)
150+
info('Loaded recipe {} (depends on {}{})'.format(
151+
name, recipe.depends,
152+
', conflicts {}'.format(recipe.conflicts) if recipe.conflicts
153+
else ''))
154+
for depend in recipe.depends:
155+
graph.add(name, depend)
156+
recipes_to_load += recipe.depends
157+
for conflict in recipe.conflicts:
158+
if graph.conflicts(conflict):
159+
warning(
160+
('{} conflicts with {}, but both have been '
161+
'included or pulled into the requirements.'
162+
.format(recipe.name, conflict)))
163+
warning(
164+
'Due to this conflict the build cannot continue, exiting.')
165+
exit(1)
166+
recipe_loaded.append(name)
167+
graph.remove_remaining_conflicts(ctx)
168+
if len(graph.graphs) > 1:
169+
info('Found multiple valid recipe sets:')
170+
for g in graph.graphs:
171+
info(' {}'.format(g.keys()))
172+
info_notify('Using the first of these: {}'
173+
.format(graph.graphs[0].keys()))
174+
elif len(graph.graphs) == 0:
175+
warning('Didn\'t find any valid dependency graphs, exiting.')
176+
exit(1)
177+
else:
178+
info('Found a single valid recipe set (this is good)')
179+
180+
build_order = list(graph.find_order(0))
181+
if bs is None: # It would be better to check against possible
182+
# orders other than the first one, but in practice
183+
# there will rarely be clashes, and the user can
184+
# specify more parameters if necessary to resolve
185+
# them.
186+
bs = Bootstrap.get_bootstrap_from_recipes(build_order, ctx)
187+
if bs is None:
188+
info('Could not find a bootstrap compatible with the '
189+
'required recipes.')
190+
info('If you think such a combination should exist, try '
191+
'specifying the bootstrap manually with --bootstrap.')
192+
exit(1)
193+
info('{} bootstrap appears compatible with the required recipes.'
194+
.format(bs.name))
195+
info('Checking this...')
196+
recipes_to_load = bs.recipe_depends
197+
# This code repeats the code from earlier! Should move to a function:
198+
while recipes_to_load:
199+
name = recipes_to_load.pop(0)
200+
if name in recipe_loaded or isinstance(name, (list, tuple)):
201+
continue
202+
try:
203+
recipe = Recipe.get_recipe(name, ctx)
204+
except ImportError:
205+
info('No recipe named {}; will attempt to install with pip'
206+
.format(name))
207+
python_modules.append(name)
208+
continue
209+
graph.add(name, name)
210+
info('Loaded recipe {} (depends on {}{})'.format(
211+
name, recipe.depends,
212+
', conflicts {}'.format(recipe.conflicts) if recipe.conflicts
213+
else ''))
214+
for depend in recipe.depends:
215+
graph.add(name, depend)
216+
recipes_to_load += recipe.depends
217+
for conflict in recipe.conflicts:
218+
if graph.conflicts(conflict):
219+
warning(
220+
('{} conflicts with {}, but both have been '
221+
'included or pulled into the requirements.'
222+
.format(recipe.name, conflict)))
223+
warning('Due to this conflict the build cannot continue, '
224+
'exiting.')
225+
exit(1)
226+
recipe_loaded.append(name)
227+
graph.remove_remaining_conflicts(ctx)
228+
build_order = list(graph.find_order(0))
229+
return build_order, python_modules, bs
230+
231+
# Do a final check that the new bs doesn't pull in any conflicts

0 commit comments

Comments
 (0)