Skip to content

Commit 0fdf735

Browse files
committed
Merge pull request kivy#134 from encukou/depsolving
WIP: Add depsolving utility
2 parents ae5c063 + 0b16d85 commit 0fdf735

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed

depsort.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#! /usr/bin/env python2
2+
3+
import argparse
4+
import sys
5+
6+
class Graph(object):
7+
def __init__(self):
8+
# `graph`: dict that maps each package to a set of its dependencies.
9+
self.graph = {}
10+
11+
def add(self, dependent, dependency):
12+
"""Add a dependency relationship to the graph"""
13+
self.graph.setdefault(dependent, set())
14+
self.graph.setdefault(dependency, set())
15+
if dependent != dependency:
16+
self.graph[dependent].add(dependency)
17+
18+
def add_optional(self, dependent, dependency):
19+
"""Add an optional (ordering only) dependency relationship to the graph
20+
21+
Only call this after all mandatory requirements are added
22+
"""
23+
if dependent in self.graph and dependency in self.graph:
24+
self.add(dependent, dependency)
25+
26+
def find_order(self):
27+
"""Do a topological sort on a dependency graph
28+
29+
:Parameters:
30+
:Returns:
31+
iterator, sorted items form first to last
32+
"""
33+
graph = dict((k, set(v)) for k, v in self.graph.items())
34+
while graph:
35+
# Find all items without a parent
36+
leftmost = [l for l, s in graph.items() if not s]
37+
if not leftmost:
38+
raise ValueError('Dependency cycle detected! %s' % graph)
39+
# If there is more than one, sort them for predictable order
40+
leftmost.sort()
41+
for result in leftmost:
42+
# Yield and remove them from the graph
43+
yield result
44+
graph.pop(result)
45+
for bset in graph.values():
46+
bset.discard(result)
47+
48+
49+
def lines_to_relationships(lines):
50+
"""Do a topological sort from a list of space-separated lines
51+
52+
:Parameters:
53+
`lines`: Iterable of lines in the format
54+
"dependent dependency_0 dependency_1 ... dependency_n"
55+
56+
:Returns:
57+
iterator of (dependent, dependency) tuples
58+
"""
59+
for line in lines:
60+
line = line.split()
61+
if line:
62+
dependent = line[0]
63+
for dependency in line:
64+
yield dependent, dependency
65+
66+
67+
def topo_sort_lines(lines, lines_optional=()):
68+
"""Do a topological sort from a list of space-separated lines
69+
70+
:Parameters:
71+
`lines`: Iterable of lines in the format
72+
"dependent dependency_0 dependency_1 ... dependency_n"
73+
74+
`lines`: Iterable of lines with *optional* (ordering-only) dependencies
75+
"dependent dependency_0 dependency_1 ... dependency_n"
76+
77+
:Returns:
78+
string, Sorted dependencies, space-separated
79+
"""
80+
graph = Graph()
81+
for dependent, dependency in lines_to_relationships(lines):
82+
graph.add(dependent, dependency)
83+
for dependent, dependency in lines_to_relationships(lines_optional):
84+
graph.add_optional(dependent, dependency)
85+
return ' '.join(graph.find_order())
86+
87+
88+
def test_depsort_1():
89+
lines = [
90+
'c a',
91+
'b c',
92+
'd b',
93+
'w z',
94+
'a w',
95+
]
96+
assert topo_sort_lines(lines) == 'z w a c b d'
97+
98+
99+
def test_depsort_2():
100+
lines = [
101+
'l k',
102+
'm l',
103+
'a k',
104+
'd a',
105+
'l d',
106+
's l',
107+
'm s',
108+
]
109+
assert topo_sort_lines(lines) == 'k a d l s m'
110+
111+
112+
def test_depsort_3():
113+
lines = [
114+
'z a',
115+
's z',
116+
'z z',
117+
's s',
118+
]
119+
assert topo_sort_lines(lines) == 'a z s'
120+
121+
122+
def test_depsort_4():
123+
lines = [
124+
'f d',
125+
'g f',
126+
'r f',
127+
't r',
128+
'y t',
129+
'g y',
130+
]
131+
assert topo_sort_lines(lines) == 'd f r t y g'
132+
133+
134+
def test_depsort_5():
135+
lines = [
136+
'a b c d e f',
137+
'e f',
138+
'g',
139+
]
140+
assert topo_sort_lines(lines) == 'b c d f g e a'
141+
142+
143+
def test_depsort_6():
144+
lines = [
145+
' numpy python ',
146+
' kivy pygame pyjnius android ',
147+
' python hostpython ',
148+
' pygame sdl ',
149+
' android pygame ',
150+
' pyjnius python ',
151+
' sdl python ',
152+
' hostpython ',
153+
]
154+
assert topo_sort_lines(lines) == (
155+
'hostpython python numpy pyjnius sdl pygame android kivy')
156+
157+
158+
def test_depsort_optional_1():
159+
lines = [' myapp openssl python ']
160+
optional = ['python openssl']
161+
assert topo_sort_lines(lines, optional) == 'openssl python myapp'
162+
163+
164+
def test_depsort_optional_2():
165+
lines = [' myapp openssl python ']
166+
# Just for testing purposes, make openssl soft-depend on python
167+
optional = ['openssl python']
168+
assert topo_sort_lines(lines, optional) == 'python openssl myapp'
169+
170+
171+
def test_depsort_optional_3():
172+
lines = ['myapp openssl']
173+
optional = ['python openssl']
174+
assert topo_sort_lines(lines, optional) == 'openssl myapp'
175+
176+
177+
def test_depsort_optional_4():
178+
lines = ['myapp python']
179+
optional = ['python openssl']
180+
assert topo_sort_lines(lines, optional) == 'python myapp'
181+
182+
183+
def test_depsort_optional_4():
184+
lines = ['myapp']
185+
optional = ['python openssl']
186+
assert topo_sort_lines(lines, optional) == 'myapp'
187+
188+
189+
def main(argv):
190+
parser = argparse.ArgumentParser(
191+
description="Sort dependencies given on standard input")
192+
parser.add_argument('--optional', type=argparse.FileType('r'),
193+
help='File with optional (ordering-only) dependencies')
194+
args = parser.parse_args(argv[1:])
195+
return topo_sort_lines(sys.stdin, lines_optional=args.optional or [])
196+
197+
198+
if __name__ == '__main__':
199+
print main(sys.argv)

0 commit comments

Comments
 (0)