1 # Copyright(c) 2009, Gentoo Foundation
3 # Licensed under the GNU General Public License, v2 or higher
7 """Display metadata about a given package."""
9 from __future__ import print_function
11 __docformat__ = 'epytext'
20 from getopt import gnu_getopt, GetoptError
21 from functools import partial
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
35 # E1101: Module 'portage.output' has no $color member
36 # portage.output creates color functions dynamically
37 # pylint: disable-msg=E1101
54 def print_help(with_description=True, with_usage=True):
55 """Print description, usage and a detailed help message.
57 @type with_description: bool
58 @param with_description: if true, print module's __doc__ string
62 print(__doc__.strip())
65 print(mod_usage(mod_name="meta"))
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")
80 def filter_keywords(matches):
81 """Filters non-unique keywords per slot.
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.
88 @param matches: set of L{gentoolkit.package.Package} instances whose
89 'key' are all the same.
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
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('-')]
99 def add_unstable(keywords):
100 """Add unstable keyword for all stable keywords to filter set."""
101 result = list(keywords)
103 ['~%s' % x for x in keywords if not x.startswith(('-', '~'))]
109 # Start from the newest
110 rev_matches = reversed(matches)
111 for pkg in rev_matches:
112 keywords_str, slot = pkg.environment(('KEYWORDS', 'SLOT'),
114 keywords = keywords_str.split()
115 result[pkg] = [x for x in keywords if x not in slot_map.get(slot, [])]
117 slot_map[slot].update(del_archmask(add_unstable(keywords)))
119 slot_map[slot] = set(del_archmask(add_unstable(keywords)))
124 def format_herds(herds):
125 """Format herd information for display."""
130 email = "(%s)" % herd[1] if herd[1] else ''
132 if CONFIG['verbose']:
133 herdstr += " %s" % (email,)
134 result.append(herdstr)
139 def format_maintainers(maints):
140 """Format maintainer information for display."""
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 ''
152 result.append(maintstr)
157 def format_upstream(upstream):
158 """Format upstream information for display."""
160 def _format_upstream_docs(docs):
163 doc_location = doc[0]
165 docstr = doc_location
166 if doc_lang is not None:
167 docstr += " (%s)" % (doc_lang,)
168 result.append(docstr)
171 def _format_upstream_ids(ids):
176 idstr = "%s ID: %s" % (site, proj_id)
182 upmaints = format_maintainers(up.maintainers)
183 for upmaint in upmaints:
184 result.append(format_line(upmaint, "Maintainer: ", " " * 13))
186 for upchange in up.changelogs:
187 result.append(format_line(upchange, "ChangeLog: ", " " * 13))
189 updocs = _format_upstream_docs(up.docs)
191 result.append(format_line(updoc, "Docs: ", " " * 13))
193 for upbug in up.bugtrackers:
194 result.append(format_line(upbug, "Bugs-to: ", " " * 13))
196 upids = _format_upstream_ids(up.remoteids)
198 result.append(format_line(upid, "Remote-ID: ", " " * 13))
203 def format_useflags(useflags):
204 """Format USE flag information for display."""
207 for flag in useflags:
208 result.append(pp.useflag(flag.name))
209 result.append(flag.description)
215 def format_keywords(keywords):
216 """Sort and colorize keywords for display."""
220 for kw in sorted(keywords, key=Keyword):
221 if kw.startswith('-'):
223 kw = pp.keyword(kw, stable=False, hard_masked=True)
224 elif kw.startswith('~'):
226 kw = pp.keyword(kw, stable=False, hard_masked=False)
229 kw = pp.keyword(kw, stable=True, hard_masked=False)
232 return ' '.join(result)
235 def format_keywords_line(pkg, fmtd_keywords, slot, verstr_len):
236 """Format the entire keywords line for display."""
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))
247 def format_homepage(homepage):
248 """format the homepage(s) entries for dispaly"""
250 for page in homepage.split():
251 result.append(format_line(page, "Homepage: ", " " * 13))
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."""
260 if CONFIG['verbose']:
261 repo = best_match.repo_name()
262 pp.uprint(" * %s [%s]" % (pp.cpv(best_match.cp), pp.section(repo)))
265 if any(QUERY_OPTS.values()):
266 # Specific information requested, less formatting
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"),
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))
280 pp.uprint(format_line(herd, "Herd: ", " " * 13))
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))
288 pp.uprint(format_line([], "Maintainer: ", " " * 13))
291 pp.uprint(format_line(maint, "Maintainer: ", " " * 13))
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)
299 upstream = format_list(upstream, "Upstream: ", " " * 13)
300 print_sequence(upstream)
301 print_sequence(homepage)
304 pkg_loc = best_match.package_path()
305 pp.uprint(format_line(pkg_loc, "Location: ", " " * 13))
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)
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
318 if QUERY_OPTS["keywords"]:
319 pp.uprint(keywords_line)
321 indent = " " * (16 + verstr_len)
322 pp.uprint(format_line(keywords_line, "Keywords: ", indent))
324 if QUERY_OPTS["description"]:
325 desc = best_match.metadata.descriptions()
326 print_sequence(format_list(desc))
328 if QUERY_OPTS["useflags"]:
329 useflags = format_useflags(best_match.metadata.use())
330 print_sequence(format_list(useflags))
332 if QUERY_OPTS["xml"]:
333 print_file(os.path.join(best_match.package_path(), 'metadata.xml'))
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.
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.
346 @param line: text to format
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
353 @return: A wrapped line
357 line = line.expandtabs().strip("\n").splitlines()
362 return first + "None specified"
364 if len(first) > len(subsequent):
367 wider_indent = subsequent
369 widest_line_len = len(max(line, key=len)) + len(wider_indent)
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)
377 result = twrap.fill(line)
379 # line will fit inside CONFIG['termWidth'], so preserve whitespace and
381 line[0] = first + line[0] # Avoid two newlines if len == 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
389 if line[-1].isspace():
390 del line[-1] # Avoid trailing blank lines
392 result = "".join(line)
397 def format_list(lst, first="", subsequent="", force_quiet=False):
398 """Feed elements of a list to format_line().
402 @param lst: list to format
404 @param first: text to prepend to the first line
405 @type subsequent: string
406 @param subsequent: text to prepend to subsequent lines
408 @return: list with element text wrapped at CONFIG['termWidth']
413 # Format the first line
414 line = format_line(lst[0], first, subsequent, force_quiet)
416 # Format subsequent lines
419 result.append(format_line(elem, subsequent, subsequent,
422 # We don't want to send a blank line to format_line()
425 if CONFIG['verbose']:
429 # Send empty list, we'll get back first + `None specified'
430 result.append(format_line(lst, first, subsequent))
435 def parse_module_options(module_opts):
436 """Parse module options and update QUERY_OPTS"""
438 opts = (x[0] for x in module_opts)
440 if opt in ('-h', '--help'):
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
459 def main(input_args):
460 """Parse input and run the program."""
462 short_opts = "hdHkmuUx"
463 long_opts = ('help', 'description', 'herd', 'keywords', 'maintainer',
464 'useflags', 'upstream', 'xml')
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))
471 print_help(with_description=False)
474 parse_module_options(module_opts)
476 # Find queries' Portage directory and throw error if invalid
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)
488 if best_match.metadata is None:
489 print(pp.warn("Package {0} is missing "
490 "metadata.xml".format(best_match.cpv)),
498 call_format_functions(best_match, matches)
502 # vim: set ts=4 sw=4 tw=79: