Merge rev 113 from djanderson's genscripts repo
authorfuzzyray <fuzzyray@gentoo.org>
Tue, 8 Dec 2009 21:53:45 +0000 (21:53 -0000)
committerfuzzyray <fuzzyray@gentoo.org>
Tue, 8 Dec 2009 21:53:45 +0000 (21:53 -0000)
svn path=/trunk/gentoolkit/; revision=703

46 files changed:
AUTHORS
CREDITS [new file with mode: 0644]
ChangeLog
MANIFEST.in
README.dev [new file with mode: 0644]
TODO
bin/eclean
bin/epkginfo
bin/equery
bin/euse
bin/revdep-rebuild
man/equery.1
pylintrc [new file with mode: 0644]
pym/gentoolkit/__init__.py
pym/gentoolkit/atom.py [new file with mode: 0644]
pym/gentoolkit/cpv.py [new file with mode: 0644]
pym/gentoolkit/dbapi.py [new file with mode: 0644]
pym/gentoolkit/dependencies.py [new file with mode: 0644]
pym/gentoolkit/deprecated/helpers.py [new file with mode: 0644]
pym/gentoolkit/equery/__init__.py
pym/gentoolkit/equery/belongs.py
pym/gentoolkit/equery/changes.py
pym/gentoolkit/equery/check.py
pym/gentoolkit/equery/depends.py
pym/gentoolkit/equery/depgraph.py
pym/gentoolkit/equery/files.py
pym/gentoolkit/equery/hasuse.py
pym/gentoolkit/equery/list_.py
pym/gentoolkit/equery/meta.py
pym/gentoolkit/equery/size.py
pym/gentoolkit/equery/uses.py
pym/gentoolkit/equery/which.py
pym/gentoolkit/errors.py
pym/gentoolkit/glsa/__init__.py
pym/gentoolkit/helpers.py
pym/gentoolkit/metadata.py [new file with mode: 0644]
pym/gentoolkit/package.py
pym/gentoolkit/pprinter.py
pym/gentoolkit/test/__init__.py [new file with mode: 0644]
pym/gentoolkit/test/equery/__init__.py [new file with mode: 0644]
pym/gentoolkit/test/equery/test_init.py [new file with mode: 0644]
pym/gentoolkit/test/test_helpers.py [new file with mode: 0644]
pym/gentoolkit/test/test_syntax.py [new file with mode: 0644]
pym/gentoolkit/textwrap_.py
pym/gentoolkit/versionmatch.py
setup.py

diff --git a/AUTHORS b/AUTHORS
index fa210a6971374c7ee63f80923f2d6d45128a8d27..5b074e6d07d43ab67c7af843dd2ed8a8b3bcf6d5 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,19 +1,19 @@
 * gentoolkit
   Original author: Karl Trygve Kalleberg <karltk@gentoo.org>
-  Further additions: Douglas Anderson <dja@gendja.com>
+  Library additions and refactor: Douglas Anderson <douglasjanderson@gmail.com>
   Current maintainer: Paul Varner <fuzzyray@gentoo.org>
 
 * eclean
   Original author: Thomas de Grenier de Latour (tgl) <degrenier@easyconnect.fr>
+
 * epkginfo
   Author: Ned Ludd <solar@gentoo.org>
   earch: Eldad Zack <eldad@gentoo.org>
-  metadata: Olinger <EvvL AT RustedHalo DOT net> 
+  metadata: Olinger <EvvL AT RustedHalo DOT net>
 
 * equery
   Original author: Karl Trygve Kalleberg <karltk@gentoo.org>
-  Modular redesign: Douglas Anderson <dja@gendja.com>
+  Modular redesign: Douglas Anderson <douglasjanderson@gmail.com>
 
 * eread
   Original author: Donnie Berkholz <dberkholz@gentoo.org>
diff --git a/CREDITS b/CREDITS
new file mode 100644 (file)
index 0000000..3f9d6f0
--- /dev/null
+++ b/CREDITS
@@ -0,0 +1,16 @@
+
+       This is an incomplete list of people who have contributed non-
+       trivial patches to gentoolkit-0.3.0 and later, in the spirit of
+       the Linux kernel CREDITS file. It is sorted alphabetically by
+       name (N). Other fields are email (E) and description (D).
+       We'd like to thank:
+
+-------------------------------------------------------------------------------
+
+N: Brian Dolbec
+E: dol-sen@users.sourceforge.net
+D: Separating dependency gathering functionality from output in depends.py.
+
+
+
+# vim: set ts=8 sw=8 tw=80:
index b5b0442e41fb7c7db8548aabfbed4cde11dac903..7d421a3a807ca2dd4d1c66450cbafdfffb2c57f6 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,6 @@
+2009-12-08: Paul Varner <fuzzyray@gentoo.org>
+       * gentoolkit: Merge rev 113 from djanderson's genscripts repo
+
 2009-10-30: Christian Ruppert <idl0r@gentoo.org>
        * revdep-rebuild: Speedup portageq queries. Include FuzzyRay's patch to
                respect EMERGE_DEFAULT_OPTS.
index 9bb143ebb3643c608c6b7f9e47cc823363252fa9..35297ebb4155d903658eea438bba5138381a38b8 100644 (file)
@@ -1,3 +1,12 @@
-include AUTHORS ChangeLog COPYING NEWS README README.Developer THANKS TODO VERSION
+include AUTHORS
+include ChangeLog
+include COPYING
+include CREDITS
+include NEWS
+include README
+include TODO
+include setup.py
 recursive-include data *
+recursive-include bin *
 recursive-include man *
+recursive-include pym *.py *.txt
diff --git a/README.dev b/README.dev
new file mode 100644 (file)
index 0000000..7a33d1c
--- /dev/null
@@ -0,0 +1,25 @@
+Adding or modifying code:
+=========================
+- If you add new code, best practice is to write a test for it.
+- If you're modifying code that doesn't have a test and you can write a test
+  for it, please do.
+- Before committing your changes to a python file, please make sure it passes
+  pylint with:
+pylint --rcfile=pylintrc yourfile.py
+- If pylint raises a warning or error that you don't agree with, it's probably
+  better to just change your code. If you're sure you have a good reason for
+  doing what you're doing, you can add an exception to our pylintrc.
+
+Creating a release:
+===================
+- Run Gentoolkit's test suite, make sure it passes:
+./setup.py test
+- Create a tag for the release
+- Set the version of the release:
+VERSION="0.3.0" ./setup set_version
+- Create a source distribution (you need to add VERSION here, too):
+VERSION="0.3.0" ./setup.py sdist
+- Reset svn version to 'svn' (default):
+./setup.py set_version
+- Clean up temporary files:
+./setup clean -a
diff --git a/TODO b/TODO
index d39ea858f58d557eb6fe0442534d4c94ee7def01..04cfbf3363f4a49f7436c717b3e8229c3bf249e3 100644 (file)
--- a/TODO
+++ b/TODO
@@ -2,16 +2,8 @@
  - add --overlay, --portdir to uses
  - add glsa pkgspec
   - query for current GLSAs on installed package(s)
- - add changelog <from> <to> [--last] [--lastentry] [--current]
-  - displays changelog entries
- - add check <pkg-spec> [--full] [--display=pkgnames,full]
-  - check md5 and timestamps (optionally also in CONFIG_PROTECT dirs)
- - add which
-  - as ewhich
-- fully deprecate dep-clean and pkg-clean
 - rewrite ekeywords and echangelog to use gentoolkit
-- add deep depends to equery
-- fully deprecate qpkg 
+- fully deprecate qpkg
 - fully deprecate pkg-size
 - merge change and echangelog
 - merge useflag and euse, have _one_ command line tool
@@ -20,9 +12,6 @@
   - merge generate-use and ufed?
 - rewrite revdep-rebuild to use gentoolkit
  - drop qpkg dependency; use equery instead
-- rewrite distfiles-clean to use gentoolkit
-- look through forums.gentoo.org for additional scripts
-- write a Gentoolkit Guide
 - write efeatures for turning on/off FEATURES in make.conf
 - look at ekeys, ewatch
 - revision bump tool
  - -m for changelog entry
  - use ~/.gentoo/gentoolkit/ebump.conf
  - use /etc/gentoolkit/ebump.conf
-- should be removed:
- - etc-update
- - epm
- - useflag
-- add 'clean' target
 
-+ check esearch, eupdatedb: separate package for now
+equery:
+       Tests:
+               +helpers2 (FileOwner._extend_realpaths test probably doesn't clean up)
+       Run pylint and write test to run pylint
+       Write test to compile all modules (full syntax check). Take from portage.
+       Add more --debug stuff
+       Write tests for Dependencies._parser
+       Refactor each module to be useful for import. Done modules:
+               +depends
+               +belongs
+               +meta
+               +changes
+               +depgraph
+
+For Next Release:
+       - allow passing VERSION='123' in front of setup.py sdist and do version
+         mangling at dist creation time.
+       - find all commits to main gentoolkit svn after rc7 push and merge where
+         appropriate
index 9953fa7390930843011cbb77e29caa3aae1bb60e..6fee381823c4805e44559bd91b81e46826cc2d52 100755 (executable)
@@ -9,7 +9,7 @@ from __future__ import with_statement
 # Meta:
 __author__ = "Thomas de Grenier de Latour (tgl)"
 __email__ = "degrenier@easyconnect.fr"
-__version__ = open('/usr/share/gentoolkit/VERSION').read().strip()
+__version__ = "svn"
 __productname__ = "eclean"
 __description__ = "A cleaning tool for Gentoo distfiles and binaries."
 
@@ -206,14 +206,14 @@ def prettySize(size,justify=False):
                size = size / 1024.
                units.pop()
        sizestr = fpformat.fix(size,approx)+units[-1]
-       if justify: 
+       if justify:
                sizestr = " " + blue("[ ") + " "*(7-len(sizestr)) \
                          + green(sizestr) + blue(" ]")
        return sizestr
 
 
 ###############################################################################
-# yesNoAllPrompt: print a prompt until user answer in yes/no/all. Return a 
+# yesNoAllPrompt: print a prompt until user answer in yes/no/all. Return a
 # boolean for answer, and also may affect the 'accept_all' option.
 # Note: i gave up with getch-like functions, to much bugs in case of escape
 # sequences. Back to raw_input.
@@ -259,8 +259,8 @@ def parseSize(size):
 
 
 ###############################################################################
-# parseTime: convert a duration "Xu" ("X" is an int, and "u" a time unit in 
-# [Y,M,W,D,H]) into an integer which is a past EPOCH date. 
+# parseTime: convert a duration "Xu" ("X" is an int, and "u" a time unit in
+# [Y,M,W,D,H]) into an integer which is a past EPOCH date.
 # Raises ParseArgsException('time') in case of failure.
 # (yep, big approximations inside... who cares?)
 def parseTime(timespec):
@@ -287,7 +287,7 @@ def parseTime(timespec):
 # dict.
 def parseArgs(myoptions={}):
 
-       # local function for interpreting command line options 
+       # local function for interpreting command line options
        # and setting myoptions accordingly
        def optionSwitch(myoption,opts,action=None):
                return_code = True
@@ -323,7 +323,7 @@ def parseArgs(myoptions={}):
                # sanity check of --destructive only options:
                for myopt in ('fetch-restricted', 'package-names'):
                        if (not myoptions['destructive']) and myoptions[myopt]:
-                               if not myoptions['quiet']:      
+                               if not myoptions['quiet']:
                                        eerror("--%s only makes sense in --destructive mode." \
                                                        % myopt, myoptions['nocolor'])
                                myoptions[myopt] = False
@@ -429,14 +429,14 @@ def parseExcludeFile(filepath):
        filecontents = file.readlines()
        file.close()
        cat_re = re.compile('^(?P<cat>[a-zA-Z0-9]+-[a-zA-Z0-9]+)(/\*)?$')
-       cp_re = re.compile('^(?P<cp>[-a-zA-Z0-9_]+/[-a-zA-Z0-9_]+)$')   
+       cp_re = re.compile('^(?P<cp>[-a-zA-Z0-9_]+/[-a-zA-Z0-9_]+)$')
        for line in filecontents:
                line = line.strip()
                if not len(line): continue
                if line[0] == '#': continue
                try: mycat = cat_re.match(line).group('cat')
                except: pass
-               else: 
+               else:
                        if not mycat in portage.settings.categories:
                                raise ParseExcludeFileException("Invalid category: "+mycat)
                        excl_dict['categories'][mycat] = None
@@ -604,7 +604,7 @@ def findPackages( \
                package_names=False):
        clean_dict = {}
        # create a full package dictionary
-       
+
        if not (os.path.isdir(pkgdir)):
                eerror("%s does not appear to be a directory." % pkgdir, myoptions['nocolor'])
                eerror("Please set PKGDIR to a sane value.", myoptions['nocolor'])
@@ -687,7 +687,7 @@ def doCleanup(clean_dict,action,myoptions):
                     or yesNoAllPrompt(myoptions, \
                                       "Do you want to delete this " \
                                       + file_type+"?"):
-                       # non-interactive mode or positive answer. 
+                       # non-interactive mode or positive answer.
                        # For each file, try to delete the file and clean it out
                        # of Packages metadata file
                        if action == 'packages':
@@ -713,7 +713,7 @@ def doCleanup(clean_dict,action,myoptions):
                                        clean_size += filesize
                                        if action == 'packages':
                                                metadata.packages[:] = [p for p in metadata.packages if 'CPV' in p and p['CPV'] != file]
-                               
+
                        if action == 'packages':
                                with open(os.path.join(pkgdir, 'Packages'), 'w') as metadata_file:
                                        metadata.write(metadata_file)
@@ -740,7 +740,7 @@ def doAction(action,myoptions,exclude_dict={}):
                        destructive=myoptions['destructive'], \
                        package_names=myoptions['package-names'], \
                        time_limit=myoptions['time-limit'])
-       else: 
+       else:
                clean_dict = findDistfiles( \
                        myoptions, \
                        exclude_dict=exclude_dict, \
@@ -796,7 +796,7 @@ def main():
                elif e.value == 'version':
                        printVersion()
                        sys.exit(0)
-               else: 
+               else:
                        printUsage(e.value)
                        sys.exit(2)
        # parse the exclusion file
@@ -811,7 +811,7 @@ def main():
                        eerror("Invalid exclusion file: %s" % myoptions['exclude-file'], \
                                        myoptions['nocolor'])
                        eerror("See format of this file in `man %s`" % __productname__, \
-                                       myoptions['nocolor'])   
+                                       myoptions['nocolor'])
                        sys.exit(1)
        else: exclude_dict={}
        # security check for non-pretend mode
index 747527a1bd5ce28d28c2a8265be0b8e91039f977..a3354031ff1ed11c34a761fa4ea2568d2785adae 100755 (executable)
@@ -19,7 +19,7 @@ from xml.sax.handler import feature_namespaces
 import portage
 from portage.output import *
 
-version = open('/usr/share/gentoolkit/VERSION').read().strip()
+__version__ = "svn"
 
 def earch(workdir):
        """Prints arch keywords for a given dir"""
@@ -177,7 +177,7 @@ def check_metadata(full_package):
 
 def usage(code):
        """Prints the uage information for this script"""
-       print green("epkginfo"), "(%s)" % version
+       print green("epkginfo"), "(%s)" % __version__
        print
        print "Usage: epkginfo [package-cat/]package"
        sys.exit(code)
index bac8a3afdffab73372f58f03c06798a47f4dde75..e7bb6ce2e714059d7e7593115efdb263a9d043ee 100755 (executable)
@@ -2,9 +2,11 @@
 #
 # Copyright 2002-2009 Gentoo Technologies, Inc.
 # Distributed under the terms of the GNU General Public License v2 or later
+#
+# $Header$
 
-"""equery is a flexible utility for Gentoo linux which can display various 
-information about packages, such as the files they own, their USE flags, 
+"""equery is a flexible utility for Gentoo linux which can display various
+information about packages, such as the files they own, their USE flags,
 the MD5 sum of each file owned by a given package, and many other things.
 """
 
@@ -27,6 +29,16 @@ except KeyboardInterrupt:
        print
        sys.exit(1)
 
-from gentoolkit import equery
+from gentoolkit import equery, errors
 
-equery.main() 
+try:
+       equery.main()
+except errors.GentoolkitException, err:
+       if '--debug' in sys.argv:
+               raise
+       else:
+               from gentoolkit import pprinter as pp
+               sys.stderr.write(pp.error(str(err)))
+               print
+               print "Add '--debug' to global options for traceback."
+               sys.exit(1)
index ca9835ecea46dd328212a80203a7b6108115a0b8..595088894158f9349b9761407d8803a4e5feadce 100755 (executable)
--- a/bin/euse
+++ b/bin/euse
@@ -7,7 +7,7 @@
 # Licensed under the GPL v2
 
 PROGRAM_NAME=euse
-PROGRAM_VERSION=$(cat /usr/share/gentoolkit/VERSION)
+VERSION="svn"
 
 MAKE_CONF_PATH=/etc/make.conf
 MAKE_GLOBALS_PATH=/etc/make.globals
@@ -67,9 +67,9 @@ check_sanity() {
        # file permission tests
        local descdir
        local make_defaults
-       
+
        descdir="$(get_portdir)/profiles"
-       
+
        [ ! -r "${MAKE_CONF_PATH}" ] && error "${MAKE_CONF_PATH} is not readable"
        [ ! -r "${MAKE_GLOBALS_PATH}" ] && error "${MAKE_GLOBALS_PATH} is not readable"
        [ ! -h "${MAKE_PROFILE_PATH}" ] && error "${MAKE_PROFILE_PATH} is not a symlink"
@@ -86,7 +86,7 @@ check_sanity() {
 
 showhelp() {
 cat << HELP
-${PROGRAM_NAME} (${PROGRAM_VERSION})
+${PROGRAM_NAME} (${VERSION})
 
 Syntax: ${PROGRAM_NAME} <option> [suboptions] [useflaglist]
 
@@ -113,7 +113,7 @@ HELP
 
 showversion() {
 cat << VER
-${PROGRAM_NAME} (${PROGRAM_VERSION})
+${PROGRAM_NAME} (${VERSION})
 Written by Marius Mauch
 
 Copyright (C) 2004-2009 Gentoo Foundation, Inc.
@@ -144,16 +144,16 @@ for x in sys.stdin.read().split():
                r.append(x)
        elif x not in r:
                r.append(x)
-print ' '.join(r)" 
+print ' '.join(r)"
 }
 
 # the following function creates a bash array ACTIVE_FLAGS that contains the
-# global use flags, indexed by origin: 0: environment, 1: make.conf, 
+# global use flags, indexed by origin: 0: environment, 1: make.conf,
 # 2: make.defaults, 3: make.globals
 get_useflags() {
        # only calculate once as calling emerge is painfully slow
        [ -n "${USE_FLAGS_CALCULATED}" ] && return
-       
+
        # backup portdir so get_portdir() doesn't give false results later
        portdir_backup="${PORTDIR}"
 
@@ -186,7 +186,7 @@ get_useflaglist() {
        local descdir
 
        descdir="$(get_portdir)/profiles"
-       
+
        if [ -z "${SCOPE}" -o "${SCOPE}" == "global" ]; then
                egrep "^[^# ]+ +-" "${descdir}/use.desc" | cut -d\  -f 1
        fi
@@ -200,9 +200,9 @@ get_all_make_defaults() {
        local curdir
        local parent
        local rvalue
-       
+
        curdir="${1:-$(get_real_path ${MAKE_PROFILE_PATH})}"
-       
+
        [ -f "${curdir}/make.defaults" ] && rvalue="${curdir}/make.defaults ${rvalue}"
        if [ -f "${curdir}/parent" ]; then
                for parent in $(egrep -v '(^#|^ *$)' ${curdir}/parent); do
@@ -218,9 +218,9 @@ get_all_make_defaults() {
 get_make_defaults() {
        local curdir
        local parent
-       
+
        curdir="${1:-$(get_real_path ${MAKE_PROFILE_PATH})}"
-       
+
        if [ ! -f "${curdir}/make.defaults" -a -f "${curdir}/parent" ]; then
                for parent in $(egrep -v '(^#|^ *$)' ${curdir}/parent); do
                        if [ -f "$(get_make_defaults ${curdir}/${parent})" ]; then
@@ -233,9 +233,9 @@ get_make_defaults() {
        echo "${curdir}/make.defaults"
 }
 
-# little helper function to get the status of a given flag in one of the 
+# little helper function to get the status of a given flag in one of the
 # ACTIVE_FLAGS elements. Arguments are 1: flag to test, 2: index of ACTIVE_FLAGS,
-# 3: echo value for positive (and as lowercase for negative) test result, 
+# 3: echo value for positive (and as lowercase for negative) test result,
 # 4 (optional): echo value for "missing" test result, defaults to blank
 get_flagstatus_helper() {
        if echo " ${ACTIVE_FLAGS[${2}]} " | grep " ${1} " > /dev/null; then
@@ -277,35 +277,35 @@ get_portdir() {
        echo "${PORTDIR}"
 }
 
-# This function takes a list of use flags and shows the status and 
+# This function takes a list of use flags and shows the status and
 # the description for each one, honoring $SCOPE
 showdesc() {
        local descdir
        local current_desc
        local found_one
        local args
-       
+
        args="${*:-*}"
-       
+
        if [ -z "${SCOPE}" ]; then
                SCOPE="global" showdesc ${args}
                echo
                SCOPE="local" showdesc ${args}
                return
        fi
-       
+
        descdir="$(get_portdir)/profiles"
-       
+
        [ "${SCOPE}" == "global" ] && echo "global use flags (searching: ${args})"
        [ "${SCOPE}" == "local" ] && echo "local use flags (searching: ${args})"
        echo "************************************************************"
-       
+
        if [ "${args}" == "*" ]; then
                args="$(get_useflaglist | sort -u)"
        fi
-       
+
        set ${args}
-       
+
        foundone=0
        while [ -n "${1}" ]; do
                if [ "${SCOPE}" == "global" ]; then
@@ -315,7 +315,7 @@ showdesc() {
                        fi
                        grep "^${1}  *-" "${descdir}/use.desc"
                fi
-               # local flags are a bit more complicated as there can be multiple 
+               # local flags are a bit more complicated as there can be multiple
                # entries per flag and we can't pipe into printf
                if [ "${SCOPE}" == "local" ]; then
                        if grep ":${1}  *-" "${descdir}/use.local.desc" > /dev/null; then
@@ -333,7 +333,7 @@ showdesc() {
                fi
                shift
        done
-       
+
        if [ ${foundone} == 0 ]; then
                echo "no matching entries found"
        fi
@@ -361,13 +361,13 @@ showinstdesc() {
 
        descdir="$(get_portdir)/profiles"
        echo "************************************************************"
-       
+
        if [ "${args}" = "*" ]; then
                args="$(get_useflaglist | sort -u)"
        fi
-       
+
        set "${args[@]}"
-       
+
        while [ -n "${1}" ]; do
                case "${SCOPE}" in
                        "global")
@@ -406,7 +406,7 @@ showinstdesc() {
                esac
                shift
        done
-       
+
        if [ ${foundone} -lt 1 ]; then
                echo "no matching entries found"
        fi
@@ -418,15 +418,15 @@ showflags() {
        local args
 
        get_useflags
-       
+
        args="${*:-*}"
-       
+
        if [ "${args}" == "*" ]; then
                args="$(get_useflaglist | sort -u)"
        fi
-       
+
        set ${args}
-       
+
        while [ -n "${1}" ]; do
                if echo " ${ACTIVE_FLAGS[9]} " | grep " ${1} " > /dev/null; then
                        printf "%-20s" ${1}
@@ -446,7 +446,7 @@ remove_flag() {
        NEW_MAKE_CONF_USE="${NEW_MAKE_CONF_USE// ${1} / }"
 }
 
-# USE flag modification function. Mainly a loop with calls to add_flag and 
+# USE flag modification function. Mainly a loop with calls to add_flag and
 # remove_flag to create a new USE string which is then inserted into make.conf.
 modify() {
        if [ -z "${*}" ]; then
@@ -458,11 +458,11 @@ modify() {
                        set $(get_useflaglist | sort -u)
                fi
        fi
-       
+
        get_useflags
-       
+
        NEW_MAKE_CONF_USE=" ${ACTIVE_FLAGS[1]} "
-       
+
        while [ -n "${1}" ]; do
                if [ "${ACTION}" == "add" ]; then
                        if echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then
@@ -491,13 +491,13 @@ modify() {
                        shift
                fi
        done
-       
+
        #echo "old flags:"
        #echo ${ACTIVE_FLAGS[1]}
        #echo
        #echo "new flags:"
        #echo ${NEW_MAKE_CONF_USE}
-       
+
        # a little loop to add linebreaks so we don't end with one ultra-long line
        NEW_MAKE_CONF_USE_2=""
        for x in ${NEW_MAKE_CONF_USE}; do
@@ -510,9 +510,9 @@ modify() {
 
        # make a backup just in case the user doesn't like the new make.conf
        cp -p "${MAKE_CONF_PATH}" "${MAKE_CONF_BACKUP_PATH}"
-       
+
        # as sed doesn't really work with multi-line patterns we have to replace USE
-       # on our own here. Basically just skip everything between USE=" and the 
+       # on our own here. Basically just skip everything between USE=" and the
        # closing ", printing our new USE line there instead.
        inuse=0
        had_use=0
@@ -536,7 +536,7 @@ modify() {
                echo -ne "${NEW_MAKE_CONF_USE_2%% }"
                echo '"'
        fi ) < "${MAKE_CONF_BACKUP_PATH}" | sed -e 's:\\ $:\\:' > "${MAKE_CONF_PATH}"
-       
+
        echo "${MAKE_CONF_PATH} was modified, a backup copy has been placed at ${MAKE_CONF_BACKUP_PATH}"
 }
 
index 55a4d6be4a0288720075eccb17bc51ba8f99ce6d..cf86e717f7e68e8197d445af922a7c4f8be393e4 100755 (executable)
@@ -18,6 +18,7 @@ unset GREP_OPTIONS
 
 # Readonly variables:
 declare -r APP_NAME="${0##*/}" # The name of this application
+declare -r VERSION="svn"
 declare -r OIFS="$IFS"         # Save the IFS
 declare -r     ENV_FILE=0_env.rr     # Contains environment variables
 declare -r   FILES_FILE=1_files.rr   # Contains a list of files to search
@@ -190,6 +191,12 @@ Calls emerge, options after -- are ignored by $APP_NAME
 and passed directly to emerge.
 
 Report bugs to <http://bugs.gentoo.org>
+
+${APP_NAME}: (${VERSION})
+
+Copyright (C) 2003-2009 Gentoo Foundation, Inc.
+This is free software; see the source for copying conditions.
+
 EOF
 }
 ##
index d4ca7731ac5e0e02ab8eae315a037bf1e00fec48..c47b8607bfb97c74c16aa361157688df44fca348 100644 (file)
-.TH "equery" "1" "Oct 2005" "gentoolkit" ""
+.TH "EQUERY" "1" "August 2009" "GENTOOLKIT"
 .SH "NAME"
-equery \- Gentoo: Package Query Tool
+equery \- Gentoo Package Query Tool
+
 .SH "SYNOPSIS"
-.B equery  
-.I [global\-opts] command [local\-opts]
-.PP 
+.BI "equery " "[global-options] " "module " "[local-options]"
 
 .SH "DESCRIPTION"
-equery is a flexible utility which may display various information about 
-packages, such as the files they own, their USE flags, the md5sum 
-of each file owned by a given package, and many other things.
-
-.SH "OPTIONS"
-The 'command' is the only mandatory option to equery.  Most commands require
-a 'pkgspec' option, which is described by <cat/>packagename<\-version>;
-namely, the package name is mandatory, while the category and version are 
-optional.
-
-[global\-opts] may be one of:
-
-.B \-q, \-\-quiet   
-causes minimal output to be emitted
-.PP 
-.B \-C, \-\-nocolor 
-turns off colours
-.PP 
+.B Equery
+is a collection of modules for querying the state of Gentoo packages, files and USE flags.
+
+.SH "GLOBAL OPTIONS"
+.HP
 .B \-h, \-\-help
-displays a help summary
-.PP 
-.B \-V, \-\-version
-displays the equery version
-.PP 
+.br
+Output a help message.
+.HP
+.B \-q, \-\-quiet
+.br
+Be less verbose where possible. In some modules, this option can increase the output speed.
+.HP
+.B \-C, \-\-no-color
+.br
+Do not colorize output.
+.HP
 .B \-N, \-\-no\-pipe
-turns off pipe detection
-.PP 
-
-Only one command will actually be run, at most.  The possible commands are: 
-.TP 
-.B belongs <local\-opts> file
-This command lists all packages owning the specified file.
-.br 
-Note: Normally, only one package will own a file. If multiple packages own the
-same file, it usually consitutes a problem, and should be reported (http://bugs.gentoo.org).
-.br 
-.IP 
-<local\-opts> is either or both of:
-.\" .br 
-.\" .B \-c, \-\-category cat 
-.\" only search in category cat
-.br 
-.B \-f, \-\-full\-regex
-supplied query is a regex
-.br 
-.B \-e, \-\-earlyout 
-stop when first match found
 .br
+Turn off automatic pipe detection. Use this option if you do not want
+.B equery
+to detect if the output is being directed to the screen or to another program and adjust color and verbosity accordingly.
+.HP
+.B \-V, \-\-version
+.br
+Display \fBGentoolkit\fP's version. Please include this in all bug reports. (see
+.B BUGS
+below)
+
+.SH "MODULES"
+.B Equery
+uses a system of modules. Each module has both a long and short name. The list below uses the notation "\fBmodule (m)\fP", where \fIm\fP is the short name and \fImodule\fP is the long name.
+.P
+You can view the
+.B help
+message for a specific module by using
+.BR "-h" ", " "--help "
+as either a global option (after
+.B equery
+and before the module name) or as a local option (after the module name).
+
+.SS
+.BI "belongs (b) [OPTIONS] " "FILE"
+List the package that owns \fIFILE\fP.
+.P
+.BI Note:
+Normally, only one package will own \fIFILE\fP. If multiple packages own the
+same file it should be reported. (see
+.B BUGS
+below)
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-f, \-\-full-regex
+.br
+The supplied query is a regular expression.
+.HP
+.B \-e, \-\-early-out
+.br
+Stop when the first match is found. This is generally a safe optimization when searching for the owner of a single file.
+.HP
 .B \-n, \-\-name-only
-don't print the version
-
-.PP 
-.B check pkgspec
-This command checks the files of the specified package against recorded MD5 
-sums and timestamps.
-.PP 
-.TP 
-.B depends <local\-opts> pkgspec
-This command displays all dependencies matching pkgspec.
-.br 
-<local\-opts> is either or both of:
-.br 
-.B \-a, \-\-all\-packages
-search in all available packages (slow)
-.br 
-.B \-d, \-\-direct
-search direct dependencies only (default)
-.br 
+.br
+Do not print the version.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery belongs --early-out /usr/bin/euse
+.EE
+.br
+Find out what package installed a certain command.
+.EX
+.HP
+emerge -p $(equery -q belongs -nf '^/usr/bin/g?vim.*')
+.EE
+.br
+Tell
+.B emerge
+to reinstall or update any package that installed a file matching a regular expression.
+
+.SS
+.BI "changes (c) [OPTIONS] " "PKG"
+Display the Gentoo ChangeLog entry for the latest installable version of \fIPKG\fP.
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-l, \-\-latest
+.br
+Display only the latest ChangeLog entry. It's not uncommon for changes to be prepended to the ChangeLog without a version header if the changes did not require a version bump. Use this option to display such entries.
+.HP
+.B \-f, \-\-full
+.br
+Display the full ChangeLog.
+.br
+\fBHint\fP: Try piping (|) the output to a pager, like 'less'.
+.HP
+.BI "\-\-limit=" "NUM"
+.br
+Limit the \fINUM\fP of entries displayed. Use  this option in conjunction with \fB--full\fP. \fB--limit=3\fP would display the three latest entries.
+.HP
+.BI "\-\-from=" "VER"
+.br
+Set which \fIVER\fP to display from. Using this option by itself is equivalent to passing \fBchanges\fP a ranged package atom, e.g., '>=foo/bar-1.5'. It can be used in conjunction with \fB--to\fP to request a more complex range of entries.
+.HP
+.BI "\-\-to=" "VER"
+.br
+Set which \fIVER\fP to display to. (See \fB--from\fP)
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery changes portage
+.EE
+.br
+Display the Gentoo ChangeLog entry for the latest installable version of Portage.
+.EX
+.HP
+equery changes '=sys-apps/portage-2.1.6*'
+.EE
+.br
+Use Portage's atom syntax. (See \fBman 5 ebuild\fP)
+.EX
+.HP
+equery changes portage --from=2.2_rc1 --to=2.2
+.EE
+.br
+Display any ChangeLog entry within a range of versions.
+
+.SS
+.BI "check (k) [OPTIONS] " "PKG"
+Check timestamps and MD5 sums for files owned by \fIPKG\fP, where \fIPKG\fP is an installed package.
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-f, \-\-full-regex
+.br
+The supplied query is a regular expression.
+.HP
+.B \-o, \-\-only-failures
+.br
+Only display packages which don't pass all checks.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery check --only-failures '*'
+.EE
+.br
+Verify timestamps and MD5 sums for all installed packages and show only packages which fail checks.
+.EX
+.HP
+equery check 'dev-python/*' dev-lang/python
+.EE
+.br
+Verify every installed package in the \fBdev-python\fP category, and Python itself.
+
+.SS
+.BI "depends (d) [OPTIONS] " "PKG"
+List all packages that depend on \fIPKG\fP.
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-a, \-\-all-packages
+.br
+Include dependencies that are not installed. This can take a while.
+.HP
 .B \-D, \-\-indirect
-search indirect dependencies (very slow)
-.br 
-.B \-\-depth=n
-Limit depth of indirect dependency tree to n levels. Setting \-\-depth=0 is the same as not specifing \-\-indirect.
-.PP 
-.TP 
-.B depgraph <local\-opts> pkgspec
-This command display a dependency tree for pkgspec, by default indented to reflect
-how dependancies relate to each other.
-.br 
-.IP 
-<local\-opts> is either or both of:
-.br 
-.B \-U, \-\-no\-useflags 
-do not show USE flags.
-.br 
-.B \-l, \-\-linear    
-do not use fancy formatting 
-.br 
-.B \-\-depth=n
-Limit depth of dependency graph to n levels.
-.PP 
-.TP 
-.B files <local\-opts> pkgspec
-This lists files owned by a particular package, optionally with extra
-information specified by <local\-opts>
-.br 
-
-<local\-opts> is any combination of:
-.br 
-.B \-\-timestamp
-output the timestamp of each file
-.br 
-.B \-\-md5sum
-output the md5sum of each file
-.br 
-.B \-\-type
-output the type of each file
-.br 
+.br
+Search for both direct and indirect dependencies.
+.HP
+.BI "\-\-depth=" "NUM"
+.br
+Limit the indirect dependency tree to a depth of \fINUM\fP. \fB--depth=0\fP is equivalent to not using \fB--indirect\fP.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery depends --indirect xulrunner
+.EE
+.br
+Figure out why a package got installed on your system.
+
+.SS
+.BI "depgraph (g) [OPTIONS] " "PKG"
+Display a direct dependency graph for every matching version of \fIPKG\fP. A dependency graph is an
+indented tree showing the relationship between packages and their dependencies.
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-A, \-\-no-atom
+.br
+Do not show the dependency atom that match the package.
+.HP
+.B \-U, \-\-no-useflags
+.br
+Do not show USE flags.
+.HP
+.B \-l, \-\-linear
+.br
+Do not format the graph by indenting dependencies. This option will print the
+recursion depth in square brackets before the package name for easier viewing
+in narrow terminals.
+.HP
+.BI "\-\-depth=" "NUM"
+.br
+Limit the dependency graph to a depth of \fINUM\fP. \fB--depth=0\fP means no
+maximum depth. Default depth is set to 1.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery depgraph --depth=0 portage
+.EE
+.br
+View a full tree of all direct and indirect compile-time, run-time, and post-merge dependencies for a package.
+
+.SS
+.BI "files (f) [OPTIONS] " "PKG"
+List files and directories installed by \fIPKG\fP.
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-m, \-\-md5sum
+.br
+Include the file's MD5 sum in the output.
+.HP
+.B \-s, \-\-timestamp
+.br
+Include the file's timestamp in the output.
+.HP
+.B \-t, \-\-type
+.br
+Include the file type in the output.
+.HP
 .B \-\-tree
-display results in a tree (turns off all other options)
-.br 
-.B \-\-filter=<rules>
-filter output based on files type or path
-.br 
-.B \t<rules>
-is a comma separated list of filtering rules. Available rules are:
-.br 
-.B \t\tdir\ 
-regular directories
-.br 
-.B \t\tobj\ 
-regular files
-.br 
-.B \t\tsym\ 
-symbolic links
-.br 
-.B \t\tdev\ 
-device nodes
-.br 
-.B \t\tfifo
-named pipes
-.br 
-.B \t\tpath
-shortest paths where some files where installed
-.br 
-.B \t\tconf
-configuration files (based on $CONFIG_PROTECT)
-.br 
-.B \t\tcmd\ 
-user commands (based on $PATH)
-.br 
-.B \t\tdoc\ 
-documentation files (from /usr/share/doc)
-.br 
-.B \t\tman\ 
-manpages (from /usr/share/man)
-.br 
-.B \t\tinfo
-info pages (from /usr/share/info)
-.PP 
-.TP 
-.B hasuse <local\-opts> useflag
-This command lists packages matching a particular USE flag in a user\-specified combination
-of installed packages, packages which are not installed, the portage tree, and
-the portage overlay tree.
-
-<local\-opts> must not include only \-I;
-if \-I is used, \-p and/or \-o must be also be present.  By default, only installed
-packages are searched.  \-o searches only the overlay tree [and possibly
-installed packages],
-.I not
-the main portage tree.
-
-.B \-i, \-\-installed
-search installed packages (default)
-.br 
-.B \-I, \-\-exclude\-installed
-do not search installed packages
-.br 
-.B \-p, \-\-portage\-tree
-also search in portage tree (/usr/portage)
-.br 
-.B \-o, \-\-overlay\-tree
-also search in overlay tree (/usr/local/portage)
-.PP 
-.TP 
-.B list <local\-opts> pkgspec
-This command lists packages matching pkgspec in a user\-specified combination
-of installed packages, packages which are not installed, the portage tree, and
-the portage overlay tree. By default the list command searches for partial name matches.
-
-<local\-opts> \-I cannot be used by itself;
-if \-I is used, \-p and/or \-o must be also be present.  By default, only installed
-packages are searched.  \-o searches only the overlay tree [and possibly
-installed packages],
-\fInot\fR the main portage tree.
-
-.B \-i, \-\-installed
-search installed packages (default)
-.br 
-.B \-I, \-\-exclude\-installed
-do not search installed packages
-.br 
-.B \-p, \-\-portage\-tree
-also search in portage tree (/usr/portage)
-.br 
-.B \-o, \-\-overlay\-tree
-also search in overlay tree (/usr/local/portage)
-.br 
-.B \-f, \-\-full\-regex
-query is a regular expression
-.br 
-.B \-e, \-\-exact\-name
-list only those packages that exactly match
-.br 
+.br
+Display files in a tree format. This option turns off all other local options.
+.HP
+.BI "\-f, \-\-filter=" "RULES"
+.br
+Filter output by file type.
+.HP
+RULES
+.br
+A comma-separated list (no spaces); choose from:
+.br
+.B dir, obj, sym, dev, path, conf, cmd, doc, man, info
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery files --tree vlc
+.EE
+.br
+View a file tree of all files installed by a package.
+.EX
+.HP
+equery files --filter=cmd vlc
+.EE
+.br
+Find out where a package installed its executables.
+
+.SS
+.BI "hasuse (h) [OPTIONS] " "USE"
+List all installed packages that have a given \fIPKG\fP flag.
+
+\fBNote\fP: \fBhasuse\fP does not currently have the ability to display if packages are built with the given USE flag or not. It can only list which packages have the flag as an option. (See \fIEXAMPLES\fP)
+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-I, \-\-exclude-installed
+.br
+Exclude installed packages from being output.
+.HP
+.B \-o, \-\-overlay-tree
+.br
+Include package from overlays in the search path.
+.HP
+.B \-p, \-\-portage-tree
+.br
+Include all packages from the Portage tree in the search path. Use this option to search through all standard Gentoo packages, including those that are not installed.
+.P
+.IR "OUTPUT" ":"
+.HP
+(See \fIOUTPUT\fP for \fBlist\fP below)
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery hasuse -pI perl
+.EE
+.br
+View all Gentoo packages that have the "perl" USE flag, exluding installed packages.
+.EX
+.HP
+USE="perl"; for PKG in $(equery -q hasuse $USE); do echo $PKG: $(equery -q uses $PKG |grep $USE); done
+.EE
+.br
+This Bash one-liner uses \fBhasuse\fP to find a list of packages that have a certain USE flag, and \fBuses\fP to check whether the flag is enabled or disabled. Modify \fBUSE="perl"\fP to change the query.
+
+.SS
+.BI "list (l) [OPTIONS] " "PKG"
+List installed versions of \fIPKG\fP or all packages matching the query pattern.
+
+.IR "LOCAL OPTIONS" ":"
+.HP
 .B \-d, \-\-duplicates
-only list installed duplicate packages
-.br 
+.br
+List only packages with more than one version installed.
+.HP
+.B \-f, \-\-full-regex
+.br
+The supplied query is a regular expression.
+.HP
+.B \-m, \-\-mask-reason
+.br
+Print the reason why a package was masked.
+.HP
+.B \-I, \-\-exclude-installed
+.br
+Exclude installed packages from being output.
+.HP
+.B \-o, \-\-overlay-tree
+.br
+Include package from overlays in the search path.
+.HP
+.B \-p, \-\-portage-tree
+.br
+Include all packages from the Portage tree in the search path. Use this option to search through all standard Gentoo packages, including those that are not installed.
+.P
+.IR "OUTPUT" ":"
 
-\fBOutput:\fR
+.EX
+$ equery list binutils
+ * Searching for binutils ...
+ [I--] [XX] sys-devel/binutils-2.18-r1 (i686-pc-linux-gnu-2.18)
+ [IP-] [ ~] sys-devel/binutils-2.19.1-r1 (i686-pc-linux-gnu-2.19.1)
+.EE
+.HP
+Location field (\fB[IPO-]\fP):
+.br
+The first field shows the location and install status of the package. It consists of three letters in square brackets. \fBI\fP indicates the package is currently installed. \fBP\fP indicates the package is available in the Portage tree. \fBO\fP indicates the package is available in at least one overlay. \fB-\fP is a place holder and has no meaning. \fB[I-O]\fP would mean that the package is installed and available from an overlay, but not available from the Portage tree.
+.HP
+Mask-status field (\fB[ ~M-XX]\fP):
+.br
+The second field shows the mask status of the package. Empty brackets indicate that the package is unmasked. A \fB~\fP means the package is masked by keyword, e.g., you are running a stable system and the package is marked testing). \fBM\fP means hard masked, e.g., the package maintainer has determined the package is unfit for widespread usage. \fB-\fP means arch masked, e.g., you are running an amd64 system, but this package only works on x86. Lastly, \fBXX\fP only occurs when the location field is \fB[I--]\fP. Together, they indicate that the package was installed from the Portage tree or an overlay, but has since been removed from that tree; therefore \fBequery\fP can not determine a mask status for it.
+.HP
+Package name:
+.br
+The third field is the full package name and version.
+.HP
+Slot:
+.br
+The fourth field, in parentheses, is the package's slot. \fB0\fP is the default slot. To find all packages with multiple slots installed, use \fB--duplicates\fP.
+.P
+\fBNote:\fP Because it takes extra processing time to determine the location, mask status and slot, you can speed up output by passing the \fB--quiet\fP global option to \fBequery\fP when you don't care about the extra information.
 
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery list '*'
+.EE
+.br
+List all installed packages. This is equivalent to '\fBequery list\fP' in \fBGentoolkit\fP versions prior to 0.3.0.
+.EX
+.HP
+equery list -op mozilla-firefox
+.EE
+.br
+List all available versions of the package exactly matching 'mozilla-firefox'. This is equivalent to '\fBequery list --exact-name -o -p mozilla-firefox\fP' in \fBGentoolkit\fP versions prior to 0.3.0.
+.EX
+.HP
+equery list '*zilla*'
+.EE
 .br
-The list command searches packages for the name given. If found, the following info will be displayed: the package location between the first square brackets (I for Installed packages, P for Portage, O for Overlay), the possible masks between the second (~ by keyword, - by arch or M hard masked), then the category and complete name and last of all, the slot in which the package is stored.
+List all packages that contain (fuzzy match) 'zilla'. This is equivalent to '\fBequery list zilla\fP' in \fBGentoolkit\fP versions prior to 0.3.0.
+.EX
+.HP
+equery list 'www-client/*'
+.EE
+.br
+List all packages in the category \fBwww-client\fP. This is equivalent to '\fBequery list --category=www-client\fP' in \fBGentoolkit\fP versions prior to 0.3.0.
+.EX
+.HP
+equery list --duplicates '*'
+.EE
+.br
+List all packages with more than one version installed. This is equivalent to '\fBequery list --duplicates\fP' in \fBGentoolkit\fP versions prior to 0.3.0.
 
-\fBExamples:\fR
+.SS
+.BI "meta (m) [OPTIONS] " "PKG"
+Display metadata about \fIPKG\fP.
 
-equery list zilla \- list all installed versions of packages containing the string 'zilla'
+\fBmeta\fP reads a file called metadata.xml which must be included with all Portage tree packages. \fBmeta\fP does not read ebuilds, so it can only return version independent metadata. Since until now there had not been an easy way for users to view metadata.xml, and because package maintainers are only required to fill out a very small portion of the file, there are still many packages without detailed metadata available. For more information about metadata.xml, see:
+.br
+.EX
+http://www.gentoo.org/proj/en/devrel/handbook/handbook.xml?part=2&chap=4
+.EE
 
-equery list \-\-exact\-name x11\-libs/gtk+  \- list all installed versions of x11\-libs/gtk+
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-c, \-\-current
+.br
+Parse a metadata.xml file in the current directory. Useful for package maintainers to review formatting.
+.HP
+.B \-d, \-\-description
+.br
+Show an extended package description.
+.HP
+.B \-H, \-\-herd
+.br
+Show the herd(s) for the package. When not piping and not passing \fB--quiet\fP as a global option, also show the herd's email address.
+.HP
+.B \-m, \-\-maintainer
+.br
+Show the package maintainer(s) email address. If the metadata is available, also show the maitainer's name and/or job description.
+.HP
+.B \-u, \-\-useflags
+.br
+Show per-package USE flag descriptions. Per-package USE flag descriptions are sometimes added to metadata.xml if the affect of the USE flag is unusual, or if the USE flag is rare enough to be undefined in the global definition file. \fBequery uses\fP now displays these same local descriptions as well, so this option is left in \fBmeta\fP for completeness only.
+.HP
+.B \-U, \-\-upstream
+.br
+Show information about the package's upstream project. This information can be extremely useful for finding an author's email, upstream bug tracker or upstream documentation. However, most maintainers do not currently provide this information.
+.HP
+.B \-x, \-\-xml
+.br
+Dump the plain XML file to the screen.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery meta gnucash
+.EE
+.br
+Show general information about maintainership, including herd, maintainer and upstream.
+.EX
+.HP
+equery meta --description screen
+.EE
+.br
+See if the package maintainer has provided an extended description.
+.EX
+.HP
+equery -N meta -H gnome |grep -o --color=never '[^( ]*@gentoo.org'
+.EE
+.br
+Extract the herd's email address to let them know they're doing a great job. Remember, bug reports should go to bugs.gentoo.org. The above example will extract one or more emails if available, or return nothing if the herd is \fBno-herd\fP.
 
-equery list \-\-full\-regex '(mozilla\-firefox|mozilla\-thunderbird)' \- list all installed versions of mozilla\-firefox and mozilla\-thunderbird 
+.SS
+.BI "size (s) [OPTIONS] " "PKG"
+Print total size of files contained in a given \fIPKG\fP.
 
-equery list \-\-duplicates \- list all installed slotted packages
-.PP 
-.TP 
-.B size <local\-opts> pkgspec
-This command outputs the number of files in the specified package, as well as
-their total size in an appropriate unit.
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-b, \-\-bytes
+.br
+Report package size in bytes.
+.HP
+.B \-f, \-\-full-regex
+.br
+The query is a regular expression.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery -q size 'www-client/*'
+.EE
+.br
+Get a one-line summary of the number of files and total size (in bytes) of those files for each installed package in a category.
 
-The possible values for <local\-opts>, if specified, are:
-.br 
-.B \-b, \-\-bytes 
-report size in bytes
-.br 
-.B \-f, \-\-full\-regex
-query is a regular expression
-.br 
-.B \-e, \-\-exact\-name
-list only those packages that exactly match
-.PP 
-.TP 
-.B uses <local\-opts> pkgspec 
-display USE flags for pkgspec.
+.SS
+.BI "uses (u) [OPTIONS] " "PKG"
+Display USE flag statuses and desriptions for a given \fRPKG\fP.
 
-The only possible value for <local\-opts>, if specified, is:
-.br 
+.IR "LOCAL OPTIONS" ":"
+.HP
 .B \-a, \-\-all
-include all package versions
-.PP 
-.B which pkgspec 
-print full path to ebuild for package pkgspec
-.PP 
-
-.SH "Unimplemented Options"
-.PP 
-.B changes
-.PP 
-.B glsa \fR \- use glsa\-check for the time being.
-.PP 
-.B stats
+.br
+Display all package versions. Without this option, \fBequery\fP will choose the best available version.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+equery uses app-misc/beagle
+.EE
+.br
+See which USE flags are enabled for a specific package.
+.EX
+.HP
+USE="perl"; for PKG in $(equery -q hasuse $USE); do echo $PKG: $(equery -q uses $PKG |grep $USE); done
+.EE
+.br
+This Bash one-liner uses \fBhasuse\fP to find a list of packages that have a certain USE flag, and \fBuses\fP to check whether the flag is enabled or disabled. Modify \fBUSE="perl"\fP to change the query.
 
+.SS
+.BI "which (w) [OPTIONS] " "PKG"
+Display the path to the ebuild that would be used by Portage with the current configuation.
 
+.IR "LOCAL OPTIONS" ":"
+.HP
+.B \-m, \-\-include-masked
+.br
+Return the path to the hightest version ebuild available.
+.P
+.IR "EXAMPLES" ":"
+.EX
+.HP
+less $(equery which xorg-server)
+.EE
+.br
+View the most recent installable ebuild.
 
 .SH "BUGS"
-Many options aren't implemented.  Command\-line parsing could use some work. 
-.br 
-Submit bug reports to http://bugs.gentoo.org
-.SH "AUTHORS"
-equery, original man page: Karl Trygve Kalleberg <karltk@gentoo.org>, 2003.
-.br 
-Massive man page updates: Katerina Barone\-Adesi <katerinab@gmail.com>, 2004.
+Submit bug reports to http://bugs.gentoo.org.
 
+.SH "AUTHORS"
+Karl Trygve Kalleberg <karltk@gentoo.org>, 2003
+.br
+Katerina Barone\-Adesi <katerinab@gmail.com>, 2004
+.br
+Douglas Anderson <douglasjanderson@gmail.com>, 2009
diff --git a/pylintrc b/pylintrc
new file mode 100644 (file)
index 0000000..9a08a7c
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,320 @@
+# lint Python modules using external checkers.
+#
+# This is the main checker controlling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+#
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+ignore=.svn
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories (IRCWEF).
+#enable-msg-cat=
+
+# Disable all messages in the listed categories (IRCWEF).
+disable-msg-cat=I
+
+# Enable the message(s) with the given id(s).
+#enable-msg=
+
+# Disable the message(s) with the given id(s).
+# :W0704: *Except doesn't do anything*
+# :W0231: *__init__ method from base class %r is not called*
+# :R0903: *Too few public methods (%s/%s)*
+disable-msg=W0704,W0231,R0903
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=colorized
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells wether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# Enable the report(s) with the given id(s).
+#enable-report=
+
+# Disable the report(s) with the given id(s).
+#disable-report=
+
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assignment
+#
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branches, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+#
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+# was no-docstring-rgx=__.*__
+no-docstring-rgx=_.*
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+# was attr-rgx=[a-z_][a-z0-9_]{2,30}$
+attr-rgx=[a-z_][a-z0-9_]{1,30}$
+
+# Regular expression which should only match correct argument names
+# was argument-rgx=[a-z_][a-z0-9_]{2,30}$
+argument-rgx=[a-z_][a-z0-9_]{1,30}$
+
+# Regular expression which should only match correct variable names
+# was variable-rgx=[a-z_][a-z0-9_]{2,30}$
+variable-rgx=[a-z_][a-z0-9_]{1,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+
+# try to find bugs in the code using type inference
+#
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+#
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existent members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+#
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+#
+[DESIGN]
+
+# Maximum number of arguments for function / method
+# was max-args=5
+max-args=10
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+# was max-attributes=7
+max-attributes=10
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+#
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+#
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+#
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='\t'
+#indent-string='    '
index 35d89aca4bf65045acaf2481d7bcf83c19cfcaac..03382b78d2c9967f4c7fe9281ccb3cdf19b3dade 100644 (file)
@@ -1,42 +1,23 @@
 #!/usr/bin/python
 #
 # Copyright 2003-2004 Karl Trygve Kalleberg
-# Copyright 2003-2009 Gentoo Technologies, Inc.
+# Copyright 2003-2009 Gentoo Foundation
 # Distributed under the terms of the GNU General Public License v2
 #
 # $Header$
 
-# =======
-# Imports 
-# =======
+"""Gentoolkit is a collection of administration scripts for Gentoo"""
 
-import portage
-try:
-       from threading import Lock
-except ImportError:
-       # If we don't have thread support, we don't need to worry about
-       # locking the global settings object. So we define a "null" Lock.
-       class Lock(object):
-               def acquire(self):
-                       pass
-               def release(self):
-                       pass
+import sys
 
-# =======
-# Globals
-# =======
-
-PORTDB = portage.db[portage.root]["porttree"].dbapi
-VARDB  = portage.db[portage.root]["vartree"].dbapi
-VIRTUALS = portage.db[portage.root]["virtuals"]
-
-Config = {
-       "verbosityLevel": 3
+CONFIG = {
+    # Color handling: -1: Use Portage settings, 0: Force off, 1: Force on
+    'color': -1,
+    # Guess piping output:
+    'piping': False if sys.stdout.isatty() else True,
+    # Set some defaults:
+    'quiet': False,
+    'debug': False
 }
 
-try:
-       settingslock = Lock()
-       settings = portage.config(clone=portage.settings)
-except portage.exception.PermissionDenied, err:
-       sys.stderr.write("Permission denied: '%s'\n" % str(err))
-       sys.exit(e.errno)
+# vim: set ts=8 sw=4 tw=79:
diff --git a/pym/gentoolkit/atom.py b/pym/gentoolkit/atom.py
new file mode 100644 (file)
index 0000000..ff150c0
--- /dev/null
@@ -0,0 +1,195 @@
+#!/usr/bin/python
+#
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+"""Subclasses portage.dep.Atom to provide methods on a Gentoo atom string."""
+
+__all__ = ('Atom',)
+
+# =======
+# Imports
+# =======
+
+import weakref
+
+import portage
+
+from gentoolkit.cpv import CPV
+from gentoolkit.versionmatch import VersionMatch
+from gentoolkit import errors
+
+# =======
+# Classes
+# =======
+
+class Atom(portage.dep.Atom, CPV):
+       """Portage's Atom class with an improved intersects method from pkgcore.
+
+       portage.dep.Atom provides the following instance variables:
+
+       @type operator: str
+       @ivar operator: one of ('=', '=*', '<', '>', '<=', '>=', '~', None)
+       @type cp: str
+       @ivar cp: cat/pkg
+       @type cpv: str
+       @ivar cpv: cat/pkg-ver (if ver)
+       @type slot: str or None
+       @ivar slot: slot passed in as cpv:#
+       """
+
+       # Necessary for Portage versions < 2.1.7
+       _atoms = weakref.WeakValueDictionary()
+
+       def __init__(self, atom):
+               self.atom = atom
+
+               try:
+                       portage.dep.Atom.__init__(self, atom)
+               except portage.exception.InvalidAtom, err:
+                       raise errors.GentoolkitInvalidAtom(err)
+
+               # Make operator compatible with intersects
+               if self.operator is None:
+                       self.operator = '='
+
+               self.cpv = CPV(self.cpv)
+
+               # use_conditional is USE flag condition for this Atom to be required:
+               # For: !build? ( >=sys-apps/sed-4.0.5 ), use_conditional = '!build'
+               self.use_conditional = None
+
+       def __repr__(self):
+               uc = self.use_conditional
+               uc = "%s? " % uc if uc is not None else ''
+               return "<%s %r>" % (self.__class__.__name__, "%s%s" % (uc, self.atom))
+
+       def __setattr__(self, name, value):
+               object.__setattr__(self, name, value)
+
+       #R0911:121:Atom.intersects: Too many return statements (20/6)
+       #R0912:121:Atom.intersects: Too many branches (23/12)
+       # pylint: disable-msg=R0911,R0912
+       def intersects(self, other):
+               """Check if a passed in package atom "intersects" this atom.
+
+               Lifted from pkgcore.
+
+               Two atoms "intersect" if a package can be constructed that
+               matches both:
+                 - if you query for just "dev-lang/python" it "intersects" both
+                       "dev-lang/python" and ">=dev-lang/python-2.4"
+                 - if you query for "=dev-lang/python-2.4" it "intersects"
+                       ">=dev-lang/python-2.4" and "dev-lang/python" but not
+                       "<dev-lang/python-2.3"
+
+               @type other: Any "Intersectable" object
+               @param other: other package to compare
+               @see: L{pkgcore.ebuild.atom}
+               """
+               # Our "cp" (cat/pkg) must match exactly:
+               if self.cpv.cp != other.cpv.cp:
+                       # Check to see if one is name only:
+                       # Avoid slow partitioning if we're definitely not matching
+                       # (yes, this is hackish, but it's faster):
+                       if self.cpv.cp[-1:] != other.cpv.cp[-1:]:
+                               return False
+
+                       if ((not self.cpv.category and self.cpv.name == other.cpv.name) or
+                               (not other.cpv.category and other.cpv.name == self.cpv.name)):
+                               return True
+                       return False
+
+               # If one of us is unversioned we intersect:
+               if not self.operator or not other.operator:
+                       return True
+
+               # If we are both "unbounded" in the same direction we intersect:
+               if (('<' in self.operator and '<' in other.operator) or
+                       ('>' in self.operator and '>' in other.operator)):
+                       return True
+
+               # If one of us is an exact match we intersect if the other matches it:
+               if self.operator == '=':
+                       if other.operator == '=*':
+                               return self.cpv.fullversion.startswith(other.cpv.fullversion)
+                       return VersionMatch(other.cpv, op=other.operator).match(self.cpv)
+               if other.operator == '=':
+                       if self.operator == '=*':
+                               return other.cpv.fullversion.startswith(self.cpv.fullversion)
+                       return VersionMatch(self.cpv, op=self.operator).match(other.cpv)
+
+               # If we are both ~ matches we match if we are identical:
+               if self.operator == other.operator == '~':
+                       return (self.cpv.version == other.cpv.version and
+                               self.cpv.revision == other.cpv.revision)
+
+               # If we are both glob matches we match if one of us matches the other.
+               if self.operator == other.operator == '=*':
+                       return (self.cpv.fullversion.startswith(other.cpv.fullversion) or
+                               other.cpv.fullversion.startswith(self.cpv.fullversion))
+
+               # If one of us is a glob match and the other a ~ we match if the glob
+               # matches the ~ (ignoring a revision on the glob):
+               if self.operator == '=*' and other.operator == '~':
+                       return other.cpv.fullversion.startswith(self.cpv.version)
+               if other.operator == '=*' and self.operator == '~':
+                       return self.cpv.fullversion.startswith(other.cpv.version)
+
+               # If we get here at least one of us is a <, <=, > or >=:
+               if self.operator in ('<', '<=', '>', '>='):
+                       ranged, other = self, other
+                       ranged.operator = self.operator
+               else:
+                       ranged, other = other, self
+                       ranged.operator = other.operator
+
+               if '<' in other.operator or '>' in other.operator:
+                       # We are both ranged, and in the opposite "direction" (or
+                       # we would have matched above). We intersect if we both
+                       # match the other's endpoint (just checking one endpoint
+                       # is not enough, it would give a false positive on <=2 vs >2)
+                       return (VersionMatch(other.cpv, op=other.operator).match(ranged) and
+                               VersionMatch(ranged.cpv, op=ranged.operator).match(other.cpv))
+
+               if other.operator == '~':
+                       # Other definitely matches its own version. If ranged also
+                       # does we're done:
+                       if VersionMatch(ranged.cpv, op=ranged.operator).match(other.cpv):
+                               return True
+                       # The only other case where we intersect is if ranged is a
+                       # > or >= on other's version and a nonzero revision. In
+                       # that case other will match ranged. Be careful not to
+                       # give a false positive for ~2 vs <2 here:
+                       return (ranged.operator in ('>', '>=') and
+                               VersionMatch(other.cpv, op=other.operator).match(ranged.cpv))
+
+               if other.operator == '=*':
+                       # a glob match definitely matches its own version, so if
+                       # ranged does too we're done:
+                       if VersionMatch(ranged.cpv, op=ranged.operator).match(other.cpv):
+                               return True
+                       if '<' in ranged.operator:
+                               # If other.revision is not defined then other does not
+                               # match anything smaller than its own fullversion:
+                               if not other.cpv.revision:
+                                       return False
+
+                               # If other.revision is defined then we can always
+                               # construct a package smaller than other.fullversion by
+                               # tagging e.g. an _alpha1 on.
+                               return ranged.cpv.fullversion.startswith(other.cpv.version)
+                       else:
+                               # Remaining cases where this intersects: there is a
+                               # package greater than ranged.fullversion and
+                               # other.fullversion that they both match.
+                               return ranged.cpv.fullversion.startswith(other.cpv.version)
+
+               # Handled all possible ops.
+               raise NotImplementedError(
+                       'Someone added an operator without adding it to intersects')
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/pym/gentoolkit/cpv.py b/pym/gentoolkit/cpv.py
new file mode 100644 (file)
index 0000000..7dc54e5
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+#
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+"""Provides attributes and methods for a category/package-version string."""
+
+__all__ = ('CPV',)
+
+# =======
+# Imports
+# =======
+
+from portage.versions import catpkgsplit, vercmp
+
+from gentoolkit import errors
+
+# =======
+# Classes
+# =======
+
+class CPV(object):
+       """Provides methods on a category/package-version string.
+
+       Will also correctly split just a package or package-version string.
+
+       Example usage:
+               >>> from gentoolkit.cpv import CPV
+               >>> cpv = CPV('sys-apps/portage-2.2-r1')
+               >>> cpv.category, cpv.name, cpv.fullversion
+               ('sys-apps', 'portage', '2.2-r1')
+               >>> str(cpv)
+               'sys-apps/portage-2.2-r1'
+               >>> # An 'rc' (release candidate) version is less than non 'rc' version:
+               ... CPV('sys-apps/portage-2') > CPV('sys-apps/portage-2_rc10')
+       """
+
+       def __init__(self, cpv):
+               if not cpv:
+                       raise errors.GentoolkitInvalidCPV(cpv)
+               self.scpv = cpv
+
+               values = split_cpv(cpv)
+               self.category = values[0]
+               self.name = values[1]
+               self.version = values[2]
+               self.revision = values[3]
+               del values
+
+               if not self.name:
+                       raise errors.GentoolkitInvalidCPV(cpv)
+
+               sep = '/' if self.category else ''
+               self.cp = sep.join((self.category, self.name))
+
+               sep = '-' if self.revision else ''
+               self.fullversion = sep.join((self.version, self.revision))
+               del sep
+
+       def __eq__(self, other):
+               if not isinstance(other, self.__class__):
+                       raise TypeError("other isn't of %s type, is %s" % (
+                               self.__class__, other.__class__)
+                       )
+               return self.scpv == other.scpv
+
+       def __ne__(self, other):
+               if not isinstance(other, self.__class__):
+                       raise TypeError("other isn't of %s type, is %s" % (
+                               self.__class__, other.__class__)
+                       )
+               return not self == other
+
+       def __lt__(self, other):
+               if not isinstance(other, self.__class__):
+                       raise TypeError("other isn't of %s type, is %s" % (
+                               self.__class__, other.__class__)
+                       )
+
+               if self.category != other.category:
+                       return self.category < other.category
+               elif self.name != other.name:
+                       return self.name < other.name
+               else:
+                       # FIXME: this cmp() hack is for vercmp not using -1,0,1
+                       # See bug 266493; this was fixed in portage-2.2_rc31
+                       #return vercmp(self.fullversion, other.fullversion)
+                       result = cmp(vercmp(self.fullversion, other.fullversion), 0)
+                       if result == -1:
+                               return True
+                       else:
+                               return False
+
+       def __gt__(self, other):
+               if not isinstance(other, self.__class__):
+                       raise TypeError("other isn't of %s type, is %s" % (
+                               self.__class__, other.__class__)
+                       )
+               return not self < other and not self == other
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, str(self))
+
+       def __str__(self):
+               return self.scpv
+
+
+# =========
+# Functions
+# =========
+
+def split_cpv(cpv):
+       """Split a cpv into category, name, version and revision.
+
+       Inlined from helpers because of circular imports.
+
+       @type cpv: str
+       @param cpv: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom or regex
+       @rtype: tuple
+       @return: (category, pkg_name, version, revision)
+               Each tuple element is a string or empty string ("").
+       """
+
+       result = catpkgsplit(cpv)
+
+       if result:
+               result = list(result)
+               if result[0] == 'null':
+                       result[0] = ''
+               if result[3] == 'r0':
+                       result[3] = ''
+       else:
+               result = cpv.split("/")
+               if len(result) == 1:
+                       result = ['', cpv, '', '']
+               else:
+                       result = result + ['', '']
+
+       return tuple(result)
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/pym/gentoolkit/dbapi.py b/pym/gentoolkit/dbapi.py
new file mode 100644 (file)
index 0000000..2866214
--- /dev/null
@@ -0,0 +1,17 @@
+#!/usr/bin/python
+#
+# Copyright 2009 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
+
+"""Provides access to Portage database api"""
+
+import portage
+
+#bindb = portage.db[portage.root]["bintree"].dbapi
+PORTDB = portage.db[portage.root]["porttree"].dbapi
+VARDB = portage.db[portage.root]["vartree"].dbapi
+#virtuals = portage.db[portage.root]["virtuals"]
+
+# vim: set ts=8 sw=4 tw=79:
diff --git a/pym/gentoolkit/dependencies.py b/pym/gentoolkit/dependencies.py
new file mode 100644 (file)
index 0000000..632ca1e
--- /dev/null
@@ -0,0 +1,317 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Provides a class for easy calculating dependencies for a given CPV."""
+
+__docformat__ = 'epytext'
+__all__ = ('Dependencies',)
+
+# =======
+# Imports
+# =======
+
+import portage
+from portage.dep import paren_reduce
+
+from gentoolkit import errors
+from gentoolkit.atom import Atom
+from gentoolkit.cpv import CPV
+from gentoolkit.helpers import find_best_match, uniqify
+from gentoolkit.dbapi import PORTDB, VARDB
+
+# =======
+# Classes
+# =======
+
+class Dependencies(CPV):
+       """Access a package's dependencies and reverse dependencies.
+
+       Example usage:
+               >>> from gentoolkit.dependencies import Dependencies
+               >>> portage = Dependencies('sys-apps/portage-2.1.6.13')
+               >>> portage
+               <Dependencies 'sys-apps/portage-2.1.6.13'>
+               >>> # All methods return gentoolkit.atom.Atom instances
+               ... portage.get_depend()
+               [<Atom '>=dev-lang/python-2.5'>, <Atom '<dev-lang/python-3.0'>, ...]
+
+       """
+       def __init__(self, cpv, op='', parser=None):
+               if isinstance(cpv, CPV):
+                       self.cpv = cpv
+               else:
+                       self.cpv = CPV(cpv)
+
+               self.operator = op
+               self.atom = self.operator + str(self.cpv)
+               self.use = []
+               self.depatom = str()
+
+               # Allow a custom parser function:
+               self.parser = parser if parser else self._parser
+
+       def __eq__(self, other):
+               if self.atom != other.atom:
+                       return False
+               else:
+                       return True
+
+       def __ne__(self, other):
+               return not self == other
+
+       def __hash__(self):
+               return hash((self.atom, self.depatom, tuple(self.use)))
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, self.atom)
+
+       def get_env_vars(self, envvars):
+               """Returns predefined env vars DEPEND, SRC_URI, etc."""
+
+               # Try to use the Portage tree first, since emerge only uses the tree
+               # when calculating dependencies
+               try:
+                       result = PORTDB.aux_get(str(self.cpv), envvars)
+               except KeyError:
+                       result = VARDB.aux_get(str(self.cpv), envvars)
+               return result
+
+       def get_depend(self):
+               """Get the contents of DEPEND and parse it with self.parser."""
+
+               try:
+                       return self.parser(self.get_env_vars(('DEPEND',))[0])
+               except portage.exception.InvalidPackageName, err:
+                       raise errors.GentoolkitInvalidCPV(err)
+
+       def get_pdepend(self):
+               """Get the contents of PDEPEND and parse it with self.parser."""
+
+               try:
+                       return self.parser(self.get_env_vars(('PDEPEND',))[0])
+               except portage.exception.InvalidPackageName, err:
+                       raise errors.GentoolkitInvalidCPV(err)
+
+       def get_rdepend(self):
+               """Get the contents of RDEPEND and parse it with self.parser."""
+
+               try:
+                       return self.parser(self.get_env_vars(('RDEPEND',))[0])
+               except portage.exception.InvalidPackageName, err:
+                       raise errors.GentoolkitInvalidCPV(err)
+
+       def get_all_depends(self):
+               """Get the contents of ?DEPEND and parse it with self.parser."""
+
+               env_vars = ('DEPEND', 'PDEPEND', 'RDEPEND')
+               try:
+                       return self.parser(' '.join(self.get_env_vars(env_vars)))
+               except portage.exception.InvalidPackageName, err:
+                       raise errors.GentoolkitInvalidCPV(err)
+
+       def graph_depends(
+               self,
+               max_depth=1,
+               printer_fn=None,
+               # The rest of these are only used internally:
+               depth=0,
+               seen=None,
+               depcache=None,
+               result=None
+       ):
+               """Graph direct dependencies for self.
+
+               Optionally gather indirect dependencies.
+
+               @type max_depth: int
+               @param max_depth: Maximum depth to recurse if.
+                       <1 means no maximum depth
+                       >0 means recurse only this depth;
+               @type printer_fn: callable
+               @param printer_fn: If None, no effect. If set, it will be applied to
+                       each result.
+               @rtype: list
+               @return: [(depth, pkg), ...]
+               """
+               if seen is None:
+                       seen = set()
+               if depcache is None:
+                       depcache = dict()
+               if result is None:
+                       result = list()
+
+               pkgdep = None
+               deps = self.get_all_depends()
+               for dep in deps:
+                       if dep.atom in depcache:
+                               continue
+                       try:
+                               pkgdep = depcache[dep.atom]
+                       except KeyError:
+                               pkgdep = find_best_match(dep.atom)
+                               depcache[dep.atom] = pkgdep
+                       if pkgdep and str(pkgdep.cpv) in seen:
+                               continue
+                       if depth < max_depth or max_depth <= 0:
+
+                               if printer_fn is not None:
+                                       printer_fn(depth, pkgdep, dep)
+                               if not pkgdep:
+                                       continue
+
+                               seen.add(str(pkgdep.cpv))
+                               result.append((
+                                       depth,
+                                       pkgdep.deps.graph_depends(
+                                               max_depth=max_depth,
+                                               printer_fn=printer_fn,
+                                               # The rest of these are only used internally:
+                                               depth=depth+1,
+                                               seen=seen,
+                                               depcache=depcache,
+                                               result=result
+                                       )
+                               ))
+
+               if depth == 0:
+                       return result
+               return pkgdep
+
+       def graph_reverse_depends(
+               self,
+               pkgset=None,
+               max_depth=-1,
+               only_direct=True,
+               printer_fn=None,
+               # The rest of these are only used internally:
+               depth=0,
+               depcache=None,
+               seen=None,
+               result=None
+       ):
+               """Graph direct reverse dependencies for self.
+
+               Example usage:
+                       >>> from gentoolkit.dependencies import Dependencies
+                       >>> ffmpeg = Dependencies('media-video/ffmpeg-0.5_p20373')
+                       >>> # I only care about installed packages that depend on me:
+                       ... from gentoolkit.helpers import get_installed_cpvs
+                       >>> # I want to pass in a sorted list. We can pass strings or
+                       ... # Package or Atom types, so I'll use Package to sort:
+                       ... from gentoolkit.package import Package
+                       >>> installed = sorted(Package(x) for x in get_installed_cpvs())
+                       >>> deptree = ffmpeg.graph_reverse_depends(
+                       ...     only_direct=False,  # Include indirect revdeps
+                       ...     pkgset=installed)   # from installed pkgset
+                       >>> len(deptree)
+                       44
+
+               @type pkgset: iterable
+               @param pkgset: sorted pkg cpv strings or any 'intersectable' objects to
+                       use for calculate our revdep graph.
+               @type max_depth: int
+               @param max_depth: Maximum depth to recurse if only_direct=False.
+                       -1 means no maximum depth;
+                        0 is the same as only_direct=True;
+                       >0 means recurse only this many times;
+               @type only_direct: bool
+               @param only_direct: to recurse or not to recurse
+               @type printer_fn: callable
+               @param printer_fn: If None, no effect. If set, it will be applied to
+                       each L{gentoolkit.atom.Atom} object as it is added to
+                       the results.
+               @rtype: list
+               @return: L{gentoolkit.dependencies.Dependencies} objects
+               """
+               if not pkgset:
+                       err = ("%s kwarg 'pkgset' must be set. "
+                               "Can be list of cpv strings or any 'intersectable' object.")
+                       raise errors.GentoolkitFatalError(err % (self.__class__.__name__,))
+
+               if depcache is None:
+                       depcache = dict()
+               if seen is None:
+                       seen = set()
+               if result is None:
+                       result = list()
+
+               if depth == 0:
+                       pkgset = tuple(Dependencies(x) for x in pkgset)
+
+               pkgdep = None
+               for pkgdep in pkgset:
+                       try:
+                               all_depends = depcache[pkgdep]
+                       except KeyError:
+                               all_depends = uniqify(pkgdep.get_all_depends())
+                               depcache[pkgdep] = all_depends
+
+                       dep_is_displayed = False
+                       for dep in all_depends:
+                               # TODO: Add ability to determine if dep is enabled by USE flag.
+                               #       Check portage.dep.use_reduce
+                               if dep.intersects(self):
+                                       pkgdep.depth = depth
+                                       pkgdep.matching_dep = dep
+                                       if printer_fn is not None:
+                                               printer_fn(pkgdep, dep_is_displayed=dep_is_displayed)
+                                       result.append(pkgdep)
+                                       dep_is_displayed = True
+
+                       # if --indirect specified, call ourselves again with the dep
+                       # Do not call if we have already called ourselves.
+                       if (
+                               dep_is_displayed and not only_direct and
+                               str(pkgdep.cpv) not in seen and
+                               (depth < max_depth or max_depth == -1)
+                       ):
+
+                               seen.add(str(pkgdep.cpv))
+                               result.append(
+                                       pkgdep.graph_reverse_depends(
+                                               pkgset=pkgset,
+                                               max_depth=max_depth,
+                                               only_direct=only_direct,
+                                               printer_fn=printer_fn,
+                                               depth=depth+1,
+                                               depcache=depcache,
+                                               seen=seen,
+                                               result=result
+                                       )
+                               )
+
+               if depth == 0:
+                       return result
+               return pkgdep
+
+       def _parser(self, deps, use_conditional=None, depth=0):
+               """?DEPEND file parser.
+
+               @rtype: list
+               @return: L{gentoolkit.atom.Atom} objects
+               """
+               result = []
+
+               if depth == 0:
+                       deps = paren_reduce(deps)
+               for tok in deps:
+                       if tok == '||':
+                               continue
+                       if tok[-1] == '?':
+                               use_conditional = tok[:-1]
+                               continue
+                       if isinstance(tok, list):
+                               asdf = self._parser(tok, use_conditional, depth=depth+1)
+                               result.extend(asdf)
+                               continue
+                       atom = Atom(tok)
+                       if use_conditional is not None:
+                               atom.use_conditional = use_conditional
+                       result.append(atom)
+
+               return result
+
+# vim: set ts=4 sw=4 tw=0:
diff --git a/pym/gentoolkit/deprecated/helpers.py b/pym/gentoolkit/deprecated/helpers.py
new file mode 100644 (file)
index 0000000..df158f2
--- /dev/null
@@ -0,0 +1,179 @@
+#!/usr/bin/python2
+#
+# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+"""The functions in this module have been deprecated and are not even
+guaranteed to work. Improved functions can be found in helpers2.py"""
+
+import warnings
+
+import portage
+from gentoolkit import *
+from package import *
+from pprinter import warn
+try:
+       from portage.util import unique_array
+except ImportError:
+       from portage_util import unique_array
+
+def find_packages(search_key, masked=False):
+       """Returns a list of Package objects that matched the search key."""
+       warnings.warn("Deprecated. Use helpers2.find_packages.", DeprecationWarning)
+       try:
+               if masked:
+                       t = portage.db["/"]["porttree"].dbapi.xmatch("match-all", search_key)
+                       t += portage.db["/"]["vartree"].dbapi.match(search_key)
+               else:
+                       t = portage.db["/"]["porttree"].dbapi.match(search_key)
+                       t += portage.db["/"]["vartree"].dbapi.match(search_key)
+       # catch the "amgigous package" Exception
+       except ValueError, e:
+               if isinstance(e[0],list):
+                       t = []
+                       for cp in e[0]:
+                               if masked:
+                                       t += portage.db["/"]["porttree"].dbapi.xmatch("match-all", cp)
+                                       t += portage.db["/"]["vartree"].dbapi.match(cp)
+                               else:
+                                       t += portage.db["/"]["porttree"].dbapi.match(cp)
+                                       t += portage.db["/"]["vartree"].dbapi.match(cp)
+               else:
+                       raise ValueError(e)
+       except portage_exception.InvalidAtom, e:
+               print warn("Invalid Atom: '%s'" % str(e))
+               return []
+       # Make the list of packages unique
+       t = unique_array(t)
+       t.sort()
+       return [Package(x) for x in t]
+
+def find_installed_packages(search_key, masked=False):
+       """Returns a list of Package objects that matched the search key."""
+       warnings.warn("Deprecated. Use helpers2.find_installed_packages.",
+               DeprecationWarning)
+       try:
+                       t = portage.db["/"]["vartree"].dbapi.match(search_key)
+       # catch the "amgigous package" Exception
+       except ValueError, e:
+               if isinstance(e[0],list):
+                       t = []
+                       for cp in e[0]:
+                               t += portage.db["/"]["vartree"].dbapi.match(cp)
+               else:
+                       raise ValueError(e)
+       except portage_exception.InvalidAtom, e:
+               print warn("Invalid Atom: '%s'" % str(e))
+               return []
+       return [Package(x) for x in t]
+
+def find_best_match(search_key):
+       """Returns a Package object for the best available candidate that
+       matched the search key."""
+       warnings.warn("Deprecated. Use helpers2.find_best_match.",
+               DeprecationWarning)
+       t = portage.db["/"]["porttree"].dep_bestmatch(search_key)
+       if t:
+               return Package(t)
+       return None
+
+def find_system_packages(prefilter=None):
+       """Returns a tuple of lists, first list is resolved system packages,
+       second is a list of unresolved packages."""
+       pkglist = settings.packages
+       resolved = []
+       unresolved = []
+       for x in pkglist:
+               cpv = x.strip()
+               if len(cpv) and cpv[0] == "*":
+                       pkg = find_best_match(cpv)
+                       if pkg:
+                               resolved.append(pkg)
+                       else:
+                               unresolved.append(cpv)
+       return (resolved, unresolved)
+
+def find_world_packages(prefilter=None):
+       """Returns a tuple of lists, first list is resolved world packages,
+       seond is unresolved package names."""
+       f = open(portage.root+portage.WORLD_FILE)
+       pkglist = f.readlines()
+       resolved = []
+       unresolved = []
+       for x in pkglist:
+               cpv = x.strip()
+               if len(cpv) and cpv[0] != "#":
+                       pkg = find_best_match(cpv)
+                       if pkg:
+                               resolved.append(pkg)
+                       else:
+                               unresolved.append(cpv)
+       return (resolved,unresolved)
+
+def find_all_installed_packages(prefilter=None):
+       """Returns a list of all installed packages, after applying the prefilter
+       function"""
+       warnings.warn("Deprecated. Use helpers2.get_installed_cpvs.",
+               DeprecationWarning)
+       t = vartree.dbapi.cpv_all()
+       if prefilter:
+               t = filter(prefilter,t)
+       return [Package(x) for x in t]
+
+def find_all_uninstalled_packages(prefilter=None):
+       """Returns a list of all uninstalled packages, after applying the prefilter
+       function"""
+       warnings.warn("Deprecated. Use helpers2.get_uninstalled_cpvs.",
+               DeprecationWarning)
+       alist = find_all_packages(prefilter)
+       return [x for x in alist if not x.is_installed()]
+
+def find_all_packages(prefilter=None):
+       """Returns a list of all known packages, installed or not, after applying
+       the prefilter function"""
+       warnings.warn("Deprecated. Use helpers2.get_cpvs.", DeprecationWarning)
+       t = porttree.dbapi.cp_all()
+       t += vartree.dbapi.cp_all()
+       if prefilter:
+               t = filter(prefilter,t)
+       t = unique_array(t)
+       t2 = []
+       for x in t:
+               t2 += porttree.dbapi.cp_list(x)
+               t2 += vartree.dbapi.cp_list(x)
+       t2 = unique_array(t2)
+       return [Package(x) for x in t2]
+
+def split_package_name(name):
+       """Returns a list on the form [category, name, version, revision]. Revision will
+       be 'r0' if none can be inferred. Category and version will be empty, if none can
+       be inferred."""
+       warnings.warn("Deprecated. Just use portage.catpkgsplit or apply "
+               "gentoolkit.package.Package to access pkg.category, pkg.revision, etc.",
+               DeprecationWarning)
+       r = portage.catpkgsplit(name)
+       if not r:
+               r = name.split("/")
+               if len(r) == 1:
+                       return ["", name, "", "r0"]
+               else:
+                       return r + ["", "r0"]
+       else:
+               r = list(r)
+       if r[0] == 'null':
+               r[0] = ''
+       return r
+
+# XXX: Defunct: use helpers2.compare_package_strings
+#def sort_package_list(pkglist):
+#      """Returns the list ordered in the same way portage would do with lowest version
+#      at the head of the list."""
+#      pkglist.sort(Package.compare_version)
+#      return pkglist
+
+if __name__ == "__main__":
+       print "This module is for import only"
index c2059384a22879515015b98185b672442d0fbdaa..13ff6bacdd4b7ea91c121be883a502515c9dee53 100644 (file)
@@ -15,27 +15,51 @@ __all__ = (
        'mod_usage'
 )
 __docformat__ = 'epytext'
+# version is dynamically set by distutils sdist
+__version__ = "svn"
 
 # =======
 # Imports
 # =======
 
 import errno
+import os
 import sys
 import time
 from getopt import getopt, GetoptError
 
-from portage import exception
+import portage
 
 import gentoolkit
-import gentoolkit.pprinter as pp
-from gentoolkit import settings, Config
+from gentoolkit import CONFIG
+from gentoolkit import errors
+from gentoolkit import pprinter as pp
 from gentoolkit.textwrap_ import TextWrapper
 
 __productname__ = "equery"
-__authors__ = """\
-Karl Trygve Kalleberg - Original author
-Douglas Anderson - Modular redesign; author of meta, changes"""
+__authors__ = (
+       'Karl Trygve Kalleberg - Original author',
+       'Douglas Anderson - Modular redesign; author of meta, changes'
+)
+
+# =======
+# Globals
+# =======
+
+NAME_MAP = {
+       'b': 'belongs',
+       'c': 'changes',
+       'k': 'check',
+       'd': 'depends',
+       'g': 'depgraph',
+       'f': 'files',
+       'h': 'hasuse',
+       'l': 'list_',
+       'm': 'meta',
+       's': 'size',
+       'u': 'uses',
+       'w': 'which'
+}
 
 # =========
 # Functions
@@ -48,7 +72,7 @@ def print_help(with_description=True):
        """
 
        if with_description:
-               print __doc__ 
+               print __doc__
        print main_usage()
        print
        print pp.globaloption("global options")
@@ -78,30 +102,15 @@ def print_help(with_description=True):
 
 
 def expand_module_name(module_name):
-       """Returns one of the values of name_map or raises KeyError"""
-
-       name_map = {
-               'b': 'belongs',
-               'c': 'changes',
-               'k': 'check',
-               'd': 'depends',
-               'g': 'depgraph',
-               'f': 'files',
-               'h': 'hasuse',
-               'l': 'list_',
-               'm': 'meta',
-               's': 'size',
-               'u': 'uses',
-               'w': 'which'
-       }
+       """Returns one of the values of NAME_MAP or raises KeyError"""
 
        if module_name == 'list':
                # list is a Python builtin type, so we must rename our module
                return 'list_'
-       elif module_name in name_map.values():
+       elif module_name in NAME_MAP.values():
                return module_name
        else:
-               return name_map[module_name]
+               return NAME_MAP[module_name]
 
 
 def format_options(options):
@@ -114,21 +123,21 @@ def format_options(options):
        """
 
        result = []
-       twrap = TextWrapper(width=Config['termWidth'])
+       twrap = TextWrapper(width=CONFIG['termWidth'])
        opts = (x[0] for x in options)
        descs = (x[1] for x in options)
        for opt, desc in zip(opts, descs):
                twrap.initial_indent = pp.emph(opt.ljust(25))
                twrap.subsequent_indent = " " * 25
                result.append(twrap.fill(desc))
-       
+
        return '\n'.join(result)
 
 
 def format_filetype(path, fdesc, show_type=False, show_md5=False,
                show_timestamp=False):
        """Format a path for printing.
-       
+
        @type path: str
        @param path: the path
        @type fdesc: list
@@ -141,7 +150,7 @@ def format_filetype(path, fdesc, show_type=False, show_md5=False,
        @type show_md5: bool
        @param show_md5: if True, append MD5 sum to the formatted string
        @type show_timestamp: bool
-       @param show_timestamp: if True, append time-of-creation after pathname 
+       @param show_timestamp: if True, append time-of-creation after pathname
        @rtype: str
        @return: formatted pathname with optional added information
        """
@@ -160,7 +169,7 @@ def format_filetype(path, fdesc, show_type=False, show_md5=False,
                ftype = "sym"
                stamp = format_timestamp(fdesc[1])
                tgt = fdesc[2].split()[0]
-               if Config["piping"]:
+               if CONFIG["piping"]:
                        fpath = path
                else:
                        fpath = pp.path_symlink(path + " -> " + tgt)
@@ -168,7 +177,9 @@ def format_filetype(path, fdesc, show_type=False, show_md5=False,
                ftype = "dev"
                fpath = path
        else:
-               pp.print_error("%s has unknown type: %s" % (path, fdesc[0]))
+               sys.stderr.write(
+                       pp.error("%s has unknown type: %s" % (path, fdesc[0]))
+               )
 
        result = ""
        if show_type:
@@ -198,24 +209,14 @@ def initialize_configuration():
                term_width = 80
 
        # Terminal size, minus a 1-char margin for text wrapping
-       Config['termWidth'] = term_width - 1
-
-       # Color handling: -1: Use Portage settings, 0: Force off, 1: Force on
-       Config['color'] = -1
-
-       Config['quiet'] = False
+       CONFIG['termWidth'] = term_width - 1
 
        # Guess color output
-       if (Config['color'] == -1 and (not sys.stdout.isatty() or
-               settings["NOCOLOR"] in ("yes", "true")) or
-               Config['color'] == 0):
+       if (CONFIG['color'] == -1 and (not sys.stdout.isatty() or
+               os.getenv("NOCOLOR") in ("yes", "true")) or CONFIG['color'] == 0):
                pp.output.nocolor()
 
-       # Guess piping output
-       if not sys.stdout.isatty():
-               Config["piping"] = True
-       else:
-               Config["piping"] = False
+       CONFIG['verbose'] = not CONFIG['piping']
 
 
 def main_usage():
@@ -232,7 +233,7 @@ def main_usage():
 
 def mod_usage(mod_name="module", arg="pkgspec", optional=False):
        """Provide a consistant usage message to the calling module.
-       
+
        @type arg: string
        @param arg: what kind of argument the module takes (pkgspec, filename, etc)
        @type optional: bool
@@ -262,88 +263,88 @@ def parse_global_options(global_opts, args):
                                print_help()
                                sys.exit(0)
                elif opt in ('-q','--quiet'):
-                       Config["quiet"] = True
+                       CONFIG['quiet'] = True
                elif opt in ('-C', '--no-color', '--nocolor'):
-                       Config['color'] = 0
+                       CONFIG['color'] = 0
                        pp.output.nocolor()
                elif opt in ('-N', '--no-pipe'):
-                       Config["piping"] = False
+                       CONFIG['piping'] = False
                elif opt in ('-V', '--version'):
                        print_version()
                        sys.exit(0)
-       
+               elif opt in ('--debug'):
+                       CONFIG['debug'] = True
+
        return need_help
-       
+
 
 def print_version():
        """Print the version of this tool to the console."""
 
-       try:
-               with open('/usr/share/gentoolkit/VERSION') as gentoolkit_version:
-                       version = gentoolkit_version.read().strip()
-       except IOError, err:
-               pp.die(2, str(err))
-
        print "%(product)s (%(version)s) - %(docstring)s" % {
                "product": pp.productname(__productname__),
-               "version": version,
+               "version": __version__,
                "docstring": __doc__
        }
 
 
 def split_arguments(args):
        """Separate module name from module arguments"""
-       
+
        return args.pop(0), args
 
 
 def main():
        """Parse input and run the program."""
 
-       initialize_configuration()
-
        short_opts = "hqCNV"
-       long_opts = ('help', 'quiet', 'nocolor', 'no-color', 'no-pipe', 'version')
+       long_opts = (
+               'help', 'quiet', 'nocolor', 'no-color', 'no-pipe', 'version', 'debug'
+       )
+
+       initialize_configuration()
 
        try:
                global_opts, args = getopt(sys.argv[1:], short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Global %s" % err)
+               sys.stderr.write(pp.error("Global %s" % err))
                print_help(with_description=False)
                sys.exit(2)
 
        # Parse global options
        need_help = parse_global_options(global_opts, args)
 
+       # FIXME: There are a few places that make use of both quiet and verbose.
+       #        Consider combining.
+       if CONFIG['quiet']:
+               CONFIG['verbose'] = False
+
        try:
                module_name, module_args = split_arguments(args)
        except IndexError:
                print_help()
                sys.exit(2)
-       
+
        if need_help:
                module_args.append('--help')
 
-       if Config['piping'] or Config['quiet']:
-               Config['verbose'] = False
-       else:
-               Config['verbose'] = True
-
        try:
                expanded_module_name = expand_module_name(module_name)
        except KeyError:
-               pp.print_error("Unknown module '%s'" % module_name)
+               sys.stderr.write(pp.error("Unknown module '%s'" % module_name))
                print_help(with_description=False)
                sys.exit(2)
 
        try:
-               loaded_module = __import__(expanded_module_name, globals(),
-                       locals(), [], -1)
+               loaded_module = __import__(
+                       expanded_module_name, globals(), locals(), [], -1
+               )
                loaded_module.main(module_args)
-       except exception.AmbiguousPackageName, err:
-               pp.print_error("Ambiguous package name. Use one of: ")
-               while err[0]:
-                       print " " + err[0].pop()
+       except portage.exception.AmbiguousPackageName, err:
+               raise errors.GentoolkitAmbiguousPackage(err)
        except IOError, err:
                if err.errno != errno.EPIPE:
                        raise
+
+if __name__ == '__main__':
+       main()
index d4da36fbd52d22c8a9fef70e9b554883dd5d08ed..e1367cfd8de572a92b106cd67d95f7ac8b97f9ad 100644 (file)
@@ -16,15 +16,13 @@ __docformat__ = 'epytext'
 # Imports
 # =======
 
-import re
 import sys
 from getopt import gnu_getopt, GetoptError
 
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_filetype, format_options, mod_usage, \
-       Config
-from gentoolkit.helpers2 import get_installed_cpvs
-from gentoolkit.package import Package
+from gentoolkit.equery import (format_filetype, format_options, mod_usage,
+       CONFIG)
+from gentoolkit.helpers import FileOwner
 
 # =======
 # Globals
@@ -36,27 +34,60 @@ QUERY_OPTS = {
        "nameOnly": False
 }
 
+# =======
+# Classes
+# =======
+
+class BelongsPrinter(object):
+       """Outputs a formatted list of packages that claim to own a files."""
+
+       def __init__(self, verbose=True, name_only=False):
+               if verbose:
+                       self.print_fn = self.print_verbose
+               else:
+                       self.print_fn = self.print_quiet
+
+               self.name_only = name_only
+
+       def __call__(self, pkg, cfile):
+               self.print_fn(pkg, cfile)
+
+       # W0613: *Unused argument %r*
+       # pylint: disable-msg=W0613
+       def print_quiet(self, pkg, cfile):
+               "Format for minimal output."
+               if self.name_only:
+                       name = pkg.cpv.cp
+               else:
+                       name = str(pkg.cpv)
+               print name
+
+       def print_verbose(self, pkg, cfile):
+               "Format for full output."
+               file_str = pp.path(format_filetype(cfile, pkg.get_contents()[cfile]))
+               if self.name_only:
+                       name = pkg.cpv.cp
+               else:
+                       name = str(pkg.cpv)
+               print pp.cpv(name), "(" + file_str + ")"
+
+
 # =========
 # Functions
 # =========
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        for opt in opts:
                if opt in ('-h','--help'):
                        print_help()
                        sys.exit(0)
-               elif opt in ('-c', '--category'):
-                       # Remove this warning after a reasonable amount of time
-                       # (djanderson, 2/2009)
-                       pp.print_warn("Module option -c, --category not implemented")
-                       print
                elif opt in ('-e', '--early-out', '--earlyout'):
                        if opt == '--earlyout':
-                               pp.print_warn("Use of --earlyout is deprecated.")
-                               pp.print_warn("Please use --early-out.")
+                               sys.stderr.write(pp.warn("Use of --earlyout is deprecated."))
+                               sys.stderr.write(pp.warn("Please use --early-out."))
                                print
                        QUERY_OPTS['earlyOut'] = True
                elif opt in ('-f', '--full-regex'):
@@ -65,31 +96,9 @@ def parse_module_options(module_opts):
                        QUERY_OPTS['nameOnly'] = True
 
 
-def prepare_search_regex(queries):
-       """Create a regex out of the queries"""
-
-       if QUERY_OPTS["fullRegex"]:
-               result = queries
-       else:
-               result = []
-               # Trim trailing and multiple slashes from queries
-               slashes = re.compile('/+')
-               for query in queries:
-                       query = slashes.sub('/', query).rstrip('/')
-                       if query.startswith('/'):
-                               query = "^%s$" % re.escape(query)
-                       else:
-                               query = "/%s$" % re.escape(query)
-                       result.append(query)
-
-       result = "|".join(result)
-
-       return re.compile(result)
-
-
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -111,15 +120,14 @@ def print_help(with_description=True):
 def main(input_args):
        """Parse input and run the program"""
 
-       # -c, --category is not implemented
-       short_opts = "hc:fen"
-       long_opts = ('help', 'category=', 'full-regex', 'early-out', 'earlyout',
+       short_opts = "h:fen"
+       long_opts = ('help', 'full-regex', 'early-out', 'earlyout',
                'name-only')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
@@ -130,31 +138,19 @@ def main(input_args):
                print_help()
                sys.exit(2)
 
-       query_re = prepare_search_regex(queries)
-
-       if Config['verbose']:
-               pp.print_info(3, " * Searching for %s ... "
-                       % (pp.regexpquery(",".join(queries))))
-       
-       matches = get_installed_cpvs()
-       
-       # Print matches to screen or pipe
-       found_match = False
-       for pkg in [Package(x) for x in matches]:
-               files = pkg.get_contents()
-               for cfile in files:
-                       if query_re.search(cfile):
-                               if QUERY_OPTS["nameOnly"]:
-                                       pkg_str = pkg.key
-                               else:
-                                       pkg_str = pkg.cpv
-                               if Config['verbose']:
-                                       file_str = pp.path(format_filetype(cfile, files[cfile]))
-                                       print "%s (%s)" % (pkg_str, file_str)
-                               else:
-                                       print pkg_str
-
-                               found_match = True
-
-               if found_match and QUERY_OPTS["earlyOut"]:
-                       break
+       if CONFIG['verbose']:
+               print " * Searching for %s ... " % (pp.regexpquery(",".join(queries)))
+
+       printer_fn = BelongsPrinter(
+               verbose=CONFIG['verbose'], name_only=QUERY_OPTS['nameOnly']
+       )
+
+       find_owner = FileOwner(
+               is_regex=QUERY_OPTS['fullRegex'],
+               early_out=QUERY_OPTS['earlyOut'],
+               printer_fn=printer_fn
+       )
+
+       find_owner(queries)
+
+# vim: set ts=4 sw=4 tw=79:
index 604bb45dacefb053d66b0a154b503c2d586ce908..09ccccaa26dea6ced4b47889652f0cf83c150dfb 100644 (file)
@@ -4,14 +4,13 @@
 #
 # $Header: $
 
-"""Display the  Gentoo ChangeLog entry for the latest installable version of a
-given package
+"""Display the Gentoo ChangeLog entry for the latest installable version of a
+given package.
 """
 
 # Move to Imports sections when Python 2.6 is stable
 from __future__ import with_statement
 
-__author__ = 'Douglas Anderson'
 __docformat__ = 'epytext'
 
 # =======
@@ -22,14 +21,11 @@ import os
 import sys
 from getopt import gnu_getopt, GetoptError
 
-from portage.versions import pkgsplit
-
 import gentoolkit.pprinter as pp
 from gentoolkit import errors
+from gentoolkit.atom import Atom
 from gentoolkit.equery import format_options, mod_usage
-from gentoolkit.helpers2 import find_best_match, find_packages
-from gentoolkit.package import Package
-from gentoolkit.versionmatch import VersionMatch
+from gentoolkit.helpers import ChangeLog, find_best_match, find_packages
 
 # =======
 # Globals
@@ -49,7 +45,7 @@ QUERY_OPTS = {
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -78,71 +74,26 @@ def print_help(with_description=True):
        ))
 
 
-def get_logpath(pkg):
-       """Test that the package's ChangeLog path is valid and readable, else
-       die.
-
-       @type pkg: gentoolkit.package.Package
-       @param pkg: package to find logpath for
-       @rtype: str
-       @return: a path to a readable ChangeLog
-       """
-
-       logpath = os.path.join(pkg.get_package_path(), 'ChangeLog')
-       if not os.path.isfile(logpath) or not os.access(logpath, os.R_OK):
-               raise errors.GentoolkitFatalError("%s does not exist or is unreadable"
-                       % pp.path(logpath))
-       
-       return logpath
-
-
 def get_match(query):
-       """Find a valid package to get the ChangeLog path from or raise
-       GentoolkitNoMatches.
+       """Find a valid package from which to get the ChangeLog path.
+
+       @raise GentoolkitNoMatches: if no matches found
        """
 
        match = matches = None
        match = find_best_match(query)
-               
+
        if not match:
                matches = find_packages(query, include_masked=True)
        else:
                matches = [match]
 
        if not matches:
-               pp.print_warn("Try using an unversioned query with "
-                       "--from and --to.")
                raise errors.GentoolkitNoMatches(query)
 
        return matches[0]
 
 
-def index_changelog(entries):
-       """Convert the list from split_changelog into a dict with VersionMatch
-       instance as the index.
-
-       @todo: UPDATE THIS
-       @type entries: list
-       @param entries: output of split_changelog
-       @rtype: dict
-       @return: dict with gentoolkit.package.Package instances as keys and the
-               corresponding ChangeLog entree as its value
-       """
-
-       result = []
-       for entry in entries:
-               # Extract the package name from the entry, ex: 
-               # *xterm-242 (07 Mar 2009) => xterm-242
-               pkg_name = entry.split(' ', 1)[0].lstrip('*')
-               if not pkg_name.strip():
-                       continue
-               pkg_split = pkgsplit(pkg_name)
-               result.append(
-                       (VersionMatch(op="=", ver=pkg_split[1], rev=pkg_split[2]), entry))
-       
-       return result
-
-
 def is_ranged(atom):
        """Return True if an atom string appears to be ranged, else False."""
 
@@ -150,7 +101,7 @@ def is_ranged(atom):
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        posargs = (x[1] for x in module_opts)
@@ -165,126 +116,39 @@ def parse_module_options(module_opts):
                elif opt in ('--limit',):
                        set_limit(posarg)
                elif opt in ('--from',):
-                       set_from(posarg)
+                       QUERY_OPTS['from'] = posarg
                elif opt in ('--to',):
-                       set_to(posarg)
-
-
-def print_matching_entries(indexed_entries, pkg, first_run):
-       """Print only the entries which interect with the pkg version."""
-
-       from_restriction = QUERY_OPTS['from']
-       to_restriction = QUERY_OPTS['to']
-
-       for entry_set in indexed_entries:
-               i, entry = entry_set
-               # a little hackery, since versionmatch doesn't store the
-               # package key, but intersects checks that it matches.
-               i.key = pkg.key
-               if from_restriction or to_restriction:
-                       if from_restriction and not from_restriction.match(i):
-                               continue
-                       if to_restriction and not to_restriction.match(i):
-                               continue
-               elif not pkg.intersects(i):
-                       continue
-
-               if not first_run:
-                       print "\n"
-               print entry.strip()
-               first_run = False
-       
-       return first_run
+                       QUERY_OPTS['to'] = posarg
 
 
-def set_from(posarg):
-       """Set a starting version to filter the ChangeLog with or die if posarg
-       is not a valid version.
-       """
-
-       pkg_split = pkgsplit('null-%s' % posarg)
+def print_entries(entries):
+       """Print entries and strip trailing whitespace from the last entry."""
 
-       if pkg_split and not is_ranged(posarg):
-               ver_match = VersionMatch(
-                       op=">=",
-                       ver=pkg_split[1], 
-                       rev=pkg_split[2] if pkg_split[2] != 'r0' else '')
-               QUERY_OPTS['from'] = ver_match
-       else:
-               err = "Module option --from requires valid unranged version (got '%s')"
-               pp.print_error(err % posarg)
-               print
-               print_help(with_description=False)
-               sys.exit(2)
+       len_entries = len(entries)
+       for i, entry in enumerate(entries):    # , start=1): in py2.6
+               i += 1
+               if i < len_entries:
+                       print entry
+               else:
+                       print entry.strip()
 
 
 def set_limit(posarg):
-       """Set a limit in QUERY_OPTS on how many ChangeLog entries to display or
-       die if posarg is not an integer.
+       """Set a limit in QUERY_OPTS on how many ChangeLog entries to display.
+
+       Die if posarg is not an integer.
        """
 
        if posarg.isdigit():
                QUERY_OPTS['limit'] = int(posarg)
        else:
                err = "Module option --limit requires integer (got '%s')"
-               pp.print_error(err % posarg)
+               sys.stderr.write(pp.error(err % posarg))
                print
                print_help(with_description=False)
                sys.exit(2)
 
 
-def set_to(posarg):
-       """Set an ending version to filter the ChangeLog with or die if posarg
-       is not a valid version.
-       """
-
-       pkg_split = pkgsplit('null-%s' % posarg)
-       if pkg_split and not is_ranged(posarg):
-               ver_match = VersionMatch(
-                       op="<=",
-                       ver=pkg_split[1], 
-                       rev=pkg_split[2] if pkg_split[2] != 'r0' else '')
-               QUERY_OPTS['to'] = ver_match
-       else:
-               err = "Module option --to requires valid unranged version (got '%s')"
-               pp.print_error(err % posarg)
-               print
-               print_help(with_description=False)
-               sys.exit(2)
-
-
-def split_changelog(logpath):
-       """Split the changelog up into individual entries.
-       
-       @type logpath: str
-       @param logpath: valid path to ChangeLog file
-       @rtype: list
-       @return: individual ChangeLog entrees
-       """
-
-       result = []
-       partial_entries = []
-       with open(logpath) as log:
-               for line in log:
-                       if line.startswith('#'):
-                               continue
-                       elif line.startswith('*'):
-                               # Append last entry to result...
-                               entry = ''.join(partial_entries)
-                               if entry and not entry.isspace():
-                                       result.append(entry)
-                               # ... and start a new entry
-                               partial_entries = [line]
-                       else:
-                               partial_entries.append(line)
-               else:
-                       # Append the final entry
-                       entry = ''.join(partial_entries)
-                       result.append(entry)
-
-       return result
-
-
 def main(input_args):
        """Parse input and run the program"""
 
@@ -294,7 +158,7 @@ def main(input_args):
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
@@ -310,36 +174,34 @@ def main(input_args):
                if not first_run:
                        print
 
-               ranged_query = None
-               if is_ranged(query):
-                       # Raises GentoolkitInvalidCPV here if invalid
-                       ranged_query = Package(query)
-
-               pkg = get_match(query)
-               logpath = get_logpath(pkg)
-               log_entries = split_changelog(logpath)
-               if not any(log_entries):
-                       raise errors.GentoolkitFatalError(
-                               "%s exists but doesn't contain entries." % pp.path(logpath))
-               indexed_entries = index_changelog(log_entries)
+               match = get_match(query)
+               changelog_path = os.path.join(match.get_package_path(), 'ChangeLog')
+               changelog = ChangeLog(changelog_path)
 
                #
                # Output
                #
 
-               if QUERY_OPTS['onlyLatest']:
-                       print log_entries[0].strip()
-               elif QUERY_OPTS['showFullLog']:
-                       end = QUERY_OPTS['limit'] or len(log_entries)
-                       for entry in log_entries[:end]:
-                               print entry
-                       first_run = False
-               elif log_entries and not indexed_entries:
-                       # We can't match anything, so just print latest:
-                       print log_entries[0].strip()
+               if (QUERY_OPTS['onlyLatest'] or (
+                       changelog.entries and not changelog.indexed_entries
+               )):
+                       print changelog.latest.strip()
                else:
-                       if ranged_query:
-                               pkg = ranged_query
-                       first_run = print_matching_entries(indexed_entries, pkg, first_run)
+                       end = QUERY_OPTS['limit'] or len(changelog.indexed_entries)
+                       if QUERY_OPTS['to'] or QUERY_OPTS['from']:
+                               print_entries(
+                                       changelog.entries_matching_range(
+                                               from_ver=QUERY_OPTS['from'],
+                                               to_ver=QUERY_OPTS['to']
+                                       )[:end]
+                               )
+                       elif QUERY_OPTS['showFullLog']:
+                               print_entries(changelog.full[:end])
+                       else:
+                               # Raises GentoolkitInvalidAtom here if invalid
+                               atom = Atom(query) if is_ranged(query) else '=' + str(match.cpv)
+                               print_entries(changelog.entries_matching_atom(atom)[:end])
 
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index 2531970d28ced4004071ad358cabed0ff6a04b09..011fd5d676f95241c0193409b0027f3ebaef83b8 100644 (file)
@@ -4,7 +4,7 @@
 #
 # $Header: $
 
-"""Check timestamps and MD5sums for files owned by a given installed package"""
+"""Check timestamps and MD5 sums for files owned by a given installed package"""
 
 __docformat__ = 'epytext'
 
@@ -14,43 +14,166 @@ __docformat__ = 'epytext'
 
 import os
 import sys
+from functools import partial
 from getopt import gnu_getopt, GetoptError
 
-try:
-       import portage.checksum as checksum
-except ImportError:
-       import portage_checksum as checksum
+import portage.checksum as checksum
 
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import do_lookup
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
 
 # =======
 # Globals
 # =======
 
 QUERY_OPTS = {
-       "categoryFilter": None,
-       "includeInstalled": False,
+       "includeInstalled": True,
        "includeOverlayTree": False,
        "includePortTree": False,
        "checkMD5sum": True,
        "checkTimestamp" : True,
        "isRegex": False,
-       "matchExact": True,
+       "onlyFailures": False,
        "printMatchInfo": False,
        "showSummary" : True,
        "showPassedFiles" : False,
        "showFailedFiles" : True
 }
 
+# =======
+# Classes
+# =======
+
+class VerifyContents(object):
+       """Verify installed packages' CONTENTS files.
+
+       The CONTENTS file contains timestamps and MD5 sums for each file owned
+       by a package.
+       """
+       def __init__(self, printer_fn=None):
+               """Create a VerifyObjects instance.
+
+               @type printer_fn: callable
+               @param printer_fn: if defined, will be applied to each result as found
+               """
+               self.check_sums = True
+               self.check_timestamps = True
+               self.printer_fn = printer_fn
+
+               self.is_regex = False
+
+       def __call__(
+               self,
+               pkgs,
+               is_regex=False,
+               check_sums=True,
+               check_timestamps=True
+       ):
+               self.is_regex = is_regex
+               self.check_sums = check_sums
+               self.check_timestamps = check_timestamps
+
+               result = {}
+               for pkg in pkgs:
+                       # _run_checks returns tuple(n_passed, n_checked, err)
+                       check_results = self._run_checks(pkg.get_contents())
+                       result[pkg.cpv] = check_results
+                       if self.printer_fn is not None:
+                               self.printer_fn(pkg.cpv, check_results)
+
+               return result
+
+       def _run_checks(self, files):
+               """Run some basic sanity checks on a package's contents.
+
+               If the file type (ftype) is not a directory or symlink, optionally
+               verify MD5 sums or mtimes via L{self._verify_obj}.
+
+               @see: gentoolkit.packages.get_contents()
+               @type files: dict
+               @param files: in form {'PATH': ['TYPE', 'TIMESTAMP', 'MD5SUM']}
+               @rtype: tuple
+               @return:
+                       n_passed (int): number of files that passed all checks
+                       n_checked (int): number of files checked
+                       errs (list): check errors' descriptions
+               """
+               n_checked = 0
+               n_passed = 0
+               errs = []
+               for cfile in files:
+                       n_checked += 1
+                       ftype = files[cfile][0]
+                       if not os.path.exists(cfile):
+                               errs.append("%s does not exist" % cfile)
+                               continue
+                       elif ftype == "dir":
+                               if not os.path.isdir(cfile):
+                                       err = "%(cfile)s exists, but is not a directory"
+                                       errs.append(err % locals())
+                                       continue
+                       elif ftype == "obj":
+                               obj_errs = self._verify_obj(files, cfile, errs)
+                               if len(obj_errs) > len(errs):
+                                       errs = obj_errs[:]
+                                       continue
+                       elif ftype == "sym":
+                               target = files[cfile][2].strip()
+                               if not os.path.islink(cfile):
+                                       err = "%(cfile)s exists, but is not a symlink"
+                                       errs.append(err % locals())
+                                       continue
+                               tgt = os.readlink(cfile)
+                               if tgt != target:
+                                       err = "%(cfile)s does not point to %(target)s"
+                                       errs.append(err % locals())
+                                       continue
+                       else:
+                               err = "%(cfile)s has unknown type %(ftype)s"
+                               errs.append(err % locals())
+                               continue
+                       n_passed += 1
+
+               return n_passed, n_checked, errs
+
+       def _verify_obj(self, files, cfile, errs):
+               """Verify the MD5 sum and/or mtime and return any errors."""
+
+               obj_errs = errs[:]
+               if self.check_sums:
+                       md5sum = files[cfile][2]
+                       try:
+                               cur_checksum = checksum.perform_md5(cfile, calc_prelink=1)
+                       except IOError:
+                               err = "Insufficient permissions to read %(cfile)s"
+                               obj_errs.append(err % locals())
+                               return obj_errs
+                       if cur_checksum != md5sum:
+                               err = "%(cfile)s has incorrect MD5sum"
+                               obj_errs.append(err % locals())
+                               return obj_errs
+               if self.check_timestamps:
+                       mtime = int(files[cfile][1])
+                       st_mtime = int(os.lstat(cfile).st_mtime)
+                       if st_mtime != mtime:
+                               err = (
+                                       "%(cfile)s has wrong mtime (is %(st_mtime)d, should be "
+                                       "%(mtime)d)"
+                               )
+                               obj_errs.append(err % locals())
+                               return obj_errs
+
+               return obj_errs
+
 # =========
 # Functions
 # =========
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -60,9 +183,14 @@ def print_help(with_description=True):
                print
 
        # Deprecation warning added by djanderson, 12/2008
-       pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.")
-       pp.print_warn("Use globbing to simulate the old behavior (see man equery).")
-       pp.print_warn("Use '*' to check all installed packages.")
+       depwarning = (
+               "Default action for this module has changed in Gentoolkit 0.3.",
+               "Use globbing to simulate the old behavior (see man equery).",
+               "Use '*' to check all installed packages.",
+               "Use 'foo-bar/*' to filter by category."
+       )
+       for line in depwarning:
+               sys.stderr.write(pp.warn(line))
        print
 
        print mod_usage(mod_name="check")
@@ -70,135 +198,73 @@ def print_help(with_description=True):
        print pp.command("options")
        print format_options((
                (" -h, --help", "display this help message"),
-               (" -c, --category CAT", "only check files from packages in CAT"),
                (" -f, --full-regex", "query is a regular expression"),
+               (" -o, --only-failures", "only display packages that do not pass"),
        ))
 
 
+def checks_printer(cpv, data, verbose=True, only_failures=False):
+       """Output formatted results of pkg file(s) checks"""
+       seen = []
+
+       n_passed, n_checked, errs = data
+       n_failed = n_checked - n_passed
+       if only_failures and not n_failed:
+               return
+       else:
+               if verbose:
+                       if not cpv in seen:
+                               print "* Checking %s ..." % (pp.emph(str(cpv)))
+                               seen.append(cpv)
+               else:
+                       print "%s:" % cpv,
+
+       if verbose:
+               for err in errs:
+                       sys.stderr.write(pp.error(err))
+
+       if verbose:
+               n_passed = pp.number(str(n_passed))
+               n_checked = pp.number(str(n_checked))
+               info = "   %(n_passed)s out of %(n_checked)s files passed"
+               print info % locals()
+       else:
+               print "failed(%s)" % n_failed
+
+
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
-       posargs = (x[1] for x in module_opts)
-       for opt, posarg in zip(opts, posargs):
+       for opt in opts:
                if opt in ('-h', '--help'):
                        print_help()
                        sys.exit(0)
-               elif opt in ('-c', '--category'):
-                       QUERY_OPTS['categoryFilter'] = posarg
                elif opt in ('-f', '--full-regex'):
                        QUERY_OPTS['isRegex'] = True
-
-
-def run_checks(files):
-       """Run some basic sanity checks on a package's contents.
-
-       If the file type (ftype) is not a directory or symlink, optionally
-       verify MD5 sums or mtimes via verify_obj().
-
-       @see: gentoolkit.packages.get_contents()
-       @type files: dict
-       @param files: in form {'PATH': ['TYPE', 'TIMESTAMP', 'MD5SUM']}
-       @rtype: tuple
-       @return:
-               passed (int): number of files that passed all checks
-               checked (int): number of files checked
-               errs (list): check errors' descriptions
-       """
-
-       checked = 0
-       passed = 0
-       errs = []
-       for cfile in files:
-               checked += 1
-               ftype = files[cfile][0]
-               if not os.path.exists(cfile):
-                       errs.append("%s does not exist" % cfile)
-                       continue
-               elif ftype == "dir":
-                       if not os.path.isdir(cfile):
-                               err = "%(cfile)s exists, but is not a directory"
-                               errs.append(err % locals())
-                               continue
-               elif ftype == "obj":
-                       new_errs = verify_obj(files, cfile, errs)
-                       if new_errs != errs:
-                               errs = new_errs
-                               continue
-               elif ftype == "sym":
-                       target = files[cfile][2].strip()
-                       if not os.path.islink(cfile):
-                               err = "%(cfile)s exists, but is not a symlink"
-                               errs.append(err % locals())
-                               continue
-                       tgt = os.readlink(cfile)
-                       if tgt != target:
-                               err = "%(cfile)s does not point to %(target)s"
-                               errs.append(err % locals())
-                               continue
-               else:
-                       err = "%(cfile)s has unknown type %(ftype)s"
-                       errs.append(err % locals())
-                       continue
-               passed += 1
-
-       return passed, checked, errs
-
-
-def verify_obj(files, cfile, errs):
-       """Verify the MD5 sum and/or mtime and return any errors."""
-
-       if QUERY_OPTS["checkMD5sum"]:
-               md5sum = files[cfile][2]
-               try: 
-                       cur_checksum = checksum.perform_md5(cfile, calc_prelink=1)
-               except IOError:
-                       err = "Insufficient permissions to read %(cfile)s"
-                       errs.append(err % locals())
-                       return errs
-               if cur_checksum != md5sum:
-                       err = "%(cfile)s has incorrect MD5sum"
-                       errs.append(err % locals())
-                       return errs
-       if QUERY_OPTS["checkTimestamp"]:
-               mtime = int(files[cfile][1])
-               st_mtime = os.lstat(cfile).st_mtime
-               if st_mtime != mtime:
-                       err = "%(cfile)s has wrong mtime (is %(st_mtime)d, " + \
-                               "should be %(mtime)d)"
-                       errs.append(err % locals())
-                       return errs
-
-       return errs
+               elif opt in ('-o', '--only-failures'):
+                       QUERY_OPTS['onlyFailures'] = True
 
 
 def main(input_args):
        """Parse input and run the program"""
 
-       short_opts = "hac:f"
-       long_opts = ('help', 'all', 'category=', 'full-regex')
+       short_opts = "hof"
+       long_opts = ('help', 'only-failures', 'full-regex')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
 
        parse_module_options(module_opts)
-       
-       if not queries and not QUERY_OPTS["includeInstalled"]:
+
+       if not queries:
                print_help()
                sys.exit(2)
-       elif queries and not QUERY_OPTS["includeInstalled"]:
-               QUERY_OPTS["includeInstalled"] = True
-       elif QUERY_OPTS["includeInstalled"]:
-               queries = ["*"]
-
-       #
-       # Output
-       #
 
        first_run = True
        for query in queries:
@@ -208,25 +274,18 @@ def main(input_args):
                matches = do_lookup(query, QUERY_OPTS)
 
                if not matches:
-                       pp.print_error("No package found matching %s" % query)
+                       raise errors.GentoolkitNoMatches(query, in_installed=True)
 
                matches.sort()
 
-               for pkg in matches:
-                       if Config['verbose']:
-                               print " * Checking %s ..." % pp.emph(pkg.cpv)
-                       else:
-                               print "%s:" % pkg.cpv
-
-                       passed, checked, errs = run_checks(pkg.get_contents())
-
-                       if Config['verbose']:
-                               for err in errs:
-                                       pp.print_error(err)
+               printer = partial(
+                       checks_printer,
+                       verbose=CONFIG['verbose'],
+                       only_failures=QUERY_OPTS['onlyFailures']
+               )
+               check = VerifyContents(printer_fn=printer)
+               check(matches)
 
-                       passed = pp.number(str(passed))
-                       checked = pp.number(str(checked))
-                       info = "   %(passed)s out of %(checked)s files passed"
-                       print info % locals()
+               first_run = False
 
-                       first_run = False
+# vim: set ts=4 sw=4 tw=79:
index a1a0d20e8b5d798fdc3a095451cc43805255f698..df2de9fde09a5b675b5d13e398f17a099d514fc2 100644 (file)
@@ -4,7 +4,7 @@
 #
 # $Header: $
 
-"""List all direct dependencies matching a given query"""
+"""List all packages that depend on a given query"""
 
 __docformat__ = 'epytext'
 
@@ -15,34 +15,82 @@ __docformat__ = 'epytext'
 import sys
 from getopt import gnu_getopt, GetoptError
 
-from portage.util import unique_array
-
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_options,  mod_usage, Config
-from gentoolkit.helpers2 import compare_package_strings, do_lookup, \
-       find_packages, get_cpvs, get_installed_cpvs
-from gentoolkit.package import Package
+from gentoolkit.dependencies import Dependencies
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import (get_cpvs, get_installed_cpvs,
+       compare_package_strings)
 
 # =======
 # Globals
 # =======
 
 QUERY_OPTS = {
-       "categoryFilter": None,
-       "includeInstalled": True,
-       "includePortTree": False,
-       "includeOverlayTree": False,
-       "isRegex": False,
+       "includeMasked": False,
        "onlyDirect": True,
-       "printMatchInfo": (not Config['quiet']),
-       "indentLevel": 0,
-       "depth": -1
+       "maxDepth": -1,
 }
 
-# Used to cache and detect looping
-PKGSEEN = set()
-PKGDEPS = {}
-DEPPKGS = {}
+# =======
+# Classes
+# =======
+
+class DependPrinter(object):
+       """Output L{gentoolkit.dependencies.Dependencies} objects."""
+       def __init__(self, verbose=True):
+               if verbose:
+                       self.print_fn = self.print_verbose
+               else:
+                       self.print_fn = self.print_quiet
+
+       def __call__(self, dep, dep_is_displayed=False):
+               self.format_depend(dep, dep_is_displayed)
+
+       @staticmethod
+       def print_verbose(indent, cpv, use_conditional, depatom):
+               """Verbosely prints a set of dep strings."""
+
+               sep = ' ? ' if (depatom and use_conditional) else ''
+               print indent + pp.cpv(cpv), "(" + use_conditional + sep + depatom + ")"
+
+       # W0613: *Unused argument %r*
+       # pylint: disable-msg=W0613
+       @staticmethod
+       def print_quiet(indent, cpv, use_conditional, depatom):
+               """Quietly prints a subset set of dep strings."""
+
+               print indent + pp.cpv(cpv)
+
+       def format_depend(self, dep, dep_is_displayed):
+               """Format a dependency for printing.
+
+               @type dep: L{gentoolkit.dependencies.Dependencies}
+               @param dep: the dependency to display
+               """
+
+               depth = getattr(dep, 'depth', 0)
+               indent = " " * depth
+               mdep = dep.matching_dep
+               use_conditional = ""
+               if mdep.use_conditional:
+                       use_conditional = " & ".join(
+                               pp.useflag(u) for u in mdep.use_conditional.split()
+                       )
+               if mdep.operator == '=*':
+                       formatted_dep = '=%s*' % str(mdep.cpv)
+               else:
+                       formatted_dep = mdep.operator + str(mdep.cpv)
+               if mdep.slot:
+                       formatted_dep += pp.emph(':') + pp.slot(mdep.slot)
+               if mdep.use:
+                       useflags = pp.useflag(','.join(mdep.use.tokens))
+                       formatted_dep += (pp.emph('[') + useflags + pp.emph(']'))
+
+               if dep_is_displayed:
+                       indent = indent + " " * len(str(dep.cpv))
+                       self.print_fn(indent, '', use_conditional, formatted_dep)
+               else:
+                       self.print_fn(indent, str(dep.cpv), use_conditional, formatted_dep)
 
 # =========
 # Functions
@@ -50,7 +98,7 @@ DEPPKGS = {}
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -63,125 +111,16 @@ def print_help(with_description=True):
        print pp.command("options")
        print format_options((
                (" -h, --help", "display this help message"),
-               (" -a, --all-packages", 
-                       "include packages that are not installed (slow)"),
+               (" -a, --all-packages",
+                       "include dependencies that are not installed (slow)"),
                (" -D, --indirect",
                        "search both direct and indirect dependencies"),
                ("     --depth=N", "limit indirect dependency tree to specified depth")
        ))
 
 
-def cache_package_list(pkg_cache=None):
-       """Ensure that the package cache is set."""
-
-       if not pkg_cache:
-               if QUERY_OPTS['includePortTree']:
-                       packages = [Package(x) for x in get_cpvs()]
-               else:
-                       packages = [Package(x) for x in get_installed_cpvs()]
-               packages.sort()
-               pkg_cache = packages
-       else:
-               packages = pkg_cache
-       
-       return packages
-
-
-def display_dependencies(cpv_is_displayed, dependency, cpv):
-       """Output dependencies calculated by find_dependencies.
-       
-       @type cpv_is_displayed: bool
-       @param cpv_is_displayed: if True, the cpv has already been printed
-       @see: gentoolkit.package.get_*_deps()
-       @type dependency: tuple
-       @param dependency: (comparator, [use flags], cpv)
-       @type cpv: string
-       @param cpv: cat/pkg-ver
-       """
-
-       atom = pp.pkgquery(dependency[0] + dependency[2])
-       indent = " " * (QUERY_OPTS["indentLevel"] * 2)
-       useflags = pp.useflag(" & ".join(dependency[1]))
-
-       if not cpv_is_displayed:
-               if dependency[1]:
-                       if Config['verbose']:
-                               print indent + pp.cpv(cpv),
-                               print "(" + useflags + " ? " + atom + ")"
-                       else:
-                               print indent + cpv
-               else:
-                       if Config['verbose']:
-                               print indent + pp.cpv(cpv),
-                               print "(" + atom + ")"
-                       else:
-                               print indent + cpv
-       elif Config['verbose']:
-               indent = indent + " " * len(cpv)
-               if dependency[1]:
-                       print indent + " (" + useflags + " ? " + atom + ")"
-               else:
-                       print indent + " (" + atom + ")"        
-
-
-def find_dependencies(matches, pkg_cache):
-       """Find dependencies for the packaged named in queries.
-
-       @type queries: list
-       @param queries: packages to find the dependencies for
-       """
-
-       for pkg in cache_package_list(pkg_cache):
-               if not pkg.cpv in PKGDEPS:
-                       try:
-                               deps = pkg.get_runtime_deps() + pkg.get_compiletime_deps()
-                               deps.extend(pkg.get_postmerge_deps())
-                       except KeyError:
-                               # If the ebuild is not found... 
-                               continue
-                       # Remove duplicate deps
-                       deps = unique_array(deps)
-                       PKGDEPS[pkg.cpv] = deps
-               else:
-                       deps = PKGDEPS[pkg.cpv]
-
-               cpv_is_displayed = False
-               for dependency in deps:
-                       # TODO: (old) determine if dependency is enabled by USE flag
-                       # Find all packages matching the dependency
-                       depstr = dependency[0] + dependency[2]
-                       if not depstr in DEPPKGS:
-                               depcpvs = find_packages(depstr, 
-                                       include_masked=QUERY_OPTS["includePortTree"])
-                               DEPPKGS[depstr] = depcpvs
-                       else:
-                               depcpvs = DEPPKGS[depstr]
-
-                       for depcpv in depcpvs:
-                               is_match = False
-                               if depcpv in matches:
-                                       is_match = True
-
-                               if is_match:
-                                       display_dependencies(cpv_is_displayed, dependency, pkg.cpv)
-                                       cpv_is_displayed = True
-                                       break
-
-               # if --indirect specified, call ourselves again with the dependency
-               # Do not call if we have already called ourselves.
-               if (cpv_is_displayed and not QUERY_OPTS["onlyDirect"] and 
-                       pkg not in PKGSEEN and 
-                       (QUERY_OPTS["indentLevel"] < QUERY_OPTS["depth"] or
-                       QUERY_OPTS["depth"] == -1)):
-
-                       PKGSEEN.add(pkg)
-                       QUERY_OPTS["indentLevel"] += 1
-                       find_dependencies([pkg], pkg_cache)
-                       QUERY_OPTS["indentLevel"] -= 1
-
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        posargs = (x[1] for x in module_opts)
@@ -190,9 +129,7 @@ def parse_module_options(module_opts):
                        print_help()
                        sys.exit(0)
                elif opt in ('-a', '--all-packages'):
-                       QUERY_OPTS['includePortTree'] = True
-               elif opt in ('-d', '--direct'):
-                       continue
+                       QUERY_OPTS['includeMasked'] = True
                elif opt in ('-D', '--indirect'):
                        QUERY_OPTS['onlyDirect'] = False
                elif opt in ('--depth'):
@@ -200,29 +137,28 @@ def parse_module_options(module_opts):
                                depth = int(posarg)
                        else:
                                err = "Module option --depth requires integer (got '%s')"
-                               pp.print_error(err % posarg)
+                               sys.stdout.write(pp.error(err % posarg))
                                print
                                print_help(with_description=False)
                                sys.exit(2)
-                       QUERY_OPTS["depth"] = depth
+                       QUERY_OPTS["maxDepth"] = depth
 
 
 def main(input_args):
        """Parse input and run the program"""
-
        short_opts = "hadD" # -d, --direct was old option for default action
        long_opts = ('help', 'all-packages', 'direct', 'indirect', 'depth=')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
 
        parse_module_options(module_opts)
-               
+
        if not queries:
                print_help()
                sys.exit(2)
@@ -231,21 +167,27 @@ def main(input_args):
        # Output
        #
 
+       dep_print = DependPrinter(verbose=CONFIG['verbose'])
        first_run = True
        for query in queries:
                if not first_run:
                        print
 
-               matches = do_lookup(query, QUERY_OPTS)
-
-               if matches:
-                       find_dependencies(matches, None)
+               pkg = Dependencies(query)
+               if QUERY_OPTS['includeMasked']:
+                       pkggetter = get_cpvs
                else:
-                       if QUERY_OPTS['includePortTree']:
-                               pp.print_error("No matching package found for %s" % query)
-                       else:
-                               pp.print_error(
-                                       "No matching package or all versions masked for %s" % query
-                               )
+                       pkggetter = get_installed_cpvs
+
+               if CONFIG['verbose']:
+                       print " * These packages depend on %s:" % pp.emph(str(pkg.cpv))
+               pkg.graph_reverse_depends(
+                       pkgset=sorted(pkggetter(), cmp=compare_package_strings),
+                       max_depth=QUERY_OPTS["maxDepth"],
+                       only_direct=QUERY_OPTS["onlyDirect"],
+                       printer_fn=dep_print
+               )
 
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index b9cd0a17bbd91be2459f5f919987cd7b7bf92a6b..18d19ba22df61385dcd956f8f9ded1510a11a8dc 100644 (file)
@@ -4,7 +4,7 @@
 #
 # $Header: $
 
-"""Display a dependency graph for a given package"""
+"""Display a direct dependency graph for a given package"""
 
 __docformat__ = 'epytext'
 
@@ -13,30 +13,30 @@ __docformat__ = 'epytext'
 # =======
 
 import sys
+from functools import partial
 from getopt import gnu_getopt, GetoptError
 
-import gentoolkit
 import gentoolkit.pprinter as pp
 from gentoolkit import errors
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import do_lookup, find_best_match
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
 
 # =======
 # Globals
 # =======
 
 QUERY_OPTS = {
-       "categoryFilter": None,
-       "depth": 0,
-       "displayUseflags": True,
-       "fancyFormat": True,
+       "depth": 1,
+       "noAtom": False,
+       "noIndent": False,
+       "noUseflags": False,
        "includeInstalled": True,
        "includePortTree": True,
        "includeOverlayTree": True,
        "includeMasked": True,
        "isRegex": False,
        "matchExact": True,
-       "printMatchInfo": (not Config['quiet'])
+       "printMatchInfo": (not CONFIG['quiet'])
 }
 
 # =========
@@ -45,7 +45,7 @@ QUERY_OPTS = {
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -53,66 +53,22 @@ def print_help(with_description=True):
        if with_description:
                print __doc__.strip()
                print
+       print "Default depth is set to 1 (direct only). Use --depth=0 for no max."
+       print
        print mod_usage(mod_name="depgraph")
        print
        print pp.command("options")
        print format_options((
                (" -h, --help", "display this help message"),
+               (" -A, --no-atom", "do not show dependency atom"),
                (" -U, --no-useflags", "do not show USE flags"),
-               (" -l, --linear", "do not use fancy formatting"),
+               (" -l, --linear", "do not format the graph by indenting dependencies"),
                ("     --depth=N", "limit dependency graph to specified depth")
        ))
 
 
-def display_graph(pkg, stats, level=0, seen_pkgs=None, suffix=""):
-       """Display a dependency graph for a package
-       
-       @type pkg: gentoolkit.package.Package
-       @param pkg: package to check dependencies of
-       @type level: int
-       @param level: current depth level
-       @type seen_pkgs: set
-       @param seen_pkgs: a set of all packages that have had their deps graphed
-       """
-
-       if not seen_pkgs:
-               seen_pkgs = set()
-
-       stats["packages"] += 1
-       stats["maxdepth"] = max(stats["maxdepth"], level)
-
-       pfx = ""
-       if QUERY_OPTS["fancyFormat"]:
-               pfx = (level * " ") + "`-- " 
-       pp.print_info(0, pfx + pkg.cpv + suffix)
-       
-       seen_pkgs.add(pkg.cpv)
-       
-       deps = pkg.get_runtime_deps() + pkg.get_compiletime_deps()
-       deps.extend(pkg.get_postmerge_deps())
-       for dep in deps:
-               suffix = ""
-               depcpv = dep[2]
-               deppkg = find_best_match(dep[0] + depcpv)
-               if not deppkg:
-                       print (pfx + dep[0] + depcpv),
-                       print "(unable to resolve: package masked or removed)"
-                       continue
-               if deppkg.get_cpv() in seen_pkgs:
-                       continue
-               if depcpv.find("virtual") == 0:
-                       suffix += " (%s)" % pp.cpv(depcpv)
-               if dep[1] and QUERY_OPTS["displayUseflags"]:
-                       suffix += " [%s]" % pp.useflagon(' '.join(dep[1]))
-               if (level < QUERY_OPTS["depth"] or QUERY_OPTS["depth"] <= 0):
-                       seen_pkgs, stats = display_graph(deppkg, stats, level+1,
-                               seen_pkgs, suffix)
-
-       return seen_pkgs, stats
-
-
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        posargs = (x[1] for x in module_opts)
@@ -120,38 +76,112 @@ def parse_module_options(module_opts):
                if opt in ('-h', '--help'):
                        print_help()
                        sys.exit(0)
+               if opt in ('-A', '--no-atom'):
+                       QUERY_OPTS["noAtom"] = True
                if opt in ('-U', '--no-useflags'):
-                       QUERY_OPTS["displayUseflags"] = False
+                       QUERY_OPTS["noUseflags"] = True
                if opt in ('-l', '--linear'):
-                       QUERY_OPTS["fancyFormat"] = False
+                       QUERY_OPTS["noIndent"] = True
                if opt in ('--depth'):
                        if posarg.isdigit():
                                depth = int(posarg)
                        else:
-                               err = "Module option --depth requires integer (got '%s')" 
-                               pp.print_error(err % posarg)
+                               err = "Module option --depth requires integer (got '%s')"
+                               sys.stderr.write(pp.error(err % posarg))
                                print
                                print_help(with_description=False)
                                sys.exit(2)
                        QUERY_OPTS["depth"] = depth
 
 
+def depgraph_printer(
+       depth,
+       pkg,
+       dep,
+       no_use=False,
+       no_atom=False,
+       no_indent=False,
+       initial_pkg=False
+):
+       """Display L{gentoolkit.dependencies.Dependencies.graph_depends} results.
+
+       @type depth: int
+       @param depth: depth of indirection, used to calculate indent
+       @type pkg: L{gentoolkit.package.Package}
+       @param pkg: "best match" package matched by B{dep}
+       @type dep: L{gentoolkit.atom.Atom}
+       @param dep: dependency that matched B{pkg}
+       @type no_use: bool
+       @param no_use: don't output USE flags
+       @type no_atom: bool
+       @param no_atom: don't output dep atom
+       @type no_indent: bool
+       @param no_indent: don't output indent based on B{depth}
+       @type initial_pkg: bool
+       @param initial_pkg: somewhat of a hack used to print the root package of
+               the graph with absolutely no indent
+       """
+       indent = '' if no_indent or initial_pkg else ' ' + (' ' * depth)
+       decorator = '[%3d] ' % depth if no_indent else '`-- '
+       use = ''
+       try:
+               atom = '' if no_atom else ' (%s)' % dep.atom
+               if not no_use and dep is not None and dep.use:
+                       use = ' [%s]' % ' '.join(
+                               pp.useflag(x, enabled=True) for x in dep.use.tokens
+                       )
+       except AttributeError:
+               # 'NoneType' object has no attribute 'atom'
+               atom = ''
+       try:
+               print ''.join((indent, decorator, pp.cpv(str(pkg.cpv)), atom, use))
+       except AttributeError:
+               # 'NoneType' object has no attribute 'cpv'
+               print ''.join((indent, decorator, "(no match for %r)" % dep.atom))
+
+
+def make_depgraph(pkg, printer_fn):
+       """Create and display depgraph for each package."""
+
+       if CONFIG['verbose']:
+               print " * direct dependency graph for %s:" % pp.cpv(str(pkg.cpv))
+       else:
+               print "%s:" % str(pkg.cpv)
+
+       # Print out the first package
+       printer_fn(0, pkg, None, initial_pkg=True)
+
+       deps = pkg.deps.graph_depends(
+               max_depth=QUERY_OPTS['depth'],
+               printer_fn=printer_fn,
+               # Use this to set this pkg as the graph's root; better way?
+               result=[(0, pkg)]
+       )
+
+       if CONFIG['verbose']:
+               pkgname = pp.cpv(str(pkg.cpv))
+               n_packages = pp.number(str(len(deps)))
+               max_seen = pp.number(str(max(x[0] for x in deps)))
+               info = "[ %s stats: packages (%s), max depth (%s) ]"
+               print info % (pkgname, n_packages, max_seen)
+
+
 def main(input_args):
        """Parse input and run the program"""
 
-       short_opts = "hUl"
-       long_opts = ('help', 'no-useflags', 'depth=')
+       short_opts = "hAUl"
+       long_opts = ('help', 'no-atom', 'no-useflags', 'depth=')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
 
        parse_module_options(module_opts)
-       
+
        if not queries:
                print_help()
                sys.exit(2)
@@ -168,22 +198,26 @@ def main(input_args):
                matches = do_lookup(query, QUERY_OPTS)
 
                if not matches:
-                       errors.GentoolkitNoMatches(query)
+                       raise errors.GentoolkitNoMatches(query)
+
+               if CONFIG['verbose']:
+                       printer = partial(
+                               depgraph_printer,
+                               no_atom=QUERY_OPTS['noAtom'],
+                               no_indent=QUERY_OPTS['noIndent'],
+                               no_use=QUERY_OPTS['noUseflags']
+                       )
+               else:
+                       printer = partial(
+                               depgraph_printer,
+                               no_atom=True,
+                               no_indent=True,
+                               no_use=True
+                       )
 
                for pkg in matches:
-                       stats = {"maxdepth": 0, "packages": 0}
-
-                       if Config['verbose']:
-                               pp.print_info(3, " * dependency graph for %s:" % pp.cpv(pkg.cpv))
-                       else:
-                               pp.print_info(0, "%s:" % pkg.cpv)
-
-                       stats = display_graph(pkg, stats)[1]
-
-                       if Config['verbose']:
-                               info = ''.join(["[ ", pp.cpv(pkg.cpv), " stats: packages (",
-                               pp.number(str(stats["packages"])), "), max depth (",
-                               pp.number(str(stats["maxdepth"])), ") ]"])
-                               pp.print_info(0, info)
+                       make_depgraph(pkg, printer)
 
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index 80caf1d35a348530ce59d63c1b55c7e973c789e2..4f588bb9aa8650e421002188a9fe06203c3d2e29 100644 (file)
@@ -16,11 +16,12 @@ import os
 import sys
 from getopt import gnu_getopt, GetoptError
 
-import gentoolkit
+import portage
+
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_filetype, format_options, mod_usage, \
-       Config
-from gentoolkit.helpers2 import do_lookup
+from gentoolkit.equery import (format_filetype, format_options, mod_usage,
+       CONFIG)
+from gentoolkit.helpers import do_lookup
 
 # =======
 # Globals
@@ -35,15 +36,16 @@ QUERY_OPTS = {
        "isRegex": False,
        "matchExact": True,
        "outputTree": False,
-       "printMatchInfo": (not Config['quiet']),
+       "printMatchInfo": (not CONFIG['quiet']),
        "showType": False,
        "showTimestamp": False,
        "showMD5": False,
        "typeFilter": None
 }
 
-FILTER_RULES = ('dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc',
-       'man', 'info')
+FILTER_RULES = (
+       'dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc', 'man', 'info'
+)
 
 # =========
 # Functions
@@ -51,7 +53,7 @@ FILTER_RULES = ('dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc',
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -69,12 +71,14 @@ def print_help(with_description=True):
                (" -t, --type", "include file type in output"),
                ("     --tree", "display results in a tree (turns off other options)"),
                (" -f, --filter=RULES", "filter output by file type"),
-               ("              RULES", 
+               ("              RULES",
                        "a comma-separated list (no spaces); choose from:")
        ))
        print " " * 24, ', '.join(pp.emph(x) for x in FILTER_RULES)
 
 
+# R0912: *Too many branches (%s/%s)*
+# pylint: disable-msg=R0912
 def display_files(contents):
        """Display the content of an installed package.
 
@@ -89,40 +93,43 @@ def display_files(contents):
 
        for name in filenames:
                if QUERY_OPTS["outputTree"]:
-                       basename = name.split("/")[1:]
+                       dirdepth = name.count('/')
+                       indent = " "
+                       if dirdepth == 2:
+                               indent = "   "
+                       elif dirdepth > 2:
+                               indent = "   " * (dirdepth - 1)
+
+                       basename = name.rsplit("/", dirdepth - 1)
                        if contents[name][0] == "dir":
                                if len(last) == 0:
                                        last = basename
-                                       print pp.path(" /" + basename[0])
+                                       print pp.path(indent + basename[0])
                                        continue
-                               numol = 0
                                for i, directory in enumerate(basename):
                                        try:
                                                if directory in last[i]:
-                                                       numol = i + 1
                                                        continue
-                                       # W0704: Except doesn't do anything
-                                       # pylint: disable-msg=W0704
                                        except IndexError:
                                                pass
                                        last = basename
                                        if len(last) == 1:
-                                               print pp.path(" " + last[0])
+                                               print pp.path(indent + last[0])
                                                continue
-                                       ind = " " * (numol * 3)
-                                       print pp.path(ind + "> " + "/" + last[-1])
+                                       print pp.path(indent + "> /" + last[-1])
                        elif contents[name][0] == "sym":
-                               print pp.path(" " * (len(last) * 3) + "+"),
+                               print pp.path(indent + "+"),
                                print pp.path_symlink(basename[-1] + " -> " + contents[name][2])
-                       else: 
-                               print pp.path(" " * (len(last) * 3) + "+ ") + basename[-1]
+                       else:
+                               print pp.path(indent + "+ ") + basename[-1]
                else:
-                       pp.print_info(0, format_filetype(
+                       print format_filetype(
                                name,
                                contents[name],
                                show_type=QUERY_OPTS["showType"],
                                show_md5=QUERY_OPTS["showMD5"],
-                               show_timestamp=QUERY_OPTS["showTimestamp"]))
+                               show_timestamp=QUERY_OPTS["showTimestamp"]
+                       )
 
 
 def filter_by_doc(contents, content_filter):
@@ -136,7 +143,7 @@ def filter_by_doc(contents, content_filter):
                        for path in contents:
                                if contents[path][0] == 'obj' and path.startswith(docpath):
                                        filtered_content[path] = contents[path]
-       
+
        return filtered_content
 
 
@@ -150,7 +157,7 @@ def filter_by_command(contents):
                if (contents[path][0] in ['obj', 'sym'] and
                        os.path.dirname(path) in userpath):
                        filtered_content[path] = contents[path]
-       
+
        return filtered_content
 
 
@@ -172,7 +179,7 @@ def filter_by_path(contents):
                        if check_subdirs:
                                while (paths and paths[-1].startswith(basepath)):
                                        paths.pop()
-       
+
        return filtered_content
 
 
@@ -180,9 +187,9 @@ def filter_by_conf(contents):
        """Return a copy of content filtered by configuration files."""
 
        filtered_content = {}
-       conf_path = gentoolkit.settings["CONFIG_PROTECT"].split()
+       conf_path = portage.settings["CONFIG_PROTECT"].split()
        conf_path = tuple(os.path.normpath(x) for x in conf_path)
-       conf_mask_path = gentoolkit.settings["CONFIG_PROTECT_MASK"].split()
+       conf_mask_path = portage.settings["CONFIG_PROTECT_MASK"].split()
        conf_mask_path = tuple(os.path.normpath(x) for x in conf_mask_path)
        for path in contents:
                if contents[path][0] == 'obj' and path.startswith(conf_path):
@@ -206,7 +213,7 @@ def filter_contents(contents):
                content_filter = QUERY_OPTS['typeFilter']
        else:
                return contents
-       
+
        filtered_content = {}
        if frozenset(('dir', 'obj', 'sym', 'dev')).intersection(content_filter):
                # Filter elements by type (as recorded in CONTENTS)
@@ -221,12 +228,12 @@ def filter_contents(contents):
                filtered_content.update(filter_by_conf(contents))
        if frozenset(('doc' ,'man' ,'info')).intersection(content_filter):
                filtered_content.update(filter_by_doc(contents, content_filter))
-       
+
        return filtered_content
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        content_filter = []
        opts = (x[0] for x in module_opts)
@@ -250,7 +257,9 @@ def parse_module_options(module_opts):
                        content_filter.extend(x.lstrip('=') for x in f_split)
                        for rule in content_filter:
                                if not rule in FILTER_RULES:
-                                       pp.print_error("Invalid filter rule '%s'" % rule)
+                                       sys.stderr.write(
+                                               pp.error("Invalid filter rule '%s'" % rule)
+                                       )
                                        print
                                        print_help(with_description=False)
                                        sys.exit(2)
@@ -260,20 +269,21 @@ def parse_module_options(module_opts):
 def main(input_args):
        """Parse input and run the program"""
 
+       # -e, --exact-name is legacy option. djanderson '09
        short_opts = "hemstf:"
        long_opts = ('help', 'exact-name', 'md5sum', 'timestamp', 'type', 'tree',
-               'filter=') 
+               'filter=')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
 
        parse_module_options(module_opts)
-       
+
        if not queries:
                print_help()
                sys.exit(2)
@@ -294,13 +304,17 @@ def main(input_args):
                matches = do_lookup(query, QUERY_OPTS)
 
                if not matches:
-                       pp.print_error("No matching packages found for %s" % query)
+                       sys.stderr.write(
+                               pp.error("No matching packages found for %s" % query)
+                       )
 
                for pkg in matches:
-                       if Config['verbose']:
-                               print " * Contents of %s:" % pp.cpv(pkg.cpv)
+                       if CONFIG['verbose']:
+                               print " * Contents of %s:" % pp.cpv(str(pkg.cpv))
 
                        contents = pkg.get_contents()
                        display_files(filter_contents(contents))
 
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index c110a228a67e65f51e920e4ae58adaf96777ae42..82b2e296950f8438f5447bd1eca34cd60812b610 100644 (file)
@@ -15,11 +15,11 @@ __docformat__ = 'epytext'
 import sys
 from getopt import gnu_getopt, GetoptError
 
-import gentoolkit
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import do_lookup, get_installed_cpvs, print_sequence
-from gentoolkit.package import Package, PackageFormatter
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
+from gentoolkit.package import PackageFormatter
 
 # =======
 # Globals
@@ -41,7 +41,7 @@ QUERY_OPTS = {
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -61,8 +61,42 @@ def print_help(with_description=True):
        ))
 
 
+def display_useflags(query, pkg):
+       """Display USE flag information for a given package."""
+
+       try:
+               useflags = [x.lstrip("+-") for x in pkg.get_env_var("IUSE").split()]
+       except errors.GentoolkitFatalError:
+               # aux_get KeyError or other unexpected result
+               return
+
+       if query not in useflags:
+               return
+
+       if CONFIG['verbose']:
+               fmt_pkg = PackageFormatter(pkg, do_format=True)
+       else:
+               fmt_pkg = PackageFormatter(pkg, do_format=False)
+
+       if (QUERY_OPTS["includeInstalled"] and
+               not QUERY_OPTS["includePortTree"] and
+               not QUERY_OPTS["includeOverlayTree"]):
+               if not 'I' in fmt_pkg.location:
+                       return
+       if (QUERY_OPTS["includePortTree"] and
+               not QUERY_OPTS["includeOverlayTree"]):
+               if not 'P' in fmt_pkg.location:
+                       return
+       if (QUERY_OPTS["includeOverlayTree"] and
+               not QUERY_OPTS["includePortTree"]):
+               if not 'O' in fmt_pkg.location:
+                       return
+       print fmt_pkg
+
+
+
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        # Parse module options
        opts = (x[0] for x in module_opts)
@@ -89,7 +123,7 @@ def main(input_args):
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
@@ -112,33 +146,11 @@ def main(input_args):
                if not first_run:
                        print
 
-               if Config['verbose']:
+               if CONFIG['verbose']:
                        print " * Searching for USE flag %s ... " % pp.emph(query)
 
                for pkg in matches:
-
-                       useflags = [x.lstrip("+-") for x in pkg.get_env_var("IUSE").split()]
-                       if query not in useflags:
-                               continue
-
-                       if Config['verbose']:
-                               pkgstr = PackageFormatter(pkg, format=True)
-                       else:
-                               pkgstr = PackageFormatter(pkg, format=False)
-
-                       if (QUERY_OPTS["includeInstalled"] and
-                               not QUERY_OPTS["includePortTree"] and
-                               not QUERY_OPTS["includeOverlayTree"]):
-                               if not 'I' in pkgstr.location:
-                                       continue
-                       if (QUERY_OPTS["includePortTree"] and
-                               not QUERY_OPTS["includeOverlayTree"]):
-                               if not 'P' in pkgstr.location:
-                                       continue
-                       if (QUERY_OPTS["includeOverlayTree"] and
-                               not QUERY_OPTS["includePortTree"]):
-                               if not 'O' in pkgstr.location:
-                                       continue
-                       print pkgstr
-
+                       display_useflags(query, pkg)
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index dd1302944a7b4d61fb60f3f447a40aa9234a5076..32f8eff8c204994e50fa1855542b61913ea2a7f4 100644 (file)
@@ -17,8 +17,8 @@ from getopt import gnu_getopt, GetoptError
 
 import gentoolkit
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import do_lookup, get_installed_cpvs
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup, get_installed_cpvs
 from gentoolkit.package import Package, PackageFormatter
 
 # =======
@@ -26,14 +26,14 @@ from gentoolkit.package import Package, PackageFormatter
 # =======
 
 QUERY_OPTS = {
-       "categoryFilter": None,
        "duplicates": False,
        "includeInstalled": True,
        "includePortTree": False,
        "includeOverlayTree": False,
        "includeMasked": True,
+       "includeMaskReason": False,
        "isRegex": False,
-       "printMatchInfo": (not Config['quiet'])
+       "printMatchInfo": (not CONFIG['quiet'])
 }
 
 # =========
@@ -42,7 +42,7 @@ QUERY_OPTS = {
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -50,11 +50,16 @@ def print_help(with_description=True):
        if with_description:
                print __doc__.strip()
                print
-       # Deprecation warning added 04/09: djanderson
-       pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.")
-       pp.print_warn("-e, --exact-name is now the default behavior.")
-       pp.print_warn("Use globbing to simulate the old behavior (see man equery).")
-       pp.print_warn("Use '*' to check all installed packages.")
+
+       # Deprecation warning added by djanderson, 12/2008
+       depwarning = (
+               "Default action for this module has changed in Gentoolkit 0.3.",
+               "Use globbing to simulate the old behavior (see man equery).",
+               "Use '*' to check all installed packages.",
+               "Use 'foo-bar/*' to filter by category."
+       )
+       for line in depwarning:
+               sys.stderr.write(pp.warn(line))
        print
 
        print mod_usage(mod_name="list")
@@ -62,9 +67,9 @@ def print_help(with_description=True):
        print pp.command("options")
        print format_options((
                (" -h, --help", "display this help message"),
-               (" -c, --category CAT", "only search in the category CAT"),
                (" -d, --duplicates", "list only installed duplicate packages"),
                (" -f, --full-regex", "query is a regular expression"),
+               (" -m, --mask-reason", "include reason for package mask"),
                (" -I, --exclude-installed",
                        "exclude installed packages from output"),
                (" -o, --overlay-tree", "list packages in overlays"),
@@ -78,10 +83,10 @@ def get_duplicates(matches):
        dups = {}
        result = []
        for pkg in matches:
-               if pkg.key in dups:
-                       dups[pkg.key].append(pkg)
+               if pkg.cp in dups:
+                       dups[pkg.cp].append(pkg)
                else:
-                       dups[pkg.key] = [pkg]
+                       dups[pkg.cp] = [pkg]
 
        for cpv in dups.values():
                if len(cpv) > 1:
@@ -91,7 +96,7 @@ def get_duplicates(matches):
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        posargs = (x[1] for x in module_opts)
@@ -99,10 +104,6 @@ def parse_module_options(module_opts):
                if opt in ('-h', '--help'):
                        print_help()
                        sys.exit(0)
-               elif opt in ('-a', '--all'):
-                       QUERY_OPTS['listAllPackages'] = True
-               elif opt in ('-c', '--category'):
-                       QUERY_OPTS['categoryFilter'] = posarg
                elif opt in ('-I', '--exclude-installed'):
                        QUERY_OPTS['includeInstalled'] = False
                elif opt in ('-p', '--portage-tree'):
@@ -111,9 +112,13 @@ def parse_module_options(module_opts):
                        QUERY_OPTS['includeOverlayTree'] = True
                elif opt in ('-f', '--full-regex'):
                        QUERY_OPTS['isRegex'] = True
+               elif opt in ('-m', '--mask-reason'):
+                       QUERY_OPTS['includeMaskReason'] = True
                elif opt in ('-e', '--exact-name'):
-                       pp.print_warn("-e, --exact-name is now default.")
-                       pp.print_warn("Use globbing to simulate the old behavior.")
+                       sys.stderr.write(pp.warn("-e, --exact-name is now default."))
+                       sys.stderr.write(
+                               pp.warn("Use globbing to simulate the old behavior.")
+                       )
                        print
                elif opt in ('-d', '--duplicates'):
                        QUERY_OPTS['duplicates'] = True
@@ -122,18 +127,20 @@ def parse_module_options(module_opts):
 def main(input_args):
        """Parse input and run the program"""
 
-       short_opts = "hc:defiIop" # -i, -e were options for default actions
+       short_opts = "hdefiImop" # -i, -e were options for default actions
 
        # 04/09: djanderson
+       # --all is no longer needed. Kept for compatibility.
        # --installed is no longer needed. Kept for compatibility.
        # --exact-name is no longer needed. Kept for compatibility.
-       long_opts = ('help', 'all', 'category=', 'installed', 'exclude-installed',
-       'portage-tree', 'overlay-tree', 'full-regex', 'exact-name', 'duplicates')
+       long_opts = ('help', 'all', 'installed', 'exclude-installed',
+       'mask-reason', 'portage-tree', 'overlay-tree', 'full-regex', 'exact-name',
+       'duplicates')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
@@ -145,6 +152,7 @@ def main(input_args):
                QUERY_OPTS["includeInstalled"] = True
                QUERY_OPTS["includePortTree"] = False
                QUERY_OPTS["includeOverlayTree"] = False
+               QUERY_OPTS["includeMaskReason"] = False
 
        if not queries:
                print_help()
@@ -168,10 +176,10 @@ def main(input_args):
                #
 
                for pkg in matches:
-                       if Config['verbose']:
-                               pkgstr = PackageFormatter(pkg, format=True)
+                       if CONFIG['verbose']:
+                               pkgstr = PackageFormatter(pkg, do_format=True)
                        else:
-                               pkgstr = PackageFormatter(pkg, format=False)
+                               pkgstr = PackageFormatter(pkg, do_format=False)
 
                        if (QUERY_OPTS["includeInstalled"] and
                                not QUERY_OPTS["includePortTree"] and
@@ -188,4 +196,29 @@ def main(input_args):
                                        continue
                        print pkgstr
 
+                       if QUERY_OPTS["includeMaskReason"]:
+                               ms_int, ms_orig = pkgstr.format_mask_status()
+                               if not ms_int > 2:
+                                       # ms_int is a number representation of mask level.
+                                       # Only 2 and above are "hard masked" and have reasons.
+                                       continue
+                               mask_reason = pkg.get_mask_reason()
+                               if not mask_reason:
+                                       # Package not on system or not masked
+                                       continue
+                               elif not any(mask_reason):
+                                       print " * No mask reason given"
+                               else:
+                                       status = ', '.join(ms_orig)
+                                       explanation = mask_reason[0]
+                                       mask_location = mask_reason[1]
+                                       print " * Masked by %r" % status
+                                       print " * %s:" % mask_location
+                                       print '\n'.join(
+                                               [' * %s' % line.lstrip(' #')
+                                                       for line in explanation.splitlines()]
+                                               )
+
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index d847f563f1c0f9180a12c8ccb4667ab8d4f24d96..fc38bff365e4dfee92dbc9ff4479a9181bc6e065 100644 (file)
@@ -9,7 +9,6 @@
 # Move to Imports section after Python-2.6 is stable
 from __future__ import with_statement
 
-__author__  = "Douglas Anderson"
 __docformat__ = 'epytext'
 
 # =======
@@ -17,18 +16,14 @@ __docformat__ = 'epytext'
 # =======
 
 import os
-import re 
+import re
 import sys
-import xml.etree.cElementTree as ET
 from getopt import gnu_getopt, GetoptError
 
-from portage import settings
-
 import gentoolkit.pprinter as pp
 from gentoolkit import errors
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import find_packages, print_sequence, print_file, \
-       uniqify
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import find_packages, print_sequence, print_file
 from gentoolkit.textwrap_ import TextWrapper
 
 # =======
@@ -40,28 +35,23 @@ from gentoolkit.textwrap_ import TextWrapper
 # pylint: disable-msg=E1101
 
 QUERY_OPTS = {
-       "current": False,
-       "description": False,
-       "herd": False,
-       "maintainer": False,
-       "useflags": False,
-       "upstream": False,
-       "xml": False
-} 
-
-# Get the location of the main Portage tree
-PORTDIR = [settings["PORTDIR"] or os.path.join(os.sep, "usr", "portage")]
-# Check for overlays
-if settings["PORTDIR_OVERLAY"]:
-       PORTDIR.extend(settings["PORTDIR_OVERLAY"].split())
+       'current': False,
+       'description': False,
+       'herd': False,
+       'keywords': False,
+       'maintainer': False,
+       'useflags': False,
+       'upstream': False,
+       'xml': False
+}
 
 # =========
 # Functions
 # =========
 
-def print_help(with_description=True):
+def print_help(with_description=True, with_usage=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -69,83 +59,207 @@ def print_help(with_description=True):
        if with_description:
                print __doc__.strip()
                print
-       print mod_usage(mod_name="meta")
-       print
+       if with_usage:
+               print mod_usage(mod_name="meta")
+               print
        print pp.command("options")
        print format_options((
                (" -h, --help", "display this help message"),
-               (" -c, --current", "parse metadata.xml in the current directory"),
                (" -d, --description", "show an extended package description"),
                (" -H, --herd", "show the herd(s) for the package"),
+               (" -k, --keywords", "show keywords for all matching package versions"),
                (" -m, --maintainer", "show the maintainer(s) for the package"),
                (" -u, --useflags", "show per-package USE flag descriptions"),
                (" -U, --upstream", "show package's upstream information"),
-               (" -x, --xml", "show the plain XML file")
+               (" -x, --xml", "show the plain metadata.xml file")
        ))
 
 
-def call_get_functions(metadata_path, package_dir, QUERY_OPTS):
+def format_herds(herds):
+       """Format herd information for display."""
+
+       result = []
+       for herd in herds:
+               herdstr = ''
+               email = "(%s)" % herd[1] if herd[1] else ''
+               herdstr = herd[0]
+               if CONFIG['verbose']:
+                       herdstr += " %s" % (email,)
+               result.append(herdstr)
+
+       return result
+
+
+def format_maintainers(maints):
+       """Format maintainer information for display."""
+
+       result = []
+       for maint in maints:
+               maintstr = ''
+               maintstr = maint.email
+               if CONFIG['verbose']:
+                       maintstr += " (%s)" % (maint.name,) if maint.name else ''
+                       maintstr += "\n%s" % (maint.description,) \
+                               if maint.description else ''
+               result.append(maintstr)
+
+       return result
+
+
+def format_upstream(upstream):
+       """Format upstream information for display."""
+
+       def _format_upstream_docs(docs):
+               result = []
+               for doc in docs:
+                       doc_location = doc[0]
+                       doc_lang = doc[1]
+                       docstr = doc_location
+                       if doc_lang is not None:
+                               docstr += " (%s)" % (doc_lang,)
+                       result.append(docstr)
+               return result
+
+       def _format_upstream_ids(ids):
+               result = []
+               for id_ in ids:
+                       site = id_[0]
+                       proj_id = id_[1]
+                       idstr = "%s ID: %s" % (site, proj_id)
+                       result.append(idstr)
+               return result
+
+       result = []
+       for up in upstream:
+               upmaints = format_maintainers(up.maintainers)
+               for upmaint in upmaints:
+                       result.append(format_line(upmaint, "Maintainer:  ", " " * 13))
+
+               for upchange in up.changelogs:
+                       result.append(format_line(upchange, "ChangeLog:   ", " " * 13))
+
+               updocs = _format_upstream_docs(up.docs)
+               for updoc in updocs:
+                       result.append(format_line(updoc, "Docs:       ", " " * 13))
+
+               for upbug in up.bugtrackers:
+                       result.append(format_line(upbug, "Bugs-to:     ", " " * 13))
+
+               upids = _format_upstream_ids(up.remoteids)
+               for upid in upids:
+                       result.append(format_line(upid, "Remote-ID:   ", " " * 13))
+
+       return result
+
+
+def format_useflags(useflags):
+       """Format USE flag information for display."""
+
+       result = []
+       for flag in useflags:
+               result.append(pp.useflag(flag.name))
+               result.append(flag.description)
+               result.append("")
+
+       return result
+
+
+def format_keywords(match):
+       """Format keywords information for display."""
+
+       kwsplit = match.get_env_var('KEYWORDS').split()
+       ver = match.cpv.fullversion
+       keywords = ''
+       for kw in kwsplit:
+               if kw.startswith('~'):
+                       keywords += " %s" % pp.useflag(kw, enabled=True)
+               else:
+                       keywords += " %s" % pp.useflag(kw, enabled=False)
+
+       if CONFIG['verbose']:
+               result = format_line(
+                       keywords, "%s: " % pp.cpv(ver), " " * (len(ver) + 2)
+                       )
+       else:
+               result = "%s:%s" % (ver, keywords)
+
+       return result
+
+# R0912: *Too many branches (%s/%s)*
+# pylint: disable-msg=R0912
+def call_format_functions(matches):
        """Call information gathering functions and display the results."""
-       
-       if Config['verbose']:
-               print get_overlay_name(package_dir)
 
-       try:
-               xml_tree = ET.parse(metadata_path)
-       except IOError:
-               pp.print_error("No metadata available")
-               first_run = False
-               return
+       # Choose a good package to reference metadata from
+       ref_pkg = get_reference_pkg(matches)
+
+       if CONFIG['verbose']:
+               repo = ref_pkg.get_repo_name()
+               print " * %s [%s]" % (pp.cpv(ref_pkg.cpv.cp), pp.section(repo))
 
        got_opts = False
-       if (QUERY_OPTS["herd"] or QUERY_OPTS["description"] or
-               QUERY_OPTS["useflags"] or QUERY_OPTS["maintainer"] or
-               QUERY_OPTS["upstream"] or QUERY_OPTS["xml"]):
+       if any(QUERY_OPTS.values()):
                # Specific information requested, less formatting
                got_opts = True
 
+       if not got_opts:
+               pkg_loc = ref_pkg.get_package_path()
+               print format_line(pkg_loc, "Location:    ", " " * 13)
+
        if QUERY_OPTS["herd"] or not got_opts:
-               herd = get_herd(xml_tree)
+               herds = format_herds(ref_pkg.metadata.get_herds(include_email=True))
                if QUERY_OPTS["herd"]:
-                       herd = format_list(herd)
+                       print_sequence(format_list(herds))
                else:
-                       herd = format_list(herd, "Herd:        ", " " * 13)
-               print_sequence(herd)
+                       for herd in herds:
+                               print format_line(herd, "Herd:        ", " " * 13)
 
        if QUERY_OPTS["maintainer"] or not got_opts:
-               maint = get_maitainer(xml_tree)
+               maints = format_maintainers(ref_pkg.metadata.get_maintainers())
                if QUERY_OPTS["maintainer"]:
-                       maint = format_list(maint)
+                       print_sequence(format_list(maints))
                else:
-                       maint = format_list(maint, "Maintainer:  ", " " * 13)
-               print_sequence(maint)
+                       if not maints:
+                               print format_line([], "Maintainer:  ", " " * 13)
+                       else:
+                               for maint in maints:
+                                       print format_line(maint, "Maintainer:  ", " " * 13)
 
        if QUERY_OPTS["upstream"] or not got_opts:
-               upstream = get_upstream(xml_tree)
+               upstream = format_upstream(ref_pkg.metadata.get_upstream())
                if QUERY_OPTS["upstream"]:
                        upstream = format_list(upstream)
                else:
                        upstream = format_list(upstream, "Upstream:    ", " " * 13)
                print_sequence(upstream)
 
+       if QUERY_OPTS["keywords"] or not got_opts:
+               for match in matches:
+                       kwds = format_keywords(match)
+                       if QUERY_OPTS["keywords"]:
+                               print kwds
+                       else:
+                               indent = " " * (15 + len(match.cpv.fullversion))
+                               print format_line(kwds, "Keywords:    ", indent)
+
        if QUERY_OPTS["description"]:
-               desc = get_description(xml_tree)
+               desc = ref_pkg.metadata.get_descriptions()
                print_sequence(format_list(desc))
 
        if QUERY_OPTS["useflags"]:
-               useflags = get_useflags(xml_tree)
+               useflags = format_useflags(ref_pkg.metadata.get_useflags())
                print_sequence(format_list(useflags))
 
        if QUERY_OPTS["xml"]:
-               print_file(metadata_path)
+               print_file(os.path.join(ref_pkg.get_package_path(), 'metadata.xml'))
 
 
 def format_line(line, first="", subsequent="", force_quiet=False):
        """Wrap a string at word boundaries and optionally indent the first line
        and/or subsequent lines with custom strings.
 
-       Preserve newlines if the longest line is not longer than 
-       Config['termWidth']. To force the preservation of newlines and indents, 
+       Preserve newlines if the longest line is not longer than
+       CONFIG['termWidth']. To force the preservation of newlines and indents,
        split the string into a list and feed it to format_line via format_list.
 
        @see: format_list()
@@ -161,7 +275,7 @@ def format_line(line, first="", subsequent="", force_quiet=False):
        """
 
        if line:
-               line = line.expandtabs().strip("\n").splitlines() 
+               line = line.expandtabs().strip("\n").splitlines()
        else:
                if force_quiet:
                        return
@@ -172,18 +286,18 @@ def format_line(line, first="", subsequent="", force_quiet=False):
                wider_indent = first
        else:
                wider_indent = subsequent
-       
+
        widest_line_len = len(max(line, key=len)) + len(wider_indent)
-       
-       if widest_line_len > Config['termWidth']:
-               twrap = TextWrapper(width=Config['termWidth'], expand_tabs=False,
+
+       if widest_line_len > CONFIG['termWidth']:
+               twrap = TextWrapper(width=CONFIG['termWidth'], expand_tabs=False,
                        initial_indent=first, subsequent_indent=subsequent)
                line = " ".join(line)
                line = re.sub("\s+", " ", line)
                line = line.lstrip()
                result = twrap.fill(line)
        else:
-               # line will fit inside Config['termWidth'], so preserve whitespace and 
+               # line will fit inside CONFIG['termWidth'], so preserve whitespace and
                # newlines
                line[0] = first + line[0]          # Avoid two newlines if len == 1
 
@@ -212,7 +326,7 @@ def format_list(lst, first="", subsequent="", force_quiet=False):
        @type subsequent: string
        @param subsequent: text to prepend to subsequent lines
        @rtype: list
-       @return: list with element text wrapped at Config['termWidth']
+       @return: list with element text wrapped at CONFIG['termWidth']
        """
 
        result = []
@@ -229,7 +343,7 @@ def format_list(lst, first="", subsequent="", force_quiet=False):
                                # We don't want to send a blank line to format_line()
                                result.append("")
        else:
-               if Config['verbose']:
+               if CONFIG['verbose']:
                        if force_quiet:
                                result = None
                        else:
@@ -239,238 +353,34 @@ def format_list(lst, first="", subsequent="", force_quiet=False):
        return result
 
 
-def get_herd(xml_tree):
-       """Return a list of text nodes for <herd>."""
-       
-       result = []
-       for elem in xml_tree.findall("herd"):
-               herd_mail = get_herd_email(elem.text)
-               if herd_mail and Config['verbose']:
-                       result.append("%s (%s)" % (elem.text, herd_mail))
-               else:
-                       result.append(elem.text) 
+def get_reference_pkg(matches):
+       """Find a package in the Portage tree to reference."""
 
-       return result
-
-
-def get_herd_email(herd):
-       """Return the email of the given herd if it's in herds.xml, else None."""
-       
-       herds_path = os.path.join(PORTDIR[0], "metadata/herds.xml")
-
-       try:
-               herds_tree = ET.parse(herds_path)
-       except IOError, err:
-               pp.print_error(str(err))
-               return None
-
-       # Some special herds are not listed in herds.xml
-       if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'):
-               return None
-       
-       for node in herds_tree.getiterator("herd"):
-               if node.findtext("name") == herd:
-                       return node.findtext("email")
-
-
-def get_description(xml_tree):
-       """Return a list of text nodes for <longdescription>.
-
-       @todo: Support the `lang' attribute
-       """
-
-       return [e.text for e in xml_tree.findall("longdescription")]
-
-
-def get_maitainer(xml_tree):
-       """Return a parsable tree of all maintainer elements and sub-elements."""
-
-       first_run = True
-       result = []
-       for node in xml_tree.findall("maintainer"):
-               if not first_run:
-                       result.append("")
-               restrict = node.get("restrict")
-               if restrict:
-                       result.append("(%s %s)" %
-                       (pp.emph("Restrict to"), pp.output.green(restrict)))
-               result.extend(e.text for e in node)
-               first_run = False
-
-       return result
-
-
-def get_overlay_name(p_dir):
-       """Determine the overlay name and return a formatted string."""
-
-       result = []
-       cat_pkg = '/'.join(p_dir.split('/')[-2:])
-       result.append(" * %s" % pp.cpv(cat_pkg))
-       o_dir = '/'.join(p_dir.split('/')[:-2])
-       if o_dir != PORTDIR[0]:
-               # o_dir is an overlay
-               o_name = o_dir.split('/')[-1]
-               o_name = ("[", o_name, "]")
-               result.append(pp.output.turquoise("".join(o_name)))
-
-       return ' '.join(result)
-
-
-def get_package_directory(query):
-       """Find a package's portage directory."""
-
-       matches = find_packages(query, include_masked=True)
-       # Prefer a package that's in the Portage tree over one in an
-       # overlay. Start with oldest first.
        pkg = None
        while list(reversed(matches)):
                pkg = matches.pop()
                if not pkg.is_overlay():
                        break
-       
-       return pkg.get_package_path() if pkg else None
-       
-
-def get_useflags(xml_tree):
-       """Return a list of formatted <useflag> lines, including blank elements
-       where blank lines should be printed."""
-
-       first_run = True
-       result = []
-       for node in xml_tree.getiterator("flag"):
-               if not first_run:
-                       result.append("")
-               flagline = pp.useflag(node.get("name"))
-               restrict = node.get("restrict")
-               if restrict:
-                       result.append("%s (%s %s)" %
-                               (flagline, pp.emph("Restrict to"), pp.output.green(restrict)))
-               else:
-                       result.append(flagline)
-               # ElementTree handles nested element text in a funky way. 
-               # So we need to dump the raw XML and parse it manually.
-               flagxml = ET.tostring(node)
-               flagxml = re.sub("\s+", " ", flagxml)
-               flagxml = re.sub("\n\t", "", flagxml)
-               flagxml = re.sub("<(pkg|cat)>(.*?)</(pkg|cat)>",
-                       pp.cpv(r"\2"), flagxml)
-               flagtext = re.sub("<.*?>", "", flagxml)
-               result.append(flagtext)
-               first_run = False
-
-       return result
-
-
-def _get_upstream_bugtracker(node):
-       """Extract and format upstream bugtracker information."""
-
-       bt_loc = [e.text for e in node.findall("bugs-to")]
-
-       return format_list(bt_loc, "Bugs to:    ", " " * 12, force_quiet=True)
-
-
-def _get_upstream_changelog(node):
-       """Extract and format upstream changelog information."""
-
-       cl_paths = [e.text for e in node.findall("changelog")]
-
-       return format_list(cl_paths, "Changelog:  ", " " * 12, force_quiet=True)
-
-
-def _get_upstream_documentation(node):
-       """Extract and format upstream documentation information."""
-
-       doc = []
-       for elem in node.findall("doc"):
-               lang = elem.get("lang")
-               if lang:
-                       lang = "(%s)" % pp.output.yellow(lang)
-               else:
-                       lang = ""
-               doc.append(" ".join([elem.text, lang]))
-
-       return format_list(doc, "Docs:       ", " " * 12, force_quiet=True)
 
-
-def _get_upstream_maintainer(node):
-       """Extract and format upstream maintainer information."""
-
-       maintainer = node.findall("maintainer")
-       maint = []
-       for elem in maintainer:
-               if elem.find("name") != None:
-                       maint.append(elem.find("name").text)
-               if elem.find("email") != None:
-                       maint.append(elem.find("email").text)
-               if elem.get("status") == "active":
-                       maint.append("(%s)" % pp.output.green("active"))
-               elif elem.get("status") == "inactive":
-                       maint.append("(%s)" % pp.output.red("inactive"))
-               elif elem.get("status") != None:
-                       maint.append("(" + elem.get("status") + ")")
-
-       return format_list(maint, "Maintainer: ", " " * 12, force_quiet=True)
-
-
-def _get_upstream_remoteid(node):
-       """Extract and format upstream remote ID."""
-
-       r_id = [e.get("type") + ": " + e.text for e in node.findall("remote-id")]
-
-       return format_list(r_id, "Remote ID:  ", " " * 12, force_quiet=True)
-
-
-def get_upstream(xml_tree):
-       """Return a list of formatted <upstream> lines, including blank elements
-       where blank lines should be printed."""
-
-       first_run = True
-       result = []
-       for node in xml_tree.findall("upstream"):
-               if not first_run:
-                       result.append("")
-
-               maint = _get_upstream_maintainer(node)
-               if maint:
-                       result.append("\n".join(maint))
-
-               changelog = _get_upstream_changelog(node)
-               if changelog:
-                       result.append("\n".join(changelog))
-
-               documentation = _get_upstream_documentation(node)
-               if documentation:
-                       result.append("\n".join(documentation))
-
-               bugs_to = _get_upstream_bugtracker(node)
-               if bugs_to:
-                       result.append("\n".join(bugs_to))
-
-               remote_id = _get_upstream_remoteid(node)
-               if remote_id:
-                       result.append("\n".join(remote_id))
-
-               first_run = False
-
-       return result
+       return pkg
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        for opt in opts:
                if opt in ('-h', '--help'):
                        print_help()
                        sys.exit(0)
-               elif opt in ('-c', '--current'):
-                       QUERY_OPTS["current"] = True
                elif opt in ('-d', '--description'):
                        QUERY_OPTS["description"] = True
                elif opt in ('-H', '--herd'):
                        QUERY_OPTS["herd"] = True
                elif opt in ('-m', '--maintainer'):
                        QUERY_OPTS["maintainer"] = True
+               elif opt in ('-k', '--keywords'):
+                       QUERY_OPTS["keywords"] = True
                elif opt in ('-u', '--useflags'):
                        QUERY_OPTS["useflags"] = True
                elif opt in ('-U', '--upstream'):
@@ -482,44 +392,37 @@ def parse_module_options(module_opts):
 def main(input_args):
        """Parse input and run the program."""
 
-       short_opts = "hcdHmuUx"
-       long_opts = ('help', 'current', 'description', 'herd', 'maintainer',
+       short_opts = "hdHkmuUx"
+       long_opts = ('help', 'description', 'herd', 'keywords', 'maintainer',
                'useflags', 'upstream', 'xml')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
 
        parse_module_options(module_opts)
-       
+
        # Find queries' Portage directory and throw error if invalid
-       if not queries and not QUERY_OPTS["current"]:
+       if not queries:
                print_help()
                sys.exit(2)
-       
-       if QUERY_OPTS["current"]:
-               package_dir = os.getcwd()
-               metadata_path = os.path.join(package_dir, "metadata.xml")
-               call_get_functions(metadata_path, package_dir, QUERY_OPTS)
-       else:
-               first_run = True
-               for query in queries:
-                       package_dir = get_package_directory(query)
-                       if not package_dir:
-                               raise errors.GentoolkitNoMatches(query)
-                       metadata_path = os.path.join(package_dir, "metadata.xml")
-
-                       # --------------------------------
-                       # Check options and call functions
-                       # --------------------------------
-               
-                       if not first_run:
-                               print
-                               
-                       call_get_functions(metadata_path, package_dir, QUERY_OPTS)
-       
-                       first_run = False
+
+       first_run = True
+       for query in queries:
+               matches = find_packages(query, include_masked=True)
+               if not matches:
+                       raise errors.GentoolkitNoMatches(query)
+
+               if not first_run:
+                       print
+
+               matches.sort()
+               call_format_functions(matches)
+
+               first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index 9cb6bc98d636b6c9faebac372c6679d4b2621e47..8570a6aa64f3d488e1c2d87595380eb18e5cef5a 100644 (file)
@@ -16,16 +16,15 @@ import sys
 from getopt import gnu_getopt, GetoptError
 
 import gentoolkit.pprinter as pp
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import do_lookup
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
 
 # =======
 # Globals
 # =======
 
 QUERY_OPTS = {
-       "categoryFilter": None,
-       "includeInstalled": False,
+       "includeInstalled": True,
        "includePortTree": False,
        "includeOverlayTree": False,
        "includeMasked": True,
@@ -41,7 +40,7 @@ QUERY_OPTS = {
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -50,11 +49,15 @@ def print_help(with_description=True):
                print __doc__.strip()
                print
 
-       # Deprecation warning added 04/09: djanderson
-       pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.")
-       pp.print_warn("-e, --exact-name is now the default behavior.")
-       pp.print_warn("Use globbing to simulate the old behavior (see man equery).")
-       pp.print_warn("Use '*' to check all installed packages.")
+       # Deprecation warning added by djanderson, 12/2008
+       depwarning = (
+               "Default action for this module has changed in Gentoolkit 0.3.",
+               "Use globbing to simulate the old behavior (see man equery).",
+               "Use '*' to check all installed packages.",
+               "Use 'foo-bar/*' to filter by category."
+       )
+       for line in depwarning:
+               sys.stderr.write(pp.warn(line))
        print
 
        print mod_usage(mod_name="size")
@@ -63,7 +66,6 @@ def print_help(with_description=True):
        print format_options((
                (" -h, --help", "display this help message"),
                (" -b, --bytes", "report size in bytes"),
-               (" -c, --category CAT", "only search in the category CAT"),
                (" -f, --full-regex", "query is a regular expression")
        ))
 
@@ -76,14 +78,14 @@ def display_size(match_set):
        """
 
        for pkg in match_set:
-               (size, files, uncounted) = pkg.size()
+               size, files, uncounted = pkg.get_size()
 
-               if Config['verbose']:
-                       print " * %s" % pp.cpv(pkg.cpv)
+               if CONFIG['verbose']:
+                       print " * %s" % pp.cpv(str(pkg.cpv))
                        print "Total files : %s".rjust(25) % pp.number(str(files))
 
                        if uncounted:
-                               pp.print_info(0, "Inaccessible files : %s".rjust(25) %
+                               print ("Inaccessible files : %s".rjust(25) %
                                        pp.number(str(uncounted)))
 
                        if QUERY_OPTS["sizeInBytes"]:
@@ -91,10 +93,10 @@ def display_size(match_set):
                        else:
                                size_str = "%s %s" % format_bytes(size)
 
-                       pp.print_info(0, "Total size  : %s".rjust(25) % size_str)
+                       print "Total size  : %s".rjust(25) % size_str
                else:
                        info = "%s: total(%d), inaccessible(%d), size(%s)"
-                       print info % (pkg.cpv, files, uncounted, size)
+                       print info % (str(pkg.cpv), files, uncounted, size)
 
 
 def format_bytes(bytes_, precision=2):
@@ -134,21 +136,19 @@ def format_bytes(bytes_, precision=2):
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
-       posargs = (x[1] for x in module_opts)
-       for opt, posarg in zip(opts, posargs):
+       for opt in opts:
                if opt in ('-h', '--help'):
                        print_help()
                        sys.exit(0)
                elif opt in ('-b', '--bytes'):
                        QUERY_OPTS["sizeInBytes"] = True
-               elif opt in ('-c', '--category'):
-                       QUERY_OPTS['categoryFilter'] = posarg
                elif opt in ('-e', '--exact-name'):
-                       pp.print_warn("-e, --exact-name is now default.")
-                       pp.print_warn("Use globbing to simulate the old behavior.")
+                       sys.stderr.write(pp.warn("-e, --exact-name is now default."))
+                       warning = pp.warn("Use globbing to simulate the old behavior.")
+                       sys.stderr.write(warning)
                        print
                elif opt in ('-f', '--full-regex'):
                        QUERY_OPTS['isRegex'] = True
@@ -159,30 +159,22 @@ def main(input_args):
 
        # -e, --exact-name is no longer needed. Kept for compatibility.
        # 04/09 djanderson
-       short_opts = "hbc:fe"
-       long_opts = ('help', 'bytes', 'category=', 'full-regex', 'exact-name')
+       short_opts = "hbfe"
+       long_opts = ('help', 'bytes', 'full-regex', 'exact-name')
 
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
 
        parse_module_options(module_opts)
-       
-       if not queries and not QUERY_OPTS["includeInstalled"]:
+
+       if not queries:
                print_help()
                sys.exit(2)
-       elif queries and not QUERY_OPTS["includeInstalled"]:
-               QUERY_OPTS["includeInstalled"] = True
-       elif QUERY_OPTS["includeInstalled"]:
-               queries = ["*"]
-
-       #
-       # Output
-       #
 
        first_run = True
        for query in queries:
@@ -192,8 +184,10 @@ def main(input_args):
                matches = do_lookup(query, QUERY_OPTS)
 
                if not matches:
-                       pp.print_error("No package found matching %s" % query)
+                       sys.stderr.write(pp.error("No package found matching %s" % query))
 
                display_size(matches)
 
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index 56fc78b23acd5dac4c22c4f50724932e4ad15e6c..88a7a879cb4436c86c1e177bacaa184e7a928753 100644 (file)
@@ -16,20 +16,17 @@ __docformat__ = 'epytext'
 # =======
 
 import os
-import re
 import sys
+from functools import partial
 from getopt import gnu_getopt, GetoptError
 from glob import glob
-import xml.etree.cElementTree as ET
 
-from portage.util import unique_array
+from portage import settings
 
-import gentoolkit
 import gentoolkit.pprinter as pp
 from gentoolkit import errors
-from gentoolkit.equery import format_options, mod_usage, Config
-from gentoolkit.helpers2 import compare_package_strings, find_best_match, \
-       find_packages
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import find_best_match, find_packages, uniqify
 from gentoolkit.textwrap_ import TextWrapper
 
 # =======
@@ -44,7 +41,7 @@ QUERY_OPTS = {"allVersions" : False}
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -66,29 +63,29 @@ def display_useflags(output):
 
        @type output: list
        @param output: [(inuse, inused, flag, desc, restrict), ...]
-                inuse (int) = 0 or 1; if 1, flag is set in make.conf
-                inused (int) = 0 or 1; if 1, package is installed with flag enabled
-                flag (str) = the name of the USE flag
-                desc (str) = the flag's description
-                restrict (str) = corresponds to the text of restrict in metadata
+               inuse (int) = 0 or 1; if 1, flag is set in make.conf
+               inused (int) = 0 or 1; if 1, package is installed with flag enabled
+               flag (str) = the name of the USE flag
+               desc (str) = the flag's description
+               restrict (str) = corresponds to the text of restrict in metadata
        """
 
        maxflag_len = len(max([t[2] for t in output], key=len))
 
        twrap = TextWrapper()
-       twrap.width = Config['termWidth']
+       twrap.width = CONFIG['termWidth']
        twrap.subsequent_indent = " " * (maxflag_len + 8)
 
        markers = ("-", "+")
-       color = [pp.useflagoff, pp.useflagon]
+       color = [pp.useflag, partial(pp.useflag, enabled=True)]
        for in_makeconf, in_installed, flag, desc, restrict in output:
-               if Config['verbose']:
+               if CONFIG['verbose']:
                        flag_name = ""
                        if in_makeconf != in_installed:
-                               flag_name += pp.emph(" %s %s" % 
+                               flag_name += pp.emph(" %s %s" %
                                        (markers[in_makeconf], markers[in_installed]))
                        else:
-                               flag_name += (" %s %s" % 
+                               flag_name += (" %s %s" %
                                        (markers[in_makeconf], markers[in_installed]))
 
                        flag_name += " " + color[in_makeconf](flag.ljust(maxflag_len))
@@ -96,7 +93,7 @@ def display_useflags(output):
 
                        # print description
                        if restrict:
-                               restrict = "(%s %s)" % (pp.emph("Restricted to"), 
+                               restrict = "(%s %s)" % (pp.emph("Restricted to"),
                                        pp.cpv(restrict))
                                twrap.initial_indent = flag_name
                                print twrap.fill(restrict)
@@ -128,8 +125,7 @@ def get_global_useflags():
        global_usedesc = {}
        # Get global USE flag descriptions
        try:
-               path = os.path.join(gentoolkit.settings["PORTDIR"], 'profiles', 
-                       'use.desc')
+               path = os.path.join(settings["PORTDIR"], 'profiles', 'use.desc')
                with open(path) as open_file:
                        for line in open_file:
                                if line.startswith('#'):
@@ -139,13 +135,16 @@ def get_global_useflags():
                                if len(fields) == 2:
                                        global_usedesc[fields[0]] = fields[1].rstrip()
        except IOError:
-               pp.print_warn("Could not load USE flag descriptions from %s" %
-                       pp.path(path))
+               sys.stderr.write(
+                       pp.warn(
+                               "Could not load USE flag descriptions from %s" % pp.path(path)
+                       )
+               )
 
        del path, open_file
        # Add USE_EXPANDED variables to usedesc hash -- Bug #238005
-       for path in glob(os.path.join(gentoolkit.settings["PORTDIR"],
-                       'profiles', 'desc', '*.desc')):
+       for path in glob(os.path.join(settings["PORTDIR"],
+               'profiles', 'desc', '*.desc')):
                try:
                        with open(path) as open_file:
                                for line in open_file:
@@ -157,49 +156,13 @@ def get_global_useflags():
                                                        (path.split("/")[-1][0:-5], fields[0])
                                                global_usedesc[expanded_useflag] = fields[1]
                except IOError:
-                       pp.print_warn("Could not load USE flag descriptions from %s" %
-                               path)
+                       sys.stderr.write(
+                               pp.warn("Could not load USE flag descriptions from %s" % path)
+                       )
 
        return global_usedesc
 
 
-def get_local_useflags(pkg):
-       """Parse package-specific flag descriptions from a package's metadata.xml.
-       
-       @see: http://www.gentoo.org/proj/en/glep/glep-0056.html
-       @type pkg: gentoolkit.package.Package
-       @param pkg: the package to find useflags for
-       @rtype: dict
-       @return: {string: tuple}
-               string = flag's name
-               tuple = (description, restrictions)
-       """
-
-       result = {}
-
-       metadata = os.path.join(pkg.get_package_path(), 'metadata.xml')
-       try:
-               xml_tree = ET.parse(metadata)
-       except IOError:
-               pp.print_error("Could not open %s" % metadata)
-               return result
-
-       for node in xml_tree.getiterator("flag"):
-               name = node.get("name")
-               restrict = node.get("restrict")
-               # ElementTree handles nested element text in a funky way.
-               # So we need to dump the raw XML and parse it manually.
-               flagxml = ET.tostring(node)
-               flagxml = re.sub("\s+", " ", flagxml)
-               flagxml = re.sub("\n\t", "", flagxml)
-               flagxml = re.sub("<(pkg|cat)>([^<]*)</(pkg|cat)>",
-                       pp.cpv("%s" % r"\2"), flagxml)
-               flagtext = re.sub("<.*?>", "", flagxml)
-               result[name] = (flagtext, restrict)
-
-       return result
-
-
 def get_matches(query):
        """Get packages matching query."""
 
@@ -208,24 +171,24 @@ def get_matches(query):
                if None in matches:
                        matches = find_packages(query, include_masked=False)
                        if matches:
-                               matches = sorted(matches, compare_package_strings)[-1:]
+                               matches.sort()
        else:
                matches = find_packages(query, include_masked=True)
 
        if not matches:
                raise errors.GentoolkitNoMatches(query)
-       
+
        return matches
 
 
 def get_output_descriptions(pkg, global_usedesc):
        """Prepare descriptions and usage information for each USE flag."""
 
-       local_usedesc = get_local_useflags(pkg)
+       local_usedesc = pkg.metadata.get_useflags()
        iuse = pkg.get_env_var("IUSE")
 
        if iuse:
-               usevar = unique_array([x.lstrip('+-') for x in iuse.split()])
+               usevar = uniqify([x.lstrip('+-') for x in iuse.split()])
                usevar.sort()
        else:
                usevar = []
@@ -233,23 +196,32 @@ def get_output_descriptions(pkg, global_usedesc):
        if pkg.is_installed():
                used_flags = pkg.get_use_flags().split()
        else:
-               used_flags = gentoolkit.settings["USE"].split()
+               used_flags = settings["USE"].split()
 
        # store (inuse, inused, flag, desc, restrict)
        output = []
        for flag in usevar:
                inuse = False
                inused = False
+
+               local_use = None
+               for use in local_usedesc:
+                       if use.name == flag:
+                               local_use = use
+                               break
+
                try:
-                       desc = local_usedesc[flag][0]
-               except KeyError:
+                       desc = local_use.description
+               except AttributeError:
                        try:
                                desc = global_usedesc[flag]
                        except KeyError:
                                desc = ""
+
                try:
-                       restrict = local_usedesc[flag][1]
-               except KeyError:
+                       restrict = local_use.restrict
+                       restrict = restrict if restrict is not None else ""
+               except AttributeError:
                        restrict = ""
 
                if flag in pkg.get_settings("USE").split():
@@ -258,12 +230,12 @@ def get_output_descriptions(pkg, global_usedesc):
                        inused = True
 
                output.append((inuse, inused, flag, desc, restrict))
-       
+
        return output
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        for opt in opts:
@@ -274,13 +246,13 @@ def parse_module_options(module_opts):
                        QUERY_OPTS['allVersions'] = True
 
 
-def print_legend(query):
+def print_legend():
        """Print a legend to explain the output format."""
 
        print "[ Legend : %s - flag is set in make.conf       ]" % pp.emph("U")
        print "[        : %s - package is installed with flag ]" % pp.emph("I")
        print "[ Colors : %s, %s                         ]" % (
-               pp.useflagon("set"), pp.useflagoff("unset"))
+               pp.useflag("set", enabled=True), pp.useflag("unset", enabled=False))
 
 
 def main(input_args):
@@ -292,7 +264,7 @@ def main(input_args):
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
@@ -312,7 +284,7 @@ def main(input_args):
                if not first_run:
                        print
 
-               if Config['verbose']:
+               if CONFIG['verbose']:
                        print " * Searching for %s ..." % pp.pkgquery(query)
 
                matches = get_matches(query)
@@ -323,15 +295,18 @@ def main(input_args):
 
                        output = get_output_descriptions(pkg, global_usedesc)
                        if output:
-                               if Config['verbose']:
-                                       print_legend(query)
+                               if CONFIG['verbose']:
+                                       print_legend()
                                        print (" * Found these USE flags for %s:" %
-                                               pp.cpv(pkg.cpv))
+                                               pp.cpv(str(pkg.cpv)))
                                        print pp.emph(" U I")
                                display_useflags(output)
                        else:
-                               if Config['verbose']:
-                                       pp.print_warn("No USE flags found for %s" %
-                                               pp.cpv(pkg.cpv))
+                               if CONFIG['verbose']:
+                                       sys.stderr.write(
+                                               pp.warn("No USE flags found for %s" % pp.cpv(pkg.cpv))
+                                       )
 
                first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
index 828dae1c6d29ababc8b168657be6c0e35ec81192..ada3fde48d9f257be5c54c0aa501003f54bd4d84 100644 (file)
@@ -21,7 +21,7 @@ from getopt import gnu_getopt, GetoptError
 import gentoolkit.pprinter as pp
 from gentoolkit import errors
 from gentoolkit.equery import format_options, mod_usage
-from gentoolkit.helpers2 import find_packages
+from gentoolkit.helpers import find_packages
 
 # =======
 # Globals
@@ -35,7 +35,7 @@ QUERY_OPTS = {"includeMasked": False}
 
 def print_help(with_description=True):
        """Print description, usage and a detailed help message.
-       
+
        @type with_description: bool
        @param with_description: if true, print module's __doc__ string
        """
@@ -53,7 +53,7 @@ def print_help(with_description=True):
 
 
 def parse_module_options(module_opts):
-       """Parse module options and update GLOBAL_OPTS"""
+       """Parse module options and update QUERY_OPTS"""
 
        opts = (x[0] for x in module_opts)
        for opt in opts:
@@ -73,7 +73,7 @@ def main(input_args):
        try:
                module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
        except GetoptError, err:
-               pp.print_error("Module %s" % err)
+               sys.stderr.write(pp.error("Module %s" % err))
                print
                print_help(with_description=False)
                sys.exit(2)
@@ -93,6 +93,10 @@ def main(input_args):
                        if ebuild_path:
                                print os.path.normpath(ebuild_path)
                        else:
-                               pp.print_warn("No ebuilds to satisfy %s" % pkg.name)
+                               sys.stderr.write(
+                                       pp.warn("No ebuilds to satisfy %s" % pkg.name)
+                               )
                else:
                        raise errors.GentoolkitNoMatches(query)
+
+# vim: set ts=4 sw=4 tw=79:
index 9843b6b007360b9e54a1ab20607ba04f521be81e..988d91c10008f545a9228a5d96721925ef95bcec 100644 (file)
@@ -4,32 +4,25 @@
 
 """Exception classes for gentoolkit"""
 
-__all__ = [
-       'FatalError',
+__all__ = (
        'GentoolkitException',
+       'GentoolkitFatalError',
+       'GentoolkitAmbiguousPackage',
        'GentoolkitInvalidAtom',
        'GentoolkitInvalidCategory',
-       'GentoolkitInvalidPackageName',
+       'GentoolkitInvalidPackage',
        'GentoolkitInvalidCPV',
        'GentoolkitInvalidRegex',
        'GentoolkitInvalidVersion',
        'GentoolkitNoMatches'
-]
-
-# =======
-# Imports
-# =======
-
-import sys
-
-import gentoolkit.pprinter as pp
+)
 
 # ==========
 # Exceptions
 # ==========
 
 class GentoolkitException(Exception):
-       """Base class for gentoolkit exceptions"""
+       """Base class for gentoolkit exceptions."""
        def __init__(self):
                pass
 
@@ -37,64 +30,85 @@ class GentoolkitException(Exception):
 class GentoolkitFatalError(GentoolkitException):
        """A fatal error occurred. Usually used to catch Portage exceptions."""
        def __init__(self, err):
-               pp.print_error("Fatal error: %s" % err)
-               sys.exit(2)
+               self.err = err
+
+       def __str__(self):
+               return "Fatal error: %s" % self.err
+
+
+class GentoolkitAmbiguousPackage(GentoolkitException):
+       """Got an ambiguous package name."""
+       def __init__(self, choices):
+               self.choices = choices
+
+       def __str__(self):
+               choices = '\n'.join("  %s" % x for x in self.choices)
+               return '\n'.join(("Ambiguous package name. Choose from:", choices))
 
 
 class GentoolkitInvalidAtom(GentoolkitException):
-       """Got a malformed package atom"""
+       """Got a malformed package atom."""
        def __init__(self, atom):
-               pp.print_error("Invalid atom: '%s'" % atom)
-               sys.exit(2)
+               self.atom = atom
+
+       def __str__(self):
+               return "Invalid atom: '%s'" % self.atom
 
 
 class GentoolkitInvalidCategory(GentoolkitException):
-       """The category was not listed in portage.settings.categories"""
+       """The category was not listed in portage.settings.categories."""
        def __init__(self, category):
-               pp.print_error("Invalid category: '%s'" % category)
-               if not category:
-                       pp.print_error("Try --category=cat1,cat2 with no spaces.")
-               sys.exit(2)
+               self.category = category
 
+       def __str__(self):
+               return "Invalid category: '%s'" % self.category
 
-class GentoolkitInvalidPackageName(GentoolkitException):
-       """Got an unknown package name"""
+
+class GentoolkitInvalidPackage(GentoolkitException):
+       """Got an unknown or invalid package."""
        def __init__(self, package):
-               pp.print_error("Invalid package name: '%s'" % package)
-               sys.exit(2)
+               self.package = package
+
+       def __str__(self):
+               return "Invalid package: '%s'" % self.package
 
 
 class GentoolkitInvalidCPV(GentoolkitException):
-       """Got an unknown package name"""
+       """Got an invalid category/package-ver string."""
        def __init__(self, cpv):
-               pp.print_error("Invalid CPV: '%s'" % cpv)
-               sys.exit(2)
+               self.cpv = cpv
+
+       def __str__(self):
+               return "Invalid CPV: '%s'" % self.cpv
 
 
 class GentoolkitInvalidRegex(GentoolkitException):
-       """The regex could not be compiled"""
+       """The regex could not be compiled."""
        def __init__(self, regex):
-               pp.print_error("Invalid regex: '%s'" % regex)
-               sys.exit(2)
+               self.regex = regex
+
+       def __str__(self):
+               return "Invalid regex: '%s'" % self.regex
 
 
 class GentoolkitInvalidVersion(GentoolkitException):
-       """Got a malformed version"""
+       """Got a malformed version."""
        def __init__(self, version):
-               pp.print_error("Malformed version: '%s'" % version)
-               sys.exit(2)
+               self.version = version
+
+       def __str__(self):
+               return "Malformed version: '%s'" % self.version
 
 
 class GentoolkitNoMatches(GentoolkitException):
-       """No packages were found matching the search query"""
-       def __init__(self, query):
-               pp.print_error("No packages matching '%s'" % query)
-               sys.exit(2)
-
-
-# XXX: Deprecated
-class FatalError:
-       def __init__(self, s):
-               self._message = s
-       def get_message(self):
-               return self._message
+       """No packages were found matching the search query."""
+       def __init__(self, query, in_installed=False):
+               self.query = query
+               self.in_installed = in_installed
+
+       def __str__(self):
+               inst = 'installed ' if self.in_installed else ''
+               return "No %spackages matching '%s'" % (inst, self.query)
+
+
+# vim: set ts=4 sw=4 tw=79:
index 2efb008ed4bbc4e0a0ce4dd0552b98ef6c2d2e41..11b7dbee0c511c9978e80a896abb705cd25e89f2 100644 (file)
@@ -33,7 +33,7 @@ except ImportError:
        import portage
 
 # Note: the space for rgt and rlt is important !!
-opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=", 
+opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=",
                         "rge": ">=~", "rle": "<=~", "rgt": " >~", "rlt": " <~"}
 NEWLINE_ESCAPE = "!;\\n"       # some random string to mark newlines that should be preserved
 SPACE_ESCAPE = "!;_"           # some random string to mark spaces that should be preserved
@@ -42,7 +42,7 @@ def center(text, width):
        """
        Returns a string containing I{text} that is padded with spaces on both
        sides. If C{len(text) >= width} I{text} is returned unchanged.
-       
+
        @type   text: String
        @param  text: the text to be embedded
        @type   width: Integer
@@ -65,15 +65,15 @@ def center(text, width):
 def wrap(text, width, caption=""):
        """
        Wraps the given text at column I{width}, optionally indenting
-       it so that no text is under I{caption}. It's possible to encode 
+       it so that no text is under I{caption}. It's possible to encode
        hard linebreaks in I{text} with L{NEWLINE_ESCAPE}.
-       
+
        @type   text: String
        @param  text: the text to be wrapped
        @type   width: Integer
        @param  width: the column at which the text should be wrapped
        @type   caption: String
-       @param  caption: this string is inserted at the beginning of the 
+       @param  caption: this string is inserted at the beginning of the
                                         return value and the paragraph is indented up to
                                         C{len(caption)}.
        @rtype:         String
@@ -84,7 +84,7 @@ def wrap(text, width, caption=""):
        text = text.replace(2*NEWLINE_ESCAPE, NEWLINE_ESCAPE+" "+NEWLINE_ESCAPE)
        words = text.split()
        indentLevel = len(caption)+1
-       
+
        for w in words:
                if line[-1] == "\n":
                        rValue += line
@@ -132,13 +132,13 @@ def get_glsa_list(repository, myconfig):
        Returns a list of all available GLSAs in the given repository
        by comparing the filelist there with the pattern described in
        the config.
-       
+
        @type   repository: String
        @param  repository: The directory or an URL that contains GLSA files
                                                (Note: not implemented yet)
        @type   myconfig: portage.config
        @param  myconfig: a GLSA aware config instance (see L{checkconfig})
-       
+
        @rtype:         List of Strings
        @return:        a list of GLSA IDs in this repository
        """
@@ -151,7 +151,7 @@ def get_glsa_list(repository, myconfig):
        dirlist = os.listdir(repository)
        prefix = myconfig["GLSA_PREFIX"]
        suffix = myconfig["GLSA_SUFFIX"]
-       
+
        for f in dirlist:
                try:
                        if f[:len(prefix)] == prefix and f[-1*len(suffix):] == suffix:
@@ -163,7 +163,7 @@ def get_glsa_list(repository, myconfig):
 def getListElements(listnode):
        """
        Get all <li> elements for a given <ol> or <ul> node.
-       
+
        @type   listnode: xml.dom.Node
        @param  listnode: <ul> or <ol> list to get the elements for
        @rtype:         List of Strings
@@ -184,7 +184,7 @@ def getText(node, format, textfd = None):
        parameter the text might be formatted by adding/removing newlines,
        tabs and spaces. This function is only useful for the GLSA DTD,
        it's not applicable for other DTDs.
-       
+
        @type   node: xml.dom.Node
        @param  node: the root node to start with the parsing
        @type   format: String
@@ -251,7 +251,7 @@ def getMultiTagsText(rootnode, tagname, format):
        """
        Returns a list with the text of all subnodes of type I{tagname}
        under I{rootnode} (which itself is not parsed) using the given I{format}.
-       
+
        @type   rootnode: xml.dom.Node
        @param  rootnode: the node to search for I{tagname}
        @type   tagname: String
@@ -267,9 +267,9 @@ def getMultiTagsText(rootnode, tagname, format):
 
 def makeAtom(pkgname, versionNode):
        """
-       creates from the given package name and information in the 
+       creates from the given package name and information in the
        I{versionNode} a (syntactical) valid portage atom.
-       
+
        @type   pkgname: String
        @param  pkgname: the name of the package for this atom
        @type   versionNode: xml.dom.Node
@@ -292,9 +292,9 @@ def makeAtom(pkgname, versionNode):
 
 def makeVersion(versionNode):
        """
-       creates from the information in the I{versionNode} a 
+       creates from the information in the I{versionNode} a
        version string (format <op><version>).
-       
+
        @type   versionNode: xml.dom.Node
        @param  versionNode: a <vulnerable> or <unaffected> Node that
                                                 contains the version information for this atom
@@ -314,17 +314,17 @@ def makeVersion(versionNode):
 
 def match(atom, portdbname, match_type="default"):
        """
-       wrapper that calls revisionMatch() or portage.dbapi.match() depending on 
+       wrapper that calls revisionMatch() or portage.dbapi.match() depending on
        the given atom.
-       
+
        @type   atom: string
        @param  atom: a <~ or >~ atom or a normal portage atom that contains the atom to match against
        @type   portdb: portage.dbapi
        @param  portdb: one of the portage databases to use as information source
        @type   match_type: string
-       @param  match_type: if != "default" passed as first argument to dbapi.xmatch 
+       @param  match_type: if != "default" passed as first argument to dbapi.xmatch
                                to apply the wanted visibility filters
-       
+
        @rtype:         list of strings
        @return:        a list with the matching versions
        """
@@ -341,15 +341,15 @@ def revisionMatch(revisionAtom, portdb, match_type="default"):
        handler for the special >~, >=~, <=~ and <~ atoms that are supposed to behave
        as > and < except that they are limited to the same version, the range only
        applies to the revision part.
-       
+
        @type   revisionAtom: string
        @param  revisionAtom: a <~ or >~ atom that contains the atom to match against
        @type   portdb: portage.dbapi
        @param  portdb: one of the portage databases to use as information source
        @type   match_type: string
-       @param  match_type: if != "default" passed as first argument to portdb.xmatch 
+       @param  match_type: if != "default" passed as first argument to portdb.xmatch
                                to apply the wanted visibility filters
-       
+
        @rtype:         list of strings
        @return:        a list with the matching versions
        """
@@ -370,35 +370,34 @@ def revisionMatch(revisionAtom, portdb, match_type="default"):
                if eval(r1+" "+revisionAtom[0:2]+" "+r2):
                        rValue.append(v)
        return rValue
-               
+
 
 def getMinUpgrade(vulnerableList, unaffectedList, minimize=True):
        """
-       Checks if the state of installed packages matches an atom in
-       I{vulnerableList} and returns an update path.
-
-        Return value is:
-         * None if the system is not affected
-         * a list of tuples (a,b) where
-                  a  is a cpv describing an installed vulnerable atom
-                  b  is a cpv describing an uninstalled unaffected atom
-                       in the same slot as a
-                     OR the empty string ("") which means no upgrade
-                       is possible
-       
+       Checks if the systemstate is matching an atom in
+       I{vulnerableList} and returns string describing
+       the lowest version for the package that matches an atom in
+       I{unaffectedList} and is greater than the currently installed
+       version. It will return an empty list if the system is affected,
+       and no upgrade is possible or None if the system is not affected.
+       Both I{vulnerableList} and I{unaffectedList} should have the
+       same base package.
+
        @type   vulnerableList: List of Strings
        @param  vulnerableList: atoms matching vulnerable package versions
        @type   unaffectedList: List of Strings
        @param  unaffectedList: atoms matching unaffected package versions
        @type   minimize:       Boolean
        @param  minimize:       True for a least-change upgrade, False for emerge-like algorithm
-       
-       @rtype:         List | None
-       @return:        None if unaffected or a list of (vuln, upgrade) atoms.
+
+       @rtype:         String | None
+       @return:        the lowest unaffected version that is greater than
+                               the installed version.
        """
+       rValue = ""
        v_installed = reduce(operator.add, [match(v, "vartree") for v in vulnerableList], [])
        u_installed = reduce(operator.add, [match(u, "vartree") for u in unaffectedList], [])
-       
+
        # remove all unaffected atoms from vulnerable list
        v_installed = list(set(v_installed).difference(set(u_installed)))
 
@@ -417,18 +416,13 @@ def getMinUpgrade(vulnerableList, unaffectedList, minimize=True):
 
        for vuln in v_installed:
                update = ""
-               # find the best update path for the vuln atom
                for c in avail_updates:
                        c_pv = portage.catpkgsplit(c)
                        i_pv = portage.catpkgsplit(vuln)
-                       if portage.pkgcmp(c_pv[1:], i_pv[1:]) <= 0:
-                               # c is less or equal than vuln
-                               continue
-                       if portage.db["/"]["porttree"].dbapi.aux_get(c, ["SLOT"]) != \
-                          portage.db["/"]["vartree"].dbapi.aux_get(vuln, ["SLOT"]):
-                               # upgrade to a different slot
-                               continue
-                       if update == "" or (minimize ^ (portage.pkgcmp(c_pv[1:], portage.catpkgsplit(update)[1:]) > 0)):
+                       if portage.pkgcmp(c_pv[1:], i_pv[1:]) > 0 \
+                                       and (update == "" \
+                                               or (minimize ^ (portage.pkgcmp(c_pv[1:], portage.catpkgsplit(update)[1:]) > 0))) \
+                                       and portage.db["/"]["porttree"].dbapi.aux_get(c, ["SLOT"]) == portage.db["/"]["vartree"].dbapi.aux_get(vuln, ["SLOT"]):
                                update = c_pv[0]+"/"+c_pv[1]+"-"+c_pv[2]
                                if c_pv[3] != "r0":             # we don't like -r0 for display
                                        update += "-"+c_pv[3]
@@ -440,7 +434,7 @@ def format_date(datestr):
        """
        Takes a date (announced, revised) date from a GLSA and formats
        it as readable text (i.e. "January 1, 2008").
-       
+
        @type   date: String
        @param  date: the date string to reformat
        @rtype:         String
@@ -450,16 +444,16 @@ def format_date(datestr):
        splitdate = datestr.split("-", 2)
        if len(splitdate) != 3:
                return datestr
-       
+
        # This cannot raise an error as we use () instead of []
        splitdate = (int(x) for x in splitdate)
-       
+
        from datetime import date
        try:
                d = date(*splitdate)
        except ValueError:
                return datestr
-       
+
        # TODO We could format to local date format '%x' here?
        return d.strftime("%B %d, %Y")
 
@@ -470,7 +464,7 @@ class GlsaTypeException(Exception):
 
 class GlsaFormatException(Exception):
        pass
-                               
+
 class GlsaArgumentException(Exception):
        pass
 
@@ -482,9 +476,9 @@ class Glsa:
        """
        def __init__(self, myid, myconfig):
                """
-               Simple constructor to set the ID, store the config and gets the 
+               Simple constructor to set the ID, store the config and gets the
                XML data by calling C{self.read()}.
-               
+
                @type   myid: String
                @param  myid: String describing the id for the GLSA object (standard
                                          GLSAs have an ID of the form YYYYMM-nn) or an existing
@@ -506,7 +500,7 @@ class Glsa:
                """
                Here we build the filename from the config and the ID and pass
                it to urllib to fetch it from the filesystem or a remote server.
-               
+
                @rtype:         None
                @return:        None
                """
@@ -523,10 +517,10 @@ class Glsa:
 
        def parse(self, myfile):
                """
-               This method parses the XML file and sets up the internal data 
+               This method parses the XML file and sets up the internal data
                structures by calling the different helper functions in this
                module.
-               
+
                @type   myfile: String
                @param  myfile: Filename to grab the XML data from
                @rtype:         None
@@ -549,7 +543,7 @@ class Glsa:
                self.title = getText(myroot.getElementsByTagName("title")[0], format="strip")
                self.synopsis = getText(myroot.getElementsByTagName("synopsis")[0], format="strip")
                self.announced = format_date(getText(myroot.getElementsByTagName("announced")[0], format="strip"))
-               
+
                count = 1
                # Support both formats of revised:
                # <revised>December 30, 2007: 02</revised>
@@ -560,15 +554,15 @@ class Glsa:
                        count = revisedEl.getAttribute("count")
                elif (self.revised.find(":") >= 0):
                        (self.revised, count) = self.revised.split(":")
-               
+
                self.revised = format_date(self.revised)
-               
+
                try:
                        self.count = int(count)
                except ValueError:
                        # TODO should this rais a GlsaFormatException?
                        self.count = 1
-               
+
                # now the optional and 0-n toplevel, #PCDATA tags and references
                try:
                        self.access = getText(myroot.getElementsByTagName("access")[0], format="strip")
@@ -576,7 +570,7 @@ class Glsa:
                        self.access = ""
                self.bugs = getMultiTagsText(myroot, "bug", format="strip")
                self.references = getMultiTagsText(myroot.getElementsByTagName("references")[0], "uri", format="keep")
-               
+
                # and now the formatted text elements
                self.description = getText(myroot.getElementsByTagName("description")[0], format="xml")
                self.workaround = getText(myroot.getElementsByTagName("workaround")[0], format="xml")
@@ -586,7 +580,7 @@ class Glsa:
                try:
                        self.background = getText(myroot.getElementsByTagName("background")[0], format="xml")
                except IndexError:
-                       self.background = ""                                    
+                       self.background = ""
 
                # finally the interesting tags (product, affected, package)
                self.glsatype = myroot.getElementsByTagName("product")[0].getAttribute("type")
@@ -611,10 +605,10 @@ class Glsa:
 
        def dump(self, outstream=sys.stdout, encoding="utf-8"):
                """
-               Dumps a plaintext representation of this GLSA to I{outfile} or 
+               Dumps a plaintext representation of this GLSA to I{outfile} or
                B{stdout} if it is ommitted. You can specify an alternate
                I{encoding} if needed (default is utf-8).
-               
+
                @type   outstream: File
                @param  outfile: Stream that should be used for writing
                                                 (defaults to sys.stdout)
@@ -655,13 +649,13 @@ class Glsa:
                myreferences = " ".join(r.replace(" ", SPACE_ESCAPE)+NEWLINE_ESCAPE for r in self.references)
                outstream.write("\n"+wrap(myreferences, width, caption="References:       "))
                outstream.write("\n")
-       
+
        def isVulnerable(self):
                """
                Tests if the system is affected by this GLSA by checking if any
                vulnerable package versions are installed. Also checks for affected
                architectures.
-               
+
                @rtype:         Boolean
                @returns:       True if the system is affected, False if not
                """
@@ -674,12 +668,12 @@ class Glsa:
                                                rValue = rValue \
                                                        or (None != getMinUpgrade([v,], path["unaff_atoms"]))
                return rValue
-       
+
        def isInjected(self):
                """
                Looks if the GLSA ID is in the GLSA checkfile to check if this
                GLSA should be marked as applied.
-               
+
                @rtype:         Boolean
                @returns:       True if the GLSA is in the inject file, False if not
                """
@@ -691,7 +685,7 @@ class Glsa:
        def inject(self):
                """
                Puts the ID of this GLSA into the GLSA checkfile, so it won't
-               show up on future checks. Should be called after a GLSA is 
+               show up on future checks. Should be called after a GLSA is
                applied or on explicit user request.
 
                @rtype:         None
@@ -702,13 +696,13 @@ class Glsa:
                        checkfile.write(self.nr+"\n")
                        checkfile.close()
                return None
-       
+
        def getMergeList(self, least_change=True):
                """
                Returns the list of package-versions that have to be merged to
-               apply this GLSA properly. The versions are as low as possible 
+               apply this GLSA properly. The versions are as low as possible
                while avoiding downgrades (see L{getMinUpgrade}).
-               
+
                @type   least_change: Boolean
                @param  least_change: True if the smallest possible upgrade should be selected,
                                        False for an emerge-like algorithm
index 6d272d3ae61dfa44c0a961d2c277f1bf079ee771..277e41fd62c5a54a51be8d8cdec75011351e9742 100644 (file)
-#!/usr/bin/python2
-#
-# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
 # Copyright(c) 2009, Gentoo Foundation
 #
-# Licensed under the GNU General Public License, v2
+# Licensed under the GNU General Public License, v2 or higher
 #
 # $Header$
 
+"""Improved versions of the original helpers functions.
+
+As a convention, functions ending in '_packages' or '_match{es}' return
+Package objects, while functions ending in 'cpvs' return a sequence of strings.
+Functions starting with 'get_' return a set of packages by default and can be
+filtered, while functions starting with 'find_' return nothing unless the
+query matches one or more packages.
+"""
+
+# Move to Imports section after Python 2.6 is stable
+from __future__ import with_statement
+
+__all__ = (
+       'ChangeLog',
+       'FileOwner',
+       'compare_package_strings',
+       'do_lookup',
+       'find_best_match',
+       'find_installed_packages',
+       'find_packages',
+       'get_cpvs',
+       'get_installed_cpvs',
+       'get_uninstalled_cpvs',
+       'uniqify',
+       'uses_globbing',
+       'split_cpv'
+)
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import fnmatch
+import os
+import re
+from functools import partial
+from itertools import chain
+
 import portage
-from gentoolkit import *
-from package import *
-from pprinter import print_warn
-try:
-       from portage.util import unique_array
-except ImportError:
-       from portage_util import unique_array
-
-def find_packages(search_key, masked=False):
-       """Returns a list of Package objects that matched the search key."""
-       try:
-               if masked:
-                       t = portage.db["/"]["porttree"].dbapi.xmatch("match-all", search_key)
-                       t += portage.db["/"]["vartree"].dbapi.match(search_key)
+from portage.versions import catpkgsplit, pkgcmp
+
+from gentoolkit import pprinter as pp
+from gentoolkit import CONFIG
+from gentoolkit import errors
+from gentoolkit.atom import Atom
+from gentoolkit.cpv import CPV
+from gentoolkit.dbapi import PORTDB, VARDB
+from gentoolkit.versionmatch import VersionMatch
+# This has to be imported below to stop circular import.
+#from gentoolkit.package import Package
+
+# =======
+# Classes
+# =======
+
+class ChangeLog(object):
+       """Provides methods for working with a Gentoo ChangeLog file.
+
+       Example usage:
+               >>> from gentoolkit.helpers import ChangeLog
+               >>> portage = ChangeLog('/usr/portage/sys-apps/portage/ChangeLog')
+               >>> print portage.latest.strip()
+               *portage-2.2_rc50 (15 Nov 2009)
+
+                 15 Nov 2009; Zac Medico <zmedico@gentoo.org> +portage-2.2_rc50.ebuild:
+                 2.2_rc50 bump. This includes all fixes in 2.1.7.5.
+               >>> len(portage.full)
+               75
+               >>> len(portage.entries_matching_range(
+               ...     from_ver='2.2_rc40',
+               ...     to_ver='2.2_rc50'))
+               11
+
+       """
+       def __init__(self, changelog_path, invalid_entry_is_fatal=False):
+               if not (os.path.isfile(changelog_path) and
+                       os.access(changelog_path, os.R_OK)):
+                       raise errors.GentoolkitFatalError(
+                               "%s does not exist or is unreadable" % pp.path(changelog_path)
+                       )
+               self.changelog_path = changelog_path
+               self.invalid_entry_is_fatal = invalid_entry_is_fatal
+
+               # Process the ChangeLog:
+               self.entries = self._split_changelog()
+               self.indexed_entries = self._index_changelog()
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, self.changelog_path)
+
+       @property
+       def full(self):
+               """Return the output of L{self._split_changelog}."""
+               return self.entries
+
+       @property
+       def latest(self):
+               """Return the newest ChangeLog entry."""
+               return self.entries[0]
+
+       def entries_matching_atom(self, atom):
+               """Return entries whose header versions match atom's version.
+
+               @type atom: L{gentoolkit.atom.Atom} or str
+               @param atom: a atom to find matching entries against
+               @rtype: list
+               @return: entries matching atom
+               @raise errors.GentoolkitInvalidAtom: if atom is a string and malformed
+               """
+               result = []
+
+               if not isinstance(atom, Atom):
+                       atom = Atom(atom)
+
+               for entry_set in self.indexed_entries:
+                       i, entry = entry_set
+                       # VersionMatch doesn't store .cp, so we'll force it to match here:
+                       i.cpv.cp = atom.cpv.cp
+                       if atom.intersects(i):
+                               result.append(entry)
+
+               return result
+
+       def entries_matching_range(self, from_ver=None, to_ver=None):
+               """Return entries whose header versions are within a range of versions.
+
+               @type from_ver: str
+               @param from_ver: valid Gentoo version
+               @type to_ver: str
+               @param to_ver: valid Gentoo version
+               @rtype: list
+               @return: entries between from_ver and to_ver
+               @raise errors.GentoolkitFatalError: if neither vers are set
+               @raise errors.GentoolkitInvalidVersion: if either ver is invalid
+               """
+               result = []
+
+               # Make sure we have at least one version set
+               if not (from_ver or to_ver):
+                       raise errors.GentoolkitFatalError(
+                               "Need to specifiy 'from_ver' or 'to_ver'"
+                       )
+
+               # Create a VersionMatch instance out of from_ver
+               from_restriction = None
+               if from_ver:
+                       try:
+                               from_ver_rev = CPV("null-%s" % from_ver)
+                       except errors.GentoolkitInvalidCPV:
+                               raise errors.GentoolkitInvalidVersion(from_ver)
+                       from_restriction = VersionMatch(from_ver_rev, op='>=')
+
+               # Create a VersionMatch instance out of to_ver
+               to_restriction = None
+               if to_ver:
+                       try:
+                               to_ver_rev = CPV("null-%s" % to_ver)
+                       except errors.GentoolkitInvalidCPV:
+                               raise errors.GentoolkitInvalidVersion(to_ver)
+                       to_restriction = VersionMatch(to_ver_rev, op='<=')
+
+               # Add entry to result if version ranges intersect it
+               for entry_set in self.indexed_entries:
+                       i, entry = entry_set
+                       if from_restriction and not from_restriction.match(i):
+                               continue
+                       if to_restriction and not to_restriction.match(i):
+                               continue
+                       result.append(entry)
+
+               return result
+
+       def _index_changelog(self):
+               """Use the output of L{self._split_changelog} to create an index list
+               of L{gentoolkit.versionmatch.VersionMatch} objects.
+
+               @rtype: list
+               @return: tuples containing a VersionMatch instance for the release
+                       version of each entry header as the first item and the entire entry
+                       as the second item
+               @raise ValueError: if self.invalid_entry_is_fatal is True and we hit an
+                       invalid entry
+               """
+
+               result = []
+               for entry in self.entries:
+                       # Extract the package name from the entry header, ex:
+                       # *xterm-242 (07 Mar 2009) => xterm-242
+                       pkg_name = entry.split(' ', 1)[0].lstrip('*')
+                       if not pkg_name.strip():
+                               continue
+                       try:
+                               entry_ver = CPV(pkg_name)
+                       except errors.GentoolkitInvalidCPV:
+                               if self.invalid_entry_is_fatal:
+                                       raise ValueError(entry_ver)
+                               continue
+
+                       result.append((VersionMatch(entry_ver, op='='), entry))
+
+               return result
+
+       def _split_changelog(self):
+               """Split the ChangeLog into individual entries.
+
+               @rtype: list
+               @return: individual ChangeLog entries
+               """
+
+               result = []
+               partial_entries = []
+               with open(self.changelog_path) as log:
+                       for line in log:
+                               if line.startswith('#'):
+                                       continue
+                               elif line.startswith('*'):
+                                       # Append last entry to result...
+                                       entry = ''.join(partial_entries)
+                                       if entry and not entry.isspace():
+                                               result.append(entry)
+                                       # ... and start a new entry
+                                       partial_entries = [line]
+                               else:
+                                       partial_entries.append(line)
+                       else:
+                               # Append the final entry
+                               entry = ''.join(partial_entries)
+                               result.append(entry)
+
+               return result
+
+
+class FileOwner(object):
+       """Creates a function for locating the owner of filename queries.
+
+       Example usage:
+               >>> from gentoolkit.helpers import FileOwner
+               >>> findowner = FileOwner()
+               >>> findowner(('/usr/bin/vim',))
+               [(<Package app-editors/vim-7.2.182>, '/usr/bin/vim')]
+       """
+       def __init__(self, is_regex=False, early_out=False, printer_fn=None):
+               """Instantiate function.
+
+               @type is_regex: bool
+               @param is_regex: funtion args are regular expressions
+               @type early_out: bool
+               @param early_out: return when first result is found (safe)
+               @type printer_fn: callable
+               @param printer_fn: If defined, will be passed useful information for
+                       printing each result as it is found.
+               """
+               self.is_regex = is_regex
+               self.early_out = early_out
+               self.printer_fn = printer_fn
+
+       def __call__(self, queries):
+               """Run the function.
+
+               @type queries: iterable
+               @param queries: filepaths or filepath regexes
+               """
+               query_re_string = self._prepare_search_regex(queries)
+               try:
+                       query_re = re.compile(query_re_string)
+               except (TypeError, re.error), err:
+                       raise errors.GentoolkitInvalidRegex(err)
+
+               use_match = False
+               if ((self.is_regex or query_re_string.startswith('^\/'))
+                       and '|' not in query_re_string ):
+                       # If we were passed a regex or a single path starting with root,
+                       # we can use re.match, else use re.search.
+                       use_match = True
+
+               return self.find_owners(query_re, use_match=use_match)
+
+       def find_owners(self, query_re, use_match=False, pkgset=None):
+               """Find owners and feed data to supplied output function.
+
+               @type query_re: _sre.SRE_Pattern
+               @param query_re: file regex
+               @type use_match: bool
+               @param use_match: use re.match or re.search
+               @type pkgset: iterable or None
+               @param pkgset: list of packages to look through
+               """
+               # FIXME: Remove when lazyimport supports objects:
+               from gentoolkit.package import Package
+
+               if use_match:
+                       query_fn = query_re.match
+               else:
+                       query_fn = query_re.search
+
+               results = []
+               found_match = False
+               if pkgset is None:
+                       pkgset = get_installed_cpvs()
+               for pkg in sorted([Package(x) for x in pkgset]):
+                       files = pkg.get_contents()
+                       for cfile in files:
+                               match = query_fn(cfile)
+                               if match:
+                                       results.append((pkg, cfile))
+                                       if self.printer_fn is not None:
+                                               self.printer_fn(pkg, cfile)
+                                       if self.early_out:
+                                               found_match = True
+                                               break
+                       if found_match:
+                               break
+               return results
+
+       @staticmethod
+       def extend_realpaths(paths):
+               """Extend a list of paths with the realpaths for any symlinks.
+
+               @type paths: list
+               @param paths: file path strs
+               @rtype: list
+               @return: the original list plus the realpaths for any symlinks
+                       so long as the realpath doesn't already exist in the list
+               @raise AttributeError: if paths does not have attribute 'extend'
+               """
+
+               osp = os.path
+               paths.extend([osp.realpath(x) for x in paths
+                       if osp.islink(x) and osp.realpath(x) not in paths])
+
+               return paths
+
+       def _prepare_search_regex(self, queries):
+               """Create a regex out of the queries"""
+
+               queries = list(queries)
+               if self.is_regex:
+                       return '|'.join(queries)
                else:
-                       t = portage.db["/"]["porttree"].dbapi.match(search_key)
-                       t += portage.db["/"]["vartree"].dbapi.match(search_key)
-       # catch the "amgigous package" Exception
-       except ValueError, e:
-               if isinstance(e[0],list):
-                       t = []
-                       for cp in e[0]:
-                               if masked:
-                                       t += portage.db["/"]["porttree"].dbapi.xmatch("match-all", cp)
-                                       t += portage.db["/"]["vartree"].dbapi.match(cp)
+                       result = []
+                       # Trim trailing and multiple slashes from queries
+                       slashes = re.compile('/+')
+                       queries = self.extend_realpaths(queries)
+                       for query in queries:
+                               query = slashes.sub('/', query).rstrip('/')
+                               if query.startswith('/'):
+                                       query = "^%s$" % re.escape(query)
                                else:
-                                       t += portage.db["/"]["porttree"].dbapi.match(cp)
-                                       t += portage.db["/"]["vartree"].dbapi.match(cp)
+                                       query = "/%s$" % re.escape(query)
+                               result.append(query)
+               result = "|".join(result)
+               return result
+
+# =========
+# Functions
+# =========
+
+def compare_package_strings(pkg1, pkg2):
+       """Similar to the builtin cmp, but for package strings. Usually called
+       as: package_list.sort(compare_package_strings)
+
+       An alternative is to use the Package descriptor from gentoolkit.package
+       >>> pkgs = [Package(x) for x in package_list]
+       >>> pkgs.sort()
+
+       @see: >>> help(cmp)
+       """
+
+       pkg1 = catpkgsplit(pkg1)
+       pkg2 = catpkgsplit(pkg2)
+       if pkg1[0] != pkg2[0]:
+               return cmp(pkg1[0], pkg2[0])
+       elif pkg1[1] != pkg2[1]:
+               return cmp(pkg1[1], pkg2[1])
+       else:
+               return pkgcmp(pkg1[1:], pkg2[1:])
+
+
+def do_lookup(query, query_opts):
+       """A high-level wrapper around gentoolkit package-finder functions.
+
+       @type query: str
+       @param query: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom, glob or regex
+       @type query_opts: dict
+       @param query_opts: user-configurable options from the calling module
+               Currently supported options are:
+
+               includeInstalled   = bool
+               includePortTree    = bool
+               includeOverlayTree = bool
+               isRegex            = bool
+               printMatchInfo     = bool           # Print info about the search
+
+       @rtype: list
+       @return: Package objects matching query
+       """
+
+       if query_opts["includeInstalled"]:
+               if query_opts["includePortTree"] or query_opts["includeOverlayTree"]:
+                       simple_package_finder = partial(find_packages, include_masked=True)
+                       complex_package_finder = get_cpvs
                else:
-                       raise ValueError(e)
-       except portage_exception.InvalidAtom, e:
-               print_warn("Invalid Atom: '%s'" % str(e))
+                       simple_package_finder = find_installed_packages
+                       complex_package_finder = get_installed_cpvs
+       elif query_opts["includePortTree"] or query_opts["includeOverlayTree"]:
+               simple_package_finder = partial(find_packages, include_masked=True)
+               complex_package_finder = get_uninstalled_cpvs
+       else:
+               raise errors.GentoolkitFatalError(
+                       "Not searching in installed, Portage tree, or overlay. "
+                       "Nothing to do."
+               )
+
+       is_simple_query = True
+       if query_opts["isRegex"] or uses_globbing(query):
+               is_simple_query = False
+
+       if is_simple_query:
+               matches = _do_simple_lookup(query, simple_package_finder, query_opts)
+       else:
+               matches = _do_complex_lookup(query, complex_package_finder, query_opts)
+
+       return matches
+
+
+def _do_complex_lookup(query, package_finder, query_opts):
+       """Find matches for a query which is a regex or includes globbing."""
+
+       # FIXME: Remove when lazyimport supports objects:
+       from gentoolkit.package import Package
+
+       result = []
+
+       if query_opts["printMatchInfo"] and not CONFIG["piping"]:
+               print_query_info(query, query_opts)
+
+       cat = split_cpv(query)[0]
+
+       pre_filter = []
+       # The "get_" functions can pre-filter against the whole package key,
+       # but since we allow globbing now, we run into issues like:
+       # >>> portage.dep.dep_getkey("sys-apps/portage-*")
+       # 'sys-apps/portage-'
+       # So the only way to guarantee we don't overrun the key is to
+       # prefilter by cat only.
+       if cat:
+               if query_opts["isRegex"]:
+                       cat_re = cat
+               else:
+                       cat_re = fnmatch.translate(cat)
+                       # [::-1] reverses a sequence, so we're emulating an ".rreplace()"
+                       # except we have to put our "new" string on backwards
+                       cat_re = cat_re[::-1].replace('$', '*./', 1)[::-1]
+               predicate = lambda x: re.match(cat_re, x)
+               pre_filter = package_finder(predicate=predicate)
+
+       # Post-filter
+       if query_opts["isRegex"]:
+               predicate = lambda x: re.search(query, x)
+       else:
+               if cat:
+                       query_re = fnmatch.translate(query)
+               else:
+                       query_re = fnmatch.translate("*/%s" % query)
+               predicate = lambda x: re.search(query_re, x)
+       if pre_filter:
+               result = [x for x in pre_filter if predicate(x)]
+       else:
+               result = package_finder(predicate=predicate)
+
+       return [Package(x) for x in result]
+
+
+def _do_simple_lookup(query, package_finder, query_opts):
+       """Find matches for a query which is an atom or string."""
+
+       result = []
+
+       if query_opts["printMatchInfo"] and CONFIG['verbose']:
+               print_query_info(query, query_opts)
+
+       result = package_finder(query)
+       if not query_opts["includeInstalled"]:
+               result = [x for x in result if not x.is_installed()]
+
+       return result
+
+
+def find_best_match(query):
+       """Return the highest unmasked version of a package matching query.
+
+       @type query: str
+       @param query: can be of the form: pkg, pkg-ver, cat/pkg, cat/pkg-ver, atom
+       @rtype: str or None
+       @raise portage.exception.InvalidAtom: if query is not valid input
+       """
+       # FIXME: Remove when lazyimport supports objects:
+       from gentoolkit.package import Package
+
+       try:
+               match = PORTDB.xmatch("bestmatch-visible", query)
+       except portage.exception.InvalidAtom, err:
+               raise errors.GentoolkitInvalidAtom(err)
+
+       return Package(match) if match else None
+
+
+def find_installed_packages(query):
+       """Return a list of Package objects that matched the search key."""
+       # FIXME: Remove when lazyimport supports objects:
+       from gentoolkit.package import Package
+
+       try:
+               matches = VARDB.match(query)
+       # catch the ambiguous package Exception
+       except portage.exception.AmbiguousPackageName, err:
+               matches = []
+               for pkgkey in err[0]:
+                       matches.extend(VARDB.match(pkgkey))
+       except portage.exception.InvalidAtom, err:
+               raise errors.GentoolkitInvalidAtom(err)
+
+       return [Package(x) for x in matches]
+
+
+def find_packages(query, include_masked=False):
+       """Returns a list of Package objects that matched the query.
+
+       @type query: str
+       @param query: can be of the form: pkg, pkg-ver, cat/pkg, cat/pkg-ver, atom
+       @type include_masked: bool
+       @param include_masked: include masked packages
+       @rtype: list
+       @return: matching Package objects
+       """
+       # FIXME: Remove when lazyimport supports objects:
+       from gentoolkit.package import Package
+
+       if not query:
                return []
-       # Make the list of packages unique
-       t = unique_array(t)
-       t.sort()
-       return [Package(x) for x in t]
 
-def find_installed_packages(search_key, masked=False):
-       """Returns a list of Package objects that matched the search key."""
        try:
-                       t = portage.db["/"]["vartree"].dbapi.match(search_key)
-       # catch the "amgigous package" Exception
-       except ValueError, e:
-               if isinstance(e[0],list):
-                       t = []
-                       for cp in e[0]:
-                               t += portage.db["/"]["vartree"].dbapi.match(cp)
+               if include_masked:
+                       matches = PORTDB.xmatch("match-all", query)
                else:
-                       raise ValueError(e)
-       except portage_exception.InvalidAtom, e:
-               print_warn("Invalid Atom: '%s'" % str(e))
-               return []
-       return [Package(x) for x in t]
-
-def find_best_match(search_key):
-       """Returns a Package object for the best available candidate that
-       matched the search key."""
-       t = portage.db["/"]["porttree"].dep_bestmatch(search_key)
-       if t:
-               return Package(t)
-       return None
-
-def find_system_packages(prefilter=None):
-       """Returns a tuple of lists, first list is resolved system packages,
-       second is a list of unresolved packages."""
-       pkglist = settings.packages
-       resolved = []
-       unresolved = []
-       for x in pkglist:
-               cpv = x.strip()
-               if len(cpv) and cpv[0] == "*":
-                       pkg = find_best_match(cpv)
-                       if pkg:
-                               resolved.append(pkg)
-                       else:
-                               unresolved.append(cpv)
-       return (resolved, unresolved)
-
-def find_world_packages(prefilter=None):
-       """Returns a tuple of lists, first list is resolved world packages,
-       seond is unresolved package names."""
-       f = open(portage.root+portage.WORLD_FILE)
-       pkglist = f.readlines()
-       resolved = []
-       unresolved = []
-       for x in pkglist:
-               cpv = x.strip()
-               if len(cpv) and cpv[0] != "#":
-                       pkg = find_best_match(cpv)
-                       if pkg:
-                               resolved.append(pkg)
-                       else:
-                               unresolved.append(cpv)
-       return (resolved,unresolved)
-
-def find_all_installed_packages(prefilter=None):
-       """Returns a list of all installed packages, after applying the prefilter
-       function"""
-       t = vartree.dbapi.cpv_all()
-       if prefilter:
-               t = filter(prefilter,t)
-       return [Package(x) for x in t]
-
-def find_all_uninstalled_packages(prefilter=None):
-       """Returns a list of all uninstalled packages, after applying the prefilter
-       function"""
-       alist = find_all_packages(prefilter)
-       return [x for x in alist if not x.is_installed()]
-
-def find_all_packages(prefilter=None):
-       """Returns a list of all known packages, installed or not, after applying
-       the prefilter function"""
-       t = porttree.dbapi.cp_all()
-       t += vartree.dbapi.cp_all()
-       if prefilter:
-               t = filter(prefilter,t)
-       t = unique_array(t)
-       t2 = []
-       for x in t:
-               t2 += porttree.dbapi.cp_list(x)
-               t2 += vartree.dbapi.cp_list(x)
-       t2 = unique_array(t2)
-       return [Package(x) for x in t2]
-
-def split_package_name(name):
-       """Returns a list on the form [category, name, version, revision]. Revision will
-       be 'r0' if none can be inferred. Category and version will be empty, if none can
-       be inferred."""
-       r = portage.catpkgsplit(name)
-       if not r:
-               r = name.split("/")
-               if len(r) == 1:
-                       return ["", name, "", "r0"]
+                       matches = PORTDB.match(query)
+               matches.extend(VARDB.match(query))
+       except portage.exception.InvalidAtom, err:
+               raise errors.GentoolkitInvalidAtom(str(err))
+
+       return [Package(x) for x in set(matches)]
+
+
+def get_cpvs(predicate=None, include_installed=True):
+       """Get all packages in the Portage tree and overlays. Optionally apply a
+       predicate.
+
+       Example usage:
+               >>> from gentoolkit.helpers import get_cpvs
+               >>> len(set(get_cpvs()))
+               26065
+               >>> fn = lambda x: x.startswith('app-portage')
+               >>> len(get_cpvs(fn, include_installed=False))
+               112
+
+       @type predicate: function
+       @param predicate: a function to filter the package list with
+       @type include_installed: bool
+       @param include_installed:
+               If True: Return the union of all_cpvs and all_installed_cpvs
+               If False: Return the difference of all_cpvs and all_installed_cpvs
+       @rtype: generator
+       @return: a generator that yields unsorted cat/pkg-ver strings from the
+               Portage tree
+       """
+
+       if predicate:
+               all_cps = iter(x for x in PORTDB.cp_all() if predicate(x))
+       else:
+               all_cps = PORTDB.cp_all()
+
+       all_cpvs = chain.from_iterable(PORTDB.cp_list(x) for x in all_cps)
+       all_installed_cpvs = get_installed_cpvs(predicate)
+
+       if include_installed:
+               for cpv in chain(all_cpvs, all_installed_cpvs):
+                       yield cpv
+       else:
+               # Consume the smaller pkg set:
+               installed_cpvs = set(all_installed_cpvs)
+               for cpv in all_cpvs:
+                       if cpv not in installed_cpvs:
+                               yield cpv
+
+
+# pylint thinks this is a global variable
+# pylint: disable-msg=C0103
+get_uninstalled_cpvs = partial(get_cpvs, include_installed=False)
+
+
+def get_installed_cpvs(predicate=None):
+       """Get all installed packages. Optionally apply a predicate.
+
+       @type predicate: function
+       @param predicate: a function to filter the package list with
+       @rtype: generator
+       @return: a generator that yields unsorted installed cat/pkg-ver strings
+               from VARDB
+       """
+
+       if predicate:
+               installed_cps = iter(x for x in VARDB.cp_all() if predicate(x))
+       else:
+               installed_cps = VARDB.cp_all()
+
+       for cpv in chain.from_iterable(VARDB.cp_list(x) for x in installed_cps):
+               yield cpv
+
+
+def print_query_info(query, query_opts):
+       """Print info about the query to the screen."""
+
+       cat, pkg = split_cpv(query)[:2]
+       if cat and not query_opts["isRegex"]:
+               cat_str = "in %s " % pp.emph(cat.lstrip('><=~!'))
+       else:
+               cat_str = ""
+
+       if query_opts["isRegex"]:
+               pkg_str = query
+       else:
+               pkg_str = pkg
+
+       print " * Searching for %s %s..." % (pp.emph(pkg_str), cat_str)
+
+
+def print_file(path):
+       """Display the contents of a file."""
+
+       with open(path) as open_file:
+               lines = open_file.read()
+               print lines.strip()
+
+
+def print_sequence(seq):
+       """Print every item of a sequence."""
+
+       for item in seq:
+               print item
+
+
+def split_cpv(query):
+       """Split a cpv into category, name, version and revision.
+
+       @type query: str
+       @param query: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom or regex
+       @rtype: tuple
+       @return: (category, pkg_name, version, revision)
+               Each tuple element is a string or empty string ("").
+       """
+
+       result = catpkgsplit(query)
+
+       if result:
+               result = list(result)
+               if result[0] == 'null':
+                       result[0] = ''
+               if result[3] == 'r0':
+                       result[3] = ''
+       else:
+               result = query.split("/")
+               if len(result) == 1:
+                       result = ['', query, '', '']
                else:
-                       return r + ["", "r0"]
+                       result = result + ['', '']
+
+       if len(result) != 4:
+               raise errors.GentoolkitInvalidPackageName(query)
+
+       return tuple(result)
+
+
+def uniqify(seq, preserve_order=True):
+       """Return a uniqified list. Optionally preserve order."""
+
+       if preserve_order:
+               seen = set()
+               result = [x for x in seq if x not in seen and not seen.add(x)]
        else:
-               r = list(r)
-       if r[0] == 'null':
-               r[0] = ''
-       return r
+               result = list(set(seq))
+
+       return result
+
+
+def uses_globbing(query):
+       """Check the query to see if it is using globbing.
 
-# XXX: Defunct: use helpers2.compare_package_strings
-#def sort_package_list(pkglist):
-#      """Returns the list ordered in the same way portage would do with lowest version
-#      at the head of the list."""
-#      pkglist.sort(Package.compare_version)
-#      return pkglist
+       @type query: str
+       @param query: user input package query
+       @rtype: bool
+       @return: True if query uses globbing, else False
+       """
 
-if __name__ == "__main__":
-       print "This module is for import only"
+       if set('!*?[]').intersection(query):
+               # Is query an atom such as '=sys-apps/portage-2.2*'?
+               if query[0] != '=':
+                       return True
 
+       return False
 
+# vim: set ts=4 sw=4 tw=79:
diff --git a/pym/gentoolkit/metadata.py b/pym/gentoolkit/metadata.py
new file mode 100644 (file)
index 0000000..9c65fd9
--- /dev/null
@@ -0,0 +1,303 @@
+#!/usr/bin/python
+#
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+"""Provides an easy-to-use python interface to Gentoo's metadata.xml file.
+
+       Example usage:
+               >>> from gentoolkit.metadata import MetaData
+               >>> pkg_md = MetaData('/usr/portage/app-misc/gourmet/metadata.xml')
+               >>> pkg_md
+               <MetaData '/usr/portage/app-misc/gourmet/metadata.xml'>
+               >>> pkg_md.get_herds()
+               ['no-herd']
+               >>> for maint in pkg_md.get_maintainers():
+               ...     print "{0} ({1})".format(maint.email, maint.name)
+               ...
+               nixphoeni@gentoo.org (Joe Sapp)
+               >>> for flag in pkg_md.get_useflags():
+               ...     print flag.name, "->", flag.description
+               ...
+               rtf -> Enable export to RTF
+               gnome-print -> Enable printing support using gnome-print
+               >>> upstream = pkg_md.get_upstream()
+               >>> upstream
+               [<_Upstream {'docs': [], 'remoteid': [], 'maintainer':
+                [<_Maintainer 'Thomas_Hinkle@alumni.brown.edu'>], 'bugtracker': [],
+                'changelog': []}>]
+               >>> upstream[0].maintainer[0].name
+               'Thomas Mills Hinkle'
+"""
+
+# Move to Imports section after Python-2.6 is stable
+from __future__ import with_statement
+
+__all__ = ('MetaData',)
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import re
+import os
+import xml.etree.cElementTree as etree
+
+from portage import settings
+
+# =======
+# Classes
+# =======
+
+class _Maintainer(object):
+       """An object for representing one maintainer.
+
+       @type email: str or None
+       @ivar email: Maintainer's email address. Used for both Gentoo and upstream.
+       @type name: str or None
+       @ivar name: Maintainer's name. Used for both Gentoo and upstream.
+       @type description: str or None
+       @ivar description: Description of what a maintainer does. Gentoo only.
+       @type restrict: str or None
+       @ivar restrict: e.g. &gt;=portage-2.2 means only maintains versions
+               of Portage greater than 2.2.
+       @type status: str or None
+       @ivar status: If set, either 'active' or 'inactive'. Upstream only.
+       """
+
+       def __init__(self, node):
+               self.email = None
+               self.name = None
+               self.description = None
+               self.restrict = node.get('restrict')
+               self.status = node.get('status')
+               maint_attrs = node.getchildren()
+               for attr in maint_attrs:
+                       setattr(self, attr.tag, attr.text)
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, self.email)
+
+
+class _Useflag(object):
+       """An object for representing one USE flag.
+
+       @todo: Is there any way to have a keyword option to leave in
+               <pkg> and <cat> for later processing?
+       @type name: str or None
+       @ivar name: USE flag
+       @type restrict: str or None
+       @ivar restrict: e.g. &gt;=portage-2.2 means flag is only avaiable in
+               versions greater than 2.2
+       @type description: str
+       @ivar description: description of the USE flag
+       """
+
+       def __init__(self, node):
+               self.name = node.get('name')
+               self.restrict = node.get('restrict')
+               _desc = ''
+               if node.text:
+                       _desc = node.text
+               for child in node.getchildren():
+                       _desc += child.text if child.text else ''
+                       _desc += child.tail if child.tail else ''
+               # This takes care of tabs and newlines left from the file
+               self.description = re.sub('\s+', ' ', _desc)
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, self.name)
+
+
+class _Upstream(object):
+       """An object for representing one package's upstream.
+
+       @type maintainers: list
+       @ivar maintainers: L{_Maintainer} objects for each upstream maintainer
+       @type changelogs: list
+       @ivar changelogs: URLs to upstream's ChangeLog file in str format
+       @type docs: list
+       @ivar docs: Sequence of tuples containing URLs to upstream documentation
+               in the first slot and 'lang' attribute in the second, e.g.,
+               [('http.../docs/en/tut.html', None), ('http.../doc/fr/tut.html', 'fr')]
+       @type bugtrackers: list
+       @ivar bugtrackers: URLs to upstream's bugtracker. May also contain an email
+               address if prepended with 'mailto:'
+       @type remoteids: list
+       @ivar remoteids: Sequence of tuples containing the project's hosting site
+               name in the first slot and the project's ID name or number for that
+               site in the second, e.g., [('sourceforge', 'systemrescuecd')]
+       """
+
+       def __init__(self, node):
+               self.node = node
+               self.maintainers = self.get_upstream_maintainers()
+               self.changelogs = self.get_upstream_changelogs()
+               self.docs = self.get_upstream_documentation()
+               self.bugtrackers = self.get_upstream_bugtrackers()
+               self.remoteids = self.get_upstream_remoteids()
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, self.__dict__)
+
+       def get_upstream_bugtrackers(self):
+               """Retrieve upstream bugtracker location from xml node."""
+               return [e.text for e in self.node.findall('bugs-to')]
+
+       def get_upstream_changelogs(self):
+               """Retrieve upstream changelog location from xml node."""
+               return [e.text for e in self.node.findall('changelog')]
+
+       def get_upstream_documentation(self):
+               """Retrieve upstream documentation location from xml node."""
+               result = []
+               for elem in self.node.findall('doc'):
+                       lang = elem.get('lang')
+                       result.append((elem.text, lang))
+               return result
+
+       def get_upstream_maintainers(self):
+               """Retrieve upstream maintainer information from xml node."""
+               return [_Maintainer(m) for m in self.node.findall('maintainer')]
+
+       def get_upstream_remoteids(self):
+               """Retrieve upstream remote ID from xml node."""
+               return [(e.text, e.get('type')) for e in self.node.findall('remote-id')]
+
+
+class MetaData(object):
+       """Access metadata.xml"""
+
+       def __init__(self, metadata_path):
+               """Parse a valid metadata.xml file.
+
+               @type metadata_path: str
+               @ivar metadata_path: path to a valid metadata.xml file
+               @raise IOError: if C{matadata_path} can not be read
+               """
+
+               self.metadata_path = metadata_path
+               self._xml_tree = etree.parse(metadata_path)
+
+               # Used for caching
+               self._herdstree = None
+               self._descriptions = None
+               self._maintainers = None
+               self._useflags = None
+               self._upstream = None
+
+       def __repr__(self):
+               return "<%s %r>" % (self.__class__.__name__, self.metadata_path)
+
+       def _get_herd_email(self, herd):
+               """Get a herd's email address.
+
+               @type herd: str
+               @param herd: herd whose email you want
+               @rtype: str or None
+               @return: email address or None if herd is not in herds.xml
+               @raise IOError: if $PORTDIR/metadata/herds.xml can not be read
+               """
+
+               if self._herdstree is None:
+                       herds_path = os.path.join(settings['PORTDIR'], 'metadata/herds.xml')
+                       self._herdstree = etree.parse(herds_path)
+
+               # Some special herds are not listed in herds.xml
+               if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'):
+                       return None
+
+               for node in self._herdstree.getiterator('herd'):
+                       if node.findtext('name') == herd:
+                               return node.findtext('email')
+
+       def get_herds(self, include_email=False):
+               """Return a list of text nodes for <herd>.
+
+               @type include_email: bool
+               @keyword include_email: if True, also look up the herd's email
+               @rtype: list
+               @return: if include_email is False, return a list of string;
+                        if include_email is True, return a list of tuples containing:
+                                        [('herd1', 'herd1@gentoo.org'), ('no-herd', None);
+               """
+
+               result = []
+               for elem in self._xml_tree.findall('herd'):
+                       if include_email:
+                               herd_mail = self._get_herd_email(elem.text)
+                               result.append((elem.text, herd_mail))
+                       else:
+                               result.append(elem.text)
+
+               return result
+
+       def get_descriptions(self):
+               """Return a list of text nodes for <longdescription>.
+
+               @rtype: list
+               @return: package description in string format
+               @todo: Support the C{lang} attribute
+               """
+
+               if self._descriptions is not None:
+                       return self._descriptions
+
+               self._descriptions = [
+                       e.text for e in self._xml_tree.findall("longdescription")
+               ]
+               return self._descriptions
+
+       def get_maintainers(self):
+               """Get maintainers' name, email and description.
+
+               @rtype: list
+               @return: a sequence of L{_Maintainer} objects in document order.
+               """
+
+               if self._maintainers is not None:
+                       return self._maintainers
+
+               self._maintainers = []
+               for node in self._xml_tree.findall('maintainer'):
+                       self._maintainers.append(_Maintainer(node))
+
+               return self._maintainers
+
+       def get_useflags(self):
+               """Get names and descriptions for USE flags defined in metadata.
+
+               @rtype: list
+               @return: a sequence of L{_Useflag} objects in document order.
+               """
+
+               if self._useflags is not None:
+                       return self._useflags
+
+               self._useflags = []
+               for node in self._xml_tree.getiterator('flag'):
+                       self._useflags.append(_Useflag(node))
+
+               return self._useflags
+
+       def get_upstream(self):
+               """Get upstream contact information.
+
+               @rtype: list
+               @return: a sequence of L{_Upstream} objects in document order.
+               """
+
+               if self._upstream is not None:
+                       return self._upstream
+
+               self._upstream = []
+               for node in self._xml_tree.findall('upstream'):
+                       self._upstream.append(_Upstream(node))
+
+               return self._upstream
+
+# vim: set ts=4 sw=4 tw=79:
index 857470a1b8205d334c4dc0f873ea821898d1485d..e3482581150f1611d37cd4cb8fc799541ad08645 100644 (file)
 #
 # $Header$
 
+"""Provides classes for accessing Portage db information for a given package."""
+
+__all__ = (
+       'Package',
+       'PackageFormatter'
+)
+
 # =======
-# Imports 
+# Imports
 # =======
 
 import os
 
 import portage
-from portage.versions import catpkgsplit, vercmp
+from portage import settings
 
 import gentoolkit.pprinter as pp
-from gentoolkit import settings, settingslock, PORTDB, VARDB
 from gentoolkit import errors
-from gentoolkit.versionmatch import VersionMatch
+from gentoolkit.cpv import CPV
+from gentoolkit.dbapi import PORTDB, VARDB
+from gentoolkit.dependencies import Dependencies
+from gentoolkit.metadata import MetaData
 
 # =======
 # Classes
 # =======
 
-class Package(object):
-       """Package descriptor. Contains convenience functions for querying the
-       state of a package, its contents, name manipulation, ebuild info and
-       similar."""
-
-       def __init__(self, arg):
-
-               self._cpv = arg
-               self.cpv = self._cpv
-
-               if self.cpv[0] in ('<', '>'):
-                       if self.cpv[1] == '=':
-                               self.operator = self.cpv[:2]
-                               self.cpv = self.cpv[2:]
-                       else:
-                               self.operator = self.cpv[0]
-                               self.cpv = self.cpv[1:]
-               elif self.cpv[0] == '=':
-                       if self.cpv[-1] == '*':
-                               self.operator = '=*'
-                               self.cpv = self.cpv[1:-1]
-                       else:
-                               self.cpv = self.cpv[1:]
-                               self.operator = '='
-               elif self.cpv[0] == '~':
-                       self.operator = '~'
-                       self.cpv = self.cpv[1:]
+class Package(CPV):
+       """Provides methods for ascertaining the state of a given CPV."""
+
+       def __init__(self, cpv):
+               if isinstance(cpv, CPV):
+                       self.cpv = cpv
                else:
-                       self.operator = '='
-                       self._cpv = '=%s' % self._cpv
+                       self.cpv = CPV(cpv)
+               del cpv
 
-               if not portage.dep.isvalidatom(self._cpv):
-                       raise errors.GentoolkitInvalidCPV(self._cpv)
+               if not all(getattr(self.cpv, x) for x in ('category', 'version')):
+                       # CPV allows some things that Package must not
+                       raise errors.GentoolkitInvalidPackage(str(self.cpv))
 
-               cpv_split = portage.catpkgsplit(self.cpv)
-
-               try:
-                       self.key = "/".join(cpv_split[:2])
-               except TypeError:
-                       # catpkgsplit returned None
-                       raise errors.GentoolkitInvalidCPV(self._cpv)
-
-               cpv_split = list(cpv_split)
-               if cpv_split[0] == 'null':
-                       cpv_split[0] = ''
-               if cpv_split[3] == 'r0':
-                       cpv_split[3] = ''
-               self.cpv_split = cpv_split
-               self._scpv = self.cpv_split # XXX: namespace compatability 03/09
-
-               self._db = None
-               self._settings = settings
-               self._settingslock = settingslock
-               self._portdir_path = os.path.realpath(settings["PORTDIR"])
-
-               self.category = self.cpv_split[0]
-               self.name = self.cpv_split[1]
-               self.version = self.cpv_split[2]
-               self.revision = self.cpv_split[3]
-               if not self.revision:
-                       self.fullversion = self.version
-               else:
-                       self.fullversion = "%s-%s" % (self.version, self.revision)
+               # Set dynamically
+               self._package_path = None
+               self._dblink = None
+               self._metadata = None
+               self._deps = None
+               self._portdir_path = None
 
        def __repr__(self):
-               return "<%s %s @%#8x>" % (self.__class__.__name__, self._cpv, id(self))
+               return "<%s %r>" % (self.__class__.__name__, str(self.cpv))
 
        def __eq__(self, other):
-               return hash(self) == hash(other)
+               if not hasattr(other, 'cpv'):
+                       return False
+               return self.cpv == other.cpv
 
        def __ne__(self, other):
-               return hash(self) != hash(other)
+               return not self == other
 
        def __lt__(self, other):
-               if not isinstance(other, self.__class__):
-                       raise TypeError("other isn't of %s type, is %s" %
-                               (self.__class__, other.__class__))
-
-               if self.category != other.category:
-                       return self.category < other.category
-               elif self.name != other.name:
-                       return self.name < other.name
-               else:
-                       # FIXME: this cmp() hack is for vercmp not using -1,0,1
-                       # See bug 266493; this was fixed in portage-2.2_rc31
-                       #return portage.vercmp(self.fullversion, other.fullversion)
-                       result = cmp(portage.vercmp(self.fullversion, other.fullversion), 0)
-                       if result == -1:
-                               return True
-                       else:
-                               return False
+               return self.cpv < other.cpv
 
        def __gt__(self, other):
-               return not self.__lt__(other)
+               return self.cpv > other.cpv
 
        def __hash__(self):
-               return hash(self._cpv)
+               return hash(str(self.cpv))
 
        def __contains__(self, key):
-               return key in self._cpv
-       
+               return key in str(self.cpv)
+
        def __str__(self):
-               return self._cpv
+               return str(self.cpv)
+
+       def _get_trees(self):
+               """Return dbapi objects for each repository that contains self."""
+
+               result = []
+               if self.is_installed():
+                       result.append(VARDB)
+               if self.exists():
+                       result.append(PORTDB)
+               if not result:
+                       raise errors.GentoolkitFatalError("Could not find package tree")
+
+               return result
+
+       @property
+       def metadata(self):
+               """Instantiate a L{gentoolkit.metadata.MetaData} object here."""
+
+               if self._metadata is None:
+                       metadata_path = os.path.join(
+                               self.get_package_path(), 'metadata.xml'
+                       )
+                       self._metadata = MetaData(metadata_path)
+
+               return self._metadata
+
+       @property
+       def dblink(self):
+               """Instantiate a L{portage.dbapi.vartree.dblink} object here."""
+
+               if self._dblink is None:
+                       self._dblink = portage.dblink(
+                               self.cpv.category,
+                               "%s-%s" % (self.cpv.name, self.cpv.fullversion),
+                               settings["ROOT"],
+                               settings
+                       )
+
+               return self._dblink
+
+       @property
+       def deps(self):
+               """Instantiate a L{gentoolkit.dependencies.Dependencies} object here."""
+
+               if self._deps is None:
+                       self._deps = Dependencies(self.cpv)
 
-       def get_name(self):
-               """Returns base name of package, no category nor version"""
-               return self.name
+               return self._deps
 
-       def get_version(self):
-               """Returns version of package, with revision number"""
-               return self.fullversion
+       def exists(self):
+               """Return True if package exists in the Portage tree, else False"""
 
-       def get_category(self):
-               """Returns category of package"""
-               return self.category
+               return bool(PORTDB.cpv_exists(str(self.cpv)))
 
-       def get_settings(self, key):
-               """Returns the value of the given key for this package (useful 
+       @staticmethod
+       def get_settings(key):
+               """Returns the value of the given key for this package (useful
                for package.* files."""
+
+               if settings.locked:
+                       settings.unlock()
                try:
-                       self._settingslock.acquire()
-                       self._settings.setcpv(self.cpv)
-                       result = self._settings[key]
+                       result = settings[key]
                finally:
-                       self._settingslock.release()
+                       settings.lock()
+               return result
+
+       def get_mask_status(self):
+               """Shortcut to L{portage.getmaskingstatus}.
+
+               @rtype: None or list
+               @return: a list containing none or some of:
+                       'profile'
+                       'package.mask'
+                       license(s)
+                       "kmask" keyword
+                       'missing keyword'
+               """
+
+               if settings.locked:
+                       settings.unlock()
+               try:
+                       result = portage.getmaskingstatus(str(self.cpv),
+                               settings=settings,
+                               portdb=PORTDB)
+               except KeyError:
+                       # getmaskingstatus doesn't support packages without ebuilds in the
+                       # Portage tree.
+                       result = None
+
                return result
 
-       def get_cpv(self):
-               """Returns full Category/Package-Version string"""
-               return self.cpv
+       def get_mask_reason(self):
+               """Shortcut to L{portage.getmaskingreason}.
+
+               @rtype: None or tuple
+               @return: empty tuple if pkg not masked OR
+                       ('mask reason', 'mask location')
+               """
+
+               try:
+                       result = portage.getmaskingreason(str(self.cpv),
+                               settings=settings,
+                               PORTDB=PORTDB,
+                               return_location=True)
+                       if result is None:
+                               result = tuple()
+               except KeyError:
+                       # getmaskingstatus doesn't support packages without ebuilds in the
+                       # Portage tree.
+                       result = None
+
+               return result
 
        def get_provide(self):
                """Return a list of provides, if any"""
+
                if self.is_installed():
-                       result = VARDB.get_provide(self.cpv)
+                       result = VARDB.get_provide(str(self.cpv))
                else:
                        try:
                                result = [self.get_env_var('PROVIDE')]
@@ -167,289 +206,114 @@ class Package(object):
                                result = []
                return result
 
-       def get_dependants(self):
-               """Retrieves a list of CPVs for all packages depending on this one"""
-               raise NotImplementedError("Not implemented yet!")
+       def get_ebuild_path(self, in_vartree=False):
+               """Returns the complete path to the .ebuild file.
 
-       def get_runtime_deps(self):
-               """Returns a linearised list of first-level run time dependencies for 
-               this package, on the form [(comparator, [use flags], cpv), ...]
+               Example usage:
+                       >>> pkg.get_ebuild_path()
+                       '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild'
+                       >>> pkg.get_ebuild_path(in_vartree=True)
+                       '/var/db/pkg/sys-apps/portage-2.1.6.13/portage-2.1.6.13.ebuild'
                """
-               # Try to use the portage tree first, since emerge only uses the tree 
-               # when calculating dependencies
-               try:
-                       rdepends = self.get_env_var("RDEPEND", PORTDB).split()
-               except KeyError:
-                       rdepends = self.get_env_var("RDEPEND", VARDB).split()
-               return self._parse_deps(rdepends)[0]
 
-       def get_compiletime_deps(self):
-               """Returns a linearised list of first-level compile time dependencies
-               for this package, on the form [(comparator, [use flags], cpv), ...]
-               """
-               # Try to use the portage tree first, since emerge only uses the tree 
-               # when calculating dependencies
-               try:
-                       depends = self.get_env_var("DEPEND", PORTDB).split()
-               except KeyError:
-                       depends = self.get_env_var("DEPEND", VARDB).split()
-               return self._parse_deps(depends)[0]
+               if in_vartree:
+                       return VARDB.findname(str(self.cpv))
+               return PORTDB.findname(str(self.cpv))
 
-       def get_postmerge_deps(self):
-               """Returns a linearised list of first-level post merge dependencies 
-               for this package, on the form [(comparator, [use flags], cpv), ...]
-               """
-               # Try to use the portage tree first, since emerge only uses the tree 
-               # when calculating dependencies
-               try:
-                       postmerge_deps = self.get_env_var("PDEPEND", PORTDB).split()
-               except KeyError:
-                       postmerge_deps = self.get_env_var("PDEPEND", VARDB).split()
-               return self._parse_deps(postmerge_deps)[0]
+       def get_package_path(self):
+               """Return the path to where the ebuilds and other files reside."""
 
-       def intersects(self, other):
-               """Check if a passed in package atom "intersects" this atom.
+               if self._package_path is None:
+                       path_split = self.get_ebuild_path().split(os.sep)
+                       self._package_path = os.sep.join(path_split[:-1])
 
-               Lifted from pkgcore.
+               return self._package_path
 
-               Two atoms "intersect" if a package can be constructed that
-               matches both:
-                 - if you query for just "dev-lang/python" it "intersects" both
-                       "dev-lang/python" and ">=dev-lang/python-2.4"
-                 - if you query for "=dev-lang/python-2.4" it "intersects"
-                       ">=dev-lang/python-2.4" and "dev-lang/python" but not
-                       "<dev-lang/python-2.3"
+       def get_repo_name(self):
+               """Using the package path, determine the repo name.
 
-               @type other: L{gentoolkit.package.Package}
-               @param other: other package to compare
-               @see: pkgcore.ebuild.atom.py
+               @rtype: str
+               @return: /usr/<THIS>portage</THIS>/cat-egory/name/
                """
-               # Our "key" (cat/pkg) must match exactly:
-               if self.key != other.key:
-                       return False
 
-               # If we are both "unbounded" in the same direction we intersect:
-               if (('<' in self.operator and '<' in other.operator) or
-                       ('>' in self.operator and '>' in other.operator)):
-                       return True
-
-               # If one of us is an exact match we intersect if the other matches it:
-               if self.operator == '=':
-                       if other.operator == '=*':
-                               return self.fullversion.startswith(other.fullversion)
-                       return VersionMatch(other).match(self)
-               if other.operator == '=':
-                       if self.operator == '=*':
-                               return other.fullversion.startswith(self.fullversion)
-                       return VersionMatch(self).match(other)
-
-               # If we are both ~ matches we match if we are identical:
-               if self.operator == other.operator == '~':
-                       return (self.version == other.version and
-                                       self.revision == other.revision)
-
-               # If we are both glob matches we match if one of us matches the other.
-               if self.operator == other.operator == '=*':
-                       return (self.fullver.startswith(other.fullver) or
-                                       other.fullver.startswith(self.fullver))
-
-               # If one of us is a glob match and the other a ~ we match if the glob
-               # matches the ~ (ignoring a revision on the glob):
-               if self.operator == '=*' and other.operator == '~':
-                       return other.fullversion.startswith(self.version)
-               if other.operator == '=*' and self.operator == '~':
-                       return self.fullversion.startswith(other.version)
-
-               # If we get here at least one of us is a <, <=, > or >=:
-               if self.operator in ('<', '<=', '>', '>='):
-                       ranged, other = self, other
-               else:
-                       ranged, other = other, self
-
-               if '<' in other.operator or '>' in other.operator:
-                       # We are both ranged, and in the opposite "direction" (or
-                       # we would have matched above). We intersect if we both
-                       # match the other's endpoint (just checking one endpoint
-                       # is not enough, it would give a false positive on <=2 vs >2)
-                       return (
-                               VersionMatch(other).match(ranged) and
-                               VersionMatch(ranged).match(other))
-
-               if other.operator == '~':
-                       # Other definitely matches its own version. If ranged also
-                       # does we're done:
-                       if VersionMatch(ranged).match(other):
-                               return True
-                       # The only other case where we intersect is if ranged is a
-                       # > or >= on other's version and a nonzero revision. In
-                       # that case other will match ranged. Be careful not to
-                       # give a false positive for ~2 vs <2 here:
-                       return ranged.operator in ('>', '>=') and VersionMatch(
-                               other.operator, other.version, other.revision).match(ranged)
-
-               if other.operator == '=*':
-                       # a glob match definitely matches its own version, so if
-                       # ranged does too we're done:
-                       if VersionMatch(
-                               ranged.operator, ranged.version, ranged.revision).match(other):
-                               return True
-                       if '<' in ranged.operator:
-                               # If other.revision is not defined then other does not
-                               # match anything smaller than its own fullver:
-                               if not other.revision:
-                                       return False
-
-                               # If other.revision is defined then we can always
-                               # construct a package smaller than other.fullver by
-                               # tagging e.g. an _alpha1 on.
-                               return ranged.fullversion.startswith(other.version)
-                       else:
-                               # Remaining cases where this intersects: there is a
-                               # package greater than ranged.fullver and
-                               # other.fullver that they both match.
-                               return ranged.fullversion.startswith(other.version)
-
-               # Handled all possible ops.
-               raise NotImplementedError(
-                       'Someone added an operator without adding it to intersects')
-
-
-       def _parse_deps(self,deps,curuse=[],level=0):
-               # store (comparator, [use predicates], cpv)
-               r = []
-               comparators = ["~","<",">","=","<=",">="]
-               end = len(deps)
-               i = 0
-               while i < end:
-                       tok = deps[i]
-                       if tok == ')':
-                               return r,i
-                       if tok[-1] == "?":
-                               tok = tok.replace("?","")
-                               sr,l = self._parse_deps(deps[i+2:],curuse=curuse+[tok],level=level+1)
-                               r += sr
-                               i += l + 3
-                               continue
-                       if tok == "||":
-                               sr,l = self._parse_deps(deps[i+2:],curuse,level=level+1)
-                               r += sr
-                               i += l + 3
-                               continue
-                       # conjunction, like in "|| ( ( foo bar ) baz )" => recurse
-                       if tok == "(":
-                               sr,l = self._parse_deps(deps[i+1:],curuse,level=level+1)
-                               r += sr
-                               i += l + 2
-                               continue
-                       # pkg block "!foo/bar" => ignore it
-                       if tok[0] == "!":
-                               i += 1
-                               continue
-                       # pick out comparator, if any
-                       cmp = ""
-                       for c in comparators:
-                               if tok.find(c) == 0:
-                                       cmp = c
-                       tok = tok[len(cmp):]
-                       r.append((cmp,curuse,tok))
-                       i += 1
-               return r,i
+               return self.get_package_path().split(os.sep)[-3]
 
-       def is_installed(self):
-               """Returns True if this package is installed (merged)"""
-               return VARDB.cpv_exists(self.cpv)
+       def get_env_var(self, var, tree=None):
+               """Returns one of the predefined env vars DEPEND, SRC_URI, etc."""
 
-       def is_overlay(self):
-               """Returns True if the package is in an overlay."""
-               ebuild, tree = portage.portdb.findname2(self.cpv)
-               return tree != self._portdir_path
+               if tree is None:
+                       tree = self._get_trees()[0]
+               try:
+                       result = tree.aux_get(str(self.cpv), [var])
+                       if len(result) != 1:
+                               raise errors.GentoolkitFatalError
+               except (KeyError, errors.GentoolkitFatalError):
+                       err = "aux_get returned unexpected results"
+                       raise errors.GentoolkitFatalError(err)
+               return result[0]
 
-       def is_masked(self):
-               """Returns true if this package is masked against installation. 
-               Note: We blindly assume that the package actually exists on disk
-               somewhere."""
-               unmasked = portage.portdb.xmatch("match-visible", self.cpv)
-               return self.cpv not in unmasked
+       def get_use_flags(self):
+               """Returns the USE flags active at time of installation."""
 
-       def get_ebuild_path(self, in_vartree=False):
-               """Returns the complete path to the .ebuild file"""
-               if in_vartree:
-                       return VARDB.getebuildpath(self.cpv)
-               return PORTDB.findname(self.cpv)
+               return self.dblink.getstring("USE")
 
-       def get_package_path(self):
-               """Returns the path to where the ChangeLog, Manifest, .ebuild files
-               reside"""
-               ebuild_path = self.get_ebuild_path()
-               path_split = ebuild_path.split("/")
-               if path_split:
-                       return os.sep.join(path_split[:-1])
+       def get_contents(self):
+               """Returns the parsed CONTENTS file.
 
-       def get_env_var(self, var, tree=None):
-               """Returns one of the predefined env vars DEPEND, RDEPEND,
-               SRC_URI,...."""
-               if tree == None:
-                       tree = VARDB
-                       if not self.is_installed():
-                               tree = PORTDB
-               result = tree.aux_get(self.cpv, [var])
-               if not result:
-                       raise errors.GentoolkitFatalError("Could not find the package tree")
-               if len(result) != 1:
-                       raise errors.GentoolkitFatalError("Should only get one element!")
-               return result[0]
+               @rtype: dict
+               @return: {'/full/path/to/obj': ['type', 'timestamp', 'md5sum'], ...}
+               """
 
-       def get_use_flags(self):
-               """Returns the USE flags active at time of installation"""
-               self._initdb()
-               if self.is_installed():
-                       return self._db.getfile("USE")
+               return self.dblink.getcontents()
 
-       def get_contents(self):
-               """Returns the full contents, as a dictionary, in the form
-               ['/bin/foo' : [ 'obj', '1052505381', '45ca8b89751...' ], ... ]"""
-               self._initdb()
-               if self.is_installed():
-                       return self._db.getcontents()
-               return {}               
+       def get_size(self):
+               """Estimates the installed size of the contents of this package.
 
-       def size(self):
-               """Estimates the installed size of the contents of this package,
-               if possible.
-               Returns (size, number of files in total, number of uncounted files)
+               @rtype: tuple
+               @return: (size, number of files in total, number of uncounted files)
                """
+
                contents = self.get_contents()
-               size = 0
-               uncounted = 0
-               files = 0
-               for x in contents:
+               size = n_uncounted = n_files = 0
+               for cfile in contents:
                        try:
-                               size += os.lstat(x).st_size
-                               files += 1
+                               size += os.lstat(cfile).st_size
+                               n_files += 1
                        except OSError:
-                               uncounted += 1
-               return (size, files, uncounted)
+                               n_uncounted += 1
+               return (size, n_files, n_uncounted)
 
-       def _initdb(self):
-               """Internal helper function; loads package information from disk,
-               when necessary.
-               """
-               if not self._db:
-                       self._db = portage.dblink(
-                               self.category,
-                               "%s-%s" % (self.name, self.fullversion),
-                               settings["ROOT"],
-                               settings
-                       )
+       def is_installed(self):
+               """Returns True if this package is installed (merged)"""
+
+               return VARDB.cpv_exists(str(self.cpv))
+
+       def is_overlay(self):
+               """Returns True if the package is in an overlay."""
+
+               ebuild, tree = PORTDB.findname2(str(self.cpv))
+               if not ebuild:
+                       return None
+               if self._portdir_path is None:
+                       self._portdir_path = os.path.realpath(settings["PORTDIR"])
+               return (tree and tree != self._portdir_path)
+
+       def is_masked(self):
+               """Returns true if this package is masked against installation.
+               Note: We blindly assume that the package actually exists on disk
+               somewhere."""
+
+               unmasked = PORTDB.xmatch("match-visible", str(self.cpv))
+               return str(self.cpv) not in unmasked
 
 
 class PackageFormatter(object):
        """When applied to a L{gentoolkit.package.Package} object, determine the
        location (Portage Tree vs. overlay), install status and masked status. That
        information can then be easily formatted and displayed.
-       
+
        Example usage:
-               >>> from gentoolkit.helpers2 import find_packages
+               >>> from gentoolkit.helpers import find_packages
                >>> from gentoolkit.package import PackageFormatter
                >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')]
                >>> for pkg in pkgs:
@@ -457,49 +321,45 @@ class PackageFormatter(object):
                ...     # tree
                ...     if set('IP').issubset(pkg.location):
                ...             print pkg
-               ... 
+               ...
                [IP-] [  ] sys-devel/gcc-4.3.2-r3 (4.3)
 
        @type pkg: L{gentoolkit.package.Package}
        @param pkg: package to format
        @type format: L{bool}
-       @param format: Whether to format the package name or not. 
+       @param format: Whether to format the package name or not.
                Essentially C{format} should be set to False when piping or when
-               quiet output is desired. If C{format} is False, only the location
+               quiet output is desired. If C{do_format} is False, only the location
                attribute will be created to save time.
        """
 
-       def __init__(self, pkg, format=True):
-               location = ''
-               maskmodes = ['  ', ' ~', ' -', 'M ', 'M~', 'M-']
-
+       def __init__(self, pkg, do_format=True):
                self.pkg = pkg
-               self.format = format
-               if format:
-                       self.arch = settings["ARCH"]
-                       self.mask = maskmodes[self.get_mask_status()]
-                       self.slot = pkg.get_env_var("SLOT")
-               self.location = self.get_package_location()
+               self.do_format = do_format
+               self.location = self.format_package_location() or ''
 
        def __repr__(self):
                return "<%s %s @%#8x>" % (self.__class__.__name__, self.pkg, id(self))
 
        def __str__(self):
-               if self.format:
+               if self.do_format:
+                       maskmodes = ['  ', ' ~', ' -', 'M ', 'M~', 'M-', 'XX']
                        return "[%(location)s] [%(mask)s] %(package)s (%(slot)s)" % {
                                'location': self.location,
-                               'mask': pp.maskflag(self.mask),
-                               'package': pp.cpv(self.pkg.cpv),
-                               'slot': self.slot
+                               'mask': pp.maskflag(maskmodes[self.format_mask_status()[0]]),
+                               'package': pp.cpv(str(self.pkg.cpv)),
+                               'slot': self.pkg.get_env_var("SLOT")
                        }
                else:
                        return self.pkg.cpv
 
-       def get_package_location(self):
-               """Get the physical location of a package on disk.
+       def format_package_location(self):
+               """Get the install status (in /var/db/?) and origin (from and overlay
+               and the Portage tree?).
 
                @rtype: str
                @return: one of:
+                       'I--' : Installed but ebuild doesn't exist on system anymore
                        '-P-' : Not installed and from the Portage tree
                        '--O' : Not installed and from an overlay
                        'IP-' : Installed and from the Portage tree
@@ -510,37 +370,47 @@ class PackageFormatter(object):
 
                if self.pkg.is_installed():
                        result[0] = 'I'
-               if self.pkg.is_overlay():
+
+               overlay = self.pkg.is_overlay()
+               if overlay is None:
+                       pass
+               elif overlay:
                        result[2] = 'O'
                else:
                        result[1] = 'P'
 
                return ''.join(result)
 
-       def get_mask_status(self):
-               """Get the mask status of a given package. 
-
-               @type pkg: L{gentoolkit.package.Package}
-               @param pkg: pkg to get mask status of
-               @type arch: str
-               @param arch: output of gentoolkit.settings["ARCH"]
-               @rtype: int
-               @return: an index for this list: ["  ", " ~", " -", "M ", "M~", "M-"]
+       def format_mask_status(self):
+               """Get the mask status of a given package.
+
+               @rtype: tuple: (int, list)
+               @return: int = an index for this list:
+                       ["  ", " ~", " -", "M ", "M~", "M-", "XX"]
                        0 = not masked
                        1 = keyword masked
                        2 = arch masked
                        3 = hard masked
                        4 = hard and keyword masked,
                        5 = hard and arch masked
+                       6 = ebuild doesn't exist on system anymore
+
+                       list = original output of portage.getmaskingstatus
                """
 
-               keywords = self.pkg.get_env_var("KEYWORDS").split()
-               mask_status = 0
-               if self.pkg.is_masked():
-                       mask_status += 3
-               if ("~%s" % self.arch) in keywords:
-                       mask_status += 1
-               elif ("-%s" % self.arch) in keywords or "-*" in keywords:
-                       mask_status += 2
+               result = 0
+               masking_status = self.pkg.get_mask_status()
+               if masking_status is None:
+                       return (6, [])
+
+               if ("~%s keyword" % self.pkg.get_settings("ARCH")) in masking_status:
+                       result += 1
+               if "missing keyword" in masking_status:
+                       result += 2
+               if set(('profile', 'package.mask')).intersection(masking_status):
+                       result += 3
+
+               return (result, masking_status)
+
 
-               return mask_status
+# vim: set ts=4 sw=4 tw=79:
index ff92a26247974a3e88ef1533d482bd1017565c0b..db8a3687037785e5a92d27214bbb0fac48fff707 100644 (file)
 #!/usr/bin/python
 #
 # Copyright 2004 Karl Trygve Kalleberg <karltk@gentoo.org>
-# Copyright 2004 Gentoo Foundation
+# Copyright 2004-2009 Gentoo Foundation
 # Distributed under the terms of the GNU General Public License v2
 #
 # $Header$
 
+"""Provides a consistent color scheme for Gentoolkit scripts."""
+
+__all__ = (
+       'command',
+       'cpv',
+       'die',
+       'emph',
+       'error',
+       'globaloption',
+       'installedflag',
+       'localoption',
+       'maskflag',
+       'number',
+       'path',
+       'path_symlink',
+       'pkgquery',
+       'productname',
+       'regexpquery',
+       'section',
+       'slot',
+       'subsection',
+       'useflag',
+       'warn'
+)
+
+# =======
+# Imports
+# =======
+
 import sys
-import gentoolkit
 
-try:
-       import portage.output as output
-except ImportError:
-       import output
+import portage.output as output
 
+# =========
+# Functions
+# =========
 
-def print_error(s):
-       """Prints an error string to stderr."""
-       sys.stderr.write(output.red("!!! ") + s + "\n")
+# output creates color functions on the fly, which confuses pylint.
+# E1101: *%s %r has no %r member*
+# pylint: disable-msg=E1101
 
-def print_info(lv, s, line_break = True):
-       """Prints an informational string to stdout."""
-       if gentoolkit.Config["verbosityLevel"] >= lv:
-               sys.stdout.write(s)
-               if line_break:
-                       sys.stdout.write("\n")
+def command(string):
+       """Print a program command string."""
+       return output.green(string)
 
-def print_warn(s):
-       """Print a warning string to stderr."""
-       sys.stderr.write("!!! " + s + "\n")
-       
-def die(err, s):
+def cpv(string):
+       """Print a category/package-<version> string."""
+       return output.green(string)
+
+def die(err, string):
        """Print an error string and die with an error code."""
-       print_error(s)
+       sys.stderr.write(error(string))
        sys.exit(err)
 
-# Colour settings
+def emph(string):
+       """Print a string as emphasized."""
+       return output.bold(string)
 
-def cpv(s):
-       """Print a category/package-<version> string."""
-       return output.green(s)
+def error(string):
+       """Prints an error string to stderr."""
+       return output.red("!!! ") + string + "\n"
 
-def slot(s):
-       """Print a slot string"""
-       return output.bold(s)
-       
-def useflag(s):
-       """Print a USE flag strign"""
-       return output.blue(s)
-
-def useflagon(s):
-       """Print an enabled USE flag string"""
-       # FIXME: Collapse into useflag with parameter
-       return output.red(s)
-
-def useflagoff(s):
-       """Print a disabled USE flag string"""
-       # FIXME: Collapse into useflag with parameter
-       return output.blue(s)
-       
-def maskflag(s):
-       """Print a masking flag string"""
-       return output.red(s)
+def globaloption(string):
+       """Print a global option string, i.e. the program global options."""
+       return output.yellow(string)
 
-def installedflag(s):
+def installedflag(string):
        """Print an installed flag string"""
-       return output.bold(s)
-       
-def number(s):
-       """Print a number string"""
-       return output.turquoise(s)
+       return output.bold(string)
 
-def pkgquery(s):
-       """Print a package query string."""
-       return output.bold(s)
+def localoption(string):
+       """Print a local option string, i.e. the program local options."""
+       return output.green(string)
 
-def regexpquery(s):
-       """Print a regular expression string"""
-       return output.bold(s)
+def maskflag(string):
+       """Print a masking flag string"""
+       return output.red(string)
+
+def number(string):
+       """Print a number string"""
+       return output.turquoise(string)
 
-def path(s):
+def path(string):
        """Print a file or directory path string"""
-       return output.bold(s)
+       return output.bold(string)
 
-def path_symlink(s):
+def path_symlink(string):
        """Print a symlink string."""
-       return output.turquoise(s)
+       return output.turquoise(string)
+
+def pkgquery(string):
+       """Print a package query string."""
+       return output.bold(string)
 
-def productname(s):
+def productname(string):
        """Print a product name string, i.e. the program name."""
-       return output.turquoise(s)
-       
-def globaloption(s):
-       """Print a global option string, i.e. the program global options."""
-       return output.yellow(s)
+       return output.turquoise(string)
 
-def localoption(s):
-       """Print a local option string, i.e. the program local options."""
-       return output.green(s)
+def regexpquery(string):
+       """Print a regular expression string"""
+       return output.bold(string)
 
-def command(s):
-       """Print a program command string."""
-       return output.green(s)
-       
-def section(s):
+def section(string):
        """Print a string as a section header."""
-       return output.turquoise(s)      
+       return output.turquoise(string)
+
+def slot(string):
+       """Print a slot string"""
+       return output.bold(string)
 
-def subsection(s):
+def subsection(string):
        """Print a string as a subsection header."""
-       return output.turquoise(s)
-       
-def emph(s):
-       """Print a string as emphasized."""
-       return output.bold(s)
+       return output.turquoise(string)
+
+def useflag(string, enabled=False):
+       """Print a USE flag string"""
+       return output.red(string) if enabled else output.blue(string)
+
+def warn(string):
+       """Print a warning string to stderr."""
+       return "!!! " + string + "\n"
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/pym/gentoolkit/test/__init__.py b/pym/gentoolkit/test/__init__.py
new file mode 100644 (file)
index 0000000..94423e9
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+# Copyright 2009 Gentoo Foundation
+#
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
diff --git a/pym/gentoolkit/test/equery/__init__.py b/pym/gentoolkit/test/equery/__init__.py
new file mode 100644 (file)
index 0000000..94423e9
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+# Copyright 2009 Gentoo Foundation
+#
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
diff --git a/pym/gentoolkit/test/equery/test_init.py b/pym/gentoolkit/test/equery/test_init.py
new file mode 100644 (file)
index 0000000..d135aa5
--- /dev/null
@@ -0,0 +1,52 @@
+import unittest
+from test import test_support
+
+from gentoolkit import equery
+
+class TestEqueryInit(unittest.TestCase):
+
+       def setUp(self):
+               pass
+
+       def tearDown(self):
+               pass
+
+       def test_expand_module_name(self):
+               # Test that module names are properly expanded
+               name_map = {
+                       'b': 'belongs',
+                       'c': 'changes',
+                       'k': 'check',
+                       'd': 'depends',
+                       'g': 'depgraph',
+                       'f': 'files',
+                       'h': 'hasuse',
+                       'l': 'list_',
+                       'm': 'meta',
+                       's': 'size',
+                       'u': 'uses',
+                       'w': 'which'
+               }
+               self.failUnlessEqual(equery.NAME_MAP, name_map)
+               for short_name, long_name in zip(name_map, name_map.values()):
+                       self.failUnlessEqual(equery.expand_module_name(short_name),
+                               long_name)
+                       self.failUnlessEqual(equery.expand_module_name(long_name),
+                               long_name)
+               unused_keys = set(map(chr, range(0, 256))).difference(name_map.keys())
+               for key in unused_keys:
+                       self.failUnlessRaises(KeyError, equery.expand_module_name, key)
+
+       def test_format_timestamp(self):
+               # Test that a certain timetamp produces the correct formatted string
+               tstamp = 1257626685.6503389
+               tstr = '2009-11-07 15:44:45'
+               self.failUnlessEqual(equery.format_timestamp(tstamp), tstr)
+
+
+def test_main():
+       test_support.run_unittest(TestEqueryInit)
+
+
+if __name__ == '__main__':
+       test_main()
diff --git a/pym/gentoolkit/test/test_helpers.py b/pym/gentoolkit/test/test_helpers.py
new file mode 100644 (file)
index 0000000..22509b7
--- /dev/null
@@ -0,0 +1,107 @@
+import os
+import unittest
+import warnings
+from tempfile import NamedTemporaryFile
+from test import test_support
+
+from gentoolkit import helpers
+
+
+class TestFileOwner(unittest.TestCase):
+
+       def setUp(self):
+               pass
+
+       def tearDown(self):
+               pass
+
+       def test_extend_realpaths(self):
+               extend_realpaths = helpers.FileOwner.extend_realpaths
+
+               # Test that symlinks's realpaths are extended
+               f1 = NamedTemporaryFile(prefix='equeryunittest')
+               f2 = NamedTemporaryFile(prefix='equeryunittest')
+               f3 = NamedTemporaryFile(prefix='equeryunittest')
+               with warnings.catch_warnings():
+                       warnings.simplefilter("ignore")
+                       sym1 = os.tmpnam()
+                       os.symlink(f1.name, sym1)
+                       sym2 = os.tmpnam()
+                       os.symlink(f3.name, sym2)
+               # We've created 3 files and 2 symlinks for testing. We're going to pass
+               # in only the first two files and both symlinks. sym1 points to f1.
+               # Since f1 is already in the list, sym1's realpath should not be added.
+               # sym2 points to f3, but f3's not in our list, so sym2's realpath
+               # should be added to the list.
+               p = [f1.name, f2.name, sym1, sym2]
+               p_xr = extend_realpaths(p)
+
+               self.failUnlessEqual(p_xr[0], f1.name)
+               self.failUnlessEqual(p_xr[1], f2.name)
+               self.failUnlessEqual(p_xr[2], sym1)
+               self.failUnlessEqual(p_xr[3], sym2)
+               self.failUnlessEqual(p_xr[4], f3.name)
+
+               # Clean up
+               os.unlink(sym1)
+               os.unlink(sym2)
+
+               # Make sure we raise an exception if we don't get acceptable input
+               self.failUnlessRaises(AttributeError, extend_realpaths, 'str')
+               self.failUnlessRaises(AttributeError, extend_realpaths, set())
+
+
+class TestGentoolkitHelpers2(unittest.TestCase):
+
+       def test_compare_package_strings(self):
+               # Test ordering of package strings, Portage has test for vercmp,
+               # so just do the rest
+               version_tests = [
+                       # different categories
+                       ('sys-apps/portage-2.1.6.8', 'sys-auth/pambase-20080318'),
+                       # different package names
+                       ('sys-apps/pkgcore-0.4.7.15-r1', 'sys-apps/portage-2.1.6.8'),
+                       # different package versions
+                       ('sys-apps/portage-2.1.6.8', 'sys-apps/portage-2.2_rc25')
+               ]
+               # Check less than
+               for vt in version_tests:
+                       self.failUnless(
+                               helpers.compare_package_strings(vt[0], vt[1]) == -1
+                       )
+               # Check greater than
+               for vt in version_tests:
+                       self.failUnless(
+                               helpers.compare_package_strings(vt[1], vt[0]) == 1
+                       )
+               # Check equal
+               vt = ('sys-auth/pambase-20080318', 'sys-auth/pambase-20080318')
+               self.failUnless(
+                       helpers.compare_package_strings(vt[0], vt[1]) == 0
+               )
+
+       def test_uses_globbing(self):
+               globbing_tests = [
+                       ('sys-apps/portage-2.1.6.13', False),
+                       ('>=sys-apps/portage-2.1.6.13', False),
+                       ('<=sys-apps/portage-2.1.6.13', False),
+                       ('~sys-apps/portage-2.1.6.13', False),
+                       ('=sys-apps/portage-2*', False),
+                       ('sys-*/*-2.1.6.13', True),
+                       ('sys-app?/portage-2.1.6.13', True),
+                       ('sys-apps/[bp]ortage-2.1.6.13', True),
+                       ('sys-apps/[!p]ortage*', True)
+               ]
+
+               for gt in globbing_tests:
+                       self.failUnless(
+                               helpers.uses_globbing(gt[0]) == gt[1]
+                       )
+
+
+def test_main():
+       test_support.run_unittest(TestGentoolkitHelpers2)
+
+
+if __name__ == '__main__':
+       test_main()
diff --git a/pym/gentoolkit/test/test_syntax.py b/pym/gentoolkit/test/test_syntax.py
new file mode 100644 (file)
index 0000000..5b00fc5
--- /dev/null
@@ -0,0 +1,31 @@
+import os
+import os.path as osp
+import unittest
+import py_compile
+
+pym_dirs = os.walk(osp.dirname(osp.dirname(osp.dirname(__file__))))
+blacklist_dirs = frozenset(('.svn', 'tests'))
+
+class TestForSyntaxErrors(unittest.TestCase):
+
+       def test_compileability(self):
+               compileables = []
+               for thisdir, subdirs, files in pym_dirs:
+                       if os.path.basename(thisdir) in blacklist_dirs:
+                               continue
+                       compileables.extend([
+                               osp.join(thisdir, f)
+                               for f in files
+                               if osp.splitext(f)[1] == '.py'
+                       ])
+
+               for c in compileables:
+                       py_compile.compile(c, doraise=True)
+
+
+def test_main():
+       test_support.run_unittest(TestGentoolkitHelpers2)
+
+
+if __name__ == '__main__':
+       test_main()
index 6851402cd42e2832a55af2818a13d60626ead4a7..845ae9dd9cd5b30e5234699f395cd46d0b1a4c4b 100644 (file)
@@ -1,5 +1,5 @@
 """This modification of textwrap allows it to wrap ANSI colorized text as if
-it weren't colorized. It also uses a much simpler word splitting regex to 
+it weren't colorized. It also uses a much simpler word splitting regex to
 prevent the splitting of ANSI colors as well as package names and versions."""
 
 import re
@@ -13,81 +13,81 @@ class TextWrapper(textwrap.TextWrapper):
 
                Split the text to wrap into indivisible chunks.
                """
-               # Only split on whitespace to avoid mangling ANSI escape codes or 
+               # Only split on whitespace to avoid mangling ANSI escape codes or
                # package names.
                wordsep_re = re.compile(r'(\s+)')
                chunks = wordsep_re.split(text)
-               chunks = filter(None, chunks)
+               chunks = [x for x in chunks if x is not None]
                return chunks
 
-       def _wrap_chunks(self, chunks): 
-               """_wrap_chunks(chunks : [string]) -> [string] 
-
-               Wrap a sequence of text chunks and return a list of lines of 
-               length 'self.width' or less.  (If 'break_long_words' is false, 
-               some lines may be longer than this.)  Chunks correspond roughly 
-               to words and the whitespace between them: each chunk is 
-               indivisible (modulo 'break_long_words'), but a line break can 
-               come between any two chunks.  Chunks should not have internal 
-               whitespace; ie. a chunk is either all whitespace or a "word". 
-               Whitespace chunks will be removed from the beginning and end of 
-               lines, but apart from that whitespace is preserved. 
-               """ 
-               lines = [] 
-               if self.width <= 0: 
-                       raise ValueError("invalid width %r (must be > 0)" % self.width) 
-
-               # Arrange in reverse order so items can be efficiently popped 
-               # from a stack of chunks. 
-               chunks.reverse() 
+       def _wrap_chunks(self, chunks):
+               """_wrap_chunks(chunks : [string]) -> [string]
+
+               Wrap a sequence of text chunks and return a list of lines of
+               length 'self.width' or less.  (If 'break_long_words' is false,
+               some lines may be longer than this.)  Chunks correspond roughly
+               to words and the whitespace between them: each chunk is
+               indivisible (modulo 'break_long_words'), but a line break can
+               come between any two chunks.  Chunks should not have internal
+               whitespace; ie. a chunk is either all whitespace or a "word".
+               Whitespace chunks will be removed from the beginning and end of
+               lines, but apart from that whitespace is preserved.
+               """
+               lines = []
+               if self.width <= 0:
+                       raise ValueError("invalid width %r (must be > 0)" % self.width)
+
+               # Arrange in reverse order so items can be efficiently popped
+               # from a stack of chunks.
+               chunks.reverse()
 
                # Regex to strip ANSI escape codes. It's only used for the
                # length calculations of indent and each chuck.
                ansi_re = re.compile('\x1b\[[0-9;]*m')
 
-               while chunks: 
+               while chunks:
 
-                       # Start the list of chunks that will make up the current line. 
-                       # cur_len is just the length of all the chunks in cur_line. 
-                       cur_line = [] 
-                       cur_len = 0 
+                       # Start the list of chunks that will make up the current line.
+                       # cur_len is just the length of all the chunks in cur_line.
+                       cur_line = []
+                       cur_len = 0
 
-                       # Figure out which static string will prefix this line. 
-                       if lines: 
-                               indent = self.subsequent_indent 
-                       else: 
-                               indent = self.initial_indent 
+                       # Figure out which static string will prefix this line.
+                       if lines:
+                               indent = self.subsequent_indent
+                       else:
+                               indent = self.initial_indent
 
                        # Maximum width for this line. Ingore ANSI escape codes.
                        width = self.width - len(re.sub(ansi_re, '', indent))
 
-                       # First chunk on line is whitespace -- drop it, unless this 
-                       # is the very beginning of the text (ie. no lines started yet). 
-                       if chunks[-1].strip() == '' and lines: 
-                               del chunks[-1] 
+                       # First chunk on line is whitespace -- drop it, unless this
+                       # is the very beginning of the text (ie. no lines started yet).
+                       if chunks[-1].strip() == '' and lines:
+                               del chunks[-1]
 
-                       while chunks: 
+                       while chunks:
                                # Ignore ANSI escape codes.
-                               l = len(re.sub(ansi_re, '', chunks[-1])) 
+                               chunk_len = len(re.sub(ansi_re, '', chunks[-1]))
 
-                               # Can at least squeeze this chunk onto the current line. 
-                               if cur_len + l <= width: 
-                                       cur_line.append(chunks.pop()) 
-                                       cur_len += 
+                               # Can at least squeeze this chunk onto the current line.
+                               if cur_len + chunk_len <= width:
+                                       cur_line.append(chunks.pop())
+                                       cur_len += chunk_len
 
-                               # Nope, this line is full. 
-                               else: 
-                                       break 
+                               # Nope, this line is full.
+                               else:
+                                       break
 
-                       # The current line is full, and the next chunk is too big to 
-                       # fit on *any* line (not just this one). 
+                       # The current line is full, and the next chunk is too big to
+                       # fit on *any* line (not just this one).
                        # Ignore ANSI escape codes.
-                       if chunks and len(re.sub(ansi_re, '', chunks[-1])) > width: 
-                               self._handle_long_word(chunks, cur_line, cur_len, width) 
+                       if chunks and len(re.sub(ansi_re, '', chunks[-1])) > width:
+                               self._handle_long_word(chunks, cur_line, cur_len, width)
 
-                       # If the last chunk on this line is all whitespace, drop it. 
-                       if cur_line and cur_line[-1].strip() == '': 
-                               del cur_line[-1] 
+                       # If the last chunk on this line is all whitespace, drop it.
+                       if cur_line and cur_line[-1].strip() == '':
+                               del cur_line[-1]
 
                        # Convert current line back to a string and store it in list
                        # of all lines (return value).
@@ -95,3 +95,5 @@ class TextWrapper(textwrap.TextWrapper):
                                lines.append(indent + ''.join(cur_line))
 
                return lines
+
+# vim: set ts=4 sw=4 tw=79:
index 4cafa00970baaad6cfb344ec6f759816a2444a5c..12f67326c4b2bc5ab22f9bfa4e0b5b4575d935ab 100644 (file)
@@ -1,6 +1,6 @@
 #! /usr/bin/python
 #
-# Copyright(c) 2009, Gentoo Foundation
+# Copyright(c) 2009 Gentoo Foundation
 # Licensed under the GNU General Public License, v2
 #
 # Copyright: 2005-2007 Brian Harring <ferringb@gmail.com>
@@ -8,52 +8,7 @@
 #
 # $Header$
 
-"""Gentoo package version comparison object from pkgcore.ebuild.atom_restricts.
-
-The VersionMatch class allows you to compare package versions according to
-Gentoo's versioning rules.
-
-The simplest way to use it is to test simple equality. In this example I've
-passed in the keyword arguments op (operator), ver (version), and
-rev (revision) explicitly:
->>> from gentoolkit.versionmatch import VersionMatch
->>> VersionMatch(op='=',ver='1',rev='') == VersionMatch(op='=',ver='1',rev='')
-True
->>> VersionMatch(op='=',ver='1',rev='') == VersionMatch(op='=',ver='2',rev='')
-False
-
-A more flexible way to use it is to pass it a single gentoolkit.package.Package
-instance which it uses to determine op, ver and rev:
->>> from gentoolkit.package import Package
->>> from gentoolkit.versionmatch import VersionMatch
->>> pkg1 = Package('sys-apps/portage-2.2')                 
->>> pkg2 = Package('sys-apps/portage-1.6')
->>> VersionMatch(pkg1) == VersionMatch(pkg2)
-False
-
-Simple equality tests aren't actually very useful because they don't understand
-different operators:
->>> VersionMatch(op='>', ver='1.5', rev='') == \
-... VersionMatch(op='=', ver='2', rev='')
-False
-
-For more complicated comparisons, we can use the match method:
->>> from gentoolkit.package import Package
->>> from gentoolkit.versionmatch import VersionMatch
->>> pkg1 = Package('>=sys-apps/portage-2.2')
->>> pkg2 = Package('=sys-apps/portage-2.2_rc30')
->>> # An "rc" (release candidate) version compares less than a non "rc" version
-... VersionMatch(pkg1).match(pkg2)
-False
->>> pkg2 = Package('=sys-apps/portage-2.2-r6')
->>> # But an "r" (revision) version compares greater than a non "r" version
-... VersionMatch(pkg1).match(pkg2)
-True
-
-@see: gentoolkit.equery.changes for examples of use in gentoolkit.
-@see: gentoolkit.package.Package.intersects for a higher level version
-      comparison method.
-"""
+"""Gentoo version comparison object from pkgcore.ebuild.atom_restricts."""
 
 # =======
 # Imports
@@ -61,15 +16,15 @@ True
 
 from portage.versions import vercmp
 
-import gentoolkit
 from gentoolkit import errors
+from gentoolkit.cpv import CPV
 
 # =======
 # Classes
 # =======
 
 class VersionMatch(object):
-       """Gentoo package version comparison object from pkgcore.ebuild.atom_restricts.
+       """Gentoo version comparison object from pkgcore.ebuild.atom_restricts.
 
        Any overriding of this class *must* maintain numerical order of
        self.vals, see intersect for reason why. vals also must be a tuple.
@@ -78,41 +33,23 @@ class VersionMatch(object):
                (0, 1):">=", (1,):">"}
 
        _convert_int2op = dict([(v, k) for k, v in _convert_op2int.iteritems()])
-       del k, v
 
-       def __init__(self, *args, **kwargs):
-               """This class will either create a VersionMatch instance out of
-               a Package instance, or from explicitly passed in operator, version,
-               and revision.
+       def __init__(self, cpv, op='='):
+               """Initialize a VersionMatch instance.
 
-               Takes EITHER one arg:
-                       <gentoolkit.package.Package> instance
-
-                       OR
-
-               three keyword args:
-                       op=str: version comparison to do,
-                               valid operators are ('<', '<=', '=', '>=', '>', '~')
-                       ver=str: version to base comparison on
-                       rev=str: revision to base comparison on
+               @type cpv: L{gentoolkit.cpv.CPV}
+               @param cpv: cpv object
+               @type op: str
+               @keyword op: operator
                """
-               if args and isinstance(args[0], (gentoolkit.package.Package,
-                               self.__class__)):
-                       self.operator = args[0].operator
-                       self.version = args[0].version
-                       self.revision = args[0].revision
-                       self.fullversion = args[0].fullversion
-               elif set(('op', 'ver', 'rev')) == set(kwargs):
-                       self.operator = kwargs['op']
-                       self.version = kwargs['ver']
-                       self.revision = kwargs['rev']
-                       if not self.revision:
-                               self.fullversion = self.version
-                       else:
-                               self.fullversion = "%s-%s" % (self.version, self.revision)
-               else:
-                       raise TypeError('__init__() takes either a Package instance '
-                               'argument or op=, ver= and rev= all passed in as keyword args')
+
+               if not isinstance(cpv, CPV):
+                       raise ValueError("cpv must be a gentoolkit.cpv.CPV instance")
+               self.cpv = cpv
+               self.operator = op
+               self.version = cpv.version
+               self.revision = cpv.revision
+               self.fullversion = cpv.fullversion
 
                if self.operator != "~" and self.operator not in self._convert_int2op:
                        raise errors.GentoolkitInvalidVersion(
@@ -121,16 +58,15 @@ class VersionMatch(object):
                if self.operator == "~":
                        if not self.version:
                                raise errors.GentoolkitInvalidVersion(
-                                       "for ~ op, version must be specified")
+                                       "for ~ op, ver must be specified")
                        self.droprevision = True
                        self.values = (0,)
                else:
                        self.droprevision = False
                        self.values = self._convert_int2op[self.operator]
 
-       def match(self, pkginst):
-               """See whether a passed in VersionMatch or Package instance matches
-               self.
+       def match(self, other):
+               """See whether a passed in VersionMatch or CPV instance matches self.
 
                Example usage:
                        >>> from gentoolkit.versionmatch import VersionMatch
@@ -138,37 +74,30 @@ class VersionMatch(object):
                        ... VersionMatch(op='=',ver='2.0',rev=''))
                        True
 
-               @type pkginst: gentoolkit.versionmatch.VersionMatch OR
-                              gentoolkit.package.Package
-               @param pkginst: version to compare with self's version
+               @type other: gentoolkit.versionmatch.VersionMatch OR
+                  gentoolkit.cpv.CPV
+               @param other: version to compare with self's version
                @rtype: bool
                """
 
                if self.droprevision:
-                       ver1, ver2 = self.version, pkginst.version
+                       ver1, ver2 = self.version, other.version
                else:
-                       ver1, ver2 = self.fullversion, pkginst.fullversion
-
-               #print "== VersionMatch.match DEBUG START =="
-               #print "ver1:", ver1
-               #print "ver2:", ver2
-               #print "vercmp(ver2, ver1):", vercmp(ver2, ver1)
-               #print "self.values:", self.values
-               #print "vercmp(ver2, ver1) in values?",
-               #print "vercmp(ver2, ver1) in self.values"
-               #print "==  VersionMatch.match DEBUG END  =="
+                       ver1, ver2 = self.fullversion, other.fullversion
 
                return vercmp(ver2, ver1) in self.values
 
        def __str__(self):
                operator = self._convert_op2int[self.values]
 
-               if self.droprevision or not self.revision:
+               if self.droprevision or not self.cpv.revision:
                        return "ver %s %s" % (operator, self.version)
-               return "ver-rev %s %s-%s" % (operator, self.version, self.revision)
+               return "ver-rev %s %s-%s" % (
+                       operator, self.version, self.revision
+               )
 
        def __repr__(self):
-               return "<%s %s @%#8x>" % (self.__class__.__name__, str(self), id(self))
+               return "<%s %r>" % (self.__class__.__name__, str(self))
 
        @staticmethod
        def _convert_ops(inst):
@@ -188,6 +117,15 @@ class VersionMatch(object):
 
                return False
 
+       def __ne__(self, other):
+               return not self == other
+
        def __hash__(self):
-               return hash((self.droprevision, self.version, self.revision,
-                       self.values))
+               return hash((
+                       self.droprevision,
+                       self.version,
+                       self.revision,
+                       self.values
+               ))
+
+# vim: set ts=4 sw=4 tw=79:
index 446b3c792b4e15a68730da2cd40194484c297738..73a641b09c28bddec12912bbe27f6a676c069e31 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -1,10 +1,91 @@
 #!/usr/bin/env python
 
-from distutils.core import setup
+import os
+import re
+import sys
+import distutils
+from distutils import core, log
+from glob import glob
 
-__version__ = open("VERSION").read().strip()
+__version__ = os.getenv('VERSION', default='9999')
 
-setup(
+cwd = os.getcwd()
+
+# Bash files that need `VERSION=""` subbed, relative to this dir:
+bash_scripts = [os.path.join(cwd, path) for path in (
+       'bin/euse',
+       'bin/revdep-rebuild',
+)]
+
+# Python files that need `__version__ = ""` subbed, relative to this dir:
+python_scripts = [os.path.join(cwd, path) for path in (
+       'bin/eclean',
+       'bin/epkginfo',
+       'bin/glsa-check',
+       'pym/gentoolkit/equery/__init__.py'
+)]
+
+
+class set_version(core.Command):
+       """Set python __version__ and bash VERSION to our __version__."""
+       description = "hardcode scripts' version using VERSION from environment"
+       user_options = []  # [(long_name, short_name, desc),]
+
+       def initialize_options (self):
+               pass
+
+       def finalize_options (self):
+               pass
+
+       def run(self):
+               ver = 'svn' if __version__ == '9999' else __version__
+               def sub(files, pattern):
+                       for f in files:
+                               updated_file = []
+                               with open(f) as s:
+                                       for line in s:
+                                               newline = re.sub(pattern, '"%s"' % ver, line, 1)
+                                               if newline != line:
+                                                       log.info("%s: %s" % (f, newline))
+                                               updated_file.append(newline)
+                               with open(f, 'w') as s:
+                                       s.writelines(updated_file)
+               quote = r'[\'"]{1}'
+               bash_re = r'(?<=^VERSION=)' + quote + '[^\'"]*' + quote
+               sub(bash_scripts, bash_re)
+               python_re = r'(?<=^__version__ = )' + quote + '[^\'"]*' + quote
+               sub(python_scripts, python_re)
+
+
+def    load_test():
+       """Only return the real test class if it's actually being run so that we
+       don't depend on snakeoil just to install."""
+
+       desc = "run the test suite"
+       if 'test' in sys.argv[1:]:
+               try:
+                       from snakeoil import distutils_extensions
+               except ImportError:
+                       sys.stderr.write("Error: We depend on dev-python/snakeoil ")
+                       sys.stderr.write("to run tests.\n")
+                       sys.exit(1)
+               class test(distutils_extensions.test):
+                       description = desc
+                       default_test_namespace = 'gentoolkit.test'
+       else:
+               class test(core.Command):
+                       description = desc
+
+       return test
+
+
+packages = [
+       '.'.join(root.split(os.sep)[1:])
+       for root, dirs, files in os.walk('pym/gentoolkit')
+       if '__init__.py' in files
+]
+
+core.setup(
        name='gentoolkit',
        version=__version__,
        description='Set of tools that work with and enhance portage.',
@@ -16,35 +97,18 @@ setup(
        download_url='http://distfiles.gentoo.org/distfiles/gentoolkit-%s.tar.gz'\
                % __version__,
        package_dir={'': 'pym'},
-       packages=(
-               'gentoolkit',
-               'gentoolkit.equery',
-               'gentoolkit.glsa'
-       ),
-       scripts=(
-               'bin/eclean',
-               'bin/epkginfo',
-               'bin/equery',
-               'bin/eread',
-               'bin/euse',
-               'bin/glsa-check',
-               'bin/revdep-rebuild'
-       ),
+       packages=packages,
+       scripts=(glob('bin/*')),
        data_files=(
                ('/etc/env.d', ['data/99gentoolkit-env']),
                ('/etc/revdep-rebuild', ['data/revdep-rebuild/99revdep-rebuild']),
-               ('/etc/eclean', [
-                       'data/eclean/distfiles.exclude',
-                       'data/eclean/packages.exclude'
-               ]),
-               ('/usr/share/man/man1', [
-                       'man/eclean.1',
-                       'man/epkginfo.1',
-                       'man/equery.1',
-                       'man/eread.1',
-                       'man/euse.1',
-                       'man/glsa-check.1',
-                       'man/revdep-rebuild.1'
-                ])
-       )
+               ('/etc/eclean', glob('data/eclean/*')),
+               ('/usr/share/man/man1', glob('man/*'))
+       ),
+       cmdclass={
+               'test': load_test(),
+               'set_version': set_version,
+       },
 )
+
+# vim: set ts=4 sw=4 tw=79: