726d546173e41a7fc8fd6a9f1eecd01ff7b14d2f
[gentoolkit.git] / pym / gentoolkit / dependencies.py
1 # Copyright(c) 2009, Gentoo Foundation
2 #
3 # Licensed under the GNU General Public License, v2
4 #
5 # $Header: $
6
7 """Provides a class for easy calculating dependencies for a given CPV."""
8
9 __docformat__ = 'epytext'
10 __all__ = ('Dependencies',)
11
12 # =======
13 # Imports
14 # =======
15
16 import portage
17 from portage.dep import paren_reduce
18
19 from gentoolkit import errors
20 from gentoolkit.atom import Atom
21 from gentoolkit.cpv import CPV
22 from gentoolkit.helpers import uniqify
23 from gentoolkit.query import Query
24
25 # =======
26 # Classes
27 # =======
28
29 class Dependencies(Query):
30         """Access a package's dependencies and reverse dependencies.
31
32         Example usage:
33                 >>> from gentoolkit.dependencies import Dependencies
34                 >>> portage = Dependencies('sys-apps/portage-9999')
35                 >>> portage
36                 <Dependencies 'sys-apps/portage-9999'>
37                 >>> # All methods return gentoolkit.atom.Atom instances
38                 ... portage.get_depend()
39                 ... # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
40                 [<Atom 'python3? =dev-lang/python-3*'>,
41                  <Atom '!python3? >=dev-lang/python-2.7'>, ...]
42
43         """
44         def __init__(self, query, parser=None):
45                 Query.__init__(self, query)
46                 self.use = []
47                 self.depatom = str()
48
49                 # Allow a custom parser function:
50                 self.parser = parser if parser else self._parser
51
52         def __eq__(self, other):
53                 if self.atom != other.atom:
54                         return False
55                 else:
56                         return True
57
58         def __ne__(self, other):
59                 return not self == other
60
61         def __hash__(self):
62                 return hash((self.atom, self.depatom, tuple(self.use)))
63
64         def __repr__(self):
65                 return "<%s %r>" % (self.__class__.__name__, self.atom)
66
67         def environment(self, envvars):
68                 """Returns predefined env vars DEPEND, SRC_URI, etc."""
69
70                 # Try to use the Portage tree first, since emerge only uses the tree
71                 # when calculating dependencies
72                 try:
73                         result = portage.db[portage.root]["porttree"].dbapi.aux_get(self.cpv, envvars)
74                 except KeyError:
75                         try:
76                                 result = portage.db[portage.root]["vartree"].dbapi.aux_get(self.cpv, envvars)
77                         except KeyError:
78                                 return []
79                 return result
80
81         def get_depend(self):
82                 """Get the contents of DEPEND and parse it with self.parser."""
83
84                 try:
85                         return self.parser(self.environment(('DEPEND',))[0])
86                 except portage.exception.InvalidPackageName as err:
87                         raise errors.GentoolkitInvalidCPV(err)
88
89         def get_pdepend(self):
90                 """Get the contents of PDEPEND and parse it with self.parser."""
91
92                 try:
93                         return self.parser(self.environment(('PDEPEND',))[0])
94                 except portage.exception.InvalidPackageName as err:
95                         raise errors.GentoolkitInvalidCPV(err)
96
97         def get_rdepend(self):
98                 """Get the contents of RDEPEND and parse it with self.parser."""
99
100                 try:
101                         return self.parser(self.environment(('RDEPEND',))[0])
102                 except portage.exception.InvalidPackageName as err:
103                         raise errors.GentoolkitInvalidCPV(err)
104
105         def get_all_depends(self):
106                 """Get the contents of ?DEPEND and parse it with self.parser."""
107
108                 env_vars = ('DEPEND', 'PDEPEND', 'RDEPEND')
109                 try:
110                         return self.parser(' '.join(self.environment(env_vars)))
111                 except portage.exception.InvalidPackageName as err:
112                         raise errors.GentoolkitInvalidCPV(err)
113
114         def graph_depends(
115                 self,
116                 max_depth=1,
117                 printer_fn=None,
118                 # The rest of these are only used internally:
119                 depth=1,
120                 seen=None,
121                 depcache=None,
122                 result=None
123         ):
124                 """Graph direct dependencies for self.
125
126                 Optionally gather indirect dependencies.
127
128                 @type max_depth: int
129                 @keyword max_depth: Maximum depth to recurse if.
130                         <1 means no maximum depth
131                         >0 means recurse only this depth;
132                 @type printer_fn: callable
133                 @keyword printer_fn: If None, no effect. If set, it will be applied to
134                         each result.
135                 @rtype: list
136                 @return: [(depth, pkg), ...]
137                 """
138                 if seen is None:
139                         seen = set()
140                 if depcache is None:
141                         depcache = dict()
142                 if result is None:
143                         result = list()
144
145                 pkgdep = None
146                 deps = self.get_all_depends()
147                 for dep in deps:
148                         if dep.atom in depcache:
149                                 continue
150                         try:
151                                 pkgdep = depcache[dep.atom]
152                         except KeyError:
153                                 pkgdep = Query(dep.atom).find_best()
154                                 depcache[dep.atom] = pkgdep
155                         if not pkgdep:
156                                 continue
157                         elif pkgdep.cpv in seen:
158                                 continue
159                         if depth <= max_depth or max_depth == 0:
160                                 if printer_fn is not None:
161                                         printer_fn(depth, pkgdep, dep)
162                                 result.append((depth,pkgdep))
163
164                                 seen.add(pkgdep.cpv)
165                                 if depth < max_depth or max_depth == 0:
166                                         # result is passed in and added to directly
167                                         # so rdeps is disposable
168                                         rdeps = pkgdep.deps.graph_depends(
169                                                         max_depth=max_depth,
170                                                         printer_fn=printer_fn,
171                                                         # The rest of these are only used internally:
172                                                         depth=depth+1,
173                                                         seen=seen,
174                                                         depcache=depcache,
175                                                         result=result
176                                                 )
177                 return result
178
179         def graph_reverse_depends(
180                 self,
181                 pkgset=None,
182                 max_depth=-1,
183                 only_direct=True,
184                 printer_fn=None,
185                 # The rest of these are only used internally:
186                 depth=0,
187                 depcache=None,
188                 seen=None,
189                 result=None
190         ):
191                 """Graph direct reverse dependencies for self.
192
193                 Example usage:
194                         >>> from gentoolkit.dependencies import Dependencies
195                         >>> ffmpeg = Dependencies('media-video/ffmpeg-0.5_p20373')
196                         >>> # I only care about installed packages that depend on me:
197                         ... from gentoolkit.helpers import get_installed_cpvs
198                         >>> # I want to pass in a sorted list. We can pass strings or
199                         ... # Package or Atom types, so I'll use Package to sort:
200                         ... from gentoolkit.package import Package
201                         >>> installed = sorted(Package(x) for x in get_installed_cpvs())
202                         >>> deptree = ffmpeg.graph_reverse_depends(
203                         ...     only_direct=False,  # Include indirect revdeps
204                         ...     pkgset=installed)   # from installed pkgset
205                         >>> len(deptree)
206                         44
207
208                 @type pkgset: iterable
209                 @keyword pkgset: sorted pkg cpv strings or anything sublassing
210                         L{gentoolkit.cpv.CPV} to use for calculate our revdep graph.
211                 @type max_depth: int
212                 @keyword max_depth: Maximum depth to recurse if only_direct=False.
213                         -1 means no maximum depth;
214                          0 is the same as only_direct=True;
215                         >0 means recurse only this many times;
216                 @type only_direct: bool
217                 @keyword only_direct: to recurse or not to recurse
218                 @type printer_fn: callable
219                 @keyword printer_fn: If None, no effect. If set, it will be applied to
220                         each L{gentoolkit.atom.Atom} object as it is added to the results.
221                 @rtype: list
222                 @return: L{gentoolkit.dependencies.Dependencies} objects
223                 """
224                 if not pkgset:
225                         err = ("%s kwarg 'pkgset' must be set. "
226                                 "Can be list of cpv strings or any 'intersectable' object.")
227                         raise errors.GentoolkitFatalError(err % (self.__class__.__name__,))
228
229                 if depcache is None:
230                         depcache = dict()
231                 if seen is None:
232                         seen = set()
233                 if result is None:
234                         result = list()
235
236                 if depth == 0:
237                         pkgset = tuple(Dependencies(x) for x in pkgset)
238
239                 pkgdep = None
240                 for pkgdep in pkgset:
241                         try:
242                                 all_depends = depcache[pkgdep]
243                         except KeyError:
244                                 all_depends = uniqify(pkgdep.get_all_depends())
245                                 depcache[pkgdep] = all_depends
246
247                         dep_is_displayed = False
248                         for dep in all_depends:
249                                 # TODO: Add ability to determine if dep is enabled by USE flag.
250                                 #       Check portage.dep.use_reduce
251                                 if dep.intersects(self):
252                                         pkgdep.depth = depth
253                                         pkgdep.matching_dep = dep
254                                         if printer_fn is not None:
255                                                 printer_fn(pkgdep, dep_is_displayed=dep_is_displayed)
256                                         result.append(pkgdep)
257                                         dep_is_displayed = True
258
259                         # if --indirect specified, call ourselves again with the dep
260                         # Do not call if we have already called ourselves.
261                         if (
262                                 dep_is_displayed and not only_direct and
263                                 pkgdep.cpv not in seen and
264                                 (depth < max_depth or max_depth == -1)
265                         ):
266
267                                 seen.add(pkgdep.cpv)
268                                 result.append(
269                                         pkgdep.graph_reverse_depends(
270                                                 pkgset=pkgset,
271                                                 max_depth=max_depth,
272                                                 only_direct=only_direct,
273                                                 printer_fn=printer_fn,
274                                                 depth=depth+1,
275                                                 depcache=depcache,
276                                                 seen=seen,
277                                                 result=result
278                                         )
279                                 )
280
281                 if depth == 0:
282                         return result
283                 return pkgdep
284
285         def _parser(self, deps, use_conditional=None, depth=0):
286                 """?DEPEND file parser.
287
288                 @rtype: list
289                 @return: L{gentoolkit.atom.Atom} objects
290                 """
291                 result = []
292
293                 if depth == 0:
294                         deps = paren_reduce(deps)
295                 for tok in deps:
296                         if tok == '||':
297                                 continue
298                         if tok[-1] == '?':
299                                 use_conditional = tok[:-1]
300                                 continue
301                         if isinstance(tok, list):
302                                 sub_r = self._parser(tok, use_conditional, depth=depth+1)
303                                 result.extend(sub_r)
304                                 use_conditional = None
305                                 continue
306                         # FIXME: This is a quick fix for bug #299260.
307                         #        A better fix is to not discard blockers in the parser,
308                         #        but to check for atom.blocker in whatever equery/depends
309                         #        (in this case) and ignore them there.
310                         # TODO: Test to see how much a performance impact ignoring
311                         #       blockers here rather than checking for atom.blocker has.
312                         if tok[0] == '!':
313                                 # We're not interested in blockers
314                                 continue
315                         # skip it if it's empty
316                         if tok and tok != '':
317                                 atom = Atom(tok)
318                                 if use_conditional is not None:
319                                         atom.use_conditional = use_conditional
320                                 result.append(atom)
321                         else:
322                                 message = "dependencies.py: _parser() found an empty " +\
323                                         "dep string token for: %s, deps= %s"
324                                 raise errors.GentoolkitInvalidAtom(message %(self.cpv, deps))
325
326                 return result
327
328 # vim: set ts=4 sw=4 tw=0: