ยปCore Development>Code coverage>Lib/packaging/depgraph.py

Python code coverage for Lib/packaging/depgraph.py

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