fix bug 380573.
[gentoolkit.git] / pym / gentoolkit / equery / meta.py
1 # Copyright(c) 2009, Gentoo Foundation
2 #
3 # Licensed under the GNU General Public License, v2 or higher
4 #
5 # $Header: $
6
7 """Display metadata about a given package."""
8
9 from __future__ import print_function
10
11 __docformat__ = 'epytext'
12
13 # =======
14 # Imports
15 # =======
16
17 import re
18 import os
19 import sys
20 from getopt import gnu_getopt, GetoptError
21 from functools import partial
22
23 import gentoolkit.pprinter as pp
24 from gentoolkit import errors
25 from gentoolkit.keyword import Keyword
26 from gentoolkit.equery import format_options, mod_usage, CONFIG
27 from gentoolkit.helpers import print_sequence, print_file
28 from gentoolkit.textwrap_ import TextWrapper
29 from gentoolkit.query import Query
30
31 # =======
32 # Globals
33 # =======
34
35 # E1101: Module 'portage.output' has no $color member
36 # portage.output creates color functions dynamically
37 # pylint: disable-msg=E1101
38
39 QUERY_OPTS = {
40         'current': False,
41         'description': False,
42         'herd': False,
43         'keywords': False,
44         'maintainer': False,
45         'useflags': False,
46         'upstream': False,
47         'xml': False
48 }
49
50 # =========
51 # Functions
52 # =========
53
54 def print_help(with_description=True, with_usage=True):
55         """Print description, usage and a detailed help message.
56
57         @type with_description: bool
58         @param with_description: if true, print module's __doc__ string
59         """
60
61         if with_description:
62                 print(__doc__.strip())
63                 print()
64         if with_usage:
65                 print(mod_usage(mod_name="meta"))
66                 print()
67         print(pp.command("options"))
68         print(format_options((
69                 (" -h, --help", "display this help message"),
70                 (" -d, --description", "show an extended package description"),
71                 (" -H, --herd", "show the herd(s) for the package"),
72                 (" -k, --keywords", "show keywords for all matching package versions"),
73                 (" -m, --maintainer", "show the maintainer(s) for the package"),
74                 (" -u, --useflags", "show per-package USE flag descriptions"),
75                 (" -U, --upstream", "show package's upstream information"),
76                 (" -x, --xml", "show the plain metadata.xml file")
77         )))
78
79
80 def filter_keywords(matches):
81         """Filters non-unique keywords per slot.
82
83         Does not filter arch mask keywords (-). Besides simple non-unique keywords,
84         also remove unstable keywords (~) if a higher version in the same slot is
85         stable. This view makes version bumps easier for package maintainers.
86
87         @type matches: array
88         @param matches: set of L{gentoolkit.package.Package} instances whose
89                 'key' are all the same.
90         @rtype: dict
91         @return: a dict with L{gentoolkit.package.Package} instance keys and
92                 'array of keywords not found in a higher version of pkg within the
93                 same slot' values.
94         """
95         def del_archmask(keywords):
96                 """Don't add arch_masked to filter set."""
97                 return [x for x in keywords if not x.startswith('-')]
98
99         def add_unstable(keywords):
100                 """Add unstable keyword for all stable keywords to filter set."""
101                 result = list(keywords)
102                 result.extend(
103                         ['~%s' % x for x in keywords if not x.startswith(('-', '~'))]
104                 )
105                 return result
106
107         result = {}
108         slot_map = {}
109         # Start from the newest
110         rev_matches = reversed(matches)
111         for pkg in rev_matches:
112                 keywords_str, slot = pkg.environment(('KEYWORDS', 'SLOT'),
113                         prefer_vdb=False)
114                 keywords = keywords_str.split()
115                 result[pkg] = [x for x in keywords if x not in slot_map.get(slot, [])]
116                 try:
117                         slot_map[slot].update(del_archmask(add_unstable(keywords)))
118                 except KeyError:
119                         slot_map[slot] = set(del_archmask(add_unstable(keywords)))
120
121         return result
122
123
124 def format_herds(herds):
125         """Format herd information for display."""
126
127         result = []
128         for herd in herds:
129                 herdstr = ''
130                 email = "(%s)" % herd[1] if herd[1] else ''
131                 herdstr = herd[0]
132                 if CONFIG['verbose']:
133                         herdstr += " %s" % (email,)
134                 result.append(herdstr)
135
136         return result
137
138
139 def format_maintainers(maints):
140         """Format maintainer information for display."""
141
142         result = []
143         for maint in maints:
144                 maintstr = ''
145                 maintstr = maint.email
146                 if CONFIG['verbose']:
147                         maintstr += " (%s)" % (maint.name,) if maint.name else ''
148                         maintstr += " - %s" % (maint.restrict,) if maint.restrict else ''
149                         maintstr += "\n%s" % (
150                                 (maint.description,) if maint.description else ''
151                         )
152                 result.append(maintstr)
153
154         return result
155
156
157 def format_upstream(upstream):
158         """Format upstream information for display."""
159
160         def _format_upstream_docs(docs):
161                 result = []
162                 for doc in docs:
163                         doc_location = doc[0]
164                         doc_lang = doc[1]
165                         docstr = doc_location
166                         if doc_lang is not None:
167                                 docstr += " (%s)" % (doc_lang,)
168                         result.append(docstr)
169                 return result
170
171         def _format_upstream_ids(ids):
172                 result = []
173                 for id_ in ids:
174                         site = id_[0]
175                         proj_id = id_[1]
176                         idstr = "%s ID: %s" % (site, proj_id)
177                         result.append(idstr)
178                 return result
179
180         result = []
181         for up in upstream:
182                 upmaints = format_maintainers(up.maintainers)
183                 for upmaint in upmaints:
184                         result.append(format_line(upmaint, "Maintainer:  ", " " * 13))
185
186                 for upchange in up.changelogs:
187                         result.append(format_line(upchange, "ChangeLog:   ", " " * 13))
188
189                 updocs = _format_upstream_docs(up.docs)
190                 for updoc in updocs:
191                         result.append(format_line(updoc, "Docs:       ", " " * 13))
192
193                 for upbug in up.bugtrackers:
194                         result.append(format_line(upbug, "Bugs-to:     ", " " * 13))
195
196                 upids = _format_upstream_ids(up.remoteids)
197                 for upid in upids:
198                         result.append(format_line(upid, "Remote-ID:   ", " " * 13))
199
200         return result
201
202
203 def format_useflags(useflags):
204         """Format USE flag information for display."""
205
206         result = []
207         for flag in useflags:
208                 result.append(pp.useflag(flag.name))
209                 result.append(flag.description)
210                 result.append("")
211
212         return result
213
214
215 def format_keywords(keywords):
216         """Sort and colorize keywords for display."""
217
218         result = []
219
220         for kw in sorted(keywords, key=Keyword):
221                 if kw.startswith('-'):
222                         # arch masked
223                         kw = pp.keyword(kw, stable=False, hard_masked=True)
224                 elif kw.startswith('~'):
225                         # keyword masked
226                         kw = pp.keyword(kw, stable=False, hard_masked=False)
227                 else:
228                         # stable
229                         kw = pp.keyword(kw, stable=True, hard_masked=False)
230                 result.append(kw)
231
232         return ' '.join(result)
233
234
235 def format_keywords_line(pkg, fmtd_keywords, slot, verstr_len):
236         """Format the entire keywords line for display."""
237
238         ver = pkg.fullversion
239         result = "%s:%s: %s" % (ver, pp.slot(slot), fmtd_keywords)
240         if CONFIG['verbose'] and fmtd_keywords:
241                 result = format_line(fmtd_keywords, "%s:%s: " % (ver, pp.slot(slot)),
242                         " " * (verstr_len + 2))
243
244         return result
245
246
247 def format_homepage(homepage):
248         """format the homepage(s) entries for dispaly"""
249         result = []
250         for page in homepage.split():
251                 result.append(format_line(page, "Homepage:    ", " " * 13))
252         return result
253
254
255 # R0912: *Too many branches (%s/%s)*
256 # pylint: disable-msg=R0912
257 def call_format_functions(best_match, matches):
258         """Call information gathering functions and display the results."""
259
260         if CONFIG['verbose']:
261                 repo = best_match.repo_name()
262                 pp.uprint(" * %s [%s]" % (pp.cpv(best_match.cp), pp.section(repo)))
263
264         got_opts = False
265         if any(QUERY_OPTS.values()):
266                 # Specific information requested, less formatting
267                 got_opts = True
268
269         if QUERY_OPTS["herd"] or not got_opts:
270                 herds = best_match.metadata.herds(include_email=True)
271                 if any(not h[0] for h in herds):
272                         print(pp.warn("The packages metadata.xml has an empty <herd> tag"),
273                                 file = sys.stderr)
274                         herds = [x for x in herds if x[0]]
275                 herds = format_herds(herds)
276                 if QUERY_OPTS["herd"]:
277                         print_sequence(format_list(herds))
278                 else:
279                         for herd in herds:
280                                 pp.uprint(format_line(herd, "Herd:        ", " " * 13))
281
282         if QUERY_OPTS["maintainer"] or not got_opts:
283                 maints = format_maintainers(best_match.metadata.maintainers())
284                 if QUERY_OPTS["maintainer"]:
285                         print_sequence(format_list(maints))
286                 else:
287                         if not maints:
288                                 pp.uprint(format_line([], "Maintainer:  ", " " * 13))
289                         else:
290                                 for maint in maints:
291                                         pp.uprint(format_line(maint, "Maintainer:  ", " " * 13))
292
293         if QUERY_OPTS["upstream"] or not got_opts:
294                 upstream = format_upstream(best_match.metadata.upstream())
295                 homepage = format_homepage(best_match.environment("HOMEPAGE"))
296                 if QUERY_OPTS["upstream"]:
297                         upstream = format_list(upstream)
298                 else:
299                         upstream = format_list(upstream, "Upstream:    ", " " * 13)
300                 print_sequence(upstream)
301                 print_sequence(homepage)
302
303         if not got_opts:
304                 pkg_loc = best_match.package_path()
305                 pp.uprint(format_line(pkg_loc, "Location:    ", " " * 13))
306
307         if QUERY_OPTS["keywords"] or not got_opts:
308                 # Get {<Package 'dev-libs/glib-2.20.5'>: [u'ia64', u'm68k', ...], ...}
309                 keyword_map = filter_keywords(matches)
310
311                 for match in matches:
312                         slot = match.environment('SLOT')
313                         verstr_len = len(match.fullversion) + len(slot)
314                         fmtd_keywords = format_keywords(keyword_map[match])
315                         keywords_line = format_keywords_line(
316                                 match, fmtd_keywords, slot, verstr_len
317                         )
318                         if QUERY_OPTS["keywords"]:
319                                 pp.uprint(keywords_line)
320                         else:
321                                 indent = " " * (16 + verstr_len)
322                                 pp.uprint(format_line(keywords_line, "Keywords:    ", indent))
323
324         if QUERY_OPTS["description"]:
325                 desc = best_match.metadata.descriptions()
326                 print_sequence(format_list(desc))
327
328         if QUERY_OPTS["useflags"]:
329                 useflags = format_useflags(best_match.metadata.use())
330                 print_sequence(format_list(useflags))
331
332         if QUERY_OPTS["xml"]:
333                 print_file(os.path.join(best_match.package_path(), 'metadata.xml'))
334
335
336 def format_line(line, first="", subsequent="", force_quiet=False):
337         """Wrap a string at word boundaries and optionally indent the first line
338         and/or subsequent lines with custom strings.
339
340         Preserve newlines if the longest line is not longer than
341         CONFIG['termWidth']. To force the preservation of newlines and indents,
342         split the string into a list and feed it to format_line via format_list.
343
344         @see: format_list()
345         @type line: string
346         @param line: text to format
347         @type first: string
348         @param first: text to prepend to the first line
349         @type subsequent: string
350         @param subsequent: text to prepend to subsequent lines
351         @type force_quiet: boolean
352         @rtype: string
353         @return: A wrapped line
354         """
355
356         if line:
357                 line = line.expandtabs().strip("\n").splitlines()
358         else:
359                 if force_quiet:
360                         return
361                 else:
362                         return first + "None specified"
363
364         if len(first) > len(subsequent):
365                 wider_indent = first
366         else:
367                 wider_indent = subsequent
368
369         widest_line_len = len(max(line, key=len)) + len(wider_indent)
370
371         if widest_line_len > CONFIG['termWidth']:
372                 twrap = TextWrapper(width=CONFIG['termWidth'], expand_tabs=False,
373                         initial_indent=first, subsequent_indent=subsequent)
374                 line = " ".join(line)
375                 line = re.sub("\s+", " ", line)
376                 line = line.lstrip()
377                 result = twrap.fill(line)
378         else:
379                 # line will fit inside CONFIG['termWidth'], so preserve whitespace and
380                 # newlines
381                 line[0] = first + line[0]          # Avoid two newlines if len == 1
382
383                 if len(line) > 1:
384                         line[0] = line[0] + "\n"
385                         for i in range(1, (len(line[1:-1]) + 1)):
386                                 line[i] = subsequent + line[i] + "\n"
387                         line[-1] = subsequent + line[-1]  # Avoid two newlines on last line
388
389                 if line[-1].isspace():
390                         del line[-1]                # Avoid trailing blank lines
391
392                 result = "".join(line)
393
394         return result
395
396
397 def format_list(lst, first="", subsequent="", force_quiet=False):
398         """Feed elements of a list to format_line().
399
400         @see: format_line()
401         @type lst: list
402         @param lst: list to format
403         @type first: string
404         @param first: text to prepend to the first line
405         @type subsequent: string
406         @param subsequent: text to prepend to subsequent lines
407         @rtype: list
408         @return: list with element text wrapped at CONFIG['termWidth']
409         """
410
411         result = []
412         if lst:
413                 # Format the first line
414                 line = format_line(lst[0], first, subsequent, force_quiet)
415                 result.append(line)
416                 # Format subsequent lines
417                 for elem in lst[1:]:
418                         if elem:
419                                 result.append(format_line(elem, subsequent, subsequent,
420                                         force_quiet))
421                         else:
422                                 # We don't want to send a blank line to format_line()
423                                 result.append("")
424         else:
425                 if CONFIG['verbose']:
426                         if force_quiet:
427                                 result = None
428                         else:
429                                 # Send empty list, we'll get back first + `None specified'
430                                 result.append(format_line(lst, first, subsequent))
431
432         return result
433
434
435 def parse_module_options(module_opts):
436         """Parse module options and update QUERY_OPTS"""
437
438         opts = (x[0] for x in module_opts)
439         for opt in opts:
440                 if opt in ('-h', '--help'):
441                         print_help()
442                         sys.exit(0)
443                 elif opt in ('-d', '--description'):
444                         QUERY_OPTS["description"] = True
445                 elif opt in ('-H', '--herd'):
446                         QUERY_OPTS["herd"] = True
447                 elif opt in ('-m', '--maintainer'):
448                         QUERY_OPTS["maintainer"] = True
449                 elif opt in ('-k', '--keywords'):
450                         QUERY_OPTS["keywords"] = True
451                 elif opt in ('-u', '--useflags'):
452                         QUERY_OPTS["useflags"] = True
453                 elif opt in ('-U', '--upstream'):
454                         QUERY_OPTS["upstream"] = True
455                 elif opt in ('-x', '--xml'):
456                         QUERY_OPTS["xml"] = True
457
458
459 def main(input_args):
460         """Parse input and run the program."""
461
462         short_opts = "hdHkmuUx"
463         long_opts = ('help', 'description', 'herd', 'keywords', 'maintainer',
464                 'useflags', 'upstream', 'xml')
465
466         try:
467                 module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
468         except GetoptError as err:
469                 sys.stderr.write(pp.error("Module %s" % err))
470                 print()
471                 print_help(with_description=False)
472                 sys.exit(2)
473
474         parse_module_options(module_opts)
475
476         # Find queries' Portage directory and throw error if invalid
477         if not queries:
478                 print_help()
479                 sys.exit(2)
480
481         first_run = True
482         for query in (Query(x) for x in queries):
483                 best_match = query.find_best()
484                 matches = query.find(include_masked=True)
485                 if best_match is None or not matches:
486                         raise errors.GentoolkitNoMatches(query)
487
488                 if best_match.metadata is None:
489                         print(pp.warn("Package {0} is missing "
490                                 "metadata.xml".format(best_match.cpv)),
491                                 file = sys.stderr)
492                         continue
493
494                 if not first_run:
495                         print()
496
497                 matches.sort()
498                 call_format_functions(best_match, matches)
499
500                 first_run = False
501
502 # vim: set ts=4 sw=4 tw=79: