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