blob: 843aab47b0804de8fe5bee5d1a4bfe159a5784e0 [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Class and functions dealing with dependencies between distributions.
2
3This module provides a DependencyGraph class to represent the
4dependencies between distributions. Auxiliary functions can generate a
5graph, find reverse dependencies, and print a graph in DOT format.
6"""
7
8import sys
9
10from io import StringIO
11from packaging.errors import PackagingError
12from packaging.version import VersionPredicate, IrrationalVersionError
13
14__all__ = ['DependencyGraph', 'generate_graph', 'dependent_dists',
15 'graph_to_dot']
16
17
18class DependencyGraph:
19 """
20 Represents a dependency graph between distributions.
21
22 The dependency relationships are stored in an ``adjacency_list`` that maps
23 distributions to a list of ``(other, label)`` tuples where ``other``
24 is a distribution and the edge is labeled with ``label`` (i.e. the version
25 specifier, if such was provided). Also, for more efficient traversal, for
26 every distribution ``x``, a list of predecessors is kept in
27 ``reverse_list[x]``. An edge from distribution ``a`` to
28 distribution ``b`` means that ``a`` depends on ``b``. If any missing
29 dependencies are found, they are stored in ``missing``, which is a
30 dictionary that maps distributions to a list of requirements that were not
31 provided by any other distributions.
32 """
33
34 def __init__(self):
35 self.adjacency_list = {}
36 self.reverse_list = {}
37 self.missing = {}
38
39 def add_distribution(self, distribution):
40 """Add the *distribution* to the graph.
41
42 :type distribution: :class:`packaging.database.Distribution` or
43 :class:`packaging.database.EggInfoDistribution`
44 """
45 self.adjacency_list[distribution] = []
46 self.reverse_list[distribution] = []
47 self.missing[distribution] = []
48
49 def add_edge(self, x, y, label=None):
50 """Add an edge from distribution *x* to distribution *y* with the given
51 *label*.
52
53 :type x: :class:`packaging.database.Distribution` or
54 :class:`packaging.database.EggInfoDistribution`
55 :type y: :class:`packaging.database.Distribution` or
56 :class:`packaging.database.EggInfoDistribution`
57 :type label: ``str`` or ``None``
58 """
59 self.adjacency_list[x].append((y, label))
60 # multiple edges are allowed, so be careful
Éric Araujodf8ef022011-06-08 04:47:13 +020061 if x not in self.reverse_list[y]:
Tarek Ziade1231a4e2011-05-19 13:07:25 +020062 self.reverse_list[y].append(x)
63
64 def add_missing(self, distribution, requirement):
65 """
66 Add a missing *requirement* for the given *distribution*.
67
68 :type distribution: :class:`packaging.database.Distribution` or
69 :class:`packaging.database.EggInfoDistribution`
70 :type requirement: ``str``
71 """
72 self.missing[distribution].append(requirement)
73
74 def _repr_dist(self, dist):
Éric Araujobab50cb2011-07-29 02:37:21 +020075 return '%r %s' % (dist.name, dist.version)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020076
77 def repr_node(self, dist, level=1):
78 """Prints only a subgraph"""
79 output = []
80 output.append(self._repr_dist(dist))
81 for other, label in self.adjacency_list[dist]:
82 dist = self._repr_dist(other)
83 if label is not None:
84 dist = '%s [%s]' % (dist, label)
85 output.append(' ' * level + str(dist))
86 suboutput = self.repr_node(other, level + 1)
87 subs = suboutput.split('\n')
88 output.extend(subs[1:])
89 return '\n'.join(output)
90
91 def __repr__(self):
92 """Representation of the graph"""
93 output = []
94 for dist, adjs in self.adjacency_list.items():
95 output.append(self.repr_node(dist))
96 return '\n'.join(output)
97
98
99def graph_to_dot(graph, f, skip_disconnected=True):
100 """Writes a DOT output for the graph to the provided file *f*.
101
102 If *skip_disconnected* is set to ``True``, then all distributions
103 that are not dependent on any other distribution are skipped.
104
105 :type f: has to support ``file``-like operations
106 :type skip_disconnected: ``bool``
107 """
108 disconnected = []
109
110 f.write("digraph dependencies {\n")
111 for dist, adjs in graph.adjacency_list.items():
112 if len(adjs) == 0 and not skip_disconnected:
113 disconnected.append(dist)
114 for other, label in adjs:
115 if not label is None:
116 f.write('"%s" -> "%s" [label="%s"]\n' %
117 (dist.name, other.name, label))
118 else:
119 f.write('"%s" -> "%s"\n' % (dist.name, other.name))
120 if not skip_disconnected and len(disconnected) > 0:
121 f.write('subgraph disconnected {\n')
122 f.write('label = "Disconnected"\n')
123 f.write('bgcolor = red\n')
124
125 for dist in disconnected:
126 f.write('"%s"' % dist.name)
127 f.write('\n')
128 f.write('}\n')
129 f.write('}\n')
130
131
132def generate_graph(dists):
133 """Generates a dependency graph from the given distributions.
134
135 :parameter dists: a list of distributions
136 :type dists: list of :class:`packaging.database.Distribution` and
137 :class:`packaging.database.EggInfoDistribution` instances
138 :rtype: a :class:`DependencyGraph` instance
139 """
140 graph = DependencyGraph()
141 provided = {} # maps names to lists of (version, dist) tuples
142
143 # first, build the graph and find out the provides
144 for dist in dists:
145 graph.add_distribution(dist)
146 provides = (dist.metadata['Provides-Dist'] +
147 dist.metadata['Provides'] +
Éric Araujobab50cb2011-07-29 02:37:21 +0200148 ['%s (%s)' % (dist.name, dist.version)])
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200149
150 for p in provides:
151 comps = p.strip().rsplit(" ", 1)
152 name = comps[0]
153 version = None
154 if len(comps) == 2:
155 version = comps[1]
156 if len(version) < 3 or version[0] != '(' or version[-1] != ')':
Éric Araujo46bdcf72011-06-08 04:40:13 +0200157 raise PackagingError('distribution %r has ill-formed'
158 'provides field: %r' % (dist.name, p))
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200159 version = version[1:-1] # trim off parenthesis
Éric Araujodf8ef022011-06-08 04:47:13 +0200160 if name not in provided:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200161 provided[name] = []
162 provided[name].append((version, dist))
163
164 # now make the edges
165 for dist in dists:
166 requires = dist.metadata['Requires-Dist'] + dist.metadata['Requires']
167 for req in requires:
168 try:
169 predicate = VersionPredicate(req)
170 except IrrationalVersionError:
171 # XXX compat-mode if cannot read the version
172 name = req.split()[0]
173 predicate = VersionPredicate(name)
174
175 name = predicate.name
176
Éric Araujodf8ef022011-06-08 04:47:13 +0200177 if name not in provided:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200178 graph.add_missing(dist, req)
179 else:
180 matched = False
181 for version, provider in provided[name]:
182 try:
183 match = predicate.match(version)
184 except IrrationalVersionError:
185 # XXX small compat-mode
186 if version.split(' ') == 1:
187 match = True
188 else:
189 match = False
190
191 if match:
192 graph.add_edge(dist, provider, req)
193 matched = True
194 break
195 if not matched:
196 graph.add_missing(dist, req)
197 return graph
198
199
200def dependent_dists(dists, dist):
201 """Recursively generate a list of distributions from *dists* that are
202 dependent on *dist*.
203
204 :param dists: a list of distributions
205 :param dist: a distribution, member of *dists* for which we are interested
206 """
Éric Araujo46bdcf72011-06-08 04:40:13 +0200207 if dist not in dists:
208 raise ValueError('given distribution %r is not a member of the list' %
209 dist.name)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200210 graph = generate_graph(dists)
211
212 dep = [dist] # dependent distributions
213 fringe = graph.reverse_list[dist] # list of nodes we should inspect
214
215 while not len(fringe) == 0:
216 node = fringe.pop()
217 dep.append(node)
218 for prev in graph.reverse_list[node]:
Éric Araujodf8ef022011-06-08 04:47:13 +0200219 if prev not in dep:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200220 fringe.append(prev)
221
222 dep.pop(0) # remove dist from dep, was there to prevent infinite loops
223 return dep
224
225
226def main():
227 from packaging.database import get_distributions
228 tempout = StringIO()
229 try:
230 old = sys.stderr
231 sys.stderr = tempout
232 try:
233 dists = list(get_distributions(use_egg_info=True))
234 graph = generate_graph(dists)
235 finally:
236 sys.stderr = old
237 except Exception as e:
238 tempout.seek(0)
239 tempout = tempout.read()
Éric Araujo3cab2f12011-06-08 04:10:57 +0200240 print('Could not generate the graph')
241 print(tempout)
242 print(e)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200243 sys.exit(1)
244
245 for dist, reqs in graph.missing.items():
246 if len(reqs) > 0:
Éric Araujo46bdcf72011-06-08 04:40:13 +0200247 print("Warning: Missing dependencies for %r:" % dist.name,
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200248 ", ".join(reqs))
249 # XXX replace with argparse
250 if len(sys.argv) == 1:
251 print('Dependency graph:')
Éric Araujo3cab2f12011-06-08 04:10:57 +0200252 print(' ', repr(graph).replace('\n', '\n '))
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200253 sys.exit(0)
254 elif len(sys.argv) > 1 and sys.argv[1] in ('-d', '--dot'):
255 if len(sys.argv) > 2:
256 filename = sys.argv[2]
257 else:
258 filename = 'depgraph.dot'
259
260 with open(filename, 'w') as f:
261 graph_to_dot(graph, f, True)
262 tempout.seek(0)
263 tempout = tempout.read()
264 print(tempout)
Éric Araujo46bdcf72011-06-08 04:40:13 +0200265 print('Dot file written at %r' % filename)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200266 sys.exit(0)
267 else:
268 print('Supported option: -d [filename]')
269 sys.exit(1)
270
271
272if __name__ == '__main__':
273 main()