apply djanderson's idea for a simpler/better fix to bug 353430 and commit 000ce49860b...
[gentoolkit.git] / pym / gentoolkit / eshowkw / keywords_content.py
1 #       vim:fileencoding=utf-8
2 # Copyright 2010 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 import portage as port
6 import os
7 from portage.output import colorize
8
9 __all__ = ['keywords_content']
10
11 from display_pretty import colorize_string
12 from display_pretty import align_string
13
14 class keywords_content:
15         class RedundancyChecker:
16                 def __listRedundant(self, masks, keywords, ignoreslots, slots):
17                         """List all redundant packages."""
18                         if ignoreslots:
19                                 return self.__listRedundantAll(masks, keywords)
20                         else:
21                                 return self.__listRedundantSlots(masks, keywords, slots)
22
23                 def __listRedundantSlots(self, masks, keywords, slots):
24                         """Search for redundant packages walking per keywords for specified slot."""
25                         output = list()
26                         for slot in self.__uniq(slots):
27                                 ms = list()
28                                 ks = list()
29                                 for m, k, s in zip(masks, keywords, slots):
30                                         if slot == s:
31                                                 ms.append(m)
32                                                 ks.append(k)
33                                 output.append(self.__compareSelected(ms, ks))
34                         # this is required because the list itself is not just one level depth
35                         return list(''.join(output))
36
37                 def __uniq(self, seq):
38                         """Remove all duplicate elements from list."""
39                         seen = {}
40                         result = []
41                         for item in seq:
42                                 if item in seen:
43                                         continue
44                                 seen[item] = 1
45                                 result.append(item)
46                         return result
47
48                 def __cleanKeyword(self, keyword):
49                         """Remove masked arches and hardmasks from keywords since we don't care about that."""
50                         return ["%s" % x for x in keyword.split()
51                                 if x != '-*' and not x.startswith('-')]
52
53                 def __listRedundantAll(self, masks, keywords):
54                         """Search for redundant packages using all versions ignoring its slotting."""
55                         return list(self.__compareSelected(list(masks), list(keywords)))
56
57                 def __compareSelected(self, masks, kws):
58                         """
59                         Rotate over list of keywords and compare each element with others.
60                         Selectively remove each already compared list from the remaining keywords.
61                         """
62                         result = []
63                         kws.reverse()
64                         masks.reverse()
65                         for i in range(len(kws)):
66                                 kw = kws.pop()
67                                 masks.pop()
68                                 if self.__compareKeywordWithRest(kw, kws, masks):
69                                         result.append('#')
70                                 else:
71                                         result.append('o')
72                         if len(result) == 0:
73                                 result.append('o')
74                         return ''.join(result)
75
76                 def __compareKeywordWithRest(self, keyword, keywords, masks):
77                         """Compare keywords with list of keywords."""
78                         kw = self.__cleanKeyword(keyword)
79                         for kwi, mask in zip(keywords, masks):
80                                 kwi = self.__cleanKeyword(kwi)
81                                 if kwi and not mask:
82                                         kw = self.__checkShadow(kw, kwi)
83                                 if not kw:
84                                         return True
85                         return False
86
87                 def __checkShadow(self, old, new):
88                         """Check if package version is overshadowed by other package version."""
89                         tmp = set(new)
90                         tmp.update("~%s" % x for x in new
91                                 if not x.startswith("~"))
92                         return list(set(old).difference(tmp))
93
94                 def __init__(self, masks, keywords, slots, ignore_slots = False):
95                         """Query all relevant data for redundancy package checking"""
96                         self.redundant = self.__listRedundant(masks, keywords, ignore_slots, slots)
97
98         class VersionChecker:
99                 def __getVersions(self, packages):
100                         """Obtain properly aligned version strings without colors."""
101                         return map(lambda x: self.__separateVersion(x), packages)
102
103                 def __separateVersion(self, cpv):
104                         """Get version string for specfied cpv"""
105                         #pv = port.versions.cpv_getversion(cpv)
106                         return self.__prependVersionInfo(cpv, self.cpv_getversion(cpv))
107
108                 # remove me when portage 2.1.9 is stable
109                 def cpv_getversion(self, mycpv):
110                         """Returns the v (including revision) from an cpv."""
111                         cp = port.versions.cpv_getkey(mycpv)
112                         if cp is None:
113                                 return None
114                         return mycpv[len(cp+"-"):]
115
116                 def __prependVersionInfo(self, cpv, pv):
117                         """Prefix version with string based on whether version is installed or masked."""
118                         mask = self.__getMaskStatus(cpv)
119                         install = self.__getInstallStatus(cpv)
120
121                         if mask and install:
122                                 pv = '[M][I]%s' % pv
123                         elif mask:
124                                 pv = '[M]%s' % pv
125                         elif install:
126                                 pv = '[I]%s' % pv
127                         return pv
128
129                 def __getMaskStatus(self, cpv):
130                         """Figure out if package is pmasked."""
131                         try:
132                                 mysettings = port.config(local_config=False)
133                                 if port.getmaskingstatus(cpv, settings=mysettings) == ['package.mask']:
134                                         return True
135                         except:
136                                 # occurs when package is not known by portdb
137                                 # so we consider it unmasked
138                                 pass
139                         return False
140
141                 def __getInstallStatus(self, cpv):
142                         """Check if package version we test is installed."""
143                         vartree = port.db[port.settings['ROOT']]['vartree'].dbapi
144                         return vartree.cpv_exists(cpv)
145
146                 def __init__(self, packages):
147                         """Query all relevant data for version data formatting"""
148                         self.versions = self.__getVersions(packages)
149                         self.masks = map(lambda x: self.__getMaskStatus(x), packages)
150
151         def __packages_sort(self, package_content):
152                 """
153                 Sort packages queried based on version and slot
154                 %% pn , repo, slot, keywords
155                 """
156                 from operator import itemgetter 
157
158                 if len(package_content) > 1:
159                         ver_map = {}
160                         for cpv in package_content:
161                                 ver_map[cpv[0]] = '-'.join(port.versions.catpkgsplit(cpv[0])[2:])
162                         def cmp_cpv(cpv1, cpv2):
163                                 return port.versions.vercmp(ver_map[cpv1[0]], ver_map[cpv2[0]])
164
165                         package_content.sort(key=port.util.cmp_sort_key(cmp_cpv))
166                         package_content.sort(key=itemgetter(2))
167
168         def __xmatch(self, pdb, package):
169                 """xmatch function that searches for all packages over all repos"""
170                 try:
171                         mycp = port.dep_expand(package, mydb=pdb, settings=pdb.settings).cp
172                 except port.exception.AmbiguousPackageName as Arg:
173                         msg_err = 'Ambiguous package name "%s".\n' % package
174                         found = 'Possibilities: %s' % Arg
175                         raise SystemExit('%s%s' % (msg_err, found))
176                 except port.exception.InvalidAtom:
177                         msg_err = 'No such package "%s"' % package
178                         raise SystemExit(msg_err)
179
180                 mysplit = mycp.split('/')
181                 mypkgs = []
182                 for oroot in pdb.porttrees:
183                         try:
184                                 file_list = os.listdir(os.path.join(oroot, mycp))
185                         except OSError:
186                                 continue
187                         for x in file_list:
188                                 pf = x[:-7] if x[-7:] == '.ebuild' else []
189                                 if pf:
190                                         ps = port.pkgsplit(pf)
191                                         if not ps or ps[0] != mysplit[1]:
192                                                 # we got garbage or ebuild with wrong name in the dir
193                                                 continue
194                                         ver_match = port.versions.ver_regexp.match("-".join(ps[1:]))
195                                         if ver_match is None or not ver_match.groups():
196                                                 # version is not allowed by portage or unset
197                                                 continue
198                                         # obtain related data from metadata and append to the pkg list
199                                         keywords, slot = self.__getMetadata(pdb, mysplit[0]+'/'+pf, oroot)
200                                         mypkgs.append([mysplit[0]+'/'+pf, oroot, slot, keywords])
201
202                 self.__packages_sort(mypkgs)
203                 return mypkgs
204
205         def __checkExist(self, pdb, package):
206                 """Check if specified package even exists."""
207                 matches = self.__xmatch(pdb, package)
208                 if len(matches) <= 0:
209                         msg_err = 'No such package "%s"' % package
210                         raise SystemExit(msg_err)
211                 return list(zip(*matches))
212
213         def __getMetadata(self, pdb, package, repo):
214                 """Obtain all required metadata from portage auxdb"""
215                 try:
216                         metadata = pdb.aux_get(package, ['KEYWORDS', 'SLOT'], repo)
217                 except KeyError:
218                         # portage prints out more verbose error for us if we were lucky
219                         raise SystemExit('Failed to obtain metadata')
220                 return metadata
221
222         def __formatKeywords(self, keywords, keywords_list, usebold = False, toplist = 'archlist'):
223                 """Loop over all keywords and replace them with nice visual identifier"""
224                 # the % is fancy separator, we use it to split keywords for rotation
225                 # so we wont loose the empty spaces
226                 return ['% %'.join([self.__prepareKeywordChar(arch, i, version.split(), usebold, toplist)
227                         for i, arch in enumerate(keywords_list)])
228                                 for version in keywords]
229
230         def __prepareKeywordChar(self, arch, field, keywords, usebold = False, toplist = 'archlist'):
231                 """
232                 Convert specified keywords for package into their visual replacements.
233                 # possibilities:
234                 # ~arch -> orange ~
235                 # -arch -> red -
236                 # arch -> green +
237                 # -* -> red *
238                 """
239                 keys = [ '~%s' % arch, '-%s' % arch, '%s' % arch, '-*' ]
240                 values = [
241                         colorize('darkyellow', '~'),
242                         colorize('darkred', '-'),
243                         colorize('darkgreen', '+'),
244                         colorize('darkred', '*')
245                 ]
246                 # check what keyword we have
247                 # here we cant just append space because it would get stripped later
248                 char = colorize('darkgray','o')
249                 for k, v in zip(keys, values):
250                         if k in keywords:
251                                 char = v
252                                 break
253                 if toplist == 'archlist' and usebold and (field)%2 == 0 and char != ' ':
254                         char = colorize('bold', char)
255                 return char
256
257         def __formatVersions(self, versions, align, length):
258                 """Append colors and align keywords properly"""
259                 # % are used as separators for further split so we wont loose spaces and coloring
260                 tmp = []
261                 for pv in versions:
262                         pv = align_string(pv, align, length)
263                         pv = '%'.join(list(pv))
264                         if pv.find('[%M%][%I%]') != -1:
265                                 tmp.append(colorize_string('darkyellow', pv))
266                         elif pv.find('[%M%]') != -1:
267                                 tmp.append(colorize_string('darkred', pv))
268                         elif pv.find('[%I%]') != -1:
269                                 tmp.append(colorize_string('bold', pv))
270                         else:
271                                 tmp.append(pv)
272                 return tmp
273
274         def __formatAdditional(self, additional, color, length):
275                 """Align additional items properly"""
276                 # % are used as separators for further split so we wont loose spaces and coloring
277                 tmp = []
278                 for x in additional:
279                         tmpc = color
280                         x = align_string(x, 'left', length)
281                         x = '%'.join(list(x))
282                         if x == 'o':
283                                 # the value is unset so the color is gray
284                                 tmpc = 'darkgray'
285                         x = colorize_string(tmpc, x)
286                         tmp.append(x)
287                 return tmp
288
289         def __prepareContentResult(self, versions, keywords, redundant, slots, slot_length, repos, linesep):
290                 """Parse version fields into one list with proper separators"""
291                 content = []
292                 oldslot = ''
293                 fieldsep = '% %|% %'
294                 normsep = '% %'
295                 for v, k, r, s, t in zip(versions, keywords, redundant, slots, repos):
296                         if oldslot != s:
297                                 oldslot = s
298                                 content.append(linesep)
299                         else:
300                                 s = '%'.join(list(''.rjust(slot_length)))
301                         content.append('%s%s%s%s%s%s%s%s%s' % (v, fieldsep, k, fieldsep, r, normsep, s, fieldsep, t))
302                 return content
303
304         def __init__(self, package, keywords_list, porttree, ignoreslots = False, content_align = 'bottom', usebold = False, toplist = 'archlist'):
305                 """Query all relevant data from portage databases."""
306                 packages, self.repositories, self.slots, self.keywords = self.__checkExist(porttree, package)
307                 # convert repositories from path to name
308                 self.repositories = [porttree.getRepositoryName(x) for x in self.repositories]
309                 self.slot_length = max([len(x) for x in self.slots])
310                 repositories_length = max([len(x) for x in self.repositories])
311                 self.keyword_length = len(keywords_list)
312                 self.versions = self.VersionChecker(packages).versions
313                 masks = self.VersionChecker(packages).masks
314                 self.version_length = max([len(x) for x in self.versions])
315                 self.version_count = len(self.versions)
316                 self.redundant = self.RedundancyChecker(masks, self.keywords, self.slots, ignoreslots).redundant
317                 redundant_length = max([len(x) for x in self.redundant])
318
319                 ver = self.__formatVersions(self.versions, content_align, self.version_length)
320                 kws = self.__formatKeywords(self.keywords, keywords_list, usebold, toplist)
321                 red = self.__formatAdditional(self.redundant, 'purple', redundant_length)
322                 slt = self.__formatAdditional(self.slots, 'bold', self.slot_length)
323                 rep = self.__formatAdditional(self.repositories, 'yellow', repositories_length)
324                 # those + nubers are spaces in printout. keywords are multiplied also because of that
325                 linesep = '%s+%s+%s+%s' % (''.ljust(self.version_length+1, '-'),
326                         ''.ljust(self.keyword_length*2+1, '-'),
327                         ''.ljust(redundant_length+self.slot_length+3, '-'),
328                         ''.ljust(repositories_length+1, '-')
329                 )
330
331                 self.content = self.__prepareContentResult(ver, kws, red, slt, self.slot_length, rep, linesep)
332                 self.content_length = len(linesep)
333                 self.cp = port.cpv_getkey(packages[0])