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