Initial commit of eshowkw, which is drop-in replacement for eshowkw from gentoolkit...
authorscarabeus <scarabeus@gentoo.org>
Thu, 28 Oct 2010 20:13:51 +0000 (20:13 -0000)
committerscarabeus <scarabeus@gentoo.org>
Thu, 28 Oct 2010 20:13:51 +0000 (20:13 -0000)
svn path=/trunk/gentoolkit/; revision=831

ChangeLog
bin/eshowkw [new file with mode: 0644]
pym/gentoolkit/eshowkw/__init__.py [new file with mode: 0644]
pym/gentoolkit/eshowkw/display_pretty.py [new file with mode: 0644]
pym/gentoolkit/eshowkw/keywords_content.py [new file with mode: 0644]
pym/gentoolkit/eshowkw/keywords_header.py [new file with mode: 0644]

index b489c9622ca8c6e2b86889bb34c50082fd3c4c64..0afec2687a6b5f94577faf8676f13c234f82e3a5 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2010-10-29: Tomáš Chvátal <scarabeus@gentoo.org>
+       * eshowkw: Add new module as drop-in replacement for eshowkw from
+       gentoolkit-dev
+
 2010-05-13: Christian Ruppert <idl0r@gentoo.org>
        * eclean/cli.py: Fix typo, bug 319349, thanks to Ulrich Müller
        <ulm@gentoo.org>.
diff --git a/bin/eshowkw b/bin/eshowkw
new file mode 100644 (file)
index 0000000..7ac5fa0
--- /dev/null
@@ -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 (file)
index 0000000..efcbfb9
--- /dev/null
@@ -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 (file)
index 0000000..b29fba1
--- /dev/null
@@ -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 (file)
index 0000000..08a728d
--- /dev/null
@@ -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 (file)
index 0000000..53105ae
--- /dev/null
@@ -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)