From: scarabeus Date: Thu, 28 Oct 2010 20:13:51 +0000 (-0000) Subject: Initial commit of eshowkw, which is drop-in replacement for eshowkw from gentoolkit... X-Git-Tag: gentoolkit-0.3.0_rc11~19 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=4a91692e01bb6d2330f058fd2a31384c6c57bda0;p=gentoolkit.git Initial commit of eshowkw, which is drop-in replacement for eshowkw from gentoolkit-dev. svn path=/trunk/gentoolkit/; revision=831 --- diff --git a/ChangeLog b/ChangeLog index b489c96..0afec26 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2010-10-29: Tomáš Chvátal + * eshowkw: Add new module as drop-in replacement for eshowkw from + gentoolkit-dev + 2010-05-13: Christian Ruppert * eclean/cli.py: Fix typo, bug 319349, thanks to Ulrich Müller . diff --git a/bin/eshowkw b/bin/eshowkw new file mode 100644 index 0000000..7ac5fa0 --- /dev/null +++ b/bin/eshowkw @@ -0,0 +1,9 @@ +#!/usr/bin/python +# vim:fileencoding=utf-8 +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import sys +from eshowkw import main as emain + +emain(sys.argv) \ No newline at end of file diff --git a/pym/gentoolkit/eshowkw/__init__.py b/pym/gentoolkit/eshowkw/__init__.py new file mode 100644 index 0000000..efcbfb9 --- /dev/null +++ b/pym/gentoolkit/eshowkw/__init__.py @@ -0,0 +1,124 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +__package__ = 'eshowkw' +__version__ = '0.5' + +import portage + +import sys, os, fnmatch +import argparse +from portage import output as porto +from portage import settings as ports +from portage import config as portc +from portage import portdbapi as portdbapi +from portage import db as portdb + +from keywords_header import keywords_header +from keywords_content import keywords_content +from display_pretty import string_rotator +from display_pretty import display + +ignore_slots = False +bold = False +order = 'bottom' +topper = 'versionlist' + +def process_display(package, keywords, dbapi): + portdata = keywords_content(package, keywords.keywords, dbapi, ignore_slots, order, bold, topper) + if topper == 'archlist': + header = string_rotator().rotateContent(keywords.content, keywords.length, bold) + extra = string_rotator().rotateContent(keywords.extra, keywords.length, bold, False) + # -1 : space is taken in account and appended by us + filler = ''.ljust(portdata.slot_length-1) + header = ['%s%s%s' % (x, filler, y) for x, y in zip(header, extra)] + content = portdata.content + header_length = portdata.version_length + content_length = keywords.length + else: + header = string_rotator().rotateContent(portdata.content, portdata.content_length, order, bold) + content = keywords.content + sep = [''.ljust(keywords.length) for x in range(portdata.slot_length-1)] + content.extend(sep) + content.extend(keywords.extra) + header_length = keywords.length + content_length = portdata.version_length + display(content, header, header_length, content_length, portdata.cp, topper) + +def process_args(argv): + """Option parsing via argc""" + parser = argparse.ArgumentParser(prog=__package__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Display keywords for specified package or for package that is in pwd.') + + parser.add_argument('-v', '--version', action='version', version=__version__, help='show package version and exit') + + parser.add_argument('package', nargs='*', default=None, help='Packages to check.') + + parser.add_argument('-a', '--arch', nargs='+', default=[], help='Display only specified arch(s)') + + parser.add_argument('-A', '--align', nargs='?', default='bottom', choices=['top', 'bottom'], + help='Specify alignment for descriptions.') + parser.add_argument('-T', '--top-position', nargs='?', default='archlist', choices=['archlist', 'versionlist'], + help='Specify which fields we want to have in top listing.') + + parser.add_argument('-B', '--bold', action='store_true', default=False, + help='Print out each other column in bold for easier visual separation.') + parser.add_argument('-C', '--color', action='store_true', default=False, + help='Force colored output') + parser.add_argument('-O', '--overlays', action='store_true', default=False, + help='Search also overlays') + parser.add_argument('-P', '--prefix', action='store_true', default=False, + help='Display prefix keywords in output.') + parser.add_argument('-S', '--ignore-slot', action='store_true', default=False, + help='Treat slots as irelevant during detection of redundant pacakges.') + + return parser.parse_args(args=argv[1:]) + +def main(argv): + global ignore_slots, bold, order, topper + + #opts parsing + opts = process_args(argv) + ignore_slots = opts.ignore_slot + use_overlays = opts.overlays + # user can do both --arch=a,b,c or --arch a b c + if len(opts.arch) > 1: + opts.arch = ','.join(opts.arch) + highlight_arch = ''.join(opts.arch).split(',') + bold = opts.bold + order = opts.align + topper = opts.top_position + prefix = opts.prefix + color = opts.color + package = opts.package + # disable colors when redirected and they are not forced on + if not color and not sys.stdout.isatty(): + # disable colors + porto.nocolor() + keywords = keywords_header(prefix, highlight_arch, order) + if len(package) > 0: + dbapi = portdb[ports['ROOT']]['porttree'].dbapi + if not use_overlays: + dbapi.porttrees = [dbapi.porttree_root] + map(lambda x: process_display(x, keywords, dbapi), package) + else: + currdir = os.getcwd() + package=os.path.basename(currdir) + # check if there are actualy some ebuilds + ebuilds = ['%s' % x for x in os.listdir(currdir) + if fnmatch.fnmatch(file, '*.ebuild')] + if len(ebuilds) <= 0: + msg_err = 'No ebuilds at "%s"' % currdir + raise SystemExit(msg_err) + ourtree = os.path.abspath('../../') + + mysettings = portc(env={'PORTDIR_OVERLAY': os.path.abspath('../../')}) + dbapi = portdbapi(mysettings=mysettings) + # specify that we want just our nice tree we are in cwd + dbapi.porttrees = [ourtree] + process_display(package, keywords, dbapi) + return 0 + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/pym/gentoolkit/eshowkw/display_pretty.py b/pym/gentoolkit/eshowkw/display_pretty.py new file mode 100644 index 0000000..b29fba1 --- /dev/null +++ b/pym/gentoolkit/eshowkw/display_pretty.py @@ -0,0 +1,102 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from portage.output import colorize +from itertools import izip_longest + +__all__ = ['string_rotator', 'colorize_string', 'align_string', 'rotate_dash', 'print_content', 'display'] + +def display(plain_list, rotated_list, plain_width, rotated_height, cp, toplist = 'archlist'): + """Render defauld display to show the keywords listing""" + # header + output = [] + output.append('Keywords for %s:' % colorize('blue', cp)) + # data + corner_image = [''.ljust(plain_width) for x in range(rotated_height)] + if toplist == 'versionlist': + corner_image.extend(plain_list) + data_printout = ['%s%s' % (x, y) + for x, y in izip_longest(corner_image, rotated_list, fillvalue=corner_image[0])] + if toplist == 'archlist': + data_printout.extend(plain_list) + output.extend(data_printout) + print print_content(output) + +def align_string(string, align, length): + """Align string to the specified alignment (left or right, and after rotation it becames top and bottom)""" + if align == 'top' or align == 'left': + string = string.ljust(length) + else: + string = string.rjust(length) + return string + +def colorize_string(color, string): + """Add coloring for specified string. Due to rotation we need to do that per character rather than per-line""" + tmp = [] + for char in list(string): + # % is whitespace separator so we wont color that :) + if char != '%': + tmp.append(colorize(color, char)) + else: + tmp.append(char) + return ''.join(tmp) + +def rotate_dash(string): + """Rotate special strings over 90 degrees for better readability.""" + chars = ['-', '|'] + subs = ['|', '-'] + out = string + for x,y in zip(chars, subs): + if string.find(x) != -1: + out = out.replace(x, y) + return out + +def print_content(content): + """Print out content (strip it out of the temporary %)""" + return '\n'.join(content).replace('%','') + +class string_rotator: + __DASH_COUNT = 0 + def __getChar(self, string, position, line, bold_separator = False): + """Return specified character from the string position""" + + # first figure out what character we want to work with + # based on order and position in the string + isdash = False + if string.startswith('|') or string.startswith('-') or string.startswith('+'): + split = list(string) + isdash = True + self.__DASH_COUNT += 1 + else: + split = string.split('%') + char = split[position] + # bolding + if not isdash and bold_separator \ + and (line-self.__DASH_COUNT)%2 == 0 \ + and char != ' ': + char = colorize('bold', char) + return char + + def rotateContent(self, elements, length, bold_separator = False, strip = True): + """ + Rotate string over 90 degrees: + string -> s + t + r + i + n + g + """ + # join used to have list of lines rather than list of chars + tmp = [] + for position in range(length): + x = '' + for i, string in enumerate(elements): + x += ' ' + self.__getChar(rotate_dash(string), position, i, bold_separator) + # spaces on dashed line should be dashed too + if x.find('+ -') != -1: + x = x.replace(' ', '-') + # strip all chars and remove empty lines + if not strip or len(x.strip(' |-')) > 0: + tmp.append(x) + return tmp diff --git a/pym/gentoolkit/eshowkw/keywords_content.py b/pym/gentoolkit/eshowkw/keywords_content.py new file mode 100644 index 0000000..08a728d --- /dev/null +++ b/pym/gentoolkit/eshowkw/keywords_content.py @@ -0,0 +1,290 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import portage as port +from portage.output import colorize + +__all__ = ['keywords_content'] + +from display_pretty import colorize_string +from display_pretty import align_string + +class keywords_content: + class RedundancyChecker: + def __listRedundant(self, keywords, ignoreslots, slots): + """List all redundant packages.""" + if ignoreslots: + return self.__listRedundantAll(keywords) + else: + return self.__listRedundantSlots(keywords, slots) + + def __listRedundantSlots(self, keywords, slots): + """Search for redundant packages walking per keywords for specified slot.""" + result = [self.__compareSelected([k for k, s in zip(keywords, slots) + if s == slot]) + for slot in self.__uniq(slots)] + # this is required because the list itself is not just one level depth + return list(''.join(result)) + + def __uniq(self, seq): + """Remove all duplicate elements from list.""" + seen = {} + result = [] + for item in seq: + if item in seen: + continue + seen[item] = 1 + result.append(item) + return result + + def __listRedundantAll(self, keywords): + """Search for redundant packages using all versions ignoring its slotting.""" + return list(self.__compareSelected(list(keywords))) + + def __compareSelected(self, kws): + """ + Rotate over list of keywords and compare each element with others. + Selectively remove each already compared list from the remaining keywords. + """ + result = [] + kws.reverse() + for i in range(len(kws)): + kw = kws.pop() + if self.__compareKeywordWithRest(kw, kws): + result.append('#') + else: + result.append('o') + if len(result) == 0: + result.append('o') + return ''.join(result) + + def __compareKeywordWithRest(self, keyword, keywords): + """Compare keywords with list of keywords.""" + for key in keywords: + if self.__checkShadow(keyword, key): + return True + return False + + def __checkShadow(self, old, new): + """Check if package version is overshadowed by other package version.""" + # remove -* and -arch since they are useless for us + newclean = ["%s" % x for x in new.split() + if x != '-*' and not x.startswith('-')] + oldclean = ["%s" % x for x in old.split() + if x != '-*' and not x.startswith('-')] + + tmp = set(newclean) + tmp.update("~%s" % x for x in newclean + if not x.startswith("~")) + if not set(oldclean).difference(tmp): + return True + else: + return False + + def __init__(self, keywords, slots, ignore_slots = False): + """Query all relevant data for redundancy package checking""" + self.redundant = self.__listRedundant(keywords, ignore_slots, slots) + + class VersionChecker: + def __getVersions(self, packages, vartree): + """Obtain properly aligned version strings without colors.""" + return self.__stripStartingSpaces(map(lambda x: self.__separateVersion(x, vartree), packages)) + + def __stripStartingSpaces(self, pvs): + """Strip starting whitespace if there is no real reason for it.""" + if not self.__require_prepend: + return map(lambda x: x.lstrip(), pvs) + else: + return pvs + + def __separateVersion(self, cpv, vartree): + """Get version string for specfied cpv""" + #pv = port.versions.cpv_getversion(cpv) + return self.__prependVersionInfo(cpv, self.cpv_getversion(cpv), vartree) + + # remove me when portage 2.1.9 is stable + def cpv_getversion(self, mycpv): + """Returns the v (including revision) from an cpv.""" + cp = port.versions.cpv_getkey(mycpv) + if cp is None: + return None + return mycpv[len(cp+"-"):] + + def __prependVersionInfo(self, cpv, pv, vartree): + """Prefix version with string based on whether version is installed or masked.""" + mask = self.__getMaskStatus(cpv) + install = self.__getInstallStatus(cpv, vartree) + + if mask and install: + pv = '[M][I]%s' % pv + self.__require_longprepend = True + elif mask: + pv = '[M]%s' % pv + self.__require_prepend = True + elif install: + pv = '[I]%s' % pv + self.__require_prepend = True + return pv + + def __getMaskStatus(self, cpv): + """ + Figure out if package is pmasked. + This also uses user settings in /etc/ so local changes are important. + """ + pmask = False + try: + if port.getmaskingstatus(cpv) == ['package.mask']: + pmask = True + except: + # occurs when package is not known by portdb + # so we consider it unmasked + pass + return pmask + + def __getInstallStatus(self, cpv, vartree): + """Check if package version we test is installed.""" + return vartree.cpv_exists(cpv) + + def __init__(self, packages, vartree): + """Query all relevant data for version data formatting""" + self.__require_longprepend = False + self.__require_prepend = False + self.versions = self.__getVersions(packages, vartree) + + def __checkExist(self, pdb, package): + """Check if specified package even exists.""" + try: + matches = pdb.xmatch('match-all', package) + except port.exception.AmbiguousPackageName as Arg: + msg_err = 'Ambiguous package name "%s".\n' % package + found = 'Possibilities: %s' % Arg + raise SystemExit('%s%s' % (msg_err, found)) + except port.exception.InvalidAtom: + msg_err = 'No such package "%s"' % package + raise SystemExit(msg_err) + if len(matches) <= 0: + msg_err = 'No such package "%s"' % package + raise SystemExit(msg_err) + return matches + + def __getMetadata(self, pdb, packages): + """Obtain all KEYWORDS and SLOT from metadata""" + try: + metadata = map(lambda x: pdb.aux_get(x, ['KEYWORDS', 'SLOT', 'repository']), packages) + except KeyError: + # portage prints out more verbose error for us if we were lucky + raise SystemExit('Failed to obtain metadata') + return list(zip(*metadata)) + + def __formatKeywords(self, keywords, keywords_list, usebold = False, toplist = 'archlist'): + """Loop over all keywords and replace them with nice visual identifier""" + # the % is fancy separator, we use it to split keywords for rotation + # so we wont loose the empty spaces + return ['% %'.join([self.__prepareKeywordChar(arch, i, version.split(), usebold, toplist) + for i, arch in enumerate(keywords_list)]) + for version in keywords] + + def __prepareKeywordChar(self, arch, field, keywords, usebold = False, toplist = 'archlist'): + """ + Convert specified keywords for package into their visual replacements. + # possibilities: + # ~arch -> orange ~ + # -arch -> red - + # arch -> green + + # -* -> red * + """ + keys = [ '~%s' % arch, '-%s' % arch, '%s' % arch, '-*' ] + nocolor_values = [ '~', '-', '+', '*' ] + values = [ + colorize('darkyellow', '~'), + colorize('darkred', '-'), + colorize('darkgreen', '+'), + colorize('darkred', '*') + ] + # check what keyword we have + # here we cant just append space because it would get stripped later + char = colorize('darkgray','o') + for k, v, n in zip(keys, values, nocolor_values): + if k in keywords: + char = v + break + if toplist == 'archlist' and usebold and (field)%2 == 0 and char != ' ': + char = colorize('bold', char) + return char + + def __formatVersions(self, versions, align, length): + """Append colors and align keywords properly""" + # % are used as separators for further split so we wont loose spaces and coloring + tmp = [] + for pv in versions: + pv = align_string(pv, align, length) + pv = '%'.join(list(pv)) + if pv.find('[%M%][%I%]') != -1: + tmp.append(colorize_string('darkyellow', pv)) + elif pv.find('[%M%]') != -1: + tmp.append(colorize_string('darkred', pv)) + elif pv.find('[%I%]') != -1: + tmp.append(colorize_string('bold', pv)) + else: + tmp.append(pv) + return tmp + + def __formatAdditional(self, additional, color, length): + """Align additional items properly""" + # % are used as separators for further split so we wont loose spaces and coloring + tmp = [] + for x in additional: + tmpc = color + x = align_string(x, 'left', length) + x = '%'.join(list(x)) + if x == 'o': + # the value is unset so the color is gray + tmpc = 'darkgray' + x = colorize_string(tmpc, x) + tmp.append(x) + return tmp + + def __prepareContentResult(self, versions, keywords, redundant, slots, slot_length, repos, linesep): + """Parse version fields into one list with proper separators""" + content = [] + oldslot = '' + fieldsep = '% %|% %' + normsep = '% %' + for v, k, r, s, t in zip(versions, keywords, redundant, slots, repos): + if oldslot != s: + oldslot = s + content.append(linesep) + else: + s = '%'.join(list(''.rjust(slot_length))) + content.append('%s%s%s%s%s%s%s%s%s' % (v, fieldsep, k, fieldsep, r, normsep, s, fieldsep, t)) + return content + + def __init__(self, package, keywords_list, porttree, ignoreslots = False, content_align = 'bottom', usebold = False, toplist = 'archlist'): + """Query all relevant data from portage databases.""" + vartree = port.db[port.settings['ROOT']]['vartree'].dbapi + packages = self.__checkExist(porttree, package) + self.keywords, self.slots, self.repositories = self.__getMetadata(porttree, packages) + self.slot_length = max([len(x) for x in self.slots]) + repositories_length = max([len(x) for x in self.repositories]) + self.keyword_length = len(keywords_list) + self.versions = self.VersionChecker(packages, vartree).versions + self.version_length = max([len(x) for x in self.versions]) + self.version_count = len(self.versions) + self.redundant = self.RedundancyChecker(self.keywords, self.slots, ignoreslots).redundant + redundant_length = max([len(x) for x in self.redundant]) + + ver = self.__formatVersions(self.versions, content_align, self.version_length) + kws = self.__formatKeywords(self.keywords, keywords_list, usebold, toplist) + red = self.__formatAdditional(self.redundant, 'purple', redundant_length) + slt = self.__formatAdditional(self.slots, 'bold', self.slot_length) + rep = self.__formatAdditional(self.repositories, 'yellow', repositories_length) + # those + nubers are spaces in printout. keywords are multiplied also because of that + linesep = '%s+%s+%s+%s' % (''.ljust(self.version_length+1, '-'), + ''.ljust(self.keyword_length*2+1, '-'), + ''.ljust(redundant_length+self.slot_length+3, '-'), + ''.ljust(repositories_length+1, '-') + ) + + self.content = self.__prepareContentResult(ver, kws, red, slt, self.slot_length, rep, linesep) + self.content_length = len(linesep) + self.cp = port.cpv_getkey(packages[0]) diff --git a/pym/gentoolkit/eshowkw/keywords_header.py b/pym/gentoolkit/eshowkw/keywords_header.py new file mode 100644 index 0000000..53105ae --- /dev/null +++ b/pym/gentoolkit/eshowkw/keywords_header.py @@ -0,0 +1,99 @@ +# Copyright 2001-2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +__all__ = ['keywords_header'] + +from portage import settings as ports +from portage.output import colorize +from display_pretty import colorize_string +from display_pretty import align_string + +class keywords_header: + __IMPARCHS = [ 'arm', 'amd64', 'x86' ] + __ADDITIONAL_FIELDS = [ 'unused', 'slot' ] + __EXTRA_FIELDS = [ 'repo' ] + + def __readKeywords(self): + """Read all available keywords from portage.""" + return [x for x in ports.archlist() + if not x.startswith('~')] + + def __sortKeywords(self, keywords, prefix = False, required_keywords = []): + """Sort keywords with short archs first""" + # user specified only some keywords to display + if len(required_keywords) != 0: + tmpkeywords = [k for k in keywords + if k in required_keywords] + # idiots might specify non-existant archs + if len(tmpkeywords) != 0: + keywords = tmpkeywords + + normal = [k for k in keywords + if len(k.split('-')) == 1] + normal.sort() + + if prefix: + longer = [k for k in keywords + if len(k.split('-')) != 1] + longer.sort() + normal.extend(longer) + return normal + + def __readAdditionalFields(self): + """Prepare list of aditional fileds displayed by eshowkw (2nd part)""" + return self.__ADDITIONAL_FIELDS + + def __readExtraFields(self): + """Prepare list of extra fileds displayed by eshowkw (3rd part)""" + return self.__EXTRA_FIELDS + + def __formatKeywords(self, keywords, align, length): + """Append colors and align keywords properly""" + tmp = [] + for keyword in keywords: + tmp2 = keyword + keyword = align_string(keyword, align, length) + # % are used as separators for further split so we wont loose spaces and coloring + keyword = '%'.join(list(keyword)) + if tmp2 in self.__IMPARCHS: + tmp.append(colorize_string('darkyellow', keyword)) + else: + tmp.append(keyword) + return tmp + + def __formatAdditional(self, additional, align, length): + """Align additional items properly""" + # % are used as separators for further split so we wont loose spaces and coloring + return ['%'.join(align_string(x, align, length)) for x in additional] + + def __prepareExtra(self, extra, align, length): + content = [] + content.append(''.ljust(length, '-')) + content.extend(self.__formatAdditional(extra, align, length)) + return content + + def __prepareResult(self, keywords, additional, align, length): + """Parse keywords and additional fields into one list with proper separators""" + content = [] + content.append(''.ljust(length, '-')) + content.extend(self.__formatKeywords(keywords, align, length)) + content.append(''.ljust(length, '-')) + content.extend(self.__formatAdditional(additional, align, length)) + return content + + def __init__(self, prefix = False, required_keywords = [], keywords_align = 'bottom'): + """Initialize keywords header.""" + additional = self.__readAdditionalFields() + extra = self.__readExtraFields() + self.keywords = self.__sortKeywords(self.__readKeywords(), prefix, required_keywords) + self.length = max( + max([len(x) for x in self.keywords]), + max([len(x) for x in additional]), + max([len(x) for x in extra]) + ) + #len(max([max(self.keywords, key=len), max(additional, key=len)], key=len)) + self.keywords_count = len(self.keywords) + self.additional_count = len(additional) + self.extra_count = len(extra) + self.content = self.__prepareResult(self.keywords, additional, keywords_align, self.length) + self.extra = self.__prepareExtra(extra, keywords_align, self.length)