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