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 # Move to Imports section after Python-2.6 is stable
10 from __future__ import with_statement
12 __author__ = "Douglas Anderson"
13 __docformat__ = 'epytext'
22 import xml.etree.cElementTree as ET
23 from getopt import gnu_getopt, GetoptError
25 from portage import settings
27 import gentoolkit.pprinter as pp
28 from gentoolkit import errors
29 from gentoolkit.equery import format_options, mod_usage, Config
30 from gentoolkit.helpers2 import find_packages, print_sequence, print_file, \
32 from gentoolkit.textwrap_ import TextWrapper
38 # E1101: Module 'portage.output' has no $color member
39 # portage.output creates color functions dynamically
40 # pylint: disable-msg=E1101
52 # Get the location of the main Portage tree
53 PORTDIR = [settings["PORTDIR"] or os.path.join(os.sep, "usr", "portage")]
55 if settings["PORTDIR_OVERLAY"]:
56 PORTDIR.extend(settings["PORTDIR_OVERLAY"].split())
62 def print_help(with_description=True):
63 """Print description, usage and a detailed help message.
65 @type with_description: bool
66 @param with_description: if true, print module's __doc__ string
72 print mod_usage(mod_name="meta")
74 print pp.command("options")
75 print format_options((
76 (" -h, --help", "display this help message"),
77 (" -c, --current", "parse metadata.xml in the current directory"),
78 (" -d, --description", "show an extended package description"),
79 (" -H, --herd", "show the herd(s) for the package"),
80 (" -m, --maintainer", "show the maintainer(s) for the package"),
81 (" -u, --useflags", "show per-package USE flag descriptions"),
82 (" -U, --upstream", "show package's upstream information"),
83 (" -x, --xml", "show the plain XML file")
87 def call_get_functions(metadata_path, package_dir, QUERY_OPTS):
88 """Call information gathering functions and display the results."""
91 print get_overlay_name(package_dir)
94 xml_tree = ET.parse(metadata_path)
96 pp.print_error("No metadata available")
101 if (QUERY_OPTS["herd"] or QUERY_OPTS["description"] or
102 QUERY_OPTS["useflags"] or QUERY_OPTS["maintainer"] or
103 QUERY_OPTS["upstream"] or QUERY_OPTS["xml"]):
104 # Specific information requested, less formatting
107 if QUERY_OPTS["herd"] or not got_opts:
108 herd = get_herd(xml_tree)
109 if QUERY_OPTS["herd"]:
110 herd = format_list(herd)
112 herd = format_list(herd, "Herd: ", " " * 13)
115 if QUERY_OPTS["maintainer"] or not got_opts:
116 maint = get_maitainer(xml_tree)
117 if QUERY_OPTS["maintainer"]:
118 maint = format_list(maint)
120 maint = format_list(maint, "Maintainer: ", " " * 13)
121 print_sequence(maint)
123 if QUERY_OPTS["upstream"] or not got_opts:
124 upstream = get_upstream(xml_tree)
125 if QUERY_OPTS["upstream"]:
126 upstream = format_list(upstream)
128 upstream = format_list(upstream, "Upstream: ", " " * 13)
129 print_sequence(upstream)
131 if QUERY_OPTS["description"]:
132 desc = get_description(xml_tree)
133 print_sequence(format_list(desc))
135 if QUERY_OPTS["useflags"]:
136 useflags = get_useflags(xml_tree)
137 print_sequence(format_list(useflags))
139 if QUERY_OPTS["xml"]:
140 print_file(metadata_path)
143 def format_line(line, first="", subsequent="", force_quiet=False):
144 """Wrap a string at word boundaries and optionally indent the first line
145 and/or subsequent lines with custom strings.
147 Preserve newlines if the longest line is not longer than
148 Config['termWidth']. To force the preservation of newlines and indents,
149 split the string into a list and feed it to format_line via format_list.
153 @param line: text to format
155 @param first: text to prepend to the first line
156 @type subsequent: string
157 @param subsequent: text to prepend to subsequent lines
158 @type force_quiet: boolean
160 @return: A wrapped line
164 line = line.expandtabs().strip("\n").splitlines()
169 return first + "None specified"
171 if len(first) > len(subsequent):
174 wider_indent = subsequent
176 widest_line_len = len(max(line, key=len)) + len(wider_indent)
178 if widest_line_len > Config['termWidth']:
179 twrap = TextWrapper(width=Config['termWidth'], expand_tabs=False,
180 initial_indent=first, subsequent_indent=subsequent)
181 line = " ".join(line)
182 line = re.sub("\s+", " ", line)
184 result = twrap.fill(line)
186 # line will fit inside Config['termWidth'], so preserve whitespace and
188 line[0] = first + line[0] # Avoid two newlines if len == 1
191 line[0] = line[0] + "\n"
192 for i in range(1, (len(line[1:-1]) + 1)):
193 line[i] = subsequent + line[i] + "\n"
194 line[-1] = subsequent + line[-1] # Avoid two newlines on last line
196 if line[-1].isspace():
197 del line[-1] # Avoid trailing blank lines
199 result = "".join(line)
201 return result.encode("utf-8")
204 def format_list(lst, first="", subsequent="", force_quiet=False):
205 """Feed elements of a list to format_line().
209 @param lst: list to format
211 @param first: text to prepend to the first line
212 @type subsequent: string
213 @param subsequent: text to prepend to subsequent lines
215 @return: list with element text wrapped at Config['termWidth']
220 # Format the first line
221 line = format_line(lst[0], first, subsequent, force_quiet)
223 # Format subsequent lines
226 result.append(format_line(elem, subsequent, subsequent,
229 # We don't want to send a blank line to format_line()
232 if Config['verbose']:
236 # Send empty list, we'll get back first + `None specified'
237 result.append(format_line(lst, first, subsequent))
242 def get_herd(xml_tree):
243 """Return a list of text nodes for <herd>."""
246 for elem in xml_tree.findall("herd"):
247 herd_mail = get_herd_email(elem.text)
248 if herd_mail and Config['verbose']:
249 result.append("%s (%s)" % (elem.text, herd_mail))
251 result.append(elem.text)
256 def get_herd_email(herd):
257 """Return the email of the given herd if it's in herds.xml, else None."""
259 herds_path = os.path.join(PORTDIR[0], "metadata/herds.xml")
262 herds_tree = ET.parse(herds_path)
264 pp.print_error(str(err))
267 # Some special herds are not listed in herds.xml
268 if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'):
271 for node in herds_tree.getiterator("herd"):
272 if node.findtext("name") == herd:
273 return node.findtext("email")
276 def get_description(xml_tree):
277 """Return a list of text nodes for <longdescription>.
279 @todo: Support the `lang' attribute
282 return [e.text for e in xml_tree.findall("longdescription")]
285 def get_maitainer(xml_tree):
286 """Return a parsable tree of all maintainer elements and sub-elements."""
290 for node in xml_tree.findall("maintainer"):
293 restrict = node.get("restrict")
295 result.append("(%s %s)" %
296 (pp.emph("Restrict to"), pp.output.green(restrict)))
297 result.extend(e.text for e in node)
303 def get_overlay_name(p_dir):
304 """Determine the overlay name and return a formatted string."""
307 cat_pkg = '/'.join(p_dir.split('/')[-2:])
308 result.append(" * %s" % pp.cpv(cat_pkg))
309 o_dir = '/'.join(p_dir.split('/')[:-2])
310 if o_dir != PORTDIR[0]:
311 # o_dir is an overlay
312 o_name = o_dir.split('/')[-1]
313 o_name = ("[", o_name, "]")
314 result.append(pp.output.turquoise("".join(o_name)))
316 return ' '.join(result)
319 def get_package_directory(query):
320 """Find a package's portage directory."""
322 matches = find_packages(query, include_masked=True)
323 # Prefer a package that's in the Portage tree over one in an
324 # overlay. Start with oldest first.
326 while list(reversed(matches)):
328 if not pkg.is_overlay():
331 return pkg.get_package_path() if pkg else None
334 def get_useflags(xml_tree):
335 """Return a list of formatted <useflag> lines, including blank elements
336 where blank lines should be printed."""
340 for node in xml_tree.getiterator("flag"):
343 flagline = pp.useflag(node.get("name"))
344 restrict = node.get("restrict")
346 result.append("%s (%s %s)" %
347 (flagline, pp.emph("Restrict to"), pp.output.green(restrict)))
349 result.append(flagline)
350 # ElementTree handles nested element text in a funky way.
351 # So we need to dump the raw XML and parse it manually.
352 flagxml = ET.tostring(node)
353 flagxml = re.sub("\s+", " ", flagxml)
354 flagxml = re.sub("\n\t", "", flagxml)
355 flagxml = re.sub("<(pkg|cat)>(.*?)</(pkg|cat)>",
356 pp.cpv(r"\2"), flagxml)
357 flagtext = re.sub("<.*?>", "", flagxml)
358 result.append(flagtext)
364 def _get_upstream_bugtracker(node):
365 """Extract and format upstream bugtracker information."""
367 bt_loc = [e.text for e in node.findall("bugs-to")]
369 return format_list(bt_loc, "Bugs to: ", " " * 12, force_quiet=True)
372 def _get_upstream_changelog(node):
373 """Extract and format upstream changelog information."""
375 cl_paths = [e.text for e in node.findall("changelog")]
377 return format_list(cl_paths, "Changelog: ", " " * 12, force_quiet=True)
380 def _get_upstream_documentation(node):
381 """Extract and format upstream documentation information."""
384 for elem in node.findall("doc"):
385 lang = elem.get("lang")
387 lang = "(%s)" % pp.output.yellow(lang)
390 doc.append(" ".join([elem.text, lang]))
392 return format_list(doc, "Docs: ", " " * 12, force_quiet=True)
395 def _get_upstream_maintainer(node):
396 """Extract and format upstream maintainer information."""
398 maintainer = node.findall("maintainer")
400 for elem in maintainer:
401 if elem.find("name") != None:
402 maint.append(elem.find("name").text)
403 if elem.find("email") != None:
404 maint.append(elem.find("email").text)
405 if elem.get("status") == "active":
406 maint.append("(%s)" % pp.output.green("active"))
407 elif elem.get("status") == "inactive":
408 maint.append("(%s)" % pp.output.red("inactive"))
409 elif elem.get("status") != None:
410 maint.append("(" + elem.get("status") + ")")
412 return format_list(maint, "Maintainer: ", " " * 12, force_quiet=True)
415 def _get_upstream_remoteid(node):
416 """Extract and format upstream remote ID."""
418 r_id = [e.get("type") + ": " + e.text for e in node.findall("remote-id")]
420 return format_list(r_id, "Remote ID: ", " " * 12, force_quiet=True)
423 def get_upstream(xml_tree):
424 """Return a list of formatted <upstream> lines, including blank elements
425 where blank lines should be printed."""
429 for node in xml_tree.findall("upstream"):
433 maint = _get_upstream_maintainer(node)
435 result.append("\n".join(maint))
437 changelog = _get_upstream_changelog(node)
439 result.append("\n".join(changelog))
441 documentation = _get_upstream_documentation(node)
443 result.append("\n".join(documentation))
445 bugs_to = _get_upstream_bugtracker(node)
447 result.append("\n".join(bugs_to))
449 remote_id = _get_upstream_remoteid(node)
451 result.append("\n".join(remote_id))
458 def parse_module_options(module_opts):
459 """Parse module options and update GLOBAL_OPTS"""
461 opts = (x[0] for x in module_opts)
463 if opt in ('-h', '--help'):
466 elif opt in ('-c', '--current'):
467 QUERY_OPTS["current"] = True
468 elif opt in ('-d', '--description'):
469 QUERY_OPTS["description"] = True
470 elif opt in ('-H', '--herd'):
471 QUERY_OPTS["herd"] = True
472 elif opt in ('-m', '--maintainer'):
473 QUERY_OPTS["maintainer"] = True
474 elif opt in ('-u', '--useflags'):
475 QUERY_OPTS["useflags"] = True
476 elif opt in ('-U', '--upstream'):
477 QUERY_OPTS["upstream"] = True
478 elif opt in ('-x', '--xml'):
479 QUERY_OPTS["xml"] = True
482 def main(input_args):
483 """Parse input and run the program."""
485 short_opts = "hcdHmuUx"
486 long_opts = ('help', 'current', 'description', 'herd', 'maintainer',
487 'useflags', 'upstream', 'xml')
490 module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
491 except GetoptError, err:
492 pp.print_error("Module %s" % err)
494 print_help(with_description=False)
497 parse_module_options(module_opts)
499 # Find queries' Portage directory and throw error if invalid
500 if not queries and not QUERY_OPTS["current"]:
504 if QUERY_OPTS["current"]:
505 package_dir = os.getcwd()
506 metadata_path = os.path.join(package_dir, "metadata.xml")
507 call_get_functions(metadata_path, package_dir, QUERY_OPTS)
510 for query in queries:
511 package_dir = get_package_directory(query)
513 raise errors.GentoolkitNoMatches(query)
514 metadata_path = os.path.join(package_dir, "metadata.xml")
516 # --------------------------------
517 # Check options and call functions
518 # --------------------------------
523 call_get_functions(metadata_path, package_dir, QUERY_OPTS)