Update with rev 9 from the genscripts repo
[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 # Move to Imports section after Python-2.6 is stable
10 from __future__ import with_statement
11
12 __author__  = "Douglas Anderson"
13 __docformat__ = 'epytext'
14
15 # =======
16 # Imports
17 # =======
18
19 import os
20 import re 
21 import sys
22 import xml.etree.cElementTree as ET
23 from getopt import gnu_getopt, GetoptError
24
25 from portage import settings
26
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, \
31         uniqify
32 from gentoolkit.textwrap_ import TextWrapper
33
34 # =======
35 # Globals
36 # =======
37
38 # E1101: Module 'portage.output' has no $color member
39 # portage.output creates color functions dynamically
40 # pylint: disable-msg=E1101
41
42 QUERY_OPTS = {
43         "current": False,
44         "description": False,
45         "herd": False,
46         "maintainer": False,
47         "useflags": False,
48         "upstream": False,
49         "xml": False
50
51
52 # Get the location of the main Portage tree
53 PORTDIR = [settings["PORTDIR"] or os.path.join(os.sep, "usr", "portage")]
54 # Check for overlays
55 if settings["PORTDIR_OVERLAY"]:
56         PORTDIR.extend(settings["PORTDIR_OVERLAY"].split())
57
58 # =========
59 # Functions
60 # =========
61
62 def print_help(with_description=True):
63         """Print description, usage and a detailed help message.
64         
65         @type with_description: bool
66         @param with_description: if true, print module's __doc__ string
67         """
68
69         if with_description:
70                 print __doc__.strip()
71                 print
72         print mod_usage(mod_name="meta")
73         print
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")
84         ))
85
86
87 def call_get_functions(metadata_path, package_dir, QUERY_OPTS):
88         """Call information gathering functions and display the results."""
89         
90         if Config['verbose']:
91                 print get_overlay_name(package_dir)
92
93         try:
94                 xml_tree = ET.parse(metadata_path)
95         except IOError:
96                 pp.print_error("No metadata available")
97                 first_run = False
98                 return
99
100         got_opts = False
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
105                 got_opts = True
106
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)
111                 else:
112                         herd = format_list(herd, "Herd:        ", " " * 13)
113                 print_sequence(herd)
114
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)
119                 else:
120                         maint = format_list(maint, "Maintainer:  ", " " * 13)
121                 print_sequence(maint)
122
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)
127                 else:
128                         upstream = format_list(upstream, "Upstream:    ", " " * 13)
129                 print_sequence(upstream)
130
131         if QUERY_OPTS["description"]:
132                 desc = get_description(xml_tree)
133                 print_sequence(format_list(desc))
134
135         if QUERY_OPTS["useflags"]:
136                 useflags = get_useflags(xml_tree)
137                 print_sequence(format_list(useflags))
138
139         if QUERY_OPTS["xml"]:
140                 print_file(metadata_path)
141
142
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.
146
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.
150
151         @see: format_list()
152         @type line: string
153         @param line: text to format
154         @type first: string
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
159         @rtype: string
160         @return: A wrapped line
161         """
162
163         if line:
164                 line = line.expandtabs().strip("\n").splitlines() 
165         else:
166                 if force_quiet:
167                         return
168                 else:
169                         return first + "None specified"
170
171         if len(first) > len(subsequent):
172                 wider_indent = first
173         else:
174                 wider_indent = subsequent
175         
176         widest_line_len = len(max(line, key=len)) + len(wider_indent)
177         
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)
183                 line = line.lstrip()
184                 result = twrap.fill(line)
185         else:
186                 # line will fit inside Config['termWidth'], so preserve whitespace and 
187                 # newlines
188                 line[0] = first + line[0]          # Avoid two newlines if len == 1
189
190                 if len(line) > 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
195
196                 if line[-1].isspace():
197                         del line[-1]                # Avoid trailing blank lines
198
199                 result = "".join(line)
200
201         return result.encode("utf-8")
202
203
204 def format_list(lst, first="", subsequent="", force_quiet=False):
205         """Feed elements of a list to format_line().
206
207         @see: format_line()
208         @type lst: list
209         @param lst: list to format
210         @type first: string
211         @param first: text to prepend to the first line
212         @type subsequent: string
213         @param subsequent: text to prepend to subsequent lines
214         @rtype: list
215         @return: list with element text wrapped at Config['termWidth']
216         """
217
218         result = []
219         if lst:
220                 # Format the first line
221                 line = format_line(lst[0], first, subsequent, force_quiet)
222                 result.append(line)
223                 # Format subsequent lines
224                 for elem in lst[1:]:
225                         if elem:
226                                 result.append(format_line(elem, subsequent, subsequent,
227                                         force_quiet))
228                         else:
229                                 # We don't want to send a blank line to format_line()
230                                 result.append("")
231         else:
232                 if Config['verbose']:
233                         if force_quiet:
234                                 result = None
235                         else:
236                                 # Send empty list, we'll get back first + `None specified'
237                                 result.append(format_line(lst, first, subsequent))
238
239         return result
240
241
242 def get_herd(xml_tree):
243         """Return a list of text nodes for <herd>."""
244         
245         result = []
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))
250                 else:
251                         result.append(elem.text) 
252
253         return result
254
255
256 def get_herd_email(herd):
257         """Return the email of the given herd if it's in herds.xml, else None."""
258         
259         herds_path = os.path.join(PORTDIR[0], "metadata/herds.xml")
260
261         try:
262                 herds_tree = ET.parse(herds_path)
263         except IOError, err:
264                 pp.print_error(str(err))
265                 return None
266
267         # Some special herds are not listed in herds.xml
268         if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'):
269                 return None
270         
271         for node in herds_tree.getiterator("herd"):
272                 if node.findtext("name") == herd:
273                         return node.findtext("email")
274
275
276 def get_description(xml_tree):
277         """Return a list of text nodes for <longdescription>.
278
279         @todo: Support the `lang' attribute
280         """
281
282         return [e.text for e in xml_tree.findall("longdescription")]
283
284
285 def get_maitainer(xml_tree):
286         """Return a parsable tree of all maintainer elements and sub-elements."""
287
288         first_run = True
289         result = []
290         for node in xml_tree.findall("maintainer"):
291                 if not first_run:
292                         result.append("")
293                 restrict = node.get("restrict")
294                 if restrict:
295                         result.append("(%s %s)" %
296                         (pp.emph("Restrict to"), pp.output.green(restrict)))
297                 result.extend(e.text for e in node)
298                 first_run = False
299
300         return result
301
302
303 def get_overlay_name(p_dir):
304         """Determine the overlay name and return a formatted string."""
305
306         result = []
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)))
315
316         return ' '.join(result)
317
318
319 def get_package_directory(query):
320         """Find a package's portage directory."""
321
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.
325         pkg = None
326         while list(reversed(matches)):
327                 pkg = matches.pop()
328                 if not pkg.is_overlay():
329                         break
330         
331         return pkg.get_package_path() if pkg else None
332         
333
334 def get_useflags(xml_tree):
335         """Return a list of formatted <useflag> lines, including blank elements
336         where blank lines should be printed."""
337
338         first_run = True
339         result = []
340         for node in xml_tree.getiterator("flag"):
341                 if not first_run:
342                         result.append("")
343                 flagline = pp.useflag(node.get("name"))
344                 restrict = node.get("restrict")
345                 if restrict:
346                         result.append("%s (%s %s)" %
347                                 (flagline, pp.emph("Restrict to"), pp.output.green(restrict)))
348                 else:
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)
359                 first_run = False
360
361         return result
362
363
364 def _get_upstream_bugtracker(node):
365         """Extract and format upstream bugtracker information."""
366
367         bt_loc = [e.text for e in node.findall("bugs-to")]
368
369         return format_list(bt_loc, "Bugs to:    ", " " * 12, force_quiet=True)
370
371
372 def _get_upstream_changelog(node):
373         """Extract and format upstream changelog information."""
374
375         cl_paths = [e.text for e in node.findall("changelog")]
376
377         return format_list(cl_paths, "Changelog:  ", " " * 12, force_quiet=True)
378
379
380 def _get_upstream_documentation(node):
381         """Extract and format upstream documentation information."""
382
383         doc = []
384         for elem in node.findall("doc"):
385                 lang = elem.get("lang")
386                 if lang:
387                         lang = "(%s)" % pp.output.yellow(lang)
388                 else:
389                         lang = ""
390                 doc.append(" ".join([elem.text, lang]))
391
392         return format_list(doc, "Docs:       ", " " * 12, force_quiet=True)
393
394
395 def _get_upstream_maintainer(node):
396         """Extract and format upstream maintainer information."""
397
398         maintainer = node.findall("maintainer")
399         maint = []
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") + ")")
411
412         return format_list(maint, "Maintainer: ", " " * 12, force_quiet=True)
413
414
415 def _get_upstream_remoteid(node):
416         """Extract and format upstream remote ID."""
417
418         r_id = [e.get("type") + ": " + e.text for e in node.findall("remote-id")]
419
420         return format_list(r_id, "Remote ID:  ", " " * 12, force_quiet=True)
421
422
423 def get_upstream(xml_tree):
424         """Return a list of formatted <upstream> lines, including blank elements
425         where blank lines should be printed."""
426
427         first_run = True
428         result = []
429         for node in xml_tree.findall("upstream"):
430                 if not first_run:
431                         result.append("")
432
433                 maint = _get_upstream_maintainer(node)
434                 if maint:
435                         result.append("\n".join(maint))
436
437                 changelog = _get_upstream_changelog(node)
438                 if changelog:
439                         result.append("\n".join(changelog))
440
441                 documentation = _get_upstream_documentation(node)
442                 if documentation:
443                         result.append("\n".join(documentation))
444
445                 bugs_to = _get_upstream_bugtracker(node)
446                 if bugs_to:
447                         result.append("\n".join(bugs_to))
448
449                 remote_id = _get_upstream_remoteid(node)
450                 if remote_id:
451                         result.append("\n".join(remote_id))
452
453                 first_run = False
454
455         return result
456
457
458 def parse_module_options(module_opts):
459         """Parse module options and update GLOBAL_OPTS"""
460
461         opts = (x[0] for x in module_opts)
462         for opt in opts:
463                 if opt in ('-h', '--help'):
464                         print_help()
465                         sys.exit(0)
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
480
481
482 def main(input_args):
483         """Parse input and run the program."""
484
485         short_opts = "hcdHmuUx"
486         long_opts = ('help', 'current', 'description', 'herd', 'maintainer',
487                 'useflags', 'upstream', 'xml')
488
489         try:
490                 module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
491         except GetoptError, err:
492                 pp.print_error("Module %s" % err)
493                 print
494                 print_help(with_description=False)
495                 sys.exit(2)
496
497         parse_module_options(module_opts)
498         
499         # Find queries' Portage directory and throw error if invalid
500         if not queries and not QUERY_OPTS["current"]:
501                 print_help()
502                 sys.exit(2)
503         
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)
508         else:
509                 first_run = True
510                 for query in queries:
511                         package_dir = get_package_directory(query)
512                         if not package_dir:
513                                 raise errors.GentoolkitNoMatches(query)
514                         metadata_path = os.path.join(package_dir, "metadata.xml")
515
516                         # --------------------------------
517                         # Check options and call functions
518                         # --------------------------------
519                 
520                         if not first_run:
521                                 print
522                                 
523                         call_get_functions(metadata_path, package_dir, QUERY_OPTS)
524         
525                         first_run = False