Use join() rather than string printing.
[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 from portage.output import colorize
7
8 __all__ = ['keywords_content']
9
10 from display_pretty import colorize_string
11 from display_pretty import align_string
12
13 class keywords_content:
14         class RedundancyChecker:
15                 def __listRedundant(self, keywords, ignoreslots, slots):
16                         """List all redundant packages."""
17                         if ignoreslots:
18                                 return self.__listRedundantAll(keywords)
19                         else:
20                                 return self.__listRedundantSlots(keywords, slots)
21
22                 def __listRedundantSlots(self, keywords, slots):
23                         """Search for redundant packages walking per keywords for specified slot."""
24                         result = [self.__compareSelected([k for k, s in zip(keywords, slots)
25                                 if s == slot])
26                                         for slot in self.__uniq(slots)]
27                         # this is required because the list itself is not just one level depth
28                         return list(''.join(result))
29
30                 def __uniq(self, seq):
31                         """Remove all duplicate elements from list."""
32                         seen = {}
33                         result = []
34                         for item in seq:
35                                 if item in seen:
36                                         continue
37                                 seen[item] = 1
38                                 result.append(item)
39                         return result
40
41                 def __listRedundantAll(self, keywords):
42                         """Search for redundant packages using all versions ignoring its slotting."""
43                         return list(self.__compareSelected(list(keywords)))
44
45                 def __compareSelected(self, kws):
46                         """
47                         Rotate over list of keywords and compare each element with others.
48                         Selectively remove each already compared list from the remaining keywords.
49                         """
50                         result = []
51                         kws.reverse()
52                         for i in range(len(kws)):
53                                 kw = kws.pop()
54                                 if self.__compareKeywordWithRest(kw, kws):
55                                         result.append('#')
56                                 else:
57                                         result.append('o')
58                         if len(result) == 0:
59                                 result.append('o')
60                         return ''.join(result)
61
62                 def __compareKeywordWithRest(self, keyword, keywords):
63                         """Compare keywords with list of keywords."""
64                         for key in keywords:
65                                 if self.__checkShadow(keyword, key):
66                                         return True
67                         return False
68
69                 def __checkShadow(self, old, new):
70                         """Check if package version is overshadowed by other package version."""
71                         # remove -* and -arch since they are useless for us
72                         newclean = ["%s" % x for x in new.split()
73                                 if x != '-*' and not x.startswith('-')]
74                         oldclean = ["%s" % x for x in old.split()
75                                 if x != '-*' and not x.startswith('-')]
76
77                         tmp = set(newclean)
78                         tmp.update("~%s" % x for x in newclean
79                                 if not x.startswith("~"))
80                         if not set(oldclean).difference(tmp):
81                                 return True
82                         else:
83                                 return False
84
85                 def __init__(self, keywords, slots, ignore_slots = False):
86                         """Query all relevant data for redundancy package checking"""
87                         self.redundant = self.__listRedundant(keywords, ignore_slots, slots)
88
89         class VersionChecker:
90                 def __getVersions(self, packages, vartree):
91                         """Obtain properly aligned version strings without colors."""
92                         return self.__stripStartingSpaces(map(lambda x: self.__separateVersion(x, vartree), packages))
93
94                 def __stripStartingSpaces(self, pvs):
95                         """Strip starting whitespace if there is no real reason for it."""
96                         if not self.__require_prepend:
97                                         return map(lambda x: x.lstrip(), pvs)
98                         else:
99                                 return pvs
100
101                 def __separateVersion(self, cpv, vartree):
102                         """Get version string for specfied cpv"""
103                         #pv = port.versions.cpv_getversion(cpv)
104                         return self.__prependVersionInfo(cpv, self.cpv_getversion(cpv), vartree)
105
106                 # remove me when portage 2.1.9 is stable
107                 def cpv_getversion(self, mycpv):
108                         """Returns the v (including revision) from an cpv."""
109                         cp = port.versions.cpv_getkey(mycpv)
110                         if cp is None:
111                                 return None
112                         return mycpv[len(cp+"-"):]
113
114                 def __prependVersionInfo(self, cpv, pv, vartree):
115                         """Prefix version with string based on whether version is installed or masked."""
116                         mask = self.__getMaskStatus(cpv)
117                         install = self.__getInstallStatus(cpv, vartree)
118
119                         if mask and install:
120                                 pv = '[M][I]%s' % pv
121                                 self.__require_longprepend = True
122                         elif mask:
123                                 pv = '[M]%s' % pv
124                                 self.__require_prepend = True
125                         elif install:
126                                 pv = '[I]%s' % pv
127                                 self.__require_prepend = True
128                         return pv
129
130                 def __getMaskStatus(self, cpv):
131                         """
132                         Figure out if package is pmasked.
133                         This also uses user settings in /etc/ so local changes are important.
134                         """
135                         pmask = False
136                         try:
137                                 if port.getmaskingstatus(cpv) == ['package.mask']:
138                                         pmask = True
139                         except:
140                                 # occurs when package is not known by portdb
141                                 # so we consider it unmasked
142                                 pass
143                         return pmask
144
145                 def __getInstallStatus(self, cpv, vartree):
146                         """Check if package version we test is installed."""
147                         return vartree.cpv_exists(cpv)
148
149                 def __init__(self, packages, vartree):
150                         """Query all relevant data for version data formatting"""
151                         self.__require_longprepend = False
152                         self.__require_prepend = False
153                         self.versions = self.__getVersions(packages, vartree)
154
155         def __checkExist(self, pdb, package):
156                 """Check if specified package even exists."""
157                 try:
158                         matches = pdb.xmatch('match-all', package)
159                 except port.exception.AmbiguousPackageName as Arg:
160                         msg_err = 'Ambiguous package name "%s".\n' % package
161                         found = 'Possibilities: %s' % Arg
162                         raise SystemExit('%s%s' % (msg_err, found))
163                 except port.exception.InvalidAtom:
164                         msg_err = 'No such package "%s"' % package
165                         raise SystemExit(msg_err)
166                 if len(matches) <= 0:
167                         msg_err = 'No such package "%s"' % package
168                         raise SystemExit(msg_err)
169                 return matches
170
171         def __getMetadata(self, pdb, packages):
172                 """Obtain all KEYWORDS and SLOT from metadata"""
173                 try:
174                         metadata = map(lambda x: pdb.aux_get(x, ['KEYWORDS', 'SLOT', 'repository']), packages)
175                 except KeyError:
176                         # portage prints out more verbose error for us if we were lucky
177                         raise SystemExit('Failed to obtain metadata')
178                 return list(zip(*metadata))
179
180         def __formatKeywords(self, keywords, keywords_list, usebold = False, toplist = 'archlist'):
181                 """Loop over all keywords and replace them with nice visual identifier"""
182                 # the % is fancy separator, we use it to split keywords for rotation
183                 # so we wont loose the empty spaces
184                 return ['% %'.join([self.__prepareKeywordChar(arch, i, version.split(), usebold, toplist)
185                         for i, arch in enumerate(keywords_list)])
186                                 for version in keywords]
187
188         def __prepareKeywordChar(self, arch, field, keywords, usebold = False, toplist = 'archlist'):
189                 """
190                 Convert specified keywords for package into their visual replacements.
191                 # possibilities:
192                 # ~arch -> orange ~
193                 # -arch -> red -
194                 # arch -> green +
195                 # -* -> red *
196                 """
197                 keys = [ '~%s' % arch, '-%s' % arch, '%s' % arch, '-*' ]
198                 nocolor_values = [ '~', '-', '+', '*' ]
199                 values = [
200                         colorize('darkyellow', '~'),
201                         colorize('darkred', '-'),
202                         colorize('darkgreen', '+'),
203                         colorize('darkred', '*')
204                 ]
205                 # check what keyword we have
206                 # here we cant just append space because it would get stripped later
207                 char = colorize('darkgray','o')
208                 for k, v, n in zip(keys, values, nocolor_values):
209                         if k in keywords:
210                                 char = v
211                                 break
212                 if toplist == 'archlist' and usebold and (field)%2 == 0 and char != ' ':
213                         char = colorize('bold', char)
214                 return char
215
216         def __formatVersions(self, versions, align, length):
217                 """Append colors and align keywords properly"""
218                 # % are used as separators for further split so we wont loose spaces and coloring
219                 tmp = []
220                 for pv in versions:
221                         pv = align_string(pv, align, length)
222                         pv = '%'.join(list(pv))
223                         if pv.find('[%M%][%I%]') != -1:
224                                 tmp.append(colorize_string('darkyellow', pv))
225                         elif pv.find('[%M%]') != -1:
226                                 tmp.append(colorize_string('darkred', pv))
227                         elif pv.find('[%I%]') != -1:
228                                 tmp.append(colorize_string('bold', pv))
229                         else:
230                                 tmp.append(pv)
231                 return tmp
232
233         def __formatAdditional(self, additional, color, length):
234                 """Align additional items properly"""
235                 # % are used as separators for further split so we wont loose spaces and coloring
236                 tmp = []
237                 for x in additional:
238                         tmpc = color
239                         x = align_string(x, 'left', length)
240                         x = '%'.join(list(x))
241                         if x == 'o':
242                                 # the value is unset so the color is gray
243                                 tmpc = 'darkgray'
244                         x = colorize_string(tmpc, x)
245                         tmp.append(x)
246                 return tmp
247
248         def __prepareContentResult(self, versions, keywords, redundant, slots, slot_length, repos, linesep):
249                 """Parse version fields into one list with proper separators"""
250                 content = []
251                 oldslot = ''
252                 fieldsep = '% %|% %'
253                 normsep = '% %'
254                 for v, k, r, s, t in zip(versions, keywords, redundant, slots, repos):
255                         if oldslot != s:
256                                 oldslot = s
257                                 content.append(linesep)
258                         else:
259                                 s = '%'.join(list(''.rjust(slot_length)))
260                         content.append('%s%s%s%s%s%s%s%s%s' % (v, fieldsep, k, fieldsep, r, normsep, s, fieldsep, t))
261                 return content
262
263         def __init__(self, package, keywords_list, porttree, ignoreslots = False, content_align = 'bottom', usebold = False, toplist = 'archlist'):
264                 """Query all relevant data from portage databases."""
265                 vartree = port.db[port.settings['ROOT']]['vartree'].dbapi
266                 packages = self.__checkExist(porttree, package)
267                 self.keywords, self.slots, self.repositories = self.__getMetadata(porttree, packages)
268                 self.slot_length = max([len(x) for x in self.slots])
269                 repositories_length = max([len(x) for x in self.repositories])
270                 self.keyword_length = len(keywords_list)
271                 self.versions = self.VersionChecker(packages, vartree).versions
272                 self.version_length = max([len(x) for x in self.versions])
273                 self.version_count = len(self.versions)
274                 self.redundant = self.RedundancyChecker(self.keywords, self.slots, ignoreslots).redundant
275                 redundant_length = max([len(x) for x in self.redundant])
276
277                 ver = self.__formatVersions(self.versions, content_align, self.version_length)
278                 kws = self.__formatKeywords(self.keywords, keywords_list, usebold, toplist)
279                 red = self.__formatAdditional(self.redundant, 'purple', redundant_length)
280                 slt = self.__formatAdditional(self.slots, 'bold', self.slot_length)
281                 rep = self.__formatAdditional(self.repositories, 'yellow', repositories_length)
282                 # those + nubers are spaces in printout. keywords are multiplied also because of that
283                 linesep = '%s+%s+%s+%s' % (''.ljust(self.version_length+1, '-'),
284                         ''.ljust(self.keyword_length*2+1, '-'),
285                         ''.ljust(redundant_length+self.slot_length+3, '-'),
286                         ''.ljust(repositories_length+1, '-')
287                 )
288
289                 self.content = self.__prepareContentResult(ver, kws, red, slt, self.slot_length, rep, linesep)
290                 self.content_length = len(linesep)
291                 self.cp = port.cpv_getkey(packages[0])