aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'gentoolkit/pym/gentoolkit')
-rw-r--r--gentoolkit/pym/gentoolkit/__init__.py23
-rw-r--r--gentoolkit/pym/gentoolkit/atom.py340
-rw-r--r--gentoolkit/pym/gentoolkit/cpv.py152
-rw-r--r--gentoolkit/pym/gentoolkit/dbapi.py17
-rw-r--r--gentoolkit/pym/gentoolkit/dependencies.py326
-rw-r--r--gentoolkit/pym/gentoolkit/deprecated/helpers.py176
-rw-r--r--gentoolkit/pym/gentoolkit/equery/__init__.py351
-rw-r--r--gentoolkit/pym/gentoolkit/equery/belongs.py156
-rw-r--r--gentoolkit/pym/gentoolkit/equery/changes.py205
-rw-r--r--gentoolkit/pym/gentoolkit/equery/check.py291
-rw-r--r--gentoolkit/pym/gentoolkit/equery/depends.py193
-rw-r--r--gentoolkit/pym/gentoolkit/equery/depgraph.py223
-rw-r--r--gentoolkit/pym/gentoolkit/equery/files.py320
-rw-r--r--gentoolkit/pym/gentoolkit/equery/hasuse.py156
-rw-r--r--gentoolkit/pym/gentoolkit/equery/list_.py224
-rw-r--r--gentoolkit/pym/gentoolkit/equery/meta.py494
-rw-r--r--gentoolkit/pym/gentoolkit/equery/size.py193
-rw-r--r--gentoolkit/pym/gentoolkit/equery/uses.py317
-rw-r--r--gentoolkit/pym/gentoolkit/equery/which.py102
-rw-r--r--gentoolkit/pym/gentoolkit/errors.py114
-rw-r--r--gentoolkit/pym/gentoolkit/glsa/__init__.py726
-rw-r--r--gentoolkit/pym/gentoolkit/helpers.py709
-rw-r--r--gentoolkit/pym/gentoolkit/metadata.py307
-rw-r--r--gentoolkit/pym/gentoolkit/package.py461
-rw-r--r--gentoolkit/pym/gentoolkit/pprinter.py131
-rw-r--r--gentoolkit/pym/gentoolkit/query.py34
-rw-r--r--gentoolkit/pym/gentoolkit/test/__init__.py6
-rw-r--r--gentoolkit/pym/gentoolkit/test/equery/__init__.py6
-rw-r--r--gentoolkit/pym/gentoolkit/test/equery/test_init.py46
-rw-r--r--gentoolkit/pym/gentoolkit/test/test_atom.py149
-rw-r--r--gentoolkit/pym/gentoolkit/test/test_cpv.py59
-rw-r--r--gentoolkit/pym/gentoolkit/test/test_helpers.py152
-rw-r--r--gentoolkit/pym/gentoolkit/test/test_syntax.py33
-rw-r--r--gentoolkit/pym/gentoolkit/textwrap_.py99
-rw-r--r--gentoolkit/pym/gentoolkit/versionmatch.py134
35 files changed, 7425 insertions, 0 deletions
diff --git a/gentoolkit/pym/gentoolkit/__init__.py b/gentoolkit/pym/gentoolkit/__init__.py
new file mode 100644
index 0000000..37d609b
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/__init__.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+#
+# Copyright 2003-2004 Karl Trygve Kalleberg
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
+
+"""Gentoolkit is a collection of administration scripts for Gentoo"""
+
+import sys
+
+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
+}
+
+# vim: set ts=8 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/atom.py b/gentoolkit/pym/gentoolkit/atom.py
new file mode 100644
index 0000000..d32a20b
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/atom.py
@@ -0,0 +1,340 @@
+#!/usr/bin/python
+#
+# Copyright 2009-2010 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 improvements 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 (modified to tuple if not 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
+ self.operator = self.blocker = self.use = self.slot = None
+
+ try:
+ portage.dep.Atom.__init__(self, atom)
+ except portage.exception.InvalidAtom:
+ raise errors.GentoolkitInvalidAtom(atom)
+
+ # Make operator compatible with intersects
+ if self.operator is None:
+ self.operator = ''
+
+ CPV.__init__(self, 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 __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ err = "other isn't of %s type, is %s"
+ raise TypeError(err % (self.__class__, other.__class__))
+
+ if self.operator != other.operator:
+ return False
+
+ if not CPV.__eq__(self, other):
+ return False
+
+ if bool(self.blocker) != bool(other.blocker):
+ return False
+
+ if self.blocker and other.blocker:
+ if self.blocker.overlap.forbid != other.blocker.overlap.forbid:
+ return False
+
+ # Don't believe Portage has something like this
+ #c = cmp(self.negate_vers, other.negate_vers)
+ #if c:
+ # return c
+
+ if self.slot != other.slot:
+ return False
+
+ this_use = None
+ if self.use is not None:
+ this_use = sorted(self.use.tokens)
+ that_use = None
+ if other.use is not None:
+ that_use = sorted(other.use.tokens)
+ if this_use != that_use:
+ return False
+
+ # Not supported by Portage Atom yet
+ #return cmp(self.repo_name, other.repo_name)
+ return True
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __lt__(self, other):
+ if not isinstance(other, self.__class__):
+ err = "other isn't of %s type, is %s"
+ raise TypeError(err % (self.__class__, other.__class__))
+
+ if self.operator != other.operator:
+ return self.operator < other.operator
+
+ if not CPV.__eq__(self, other):
+ return CPV.__lt__(self, other)
+
+ if bool(self.blocker) != bool(other.blocker):
+ # We want non blockers, then blockers, so only return True
+ # if self.blocker is True and other.blocker is False.
+ return bool(self.blocker) > bool(other.blocker)
+
+ if self.blocker and other.blocker:
+ if self.blocker.overlap.forbid != other.blocker.overlap.forbid:
+ # we want !! prior to !
+ return (self.blocker.overlap.forbid <
+ other.blocker.overlap.forbid)
+
+ # Don't believe Portage has something like this
+ #c = cmp(self.negate_vers, other.negate_vers)
+ #if c:
+ # return c
+
+ if self.slot != other.slot:
+ return self.slot < other.slot
+
+ this_use = None
+ if self.use is not None:
+ this_use = sorted(self.use.tokens)
+ that_use = None
+ if other.use is not None:
+ that_use = sorted(other.use.tokens)
+ if this_use != that_use:
+ return this_use < that_use
+
+ # Not supported by Portage Atom yet
+ #return cmp(self.repo_name, other.repo_name)
+
+ return False
+
+ def __gt__(self, other):
+ if not isinstance(other, self.__class__):
+ err = "other isn't of %s type, is %s"
+ raise TypeError(err % (self.__class__, other.__class__))
+
+ return not self <= other
+
+ def __le__(self, other):
+ if not isinstance(other, self.__class__):
+ raise TypeError("other isn't of %s type, is %s" % (
+ self.__class__, other.__class__)
+ )
+ return self < other or self == other
+
+ def __ge__(self, other):
+ if not isinstance(other, self.__class__):
+ raise TypeError("other isn't of %s type, is %s" % (
+ self.__class__, other.__class__)
+ )
+ return self > other or self == other
+
+ 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: L{gentoolkit.atom.Atom} or
+ L{gentoolkit.versionmatch.VersionMatch}
+ @param other: other package to compare
+ @see: L{pkgcore.ebuild.atom}
+ """
+ # Our "cp" (cat/pkg) must match exactly:
+ if self.cp != other.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.cp[-1:] != other.cp[-1:]:
+ return False
+
+ if ((not self.category and self.name == other.name) or
+ (not other.category and other.name == self.name)):
+ return True
+ return False
+
+ # Slot dep only matters if we both have one. If we do they
+ # must be identical:
+ this_slot = getattr(self, 'slot', None)
+ that_slot = getattr(other, 'slot', None)
+ if (this_slot is not None and that_slot is not None and
+ this_slot != that_slot):
+ return False
+
+ # TODO: Uncomment when Portage's Atom supports repo
+ #if (self.repo_name is not None and other.repo_name is not None and
+ # self.repo_name != other.repo_name):
+ # return False
+
+ # Use deps are similar: if one of us forces a flag on and the
+ # other forces it off we do not intersect. If only one of us
+ # cares about a flag it is irrelevant.
+
+ # Skip the (very common) case of one of us not having use deps:
+ this_use = getattr(self, 'use', None)
+ that_use = getattr(other, 'use', None)
+ if this_use and that_use:
+ # Set of flags we do not have in common:
+ flags = set(this_use.tokens) ^ set(that_use.tokens)
+ for flag in flags:
+ # If this is unset and we also have the set version we fail:
+ if flag[0] == '-' and flag[1:] in flags:
+ return False
+
+ # Remaining thing to check is version restrictions. Get the
+ # ones we can check without actual version comparisons out of
+ # the way first.
+
+ # 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.fullversion.startswith(other.fullversion)
+ return VersionMatch(other, op=other.operator).match(self)
+ if other.operator == '=':
+ if self.operator == '=*':
+ return other.fullversion.startswith(self.fullversion)
+ return VersionMatch(self, op=self.operator).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.fullversion.startswith(other.fullversion) or
+ other.fullversion.startswith(self.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.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 ('<', '<=', '>', '>='):
+ # pylint screwup:
+ # E0601: Using variable 'ranged' before assignment
+ # pylint: disable-msg=E0601
+ ranged, ranged.operator = self, self.operator
+ else:
+ ranged, ranged.operator = other, other.operator
+ other, other.operator = self, self.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, op=other.operator).match(ranged) and
+ VersionMatch(ranged, op=ranged.operator).match(other)
+ )
+
+ if other.operator == '~':
+ # Other definitely matches its own version. If ranged also
+ # does we're done:
+ if VersionMatch(ranged, op=ranged.operator).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, op=other.operator).match(ranged))
+
+ if other.operator == '=*':
+ # a glob match definitely matches its own version, so if
+ # ranged does too we're done:
+ if VersionMatch(ranged, op=ranged.operator).match(other):
+ return True
+ if '<' in ranged.operator:
+ # If other.revision is not defined then other does not
+ # match anything smaller than its own fullversion:
+ if other.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.fullversion.startswith(other.version)
+ else:
+ # Remaining cases where this intersects: there is a
+ # package greater than ranged.fullversion and
+ # other.fullversion 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')
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/cpv.py b/gentoolkit/pym/gentoolkit/cpv.py
new file mode 100644
index 0000000..f390e45
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/cpv.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+#
+# Copyright 2009-2010 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')
+ True
+ """
+
+ def __init__(self, cpv):
+ self.cpv = 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__):
+ return False
+ return self.cpv == other.cpv
+
+ def __ne__(self, 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 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
+
+ def __le__(self, other):
+ if not isinstance(other, self.__class__):
+ raise TypeError("other isn't of %s type, is %s" % (
+ self.__class__, other.__class__)
+ )
+ return self < other or self == other
+
+ def __ge__(self, other):
+ if not isinstance(other, self.__class__):
+ raise TypeError("other isn't of %s type, is %s" % (
+ self.__class__, other.__class__)
+ )
+ return self > other or self == other
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, str(self))
+
+ def __str__(self):
+ return self.cpv
+
+
+# =========
+# Functions
+# =========
+
+def split_cpv(cpv):
+ """Split a cpv into category, name, version and revision.
+
+ Inlined from helpers because of circular imports.
+
+ @todo: this function is slow and accepts some crazy things for cpv
+ @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/gentoolkit/pym/gentoolkit/dbapi.py b/gentoolkit/pym/gentoolkit/dbapi.py
new file mode 100644
index 0000000..fae1b6f
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/dbapi.py
@@ -0,0 +1,17 @@
+#!/usr/bin/python
+#
+# Copyright 2009-2010 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/gentoolkit/pym/gentoolkit/dependencies.py b/gentoolkit/pym/gentoolkit/dependencies.py
new file mode 100644
index 0000000..2d5e28b
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/dependencies.py
@@ -0,0 +1,326 @@
+# Copyright 2009-2010 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.__dict__.update(cpv.__dict__)
+ else:
+ CPV.__init__(self, cpv)
+
+ self.operator = op
+ self.atom = self.operator + 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 environment(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(self.cpv, envvars)
+ except KeyError:
+ result = VARDB.aux_get(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.environment(('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.environment(('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.environment(('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.environment(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
+ @keyword max_depth: Maximum depth to recurse if.
+ <1 means no maximum depth
+ >0 means recurse only this depth;
+ @type printer_fn: callable
+ @keyword 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 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(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
+ @keyword pkgset: sorted pkg cpv strings or anything sublassing
+ L{gentoolkit.cpv.CPV} to use for calculate our revdep graph.
+ @type max_depth: int
+ @keyword 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
+ @keyword only_direct: to recurse or not to recurse
+ @type printer_fn: callable
+ @keyword 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
+ pkgdep.cpv not in seen and
+ (depth < max_depth or max_depth == -1)
+ ):
+
+ seen.add(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):
+ sub_r = self._parser(tok, use_conditional, depth=depth+1)
+ result.extend(sub_r)
+ use_conditional = None
+ continue
+ # FIXME: This is a quick fix for bug #299260.
+ # A better fix is to not discard blockers in the parser,
+ # but to check for atom.blocker in whatever equery/depends
+ # (in this case) and ignore them there.
+ # TODO: Test to see how much a performance impact ignoring
+ # blockers here rather than checking for atom.blocker has.
+ if tok[0] == '!':
+ # We're not interested in blockers
+ 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/gentoolkit/pym/gentoolkit/deprecated/helpers.py b/gentoolkit/pym/gentoolkit/deprecated/helpers.py
new file mode 100644
index 0000000..68514d6
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/deprecated/helpers.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python2
+#
+# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+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"
diff --git a/gentoolkit/pym/gentoolkit/equery/__init__.py b/gentoolkit/pym/gentoolkit/equery/__init__.py
new file mode 100644
index 0000000..5833f29
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/__init__.py
@@ -0,0 +1,351 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Gentoo package query tool"""
+
+# Move to Imports section after Python 2.6 is stable
+from __future__ import with_statement
+
+__all__ = (
+ 'format_options',
+ 'format_package_names',
+ '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
+
+import portage
+
+import gentoolkit
+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 - 0.3.0 author'
+)
+
+# =======
+# 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
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @param with_description (bool): Option to print module's __doc__ or not
+ """
+
+ if with_description:
+ print __doc__
+ print main_usage()
+ print
+ print pp.globaloption("global options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -q, --quiet", "minimal output"),
+ (" -C, --no-color", "turn off colors"),
+ (" -N, --no-pipe", "turn off pipe detection"),
+ (" -V, --version", "display version info")
+ ))
+ print
+ print pp.command("modules") + " (" + pp.command("short name") + ")"
+ print format_options((
+ (" (b)elongs", "list what package FILES belong to"),
+ (" (c)hanges", "list changelog entries for ATOM"),
+ (" chec(k)", "verify checksums and timestamps for PKG"),
+ (" (d)epends", "list all packages directly depending on ATOM"),
+ (" dep(g)raph", "display a tree of all dependencies for PKG"),
+ (" (f)iles", "list all files installed by PKG"),
+ (" (h)asuse", "list all packages that have USE flag"),
+ (" (l)ist", "list package matching PKG"),
+ (" (m)eta", "display metadata about PKG"),
+ (" (s)ize", "display total size of all files owned by PKG"),
+ (" (u)ses", "display USE flags for PKG"),
+ (" (w)hich", "print full path to ebuild for PKG")
+ ))
+
+
+def expand_module_name(module_name):
+ """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():
+ return module_name
+ else:
+ return NAME_MAP[module_name]
+
+
+def format_options(options):
+ """Format module options.
+
+ @type options: list
+ @param options: [('option 1', 'description 1'), ('option 2', 'des... )]
+ @rtype: str
+ @return: formatted options string
+ """
+
+ result = []
+ 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
+ @param fdesc: [file_type, timestamp, MD5 sum/symlink target]
+ file_type is one of dev, dir, obj, sym.
+ If file_type is dir, there is no timestamp or MD5 sum.
+ If file_type is sym, fdesc[2] is the target of the symlink.
+ @type show_type: bool
+ @param show_type: if True, prepend the file's type to the formatted string
+ @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
+ @rtype: str
+ @return: formatted pathname with optional added information
+ """
+
+ ftype = fpath = stamp = md5sum = ""
+
+ if fdesc[0] == "obj":
+ ftype = "file"
+ fpath = path
+ stamp = format_timestamp(fdesc[1])
+ md5sum = fdesc[2]
+ elif fdesc[0] == "dir":
+ ftype = "dir"
+ fpath = pp.path(path)
+ elif fdesc[0] == "sym":
+ ftype = "sym"
+ stamp = format_timestamp(fdesc[1])
+ tgt = fdesc[2].split()[0]
+ if CONFIG["piping"]:
+ fpath = path
+ else:
+ fpath = pp.path_symlink(path + " -> " + tgt)
+ elif fdesc[0] == "dev":
+ ftype = "dev"
+ fpath = path
+ else:
+ sys.stderr.write(
+ pp.error("%s has unknown type: %s" % (path, fdesc[0]))
+ )
+
+ result = ""
+ if show_type:
+ result += "%4s " % ftype
+ result += fpath
+ if show_timestamp:
+ result += " " + stamp
+ if show_md5:
+ result += " " + md5sum
+
+ return result
+
+
+def format_timestamp(timestamp):
+ """Format a timestamp into, e.g., '2009-01-31 21:19:44' format"""
+
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(timestamp)))
+
+
+def initialize_configuration():
+ """Setup the standard equery config"""
+
+ # Get terminal size
+ term_width = pp.output.get_term_size()[1]
+ if term_width == -1:
+ # get_term_size() failed. Set a sane default width:
+ term_width = 80
+
+ # Terminal size, minus a 1-char margin for text wrapping
+ CONFIG['termWidth'] = term_width - 1
+
+ # Guess color output
+ if (CONFIG['color'] == -1 and (not sys.stdout.isatty() or
+ os.getenv("NOCOLOR") in ("yes", "true")) or CONFIG['color'] == 0):
+ pp.output.nocolor()
+
+ CONFIG['verbose'] = not CONFIG['piping']
+
+
+def main_usage():
+ """Return the main usage message for equery"""
+
+ return "%(usage)s %(product)s [%(g_opts)s] %(mod_name)s [%(mod_opts)s]" % {
+ 'usage': pp.emph("Usage:"),
+ 'product': pp.productname(__productname__),
+ 'g_opts': pp.globaloption("global-options"),
+ 'mod_name': pp.command("module-name"),
+ 'mod_opts': pp.localoption("module-options")
+ }
+
+
+def mod_usage(mod_name="module", arg="pkgspec", optional=False):
+ """Provide a consistent usage message to the calling module.
+
+ @type arg: string
+ @param arg: what kind of argument the module takes (pkgspec, filename, etc)
+ @type optional: bool
+ @param optional: is the argument optional?
+ """
+
+ return "%(usage)s: %(mod_name)s [%(opts)s] %(arg)s" % {
+ 'usage': pp.emph("Usage"),
+ 'mod_name': pp.command(mod_name),
+ 'opts': pp.localoption("options"),
+ 'arg': ("[%s]" % pp.emph(arg)) if optional else pp.emph(arg)
+ }
+
+
+def parse_global_options(global_opts, args):
+ """Parse global input args and return True if we should display help for
+ the called module, else False (or display help and exit from here).
+ """
+
+ need_help = False
+ opts = (opt[0] for opt in global_opts)
+ for opt in opts:
+ if opt in ('-h', '--help'):
+ if args:
+ need_help = True
+ else:
+ print_help()
+ sys.exit(0)
+ elif opt in ('-q','--quiet'):
+ CONFIG['quiet'] = True
+ elif opt in ('-C', '--no-color', '--nocolor'):
+ CONFIG['color'] = 0
+ pp.output.nocolor()
+ elif opt in ('-N', '--no-pipe'):
+ CONFIG['piping'] = False
+ CONFIG['verbose'] = True
+ 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."""
+
+ print "%(product)s (%(version)s) - %(docstring)s" % {
+ "product": pp.productname(__productname__),
+ "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."""
+
+ short_opts = "hqCNV"
+ 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:
+ 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')
+
+ try:
+ expanded_module_name = expand_module_name(module_name)
+ except KeyError:
+ 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.main(module_args)
+ except portage.exception.AmbiguousPackageName, err:
+ raise errors.GentoolkitAmbiguousPackage(err)
+ except IOError, err:
+ if err.errno != errno.EPIPE:
+ raise
+
+if __name__ == '__main__':
+ main()
diff --git a/gentoolkit/pym/gentoolkit/equery/belongs.py b/gentoolkit/pym/gentoolkit/equery/belongs.py
new file mode 100644
index 0000000..3845b9d
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/belongs.py
@@ -0,0 +1,156 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""List all packages owning a particular file
+
+Note: Normally, only one package will own a file. If multiple packages own
+ the same file, it usually constitutes a problem, and should be reported.
+"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+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.helpers import FileOwner
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "fullRegex": False,
+ "earlyOut": False,
+ "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.parsed_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 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 ('-e', '--early-out', '--earlyout'):
+ if opt == '--earlyout':
+ 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'):
+ QUERY_OPTS['fullRegex'] = True
+ elif opt in ('-n', '--name-only'):
+ QUERY_OPTS['nameOnly'] = True
+
+
+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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="belongs", arg="filename")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -f, --full-regex", "supplied query is a regex" ),
+ (" -e, --early-out", "stop when first match is found"),
+ (" -n, --name-only", "don't print the version")
+ ))
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ 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:
+ 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)
+
+ 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:
diff --git a/gentoolkit/pym/gentoolkit/equery/changes.py b/gentoolkit/pym/gentoolkit/equery/changes.py
new file mode 100644
index 0000000..8adf246
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/changes.py
@@ -0,0 +1,205 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""Displays the ChangeLog entry for the latest installable version of an atom"""
+
+# Move to Imports sections when Python 2.6 is stable
+from __future__ import with_statement
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.atom import Atom
+from gentoolkit.equery import format_options, mod_usage
+from gentoolkit.helpers import ChangeLog, find_best_match, find_packages
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ 'onlyLatest': False,
+ 'showFullLog': False,
+ 'limit': None,
+ 'from': None,
+ 'to': None
+}
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="changes")
+ print
+ print pp.emph("examples")
+ print (" c portage # show latest visible "
+ "version's entry")
+ print " c portage --full --limit=3 # show 3 latest entries"
+ print " c '=sys-apps/portage-2.1.6*' # use atom syntax"
+ print " c portage --from=2.2_rc20 --to=2.2_rc30 # use version ranges"
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -l, --latest", "display only the latest ChangeLog entry"),
+ (" -f, --full", "display the full ChangeLog"),
+ (" --limit=NUM",
+ "limit the number of entries displayed (with --full)"),
+ (" --from=VER", "set which version to display from"),
+ (" --to=VER", "set which version to display to"),
+ ))
+
+
+def get_match(query):
+ """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:
+ raise errors.GentoolkitNoMatches(query)
+
+ return matches[0]
+
+
+def is_ranged(atom):
+ """Return True if an atom string appears to be ranged, else False."""
+
+ return atom.startswith(('~', '<', '>')) or atom.endswith('*')
+
+
+def parse_module_options(module_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):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-f', '--full'):
+ QUERY_OPTS['showFullLog'] = True
+ elif opt in ('-l', '--latest'):
+ QUERY_OPTS['onlyLatest'] = True
+ elif opt in ('--limit',):
+ set_limit(posarg)
+ elif opt in ('--from',):
+ QUERY_OPTS['from'] = posarg
+ elif opt in ('--to',):
+ QUERY_OPTS['to'] = posarg
+
+
+def print_entries(entries):
+ """Print entries and strip trailing whitespace from the last entry."""
+
+ 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.
+
+ Die if posarg is not an integer.
+ """
+
+ if posarg.isdigit():
+ QUERY_OPTS['limit'] = int(posarg)
+ else:
+ err = "Module option --limit requires integer (got '%s')"
+ sys.stderr.write(pp.error(err % posarg))
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hlf"
+ long_opts = ('help', 'full', 'from=', 'latest', 'limit=', 'to=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, 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)
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ match = get_match(query)
+ changelog_path = os.path.join(match.package_path(), 'ChangeLog')
+ changelog = ChangeLog(changelog_path)
+
+ #
+ # Output
+ #
+
+ if (QUERY_OPTS['onlyLatest'] or (
+ changelog.entries and not changelog.indexed_entries
+ )):
+ print changelog.latest.strip()
+ else:
+ 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:
diff --git a/gentoolkit/pym/gentoolkit/equery/check.py b/gentoolkit/pym/gentoolkit/equery/check.py
new file mode 100644
index 0000000..5969804
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/check.py
@@ -0,0 +1,291 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Checks timestamps and MD5 sums for files owned by a given installed package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from functools import partial
+from getopt import gnu_getopt, GetoptError
+
+import portage.checksum as checksum
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "includeInstalled": True,
+ "includeOverlayTree": False,
+ "includePortTree": False,
+ "checkMD5sum": True,
+ "checkTimestamp" : True,
+ "isRegex": False,
+ "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.parsed_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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+
+ # 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="check")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -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 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 ('-f', '--full-regex'):
+ QUERY_OPTS['isRegex'] = True
+ elif opt in ('-o', '--only-failures'):
+ QUERY_OPTS['onlyFailures'] = True
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ 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:
+ 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)
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ raise errors.GentoolkitNoMatches(query, in_installed=True)
+
+ matches.sort()
+
+ printer = partial(
+ checks_printer,
+ verbose=CONFIG['verbose'],
+ only_failures=QUERY_OPTS['onlyFailures']
+ )
+ check = VerifyContents(printer_fn=printer)
+ check(matches)
+
+ first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/equery/depends.py b/gentoolkit/pym/gentoolkit/equery/depends.py
new file mode 100644
index 0000000..a1061fc
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/depends.py
@@ -0,0 +1,193 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""List all packages that depend on a atom given query"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+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 = {
+ "includeMasked": False,
+ "onlyDirect": True,
+ "maxDepth": -1,
+}
+
+# =======
+# 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
+# =========
+
+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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="depends")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -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 parse_module_options(module_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):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-a', '--all-packages'):
+ QUERY_OPTS['includeMasked'] = True
+ elif opt in ('-D', '--indirect'):
+ QUERY_OPTS['onlyDirect'] = False
+ elif opt in ('--depth'):
+ if posarg.isdigit():
+ depth = int(posarg)
+ else:
+ err = "Module option --depth requires integer (got '%s')"
+ sys.stdout.write(pp.error(err % posarg))
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+ 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:
+ 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)
+
+ #
+ # Output
+ #
+
+ dep_print = DependPrinter(verbose=CONFIG['verbose'])
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ pkg = Dependencies(query)
+ if QUERY_OPTS['includeMasked']:
+ pkggetter = get_cpvs
+ else:
+ 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:
diff --git a/gentoolkit/pym/gentoolkit/equery/depgraph.py b/gentoolkit/pym/gentoolkit/equery/depgraph.py
new file mode 100644
index 0000000..9c7e346
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/depgraph.py
@@ -0,0 +1,223 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Display a direct dependency graph for a given package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from functools import partial
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "depth": 1,
+ "noAtom": False,
+ "noIndent": False,
+ "noUseflags": False,
+ "includeInstalled": True,
+ "includePortTree": True,
+ "includeOverlayTree": True,
+ "includeMasked": True,
+ "isRegex": False,
+ "matchExact": True,
+ "printMatchInfo": (not CONFIG['quiet'])
+}
+
+# =========
+# 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
+ """
+
+ 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 format the graph by indenting dependencies"),
+ (" --depth=N", "limit dependency graph to specified depth")
+ ))
+
+
+def parse_module_options(module_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):
+ 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["noUseflags"] = True
+ if opt in ('-l', '--linear'):
+ QUERY_OPTS["noIndent"] = True
+ if opt in ('--depth'):
+ if posarg.isdigit():
+ depth = int(posarg)
+ else:
+ 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 = "hAUl"
+ long_opts = ('help', 'no-atom', 'no-useflags', 'depth=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, 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)
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ 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:
+ make_depgraph(pkg, printer)
+
+ first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/equery/files.py b/gentoolkit/pym/gentoolkit/equery/files.py
new file mode 100644
index 0000000..f0c40ee
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/files.py
@@ -0,0 +1,320 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""List files owned by a given package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import portage
+
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import (format_filetype, format_options, mod_usage,
+ CONFIG)
+from gentoolkit.helpers import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": True,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False,
+ "matchExact": True,
+ "outputTree": False,
+ "printMatchInfo": (not CONFIG['quiet']),
+ "showType": False,
+ "showTimestamp": False,
+ "showMD5": False,
+ "typeFilter": None
+}
+
+FILTER_RULES = (
+ 'dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc', 'man', 'info'
+)
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="files")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -m, --md5sum", "include MD5 sum in output"),
+ (" -s, --timestamp", "include timestamp in output"),
+ (" -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",
+ "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.
+
+ @see: gentoolkit.package.Package.parsed_contents
+ @type contents: dict
+ @param contents: {'path': ['filetype', ...], ...}
+ """
+
+ filenames = contents.keys()
+ filenames.sort()
+ last = []
+
+ for name in filenames:
+ if QUERY_OPTS["outputTree"]:
+ 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(indent + basename[0])
+ continue
+ for i, directory in enumerate(basename):
+ try:
+ if directory in last[i]:
+ continue
+ except IndexError:
+ pass
+ last = basename
+ if len(last) == 1:
+ print pp.path(indent + last[0])
+ continue
+ print pp.path(indent + "> /" + last[-1])
+ elif contents[name][0] == "sym":
+ print pp.path(indent + "+"),
+ print pp.path_symlink(basename[-1] + " -> " + contents[name][2])
+ else:
+ print pp.path(indent + "+ ") + basename[-1]
+ else:
+ print format_filetype(
+ name,
+ contents[name],
+ show_type=QUERY_OPTS["showType"],
+ show_md5=QUERY_OPTS["showMD5"],
+ show_timestamp=QUERY_OPTS["showTimestamp"]
+ )
+
+
+def filter_by_doc(contents, content_filter):
+ """Return a copy of content filtered by documentation."""
+
+ filtered_content = {}
+ for doctype in ('doc' ,'man' ,'info'):
+ # List only files from /usr/share/{doc,man,info}
+ if doctype in content_filter:
+ docpath = os.path.join(os.sep, 'usr', 'share', doctype)
+ for path in contents:
+ if contents[path][0] == 'obj' and path.startswith(docpath):
+ filtered_content[path] = contents[path]
+
+ return filtered_content
+
+
+def filter_by_command(contents):
+ """Return a copy of content filtered by executable commands."""
+
+ filtered_content = {}
+ userpath = os.environ["PATH"].split(os.pathsep)
+ userpath = [os.path.normpath(x) for x in userpath]
+ for path in contents:
+ if (contents[path][0] in ['obj', 'sym'] and
+ os.path.dirname(path) in userpath):
+ filtered_content[path] = contents[path]
+
+ return filtered_content
+
+
+def filter_by_path(contents):
+ """Return a copy of content filtered by file paths."""
+
+ filtered_content = {}
+ paths = list(reversed(sorted(contents.keys())))
+ while paths:
+ basepath = paths.pop()
+ if contents[basepath][0] == 'dir':
+ check_subdirs = False
+ for path in paths:
+ if (contents[path][0] != "dir" and
+ os.path.dirname(path) == basepath):
+ filtered_content[basepath] = contents[basepath]
+ check_subdirs = True
+ break
+ if check_subdirs:
+ while (paths and paths[-1].startswith(basepath)):
+ paths.pop()
+
+ return filtered_content
+
+
+def filter_by_conf(contents):
+ """Return a copy of content filtered by configuration files."""
+
+ filtered_content = {}
+ conf_path = portage.settings["CONFIG_PROTECT"].split()
+ conf_path = tuple(os.path.normpath(x) for x in conf_path)
+ 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):
+ if not path.startswith(conf_mask_path):
+ filtered_content[path] = contents[path]
+
+ return filtered_content
+
+
+def filter_contents(contents):
+ """Filter files by type if specified by the user.
+
+ @see: gentoolkit.package.Package.parsed_contents
+ @type contents: dict
+ @param contents: {'path': ['filetype', ...], ...}
+ @rtype: dict
+ @return: contents with unrequested filetypes stripped
+ """
+
+ if QUERY_OPTS['typeFilter']:
+ 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)
+ for path in contents:
+ if contents[path][0] in content_filter:
+ filtered_content[path] = contents[path]
+ if "cmd" in content_filter:
+ filtered_content.update(filter_by_command(contents))
+ if "path" in content_filter:
+ filtered_content.update(filter_by_path(contents))
+ if "conf" in content_filter:
+ 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 QUERY_OPTS"""
+
+ content_filter = []
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-e', '--exact-name'):
+ QUERY_OPTS["matchExact"] = True
+ elif opt in ('-m', '--md5sum'):
+ QUERY_OPTS["showMD5"] = True
+ elif opt in ('-s', '--timestamp'):
+ QUERY_OPTS["showTimestamp"] = True
+ elif opt in ('-t', '--type'):
+ QUERY_OPTS["showType"] = True
+ elif opt in ('--tree'):
+ QUERY_OPTS["outputTree"] = True
+ elif opt in ('-f', '--filter'):
+ f_split = posarg.split(',')
+ content_filter.extend(x.lstrip('=') for x in f_split)
+ for rule in content_filter:
+ if not rule in FILTER_RULES:
+ sys.stderr.write(
+ pp.error("Invalid filter rule '%s'" % rule)
+ )
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+ QUERY_OPTS["typeFilter"] = content_filter
+
+
+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=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, 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)
+
+ # Turn off filtering for tree output
+ if QUERY_OPTS["outputTree"]:
+ QUERY_OPTS["typeFilter"] = None
+
+ #
+ # Output files
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ 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(str(pkg.cpv))
+
+ contents = pkg.parsed_contents()
+ display_files(filter_contents(contents))
+
+ first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/equery/hasuse.py b/gentoolkit/pym/gentoolkit/equery/hasuse.py
new file mode 100644
index 0000000..8d51013
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/hasuse.py
@@ -0,0 +1,156 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""List all installed packages that have a given USE flag"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+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
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": True,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False, # Necessary for do_lookup, don't change
+ "printMatchInfo": False
+}
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="hasuse", arg="USE-flag")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -I, --exclude-installed",
+ "exclude installed packages from search path"),
+ (" -o, --overlay-tree", "include overlays in search path"),
+ (" -p, --portage-tree", "include entire portage tree in search path")
+ ))
+
+
+def display_useflags(query, pkg):
+ """Display USE flag information for a given package."""
+
+ try:
+ useflags = [x.lstrip("+-") for x in pkg.environment("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 QUERY_OPTS"""
+
+ # Parse module options
+ 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 ('-I', '--exclue-installed'):
+ QUERY_OPTS['includeInstalled'] = False
+ elif opt in ('-p', '--portage-tree'):
+ QUERY_OPTS['includePortTree'] = True
+ elif opt in ('-o', '--overlay-tree'):
+ QUERY_OPTS['includeOverlayTree'] = True
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hiIpo" # -i was option for default action
+ # --installed is no longer needed, kept for compatibility (djanderson '09)
+ long_opts = ('help', 'installed', 'exclude-installed', 'portage-tree',
+ 'overlay-tree')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, 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)
+
+ matches = do_lookup("*", QUERY_OPTS)
+ matches.sort()
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ if CONFIG['verbose']:
+ print " * Searching for USE flag %s ... " % pp.emph(query)
+
+ for pkg in matches:
+ display_useflags(query, pkg)
+ first_run = False
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/equery/list_.py b/gentoolkit/pym/gentoolkit/equery/list_.py
new file mode 100644
index 0000000..3de8355
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/list_.py
@@ -0,0 +1,224 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""List installed packages matching the query pattern"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+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.helpers import do_lookup, get_installed_cpvs
+from gentoolkit.package import Package, PackageFormatter
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "duplicates": False,
+ "includeInstalled": True,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "includeMaskReason": False,
+ "isRegex": False,
+ "printMatchInfo": (not CONFIG['quiet'])
+}
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+
+ # 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")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -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"),
+ (" -p, --portage-tree", "list packages in the main portage tree")
+ ))
+
+
+def get_duplicates(matches):
+ """Return only packages that have more than one version installed."""
+
+ dups = {}
+ result = []
+ for pkg in matches:
+ if pkg.cpv.cp in dups:
+ dups[pkg.cpv.cp].append(pkg)
+ else:
+ dups[pkg.cpv.cp] = [pkg]
+
+ for cpv in dups.values():
+ if len(cpv) > 1:
+ result.extend(cpv)
+
+ return result
+
+
+def parse_module_options(module_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):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-I', '--exclude-installed'):
+ QUERY_OPTS['includeInstalled'] = False
+ elif opt in ('-p', '--portage-tree'):
+ QUERY_OPTS['includePortTree'] = True
+ elif opt in ('-o', '--overlay-tree'):
+ 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'):
+ 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
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ 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', '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:
+ sys.stderr.write(pp.error("Module %s" % err))
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ # Only search installed packages when listing duplicate packages
+ if QUERY_OPTS["duplicates"]:
+ QUERY_OPTS["includeInstalled"] = True
+ QUERY_OPTS["includePortTree"] = False
+ QUERY_OPTS["includeOverlayTree"] = False
+ QUERY_OPTS["includeMaskReason"] = False
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ # Find duplicate packages
+ if QUERY_OPTS["duplicates"]:
+ matches = get_duplicates(matches)
+
+ matches.sort()
+
+ #
+ # Output
+ #
+
+ for pkg in matches:
+ if CONFIG['verbose']:
+ pkgstr = PackageFormatter(pkg, do_format=True)
+ else:
+ pkgstr = PackageFormatter(pkg, do_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
+
+ 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.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:
diff --git a/gentoolkit/pym/gentoolkit/equery/meta.py b/gentoolkit/pym/gentoolkit/equery/meta.py
new file mode 100644
index 0000000..19c23a6
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/meta.py
@@ -0,0 +1,494 @@
+# Copyright 2009-2010 Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""Display metadata about a given package"""
+
+# Move to Imports section after Python-2.6 is stable
+from __future__ import with_statement
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import re
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import find_packages, print_sequence, print_file
+from gentoolkit.textwrap_ import TextWrapper
+
+# =======
+# Globals
+# =======
+
+# E1101: Module 'portage.output' has no $color member
+# portage.output creates color functions dynamically
+# pylint: disable-msg=E1101
+
+QUERY_OPTS = {
+ 'current': False,
+ 'description': False,
+ 'herd': False,
+ 'keywords': False,
+ 'maintainer': False,
+ 'useflags': False,
+ 'upstream': False,
+ 'xml': False
+}
+
+# =========
+# Functions
+# =========
+
+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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ if with_usage:
+ print mod_usage(mod_name="meta")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -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 metadata.xml file")
+ ))
+
+
+def filter_keywords(matches):
+ """Filters non-unique keywords per slot.
+
+ Does not filter arch mask keywords (-). Besides simple non-unique keywords,
+ also remove unstable keywords (~) if a higher version in the same slot is
+ stable. This view makes version bumps easier for package maintainers.
+
+ @type matches: array
+ @param matches: set of L{gentoolkit.package.Package} instances whose
+ 'key' are all the same.
+ @rtype: dict
+ @return: a dict with L{gentoolkit.package.Package} instance keys and
+ 'array of keywords not found in a higher version of pkg within the
+ same slot' values.
+ """
+ def del_archmask(keywords):
+ """Don't add arch_masked to filter set."""
+ return [x for x in keywords if not x.startswith('-')]
+
+ def add_unstable(keywords):
+ """Add unstable keyword for all stable keywords to filter set."""
+ result = list(keywords)
+ result.extend(
+ ['~%s' % x for x in keywords if not x.startswith(('-', '~'))]
+ )
+ return result
+
+ result = {}
+ slot_map = {}
+ # Start from the newest
+ rev_matches = reversed(matches)
+ for pkg in rev_matches:
+ keywords_str, slot = pkg.environment(('KEYWORDS', 'SLOT'),
+ prefer_vdb=False)
+ keywords = keywords_str.split()
+ result[pkg] = [x for x in keywords if x not in slot_map.get(slot, [])]
+ try:
+ slot_map[slot].update(del_archmask(add_unstable(keywords)))
+ except KeyError:
+ slot_map[slot] = set(del_archmask(add_unstable(keywords)))
+
+ return result
+
+
+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 += " - %s" % (maint.restrict,) if maint.restrict 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(keywords):
+ """Sort and colorize keywords for display."""
+
+ result = []
+
+ for kw in sorted(keywords):
+ if kw.startswith('-'):
+ # arch masked
+ kw = pp.keyword(kw, stable=False, hard_masked=True)
+ elif kw.startswith('~'):
+ # keyword masked
+ kw = pp.keyword(kw, stable=False, hard_masked=False)
+ else:
+ # stable
+ kw = pp.keyword(kw, stable=True, hard_masked=False)
+ result.append(kw)
+
+ return ' '.join(result)
+
+
+def format_keywords_line(pkg, fmtd_keywords, slot, verstr_len):
+ """Format the entire keywords line for display."""
+
+ ver = pkg.fullversion
+ result = "%s:%s: %s" % (ver, pp.slot(slot), fmtd_keywords)
+ if CONFIG['verbose'] and fmtd_keywords:
+ result = format_line(fmtd_keywords, "%s:%s: " % (ver, pp.slot(slot)),
+ " " * (verstr_len + 2))
+
+ 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."""
+
+ # Choose a good package to reference metadata from
+ ref_pkg = get_reference_pkg(matches)
+
+ if CONFIG['verbose']:
+ repo = ref_pkg.repo_name()
+ print " * %s [%s]" % (pp.cpv(ref_pkg.cp), pp.section(repo))
+
+ got_opts = False
+ if any(QUERY_OPTS.values()):
+ # Specific information requested, less formatting
+ got_opts = True
+
+ if QUERY_OPTS["herd"] or not got_opts:
+ herds = format_herds(ref_pkg.metadata.herds(include_email=True))
+ if QUERY_OPTS["herd"]:
+ print_sequence(format_list(herds))
+ else:
+ for herd in herds:
+ print format_line(herd, "Herd: ", " " * 13)
+
+ if QUERY_OPTS["maintainer"] or not got_opts:
+ maints = format_maintainers(ref_pkg.metadata.maintainers())
+ if QUERY_OPTS["maintainer"]:
+ print_sequence(format_list(maints))
+ else:
+ 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 = format_upstream(ref_pkg.metadata.upstream())
+ if QUERY_OPTS["upstream"]:
+ upstream = format_list(upstream)
+ else:
+ upstream = format_list(upstream, "Upstream: ", " " * 13)
+ print_sequence(upstream)
+
+ if not got_opts:
+ pkg_loc = ref_pkg.package_path()
+ print format_line(pkg_loc, "Location: ", " " * 13)
+
+ if QUERY_OPTS["keywords"] or not got_opts:
+ # Get {<Package 'dev-libs/glib-2.20.5'>: [u'ia64', u'm68k', ...], ...}
+ keyword_map = filter_keywords(matches)
+
+ for match in matches:
+ slot = match.environment('SLOT')
+ verstr_len = len(match.fullversion) + len(slot)
+ fmtd_keywords = format_keywords(keyword_map[match])
+ keywords_line = format_keywords_line(
+ match, fmtd_keywords, slot, verstr_len
+ )
+ if QUERY_OPTS["keywords"]:
+ print keywords_line
+ else:
+ indent = " " * (16 + verstr_len)
+ print format_line(keywords_line, "Keywords: ", indent)
+
+ if QUERY_OPTS["description"]:
+ desc = ref_pkg.metadata.descriptions()
+ print_sequence(format_list(desc))
+
+ if QUERY_OPTS["useflags"]:
+ useflags = format_useflags(ref_pkg.metadata.use())
+ print_sequence(format_list(useflags))
+
+ if QUERY_OPTS["xml"]:
+ print_file(os.path.join(ref_pkg.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,
+ split the string into a list and feed it to format_line via format_list.
+
+ @see: format_list()
+ @type line: string
+ @param line: text to format
+ @type first: string
+ @param first: text to prepend to the first line
+ @type subsequent: string
+ @param subsequent: text to prepend to subsequent lines
+ @type force_quiet: boolean
+ @rtype: string
+ @return: A wrapped line
+ """
+
+ if line:
+ line = line.expandtabs().strip("\n").splitlines()
+ else:
+ if force_quiet:
+ return
+ else:
+ return first + "None specified"
+
+ if len(first) > len(subsequent):
+ 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,
+ 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
+ # newlines
+ line[0] = first + line[0] # Avoid two newlines if len == 1
+
+ if len(line) > 1:
+ line[0] = line[0] + "\n"
+ for i in range(1, (len(line[1:-1]) + 1)):
+ line[i] = subsequent + line[i] + "\n"
+ line[-1] = subsequent + line[-1] # Avoid two newlines on last line
+
+ if line[-1].isspace():
+ del line[-1] # Avoid trailing blank lines
+
+ result = "".join(line)
+
+ return result.encode("utf-8")
+
+
+def format_list(lst, first="", subsequent="", force_quiet=False):
+ """Feed elements of a list to format_line().
+
+ @see: format_line()
+ @type lst: list
+ @param lst: list to format
+ @type first: string
+ @param first: text to prepend to the first line
+ @type subsequent: string
+ @param subsequent: text to prepend to subsequent lines
+ @rtype: list
+ @return: list with element text wrapped at CONFIG['termWidth']
+ """
+
+ result = []
+ if lst:
+ # Format the first line
+ line = format_line(lst[0], first, subsequent, force_quiet)
+ result.append(line)
+ # Format subsequent lines
+ for elem in lst[1:]:
+ if elem:
+ result.append(format_line(elem, subsequent, subsequent,
+ force_quiet))
+ else:
+ # We don't want to send a blank line to format_line()
+ result.append("")
+ else:
+ if CONFIG['verbose']:
+ if force_quiet:
+ result = None
+ else:
+ # Send empty list, we'll get back first + `None specified'
+ result.append(format_line(lst, first, subsequent))
+
+ return result
+
+
+def get_reference_pkg(matches):
+ """Find a package in the Portage tree to reference."""
+
+ pkg = None
+ rev_matches = list(reversed(matches))
+ while rev_matches:
+ pkg = rev_matches.pop()
+ if not pkg.is_overlay():
+ break
+
+ return pkg
+
+
+def parse_module_options(module_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 ('-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'):
+ QUERY_OPTS["upstream"] = True
+ elif opt in ('-x', '--xml'):
+ QUERY_OPTS["xml"] = True
+
+
+def main(input_args):
+ """Parse input and run the program."""
+
+ 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:
+ 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:
+ print_help()
+ sys.exit(2)
+
+ 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:
diff --git a/gentoolkit/pym/gentoolkit/equery/size.py b/gentoolkit/pym/gentoolkit/equery/size.py
new file mode 100644
index 0000000..4d2a686
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/size.py
@@ -0,0 +1,193 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Print total size of files contained in a given package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "includeInstalled": True,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False,
+ "matchExact": False,
+ "printMatchInfo": False,
+ "sizeInBytes": False
+}
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+
+ # 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")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -b, --bytes", "report size in bytes"),
+ (" -f, --full-regex", "query is a regular expression")
+ ))
+
+
+def display_size(match_set):
+ """Display the total size of all accessible files owned by packages.
+
+ @type match_set: list
+ @param match_set: package cat/pkg-ver strings
+ """
+
+ for pkg in match_set:
+ size, files, uncounted = pkg.size()
+
+ if CONFIG['verbose']:
+ print " * %s" % pp.cpv(str(pkg.cpv))
+ print "Total files : %s".rjust(25) % pp.number(str(files))
+
+ if uncounted:
+ print ("Inaccessible files : %s".rjust(25) %
+ pp.number(str(uncounted)))
+
+ if QUERY_OPTS["sizeInBytes"]:
+ size_str = pp.number(str(size))
+ else:
+ size_str = "%s %s" % format_bytes(size)
+
+ print "Total size : %s".rjust(25) % size_str
+ else:
+ info = "%s: total(%d), inaccessible(%d), size(%s)"
+ print info % (str(pkg.cpv), files, uncounted, size)
+
+
+def format_bytes(bytes_, precision=2):
+ """Format bytes into human-readable format (IEC naming standard).
+
+ @see: http://mail.python.org/pipermail/python-list/2008-August/503423.html
+ @rtype: tuple
+ @return: (str(num), str(label))
+ """
+
+ labels = (
+ (1<<40L, 'TiB'),
+ (1<<30L, 'GiB'),
+ (1<<20L, 'MiB'),
+ (1<<10L, 'KiB'),
+ (1, 'bytes')
+ )
+
+ if bytes_ == 0:
+ return (pp.number('0'), 'bytes')
+ elif bytes_ == 1:
+ return (pp.number('1'), 'byte')
+
+ for factor, label in labels:
+ if not bytes_ >= factor:
+ continue
+
+ float_split = str(bytes_/float(factor)).split('.')
+ integer = float_split[0]
+ decimal = float_split[1]
+ if int(decimal[0:precision]):
+ float_string = '.'.join([integer, decimal[0:precision]])
+ else:
+ float_string = integer
+
+ return (pp.number(float_string), label)
+
+
+def parse_module_options(module_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 ('-b', '--bytes'):
+ QUERY_OPTS["sizeInBytes"] = True
+ elif opt in ('-e', '--exact-name'):
+ 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
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ # -e, --exact-name is no longer needed. Kept for compatibility.
+ # 04/09 djanderson
+ 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:
+ 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)
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ 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:
diff --git a/gentoolkit/pym/gentoolkit/equery/uses.py b/gentoolkit/pym/gentoolkit/equery/uses.py
new file mode 100644
index 0000000..8358d49
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/uses.py
@@ -0,0 +1,317 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Display USE flags for a given package"""
+
+# Move to imports section when Python 2.6 is stable
+from __future__ import with_statement
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from functools import partial
+from getopt import gnu_getopt, GetoptError
+from glob import glob
+
+from portage import settings
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, CONFIG
+from gentoolkit.helpers import find_best_match, find_packages, uniqify
+from gentoolkit.textwrap_ import TextWrapper
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {"allVersions" : False}
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name=__name__.split('.')[-1])
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -a, --all", "include all package versions")
+ ))
+
+
+def display_useflags(output):
+ """Print USE flag descriptions and statuses.
+
+ @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
+ """
+
+ maxflag_len = len(max([t[2] for t in output], key=len))
+
+ twrap = TextWrapper()
+ twrap.width = CONFIG['termWidth']
+ twrap.subsequent_indent = " " * (maxflag_len + 8)
+
+ markers = ("-", "+")
+ color = (
+ partial(pp.useflag, enabled=False), partial(pp.useflag, enabled=True)
+ )
+ for in_makeconf, in_installed, flag, desc, restrict in output:
+ if CONFIG['verbose']:
+ flag_name = ""
+ if in_makeconf != in_installed:
+ flag_name += pp.emph(" %s %s" %
+ (markers[in_makeconf], markers[in_installed]))
+ else:
+ flag_name += (" %s %s" %
+ (markers[in_makeconf], markers[in_installed]))
+
+ flag_name += " " + color[in_makeconf](flag.ljust(maxflag_len))
+ flag_name += " : "
+
+ # print description
+ if restrict:
+ restrict = "(%s %s)" % (pp.emph("Restricted to"),
+ pp.cpv(restrict))
+ twrap.initial_indent = flag_name
+ print twrap.fill(restrict)
+ if desc:
+ twrap.initial_indent = twrap.subsequent_indent
+ print twrap.fill(desc)
+ else:
+ print " : <unknown>"
+ else:
+ if desc:
+ twrap.initial_indent = flag_name
+ desc = twrap.fill(desc)
+ print desc
+ else:
+ twrap.initial_indent = flag_name
+ print twrap.fill("<unknown>")
+ else:
+ print markers[in_makeconf] + flag
+
+
+def get_global_useflags():
+ """Get global and expanded USE flag variables from
+ PORTDIR/profiles/use.desc and PORTDIR/profiles/desc/*.desc respectively.
+
+ @rtype: dict
+ @return: {'flag_name': 'flag description', ...}
+ """
+
+ global_usedesc = {}
+ # Get global USE flag descriptions
+ try:
+ path = os.path.join(settings["PORTDIR"], 'profiles', 'use.desc')
+ with open(path) as open_file:
+ for line in open_file:
+ if line.startswith('#'):
+ continue
+ # Ex. of fields: ['syslog', 'Enables support for syslog\n']
+ fields = line.split(" - ", 1)
+ if len(fields) == 2:
+ global_usedesc[fields[0]] = fields[1].rstrip()
+ except IOError:
+ 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(settings["PORTDIR"],
+ 'profiles', 'desc', '*.desc')):
+ try:
+ with open(path) as open_file:
+ for line in open_file:
+ if line.startswith('#'):
+ continue
+ fields = [field.strip() for field in line.split(" - ", 1)]
+ if len(fields) == 2:
+ expanded_useflag = "%s_%s" % \
+ (path.split("/")[-1][0:-5], fields[0])
+ global_usedesc[expanded_useflag] = fields[1]
+ except IOError:
+ sys.stderr.write(
+ pp.warn("Could not load USE flag descriptions from %s" % path)
+ )
+
+ return global_usedesc
+
+
+def get_matches(query):
+ """Get packages matching query."""
+
+ if not QUERY_OPTS["allVersions"]:
+ matches = [find_best_match(query)]
+ if None in matches:
+ matches = find_packages(query, include_masked=False)
+ if matches:
+ 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 = pkg.metadata.use()
+ iuse = pkg.environment("IUSE")
+
+ if iuse:
+ usevar = uniqify([x.lstrip('+-') for x in iuse.split()])
+ usevar.sort()
+ else:
+ usevar = []
+
+ if pkg.is_installed():
+ used_flags = pkg.use().split()
+ else:
+ 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_use.description
+ except AttributeError:
+ try:
+ desc = global_usedesc[flag]
+ except KeyError:
+ desc = ""
+
+ try:
+ restrict = local_use.restrict
+ restrict = restrict if restrict is not None else ""
+ except AttributeError:
+ restrict = ""
+
+ if flag in pkg.settings("USE").split():
+ inuse = True
+ if flag in used_flags:
+ inused = True
+
+ output.append((inuse, inused, flag, desc, restrict))
+
+ return output
+
+
+def parse_module_options(module_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 ('-a', '--all'):
+ QUERY_OPTS['allVersions'] = True
+
+
+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.useflag("set", enabled=True), pp.useflag("unset", enabled=False))
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "ha"
+ long_opts = ('help', 'all')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, 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)
+
+ #
+ # Output
+ #
+
+ first_run = True
+ legend_printed = False
+ for query in queries:
+ if not first_run:
+ print
+
+ if CONFIG['verbose']:
+ print " * Searching for %s ..." % pp.pkgquery(query)
+
+ matches = get_matches(query)
+ matches.sort()
+
+ global_usedesc = get_global_useflags()
+ for pkg in matches:
+
+ output = get_output_descriptions(pkg, global_usedesc)
+ if output:
+ if CONFIG['verbose']:
+ if not legend_printed:
+ print_legend()
+ legend_printed = True
+ print (" * Found these USE flags for %s:" %
+ pp.cpv(str(pkg.cpv)))
+ print pp.emph(" U I")
+ display_useflags(output)
+ else:
+ 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:
diff --git a/gentoolkit/pym/gentoolkit/equery/which.py b/gentoolkit/pym/gentoolkit/equery/which.py
new file mode 100644
index 0000000..be4f5e8
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/equery/which.py
@@ -0,0 +1,102 @@
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Display the path to the ebuild that would be used by Portage with the current
+configuration
+"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+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.helpers import find_packages
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {"includeMasked": False}
+
+# =========
+# 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
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="which")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -m, --include-masked", "return highest version ebuild available")
+ ))
+
+
+def parse_module_options(module_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 ('-m', '--include-masked'):
+ QUERY_OPTS['includeMasked'] = True
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hm"
+ long_opts = ('help', 'include-masked')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, 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)
+
+ for query in queries:
+
+ matches = find_packages(query, QUERY_OPTS['includeMasked'])
+ if matches:
+ pkg = sorted(matches).pop()
+ ebuild_path = pkg.ebuild_path()
+ if ebuild_path:
+ print os.path.normpath(ebuild_path)
+ else:
+ sys.stderr.write(
+ pp.warn("No ebuilds to satisfy %s" % pkg.cpv.name)
+ )
+ else:
+ raise errors.GentoolkitNoMatches(query)
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/errors.py b/gentoolkit/pym/gentoolkit/errors.py
new file mode 100644
index 0000000..9bcc5f9
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/errors.py
@@ -0,0 +1,114 @@
+# Copyright(c) 2004-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or later
+
+"""Exception classes for gentoolkit"""
+
+__all__ = (
+ 'GentoolkitException',
+ 'GentoolkitFatalError',
+ 'GentoolkitAmbiguousPackage',
+ 'GentoolkitInvalidAtom',
+ 'GentoolkitInvalidCategory',
+ 'GentoolkitInvalidPackage',
+ 'GentoolkitInvalidCPV',
+ 'GentoolkitInvalidRegex',
+ 'GentoolkitInvalidVersion',
+ 'GentoolkitNoMatches'
+)
+
+# ==========
+# Exceptions
+# ==========
+
+class GentoolkitException(Exception):
+ """Base class for gentoolkit exceptions."""
+ def __init__(self):
+ pass
+
+
+class GentoolkitFatalError(GentoolkitException):
+ """A fatal error occurred. Usually used to catch Portage exceptions."""
+ def __init__(self, err):
+ 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."""
+ def __init__(self, atom):
+ self.atom = atom
+
+ def __str__(self):
+ return "Invalid atom: '%s'" % self.atom
+
+
+class GentoolkitInvalidCategory(GentoolkitException):
+ """The category was not listed in portage.settings.categories."""
+ def __init__(self, category):
+ self.category = category
+
+ def __str__(self):
+ return "Invalid category: '%s'" % self.category
+
+
+class GentoolkitInvalidPackage(GentoolkitException):
+ """Got an unknown or invalid package."""
+ def __init__(self, package):
+ self.package = package
+
+ def __str__(self):
+ return "Invalid package: '%s'" % self.package
+
+
+class GentoolkitInvalidCPV(GentoolkitException):
+ """Got an invalid category/package-ver string."""
+ def __init__(self, cpv):
+ self.cpv = cpv
+
+ def __str__(self):
+ return "Invalid CPV: '%s'" % self.cpv
+
+
+class GentoolkitInvalidRegex(GentoolkitException):
+ """The regex could not be compiled."""
+ def __init__(self, regex):
+ self.regex = regex
+
+ def __str__(self):
+ return "Invalid regex: '%s'" % self.regex
+
+
+class GentoolkitInvalidVersion(GentoolkitException):
+ """Got a malformed version."""
+ def __init__(self, version):
+ 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, 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:
diff --git a/gentoolkit/pym/gentoolkit/glsa/__init__.py b/gentoolkit/pym/gentoolkit/glsa/__init__.py
new file mode 100644
index 0000000..11b7dbe
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/glsa/__init__.py
@@ -0,0 +1,726 @@
+# $Header$
+
+# This program is licensed under the GPL, version 2
+
+# WARNING: this code is only tested by a few people and should NOT be used
+# on production systems at this stage. There are possible security holes and probably
+# bugs in this code. If you test it please report ANY success or failure to
+# me (genone@gentoo.org).
+
+# The following planned features are currently on hold:
+# - getting GLSAs from http/ftp servers (not really useful without the fixed ebuilds)
+# - GPG signing/verification (until key policy is clear)
+
+__author__ = "Marius Mauch <genone@gentoo.org>"
+
+import os
+import sys
+import urllib
+import codecs
+import re
+import operator
+import xml.dom.minidom
+from StringIO import StringIO
+
+if sys.version_info[0:2] < (2,3):
+ raise NotImplementedError("Python versions below 2.3 have broken XML code " \
+ +"and are not supported")
+
+try:
+ import portage
+except ImportError:
+ sys.path.insert(0, "/usr/lib/portage/pym")
+ import portage
+
+# Note: the space for rgt and rlt is important !!
+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
+
+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
+ @param width: the minimum length of the returned string
+ @rtype: String
+ @return: the expanded string or I{text}
+ """
+ if len(text) >= width:
+ return text
+ margin = (width-len(text))/2
+ rValue = " "*margin
+ rValue += text
+ if 2*margin + len(text) == width:
+ rValue += " "*margin
+ elif 2*margin + len(text) + 1 == width:
+ rValue += " "*(margin+1)
+ return rValue
+
+
+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
+ 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
+ return value and the paragraph is indented up to
+ C{len(caption)}.
+ @rtype: String
+ @return: the wrapped and indented paragraph
+ """
+ rValue = ""
+ line = 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
+ line = " "*indentLevel
+ if len(line)+len(w.replace(NEWLINE_ESCAPE, ""))+1 > width:
+ rValue += line+"\n"
+ line = " "*indentLevel+w.replace(NEWLINE_ESCAPE, "\n")
+ elif w.find(NEWLINE_ESCAPE) >= 0:
+ if len(line.strip()) > 0:
+ rValue += line+" "+w.replace(NEWLINE_ESCAPE, "\n")
+ else:
+ rValue += line+w.replace(NEWLINE_ESCAPE, "\n")
+ line = " "*indentLevel
+ else:
+ if len(line.strip()) > 0:
+ line += " "+w
+ else:
+ line += w
+ if len(line) > 0:
+ rValue += line.replace(NEWLINE_ESCAPE, "\n")
+ rValue = rValue.replace(SPACE_ESCAPE, " ")
+ return rValue
+
+def checkconfig(myconfig):
+ """
+ takes a portage.config instance and adds GLSA specific keys if
+ they are not present. TO-BE-REMOVED (should end up in make.*)
+ """
+ mysettings = {
+ "GLSA_DIR": portage.settings["PORTDIR"]+"/metadata/glsa/",
+ "GLSA_PREFIX": "glsa-",
+ "GLSA_SUFFIX": ".xml",
+ "CHECKFILE": "/var/lib/portage/glsa_injected",
+ "GLSA_SERVER": "www.gentoo.org/security/en/glsa/", # not completely implemented yet
+ "CHECKMODE": "local", # not completely implemented yet
+ "PRINTWIDTH": "76"
+ }
+ for k in mysettings.keys():
+ if k not in myconfig:
+ myconfig[k] = mysettings[k]
+ return myconfig
+
+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
+ """
+ # TODO: remote fetch code for listing
+
+ rValue = []
+
+ if not os.access(repository, os.R_OK):
+ return []
+ 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:
+ rValue.append(f[len(prefix):-1*len(suffix)])
+ except IndexError:
+ pass
+ return rValue
+
+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
+ @return: a list that contains the value of the <li> elements
+ """
+ if not listnode.nodeName in ["ul", "ol"]:
+ raise GlsaFormatException("Invalid function call: listnode is not <ul> or <ol>")
+ rValue = [getText(li, format="strip") \
+ for li in listnode.childNodes \
+ if li.nodeType == xml.dom.Node.ELEMENT_NODE]
+ return rValue
+
+def getText(node, format, textfd = None):
+ """
+ This is the main parser function. It takes a node and traverses
+ recursive over the subnodes, getting the text of each (and the
+ I{link} attribute for <uri> and <mail>). Depending on the I{format}
+ 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
+ @param format: this should be either I{strip}, I{keep} or I{xml}
+ I{keep} just gets the text and does no formatting.
+ I{strip} replaces newlines and tabs with spaces and
+ replaces multiple spaces with one space.
+ I{xml} does some more formatting, depending on the
+ type of the encountered nodes.
+ @type textfd: writable file-like object
+ @param textfd: the file-like object to write the output to
+ @rtype: String
+ @return: the (formatted) content of the node and its subnodes
+ except if textfd was not none
+ """
+ if not textfd:
+ textfd = StringIO()
+ returnNone = False
+ else:
+ returnNone = True
+ if format in ["strip", "keep"]:
+ if node.nodeName in ["uri", "mail"]:
+ textfd.write(node.childNodes[0].data+": "+node.getAttribute("link"))
+ else:
+ for subnode in node.childNodes:
+ if subnode.nodeName == "#text":
+ textfd.write(subnode.data)
+ else:
+ getText(subnode, format, textfd)
+ else: # format = "xml"
+ for subnode in node.childNodes:
+ if subnode.nodeName == "p":
+ for p_subnode in subnode.childNodes:
+ if p_subnode.nodeName == "#text":
+ textfd.write(p_subnode.data.strip())
+ elif p_subnode.nodeName in ["uri", "mail"]:
+ textfd.write(p_subnode.childNodes[0].data)
+ textfd.write(" ( "+p_subnode.getAttribute("link")+" )")
+ textfd.write(NEWLINE_ESCAPE)
+ elif subnode.nodeName == "ul":
+ for li in getListElements(subnode):
+ textfd.write("-"+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" ")
+ elif subnode.nodeName == "ol":
+ i = 0
+ for li in getListElements(subnode):
+ i = i+1
+ textfd.write(str(i)+"."+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" ")
+ elif subnode.nodeName == "code":
+ textfd.write(getText(subnode, format="keep").lstrip().replace("\n", NEWLINE_ESCAPE))
+ textfd.write(NEWLINE_ESCAPE)
+ elif subnode.nodeName == "#text":
+ textfd.write(subnode.data)
+ else:
+ raise GlsaFormatException("Invalid Tag found: ", subnode.nodeName)
+ if returnNone:
+ return None
+ rValue = textfd.getvalue()
+ if format == "strip":
+ rValue = rValue.strip(" \n\t")
+ rValue = re.sub("[\s]{2,}", " ", rValue)
+ return rValue
+
+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
+ @param tagname: the name of the tags to search for
+ @type format: String
+ @param format: see L{getText}
+ @rtype: List of Strings
+ @return: a list containing the text of all I{tagname} childnodes
+ """
+ rValue = [getText(e, format) \
+ for e in rootnode.getElementsByTagName(tagname)]
+ return rValue
+
+def makeAtom(pkgname, versionNode):
+ """
+ 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
+ @param versionNode: a <vulnerable> or <unaffected> Node that
+ contains the version information for this atom
+ @rtype: String
+ @return: the portage atom
+ """
+ rValue = opMapping[versionNode.getAttribute("range")] \
+ + pkgname \
+ + "-" + getText(versionNode, format="strip")
+ try:
+ slot = versionNode.getAttribute("slot").strip()
+ except KeyError:
+ pass
+ else:
+ if slot and slot != "*":
+ rValue += ":" + slot
+ return str(rValue)
+
+def makeVersion(versionNode):
+ """
+ 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
+ @rtype: String
+ @return: the version string
+ """
+ rValue = opMapping[versionNode.getAttribute("range")] \
+ +getText(versionNode, format="strip")
+ try:
+ slot = versionNode.getAttribute("slot").strip()
+ except KeyError:
+ pass
+ else:
+ if slot and slot != "*":
+ rValue += ":" + slot
+ return rValue
+
+def match(atom, portdbname, match_type="default"):
+ """
+ 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
+ to apply the wanted visibility filters
+
+ @rtype: list of strings
+ @return: a list with the matching versions
+ """
+ db = portage.db["/"][portdbname].dbapi
+ if atom[2] == "~":
+ return revisionMatch(atom, db, match_type=match_type)
+ elif match_type == "default" or not hasattr(db, "xmatch"):
+ return db.match(atom)
+ else:
+ return db.xmatch(match_type, atom)
+
+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
+ to apply the wanted visibility filters
+
+ @rtype: list of strings
+ @return: a list with the matching versions
+ """
+ if match_type == "default" or not hasattr(portdb, "xmatch"):
+ if ":" in revisionAtom:
+ mylist = portdb.match(re.sub(r'-r[0-9]+(:[^ ]+)?$', r'\1', revisionAtom[2:]))
+ else:
+ mylist = portdb.match(re.sub("-r[0-9]+$", "", revisionAtom[2:]))
+ else:
+ if ":" in revisionAtom:
+ mylist = portdb.xmatch(match_type, re.sub(r'-r[0-9]+(:[^ ]+)?$', r'\1', revisionAtom[2:]))
+ else:
+ mylist = portdb.xmatch(match_type, re.sub("-r[0-9]+$", "", revisionAtom[2:]))
+ rValue = []
+ for v in mylist:
+ r1 = portage.pkgsplit(v)[-1][1:]
+ r2 = portage.pkgsplit(revisionAtom[3:])[-1][1:]
+ if eval(r1+" "+revisionAtom[0:2]+" "+r2):
+ rValue.append(v)
+ return rValue
+
+
+def getMinUpgrade(vulnerableList, unaffectedList, minimize=True):
+ """
+ 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: 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)))
+
+ if not v_installed:
+ return None
+
+ # this tuple holds all vulnerable atoms, and the related upgrade atom
+ vuln_update = []
+ avail_updates = set()
+ for u in unaffectedList:
+ # TODO: This had match_type="match-all" before. I don't think it should
+ # since we disregarded masked items later anyway (match(=rValue, "porttree"))
+ avail_updates.update(match(u, "porttree"))
+ # if an atom is already installed, we should not consider it for upgrades
+ avail_updates.difference_update(u_installed)
+
+ for vuln in v_installed:
+ update = ""
+ 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 \
+ 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]
+ vuln_update.append([vuln, update])
+
+ return vuln_update
+
+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
+ @return: a reformatted string, or the original string
+ if it cannot be reformatted.
+ """
+ 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")
+
+# simple Exception classes to catch specific errors
+class GlsaTypeException(Exception):
+ def __init__(self, doctype):
+ Exception.__init__(self, "wrong DOCTYPE: %s" % doctype)
+
+class GlsaFormatException(Exception):
+ pass
+
+class GlsaArgumentException(Exception):
+ pass
+
+# GLSA xml data wrapper class
+class Glsa:
+ """
+ This class is a wrapper for the XML data and provides methods to access
+ and display the contained data.
+ """
+ def __init__(self, myid, myconfig):
+ """
+ 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
+ filename containing a GLSA.
+ @type myconfig: portage.config
+ @param myconfig: the config that should be used for this object.
+ """
+ if re.match(r'\d{6}-\d{2}', myid):
+ self.type = "id"
+ elif os.path.exists(myid):
+ self.type = "file"
+ else:
+ raise GlsaArgumentException("Given ID "+myid+" isn't a valid GLSA ID or filename.")
+ self.nr = myid
+ self.config = myconfig
+ self.read()
+
+ def read(self):
+ """
+ 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
+ """
+ if self.config["CHECKMODE"] == "local":
+ repository = "file://" + self.config["GLSA_DIR"]
+ else:
+ repository = self.config["GLSA_SERVER"]
+ if self.type == "file":
+ myurl = "file://"+self.nr
+ else:
+ myurl = repository + self.config["GLSA_PREFIX"] + str(self.nr) + self.config["GLSA_SUFFIX"]
+ self.parse(urllib.urlopen(myurl))
+ return None
+
+ def parse(self, myfile):
+ """
+ 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
+ @returns: None
+ """
+ self.DOM = xml.dom.minidom.parse(myfile)
+ if not self.DOM.doctype:
+ raise GlsaTypeException(None)
+ elif self.DOM.doctype.systemId == "http://www.gentoo.org/dtd/glsa.dtd":
+ self.dtdversion = 0
+ elif self.DOM.doctype.systemId == "http://www.gentoo.org/dtd/glsa-2.dtd":
+ self.dtdversion = 2
+ else:
+ raise GlsaTypeException(self.DOM.doctype.systemId)
+ myroot = self.DOM.getElementsByTagName("glsa")[0]
+ if self.type == "id" and myroot.getAttribute("id") != self.nr:
+ raise GlsaFormatException("filename and internal id don't match:" + myroot.getAttribute("id") + " != " + self.nr)
+
+ # the simple (single, required, top-level, #PCDATA) tags first
+ 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>
+ # <revised count="2">2007-12-30</revised>
+ revisedEl = myroot.getElementsByTagName("revised")[0]
+ self.revised = getText(revisedEl, format="strip")
+ if (revisedEl.attributes.has_key("count")):
+ 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")
+ except IndexError:
+ 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")
+ self.resolution = getText(myroot.getElementsByTagName("resolution")[0], format="xml")
+ self.impact_text = getText(myroot.getElementsByTagName("impact")[0], format="xml")
+ self.impact_type = myroot.getElementsByTagName("impact")[0].getAttribute("type")
+ try:
+ self.background = getText(myroot.getElementsByTagName("background")[0], format="xml")
+ except IndexError:
+ self.background = ""
+
+ # finally the interesting tags (product, affected, package)
+ self.glsatype = myroot.getElementsByTagName("product")[0].getAttribute("type")
+ self.product = getText(myroot.getElementsByTagName("product")[0], format="strip")
+ self.affected = myroot.getElementsByTagName("affected")[0]
+ self.packages = {}
+ for p in self.affected.getElementsByTagName("package"):
+ name = p.getAttribute("name")
+ if not self.packages.has_key(name):
+ self.packages[name] = []
+ tmp = {}
+ tmp["arch"] = p.getAttribute("arch")
+ tmp["auto"] = (p.getAttribute("auto") == "yes")
+ tmp["vul_vers"] = [makeVersion(v) for v in p.getElementsByTagName("vulnerable")]
+ tmp["unaff_vers"] = [makeVersion(v) for v in p.getElementsByTagName("unaffected")]
+ tmp["vul_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("vulnerable")]
+ tmp["unaff_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("unaffected")]
+ self.packages[name].append(tmp)
+ # TODO: services aren't really used yet
+ self.services = self.affected.getElementsByTagName("service")
+ return None
+
+ def dump(self, outstream=sys.stdout, encoding="utf-8"):
+ """
+ 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)
+ """
+ outstream = codecs.getwriter(encoding)(outstream)
+ width = int(self.config["PRINTWIDTH"])
+ outstream.write(center("GLSA %s: \n%s" % (self.nr, self.title), width)+"\n")
+ outstream.write((width*"=")+"\n")
+ outstream.write(wrap(self.synopsis, width, caption="Synopsis: ")+"\n")
+ outstream.write("Announced on: %s\n" % self.announced)
+ outstream.write("Last revised on: %s : %02d\n\n" % (self.revised, self.count))
+ if self.glsatype == "ebuild":
+ for k in self.packages.keys():
+ pkg = self.packages[k]
+ for path in pkg:
+ vul_vers = "".join(path["vul_vers"])
+ unaff_vers = "".join(path["unaff_vers"])
+ outstream.write("Affected package: %s\n" % k)
+ outstream.write("Affected archs: ")
+ if path["arch"] == "*":
+ outstream.write("All\n")
+ else:
+ outstream.write("%s\n" % path["arch"])
+ outstream.write("Vulnerable: %s\n" % vul_vers)
+ outstream.write("Unaffected: %s\n\n" % unaff_vers)
+ elif self.glsatype == "infrastructure":
+ pass
+ if len(self.bugs) > 0:
+ outstream.write("\nRelated bugs: ")
+ outstream.write(", ".join(self.bugs))
+ outstream.write("\n")
+ if self.background:
+ outstream.write("\n"+wrap(self.background, width, caption="Background: "))
+ outstream.write("\n"+wrap(self.description, width, caption="Description: "))
+ outstream.write("\n"+wrap(self.impact_text, width, caption="Impact: "))
+ outstream.write("\n"+wrap(self.workaround, width, caption="Workaround: "))
+ outstream.write("\n"+wrap(self.resolution, width, caption="Resolution: "))
+ 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
+ """
+ rValue = False
+ for k in self.packages.keys():
+ pkg = self.packages[k]
+ for path in pkg:
+ if path["arch"] == "*" or self.config["ARCH"] in path["arch"].split():
+ for v in path["vul_atoms"]:
+ 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
+ """
+ if not os.access(self.config["CHECKFILE"], os.R_OK):
+ return False
+ aList = portage.grabfile(self.config["CHECKFILE"])
+ return (self.nr in aList)
+
+ 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
+ applied or on explicit user request.
+
+ @rtype: None
+ @returns: None
+ """
+ if not self.isInjected():
+ checkfile = open(self.config["CHECKFILE"], "a+")
+ 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
+ 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
+ @rtype: List of Strings
+ @return: list of package-versions that have to be merged
+ """
+ return list(set(update for (vuln, update) in self.getAffectionTable(least_change) if update))
+
+ def getAffectionTable(self, least_change=True):
+ """
+ Will initialize the self.systemAffection list of
+ atoms installed on the system that are affected
+ by this GLSA, and the atoms that are minimal upgrades.
+ """
+ systemAffection = []
+ for pkg in self.packages.keys():
+ for path in self.packages[pkg]:
+ update = getMinUpgrade(path["vul_atoms"], path["unaff_atoms"], minimize=least_change)
+ if update:
+ systemAffection.extend(update)
+ return systemAffection
diff --git a/gentoolkit/pym/gentoolkit/helpers.py b/gentoolkit/pym/gentoolkit/helpers.py
new file mode 100644
index 0000000..1f25666
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/helpers.py
@@ -0,0 +1,709 @@
+# Copyright 2009-2010 Gentoo Foundation
+#
+# 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 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()
+ self.full = self.entries
+ self.latest = self.entries[0]
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.changelog_path)
+
+ 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.cp = atom.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
+
+ pkgset = get_installed_cpvs()
+
+ return self.find_owners(query_re, use_match=use_match, pkgset=pkgset)
+
+ 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
+ for pkg in sorted([Package(x) for x in pkgset]):
+ files = pkg.parsed_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:
+ 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:
+ 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 CPV descriptor from gentoolkit.cpv:
+ >>> cpvs = sorted(CPV(x) for x in package_list)
+
+ @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:
+ 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 []
+
+ try:
+ if include_masked:
+ matches = PORTDB.xmatch("match-all", query)
+ else:
+ 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:
+ 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:
+ result = list(set(seq))
+
+ return result
+
+
+def uses_globbing(query):
+ """Check the query to see if it is using globbing.
+
+ @type query: str
+ @param query: user input package query
+ @rtype: bool
+ @return: True if query uses globbing, else False
+ """
+
+ 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/gentoolkit/pym/gentoolkit/metadata.py b/gentoolkit/pym/gentoolkit/metadata.py
new file mode 100644
index 0000000..93538b3
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/metadata.py
@@ -0,0 +1,307 @@
+#!/usr/bin/python
+#
+# Copyright 2009-2010 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.herds()
+ ['no-herd']
+ >>> for maint in pkg_md.maintainers():
+ ... print "{0} ({1})".format(maint.email, maint.name)
+ ...
+ nixphoeni@gentoo.org (Joe Sapp)
+ >>> for flag in pkg_md.use():
+ ... print flag.name, "->", flag.description
+ ...
+ rtf -> Enable export to RTF
+ gnome-print -> Enable printing support using gnome-print
+ >>> upstream = pkg_md.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. Should be DEPEND string with < and >
+ converted to &lt; and &gt; respectively.
+ @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.upstream_maintainers()
+ self.changelogs = self.upstream_changelogs()
+ self.docs = self.upstream_documentation()
+ self.bugtrackers = self.upstream_bugtrackers()
+ self.remoteids = self.upstream_remoteids()
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.__dict__)
+
+ def upstream_bugtrackers(self):
+ """Retrieve upstream bugtracker location from xml node."""
+ return [e.text for e in self.node.findall('bugs-to')]
+
+ def upstream_changelogs(self):
+ """Retrieve upstream changelog location from xml node."""
+ return [e.text for e in self.node.findall('changelog')]
+
+ def 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 upstream_maintainers(self):
+ """Retrieve upstream maintainer information from xml node."""
+ return [_Maintainer(m) for m in self.node.findall('maintainer')]
+
+ def 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
+ @param metadata_path: path to a valid metadata.xml file
+ @raise IOError: if C{metadata_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')
+ try:
+ self._herdstree = etree.parse(herds_path)
+ except IOError:
+ # For some trees, herds.xml may not exist. Bug #300108.
+ 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 self._herdstree.getiterator('herd'):
+ if node.findtext('name') == herd:
+ return node.findtext('email')
+
+ def 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 strings;
+ 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 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
+
+ long_descriptions = self._xml_tree.findall("longdescription")
+ self._descriptions = [e.text for e in long_descriptions]
+ return self._descriptions
+
+ def 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 use(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 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:
diff --git a/gentoolkit/pym/gentoolkit/package.py b/gentoolkit/pym/gentoolkit/package.py
new file mode 100644
index 0000000..fb68965
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/package.py
@@ -0,0 +1,461 @@
+#!/usr/bin/python
+#
+# Copyright 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright 2004-2010 Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+"""Provides an interface to package information stored by package managers.
+
+The Package class is the heart of much of Gentoolkit. Given a CPV
+(category/package-version) string, it can reveal the package's status in the
+tree and VARDB (/var/db/), provide rich comparison and sorting, and expose
+important parts of Portage's back-end.
+
+Example usage:
+ >>> portage = Package('sys-apps/portage-2.1.6.13')
+ >>> portage.ebuild_path()
+ '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild'
+ >>> portage.is_masked()
+ False
+ >>> portage.is_installed()
+ True
+"""
+
+__all__ = (
+ 'Package',
+ 'PackageFormatter'
+)
+
+# =======
+# Imports
+# =======
+
+import os
+
+import portage
+from portage import settings
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.cpv import CPV
+from gentoolkit.dbapi import PORTDB, VARDB
+from gentoolkit.dependencies import Dependencies
+from gentoolkit.metadata import MetaData
+
+# =======
+# Classes
+# =======
+
+class Package(CPV):
+ """Exposes the state of a given CPV."""
+
+ def __init__(self, cpv):
+ if isinstance(cpv, CPV):
+ self.__dict__.update(cpv.__dict__)
+ else:
+ CPV.__init__(self, cpv)
+ del cpv
+
+ if not all(hasattr(self, x) for x in ('category', 'version')):
+ # CPV allows some things that Package must not
+ raise errors.GentoolkitInvalidPackage(self.cpv)
+
+ # Set dynamically
+ self._package_path = None
+ self._dblink = None
+ self._metadata = None
+ self._deps = None
+ self._portdir_path = None
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.cpv)
+
+ def __hash__(self):
+ return hash(self.cpv)
+
+ def __contains__(self, key):
+ return key in self.cpv
+
+ def __str__(self):
+ return self.cpv
+
+ @property
+ def metadata(self):
+ """Instantiate a L{gentoolkit.metadata.MetaData} object here."""
+
+ if self._metadata is None:
+ metadata_path = os.path.join(
+ self.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.category,
+ "%s-%s" % (self.name, self.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)
+
+ return self._deps
+
+ def environment(self, envvars, prefer_vdb=True, fallback=True):
+ """Returns one or more of the predefined environment variables.
+
+ Available envvars are:
+ ----------------------
+ BINPKGMD5 COUNTER FEATURES LICENSE SRC_URI
+ CATEGORY CXXFLAGS HOMEPAGE PDEPEND USE
+ CBUILD DEFINED_PHASES INHERITED PF
+ CFLAGS DEPEND IUSE PROVIDE
+ CHOST DESCRIPTION KEYWORDS RDEPEND
+ CONTENTS EAPI LDFLAGS SLOT
+
+ Example usage:
+ >>> pkg = Package('sys-apps/portage-2.1.6.13')
+ >>> pkg.environment('USE')
+ 'elibc_glibc kernel_linux userland_GNU x86'
+ >>> pkg.environment(('USE', 'IUSE'))
+ ['elibc_glibc kernel_linux userland_GNU x86',
+ 'build doc epydoc selinux linguas_pl']
+
+ @type envvars: str or array
+ @param envvars: one or more of (DEPEND, SRC_URI, etc.)
+ @type prefer_vdb: bool
+ @keyword prefer_vdb: if True, look in the vardb before portdb, else
+ reverse order. Specifically KEYWORDS will get more recent
+ information by preferring portdb.
+ @type fallback: bool
+ @keyword fallback: query only the preferred db if False
+ @rtype: str or list
+ @return: str if envvars is str, list if envvars is array
+ @raise KeyError: if key is not found in requested db(s)
+ """
+
+ got_string = False
+ if isinstance(envvars, basestring):
+ got_string = True
+ envvars = (envvars,)
+ if prefer_vdb:
+ try:
+ result = VARDB.aux_get(self.cpv, envvars)
+ except KeyError:
+ try:
+ if not fallback:
+ raise KeyError
+ result = PORTDB.aux_get(self.cpv, envvars)
+ except KeyError:
+ err = "aux_get returned unexpected results"
+ raise errors.GentoolkitFatalError(err)
+ else:
+ try:
+ result = PORTDB.aux_get(self.cpv, envvars)
+ except KeyError:
+ try:
+ if not fallback:
+ raise KeyError
+ result = VARDB.aux_get(self.cpv, envvars)
+ except KeyError:
+ err = "aux_get returned unexpected results"
+ raise errors.GentoolkitFatalError(err)
+
+ if got_string:
+ return result[0]
+ return result
+
+ def exists(self):
+ """Return True if package exists in the Portage tree, else False"""
+
+ return bool(PORTDB.cpv_exists(self.cpv))
+
+ @staticmethod
+ def settings(key):
+ """Returns the value of the given key for this package (useful
+ for package.* files."""
+
+ if settings.locked:
+ settings.unlock()
+ try:
+ result = settings[key]
+ finally:
+ settings.lock()
+ return result
+
+ def 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(self.cpv,
+ settings=settings,
+ portdb=PORTDB)
+ except KeyError:
+ # getmaskingstatus doesn't support packages without ebuilds in the
+ # Portage tree.
+ result = None
+
+ return result
+
+ def 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(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 ebuild_path(self, in_vartree=False):
+ """Returns the complete path to the .ebuild file.
+
+ Example usage:
+ >>> pkg.ebuild_path()
+ '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild'
+ >>> pkg.ebuild_path(in_vartree=True)
+ '/var/db/pkg/sys-apps/portage-2.1.6.13/portage-2.1.6.13.ebuild'
+ """
+
+ if in_vartree:
+ return VARDB.findname(self.cpv)
+ return PORTDB.findname(self.cpv)
+
+ def package_path(self, in_vartree=False):
+ """Return the path to where the ebuilds and other files reside."""
+
+ if in_vartree:
+ return self.dblink.getpath()
+ return os.sep.join(self.ebuild_path().split(os.sep)[:-1])
+
+ def repo_name(self, fallback=True):
+ """Determine the repository name.
+
+ @type fallback: bool
+ @param fallback: if the repo_name file does not exist, return the
+ repository name from the path
+ @rtype: str
+ @return: output of the repository metadata file, which stores the
+ repo_name variable, or try to get the name of the repo from
+ the path.
+ @raise GentoolkitFatalError: if fallback is False and repo_name is
+ not specified by the repository.
+ """
+
+ try:
+ return self.environment('repository')
+ except errors.GentoolkitFatalError:
+ if fallback:
+ return self.package_path().split(os.sep)[-3]
+ raise
+
+ def use(self):
+ """Returns the USE flags active at time of installation."""
+
+ return self.dblink.getstring("USE")
+
+ def parsed_contents(self):
+ """Returns the parsed CONTENTS file.
+
+ @rtype: dict
+ @return: {'/full/path/to/obj': ['type', 'timestamp', 'md5sum'], ...}
+ """
+
+ return self.dblink.getcontents()
+
+ def size(self):
+ """Estimates the installed size of the contents of this package.
+
+ @rtype: tuple
+ @return: (size, number of files in total, number of uncounted files)
+ """
+
+ seen = set()
+ content_stats = (os.lstat(x) for x in self.parsed_contents())
+ # Remove hardlinks by checking for duplicate inodes. Bug #301026.
+ unique_file_stats = (x for x in content_stats if x.st_ino not in seen
+ and not seen.add(x.st_ino))
+ size = n_uncounted = n_files = 0
+ for st in unique_file_stats:
+ try:
+ size += st.st_size
+ n_files += 1
+ except OSError:
+ n_uncounted += 1
+ return (size, n_files, n_uncounted)
+
+ def is_installed(self):
+ """Returns True if this package is installed (merged)"""
+
+ return self.dblink.exists()
+
+ def is_overlay(self):
+ """Returns True if the package is in an overlay."""
+
+ ebuild, tree = PORTDB.findname2(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", self.cpv)
+ return 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.helpers import find_packages
+ >>> from gentoolkit.package import PackageFormatter
+ >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')]
+ >>> for pkg in pkgs:
+ ... # Only print packages that are installed and from the Portage
+ ... # 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.
+ Essentially C{format} should be set to False when piping or when
+ quiet output is desired. If C{do_format} is False, only the location
+ attribute will be created to save time.
+ """
+
+ def __init__(self, pkg, do_format=True):
+ self.pkg = pkg
+ 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.do_format:
+ maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-', '??']
+ maskmode = maskmodes[self.format_mask_status()[0]]
+ return "[%(location)s] [%(mask)s] %(package)s:%(slot)s" % {
+ 'location': self.location,
+ 'mask': pp.keyword(
+ maskmode,
+ stable=not maskmode.strip(),
+ hard_masked=set(('M', '?', '-')).intersection(maskmode)
+ ),
+ 'package': pp.cpv(str(self.pkg.cpv)),
+ 'slot': pp.slot(self.pkg.environment("SLOT"))
+ }
+ else:
+ return str(self.pkg.cpv)
+
+ 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
+ 'I-O' : Installed and from an overlay
+ """
+
+ result = ['-', '-', '-']
+
+ if self.pkg.is_installed():
+ result[0] = 'I'
+
+ overlay = self.pkg.is_overlay()
+ if overlay is None:
+ pass
+ elif overlay:
+ result[2] = 'O'
+ else:
+ result[1] = 'P'
+
+ return ''.join(result)
+
+ 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-", "??"]
+ 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
+ """
+
+ result = 0
+ masking_status = self.pkg.mask_status()
+ if masking_status is None:
+ return (6, [])
+
+ if ("~%s keyword" % self.pkg.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)
+
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/pprinter.py b/gentoolkit/pym/gentoolkit/pprinter.py
new file mode 100644
index 0000000..c070a0f
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/pprinter.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+#
+# Copyright 2004 Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright 2004-2010 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',
+ 'number',
+ 'path',
+ 'path_symlink',
+ 'pkgquery',
+ 'productname',
+ 'regexpquery',
+ 'section',
+ 'slot',
+ 'subsection',
+ 'useflag',
+ 'warn'
+)
+
+# =======
+# Imports
+# =======
+
+import sys
+
+import portage.output as output
+
+# =========
+# Functions
+# =========
+
+# output creates color functions on the fly, which confuses pylint.
+# E1101: *%s %r has no %r member*
+# pylint: disable-msg=E1101
+
+def command(string):
+ """Returns a program command string."""
+ return output.green(string)
+
+def cpv(string):
+ """Returns a category/package-<version> string."""
+ return output.green(string)
+
+def die(err, string):
+ """Returns an error string and die with an error code."""
+ sys.stderr.write(error(string))
+ sys.exit(err)
+
+def emph(string):
+ """Returns a string as emphasized."""
+ return output.bold(string)
+
+def error(string):
+ """Prints an error string."""
+ return output.red("!!! ") + string + "\n"
+
+def globaloption(string):
+ """Returns a global option string, i.e. the program global options."""
+ return output.yellow(string)
+
+def localoption(string):
+ """Returns a local option string, i.e. the program local options."""
+ return output.green(string)
+
+def number(string):
+ """Returns a number string."""
+ return output.turquoise(string)
+
+def path(string):
+ """Returns a file or directory path string."""
+ return output.bold(string)
+
+def path_symlink(string):
+ """Returns a symlink string."""
+ return output.turquoise(string)
+
+def pkgquery(string):
+ """Returns a package query string."""
+ return output.bold(string)
+
+def productname(string):
+ """Returns a product name string, i.e. the program name."""
+ return output.turquoise(string)
+
+def regexpquery(string):
+ """Returns a regular expression string."""
+ return output.bold(string)
+
+def section(string):
+ """Returns a string as a section header."""
+ return output.turquoise(string)
+
+def slot(string):
+ """Returns a slot string"""
+ return output.bold(string)
+
+def subsection(string):
+ """Returns a string as a subsection header."""
+ return output.turquoise(string)
+
+def useflag(string, enabled=True):
+ """Returns a USE flag string."""
+ return output.blue(string) if enabled else output.red(string)
+
+def keyword(string, stable=True, hard_masked=False):
+ """Returns a keyword string."""
+ if stable:
+ return output.green(string)
+ if hard_masked:
+ return output.red(string)
+ # keyword masked:
+ return output.blue(string)
+
+def warn(string):
+ """Returns a warning string."""
+ return "!!! " + string + "\n"
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/query.py b/gentoolkit/pym/gentoolkit/query.py
new file mode 100644
index 0000000..4802bc1
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/query.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+#
+# Copyright 2004-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+"""Provides common methods on a package query."""
+
+__all__ = (
+ 'Query',
+)
+
+# =======
+# Imports
+# =======
+
+from gentoolkit.cpv import CPV
+#from gentoolkit.helpers import *
+
+# =======
+# Classes
+# =======
+
+class Query(CPV):
+ """Provides common methods on a package query."""
+
+ def __init__(self, cpv):
+ if isinstance(cpv, CPV):
+ self.cpv = cpv
+ else:
+ self.cpv = CPV(cpv)
+ del cpv
diff --git a/gentoolkit/pym/gentoolkit/test/__init__.py b/gentoolkit/pym/gentoolkit/test/__init__.py
new file mode 100644
index 0000000..901e478
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/__init__.py
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+# Copyright 2009-2010 Gentoo Foundation
+#
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
diff --git a/gentoolkit/pym/gentoolkit/test/equery/__init__.py b/gentoolkit/pym/gentoolkit/test/equery/__init__.py
new file mode 100644
index 0000000..901e478
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/equery/__init__.py
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+# Copyright 2009-2010 Gentoo Foundation
+#
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
diff --git a/gentoolkit/pym/gentoolkit/test/equery/test_init.py b/gentoolkit/pym/gentoolkit/test/equery/test_init.py
new file mode 100644
index 0000000..98e2648
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/equery/test_init.py
@@ -0,0 +1,46 @@
+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_main():
+ test_support.run_unittest(TestEqueryInit)
+
+
+if __name__ == '__main__':
+ test_main()
diff --git a/gentoolkit/pym/gentoolkit/test/test_atom.py b/gentoolkit/pym/gentoolkit/test/test_atom.py
new file mode 100644
index 0000000..0c5a786
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/test_atom.py
@@ -0,0 +1,149 @@
+# Copyright(c) 2009-2010, Gentoo Foundation, Inc.
+# Copyright: 2006-2008 Brian Harring <ferringb@gmail.com>
+#
+# License: GPL2/BSD
+
+# $Header$
+
+import unittest
+from test import test_support
+
+from gentoolkit.atom import *
+
+"""Atom test suite (verbatim) from pkgcore."""
+
+class TestGentoolkitAtom(unittest.TestCase):
+
+ def assertEqual2(self, o1, o2):
+ # logic bugs hidden behind short circuiting comparisons for metadata
+ # is why we test the comparison *both* ways.
+ self.assertEqual(o1, o2)
+ c = cmp(o1, o2)
+ self.assertEqual(c, 0,
+ msg="checking cmp for %r, %r, aren't equal: got %i" % (o1, o2, c))
+ self.assertEqual(o2, o1)
+ c = cmp(o2, o1)
+ self.assertEqual(c, 0,
+ msg="checking cmp for %r, %r,aren't equal: got %i" % (o2, o1, c))
+
+ def assertNotEqual2(self, o1, o2):
+ # is why we test the comparison *both* ways.
+ self.assertNotEqual(o1, o2)
+ c = cmp(o1, o2)
+ self.assertNotEqual(c, 0,
+ msg="checking cmp for %r, %r, not supposed to be equal, got %i"
+ % (o1, o2, c))
+ self.assertNotEqual(o2, o1)
+ c = cmp(o2, o1)
+ self.assertNotEqual(c, 0,
+ msg="checking cmp for %r, %r, not supposed to be equal, got %i"
+ % (o2, o1, c))
+
+ def test_comparison(self):
+ self.assertEqual2(Atom('cat/pkg'), Atom('cat/pkg'))
+ self.assertNotEqual2(Atom('cat/pkg'), Atom('cat/pkgb'))
+ self.assertNotEqual2(Atom('cata/pkg'), Atom('cat/pkg'))
+ self.assertNotEqual2(Atom('cat/pkg'), Atom('!cat/pkg'))
+ self.assertEqual2(Atom('!cat/pkg'), Atom('!cat/pkg'))
+ self.assertNotEqual2(Atom('=cat/pkg-0.1:0'),
+ Atom('=cat/pkg-0.1'))
+ self.assertNotEqual2(Atom('=cat/pkg-1[foon]'),
+ Atom('=cat/pkg-1'))
+ self.assertEqual2(Atom('=cat/pkg-0'), Atom('=cat/pkg-0'))
+ self.assertNotEqual2(Atom('<cat/pkg-2'), Atom('>cat/pkg-2'))
+ self.assertNotEqual2(Atom('=cat/pkg-2*'), Atom('=cat/pkg-2'))
+ # Portage Atom doesn't have 'negate_version' capability
+ #self.assertNotEqual2(Atom('=cat/pkg-2', True), Atom('=cat/pkg-2'))
+
+ # use...
+ self.assertNotEqual2(Atom('cat/pkg[foo]'), Atom('cat/pkg'))
+ self.assertNotEqual2(Atom('cat/pkg[foo]'),
+ Atom('cat/pkg[-foo]'))
+ self.assertEqual2(Atom('cat/pkg[foo,-bar]'),
+ Atom('cat/pkg[-bar,foo]'))
+
+ # repoid not supported by Portage Atom yet
+ ## repoid
+ #self.assertEqual2(Atom('cat/pkg::a'), Atom('cat/pkg::a'))
+ #self.assertNotEqual2(Atom('cat/pkg::a'), Atom('cat/pkg::b'))
+ #self.assertNotEqual2(Atom('cat/pkg::a'), Atom('cat/pkg'))
+
+ # slots.
+ self.assertNotEqual2(Atom('cat/pkg:1'), Atom('cat/pkg'))
+ self.assertEqual2(Atom('cat/pkg:2'), Atom('cat/pkg:2'))
+ # http://dev.gentoo.org/~tanderson/pms/eapi-2-approved/pms.html#x1-190002.1.2
+ self.assertEqual2(Atom('cat/pkg:AZaz09+_.-'), Atom('cat/pkg:AZaz09+_.-'))
+ for lesser, greater in (('0.1', '1'), ('1', '1-r1'), ('1.1', '1.2')):
+ self.assertTrue(Atom('=d/b-%s' % lesser) <
+ Atom('=d/b-%s' % greater),
+ msg="d/b-%s < d/b-%s" % (lesser, greater))
+ self.assertFalse(Atom('=d/b-%s' % lesser) >
+ Atom('=d/b-%s' % greater),
+ msg="!: d/b-%s < d/b-%s" % (lesser, greater))
+ self.assertTrue(Atom('=d/b-%s' % greater) >
+ Atom('=d/b-%s' % lesser),
+ msg="d/b-%s > d/b-%s" % (greater, lesser))
+ self.assertFalse(Atom('=d/b-%s' % greater) <
+ Atom('=d/b-%s' % lesser),
+ msg="!: d/b-%s > d/b-%s" % (greater, lesser))
+
+ #self.assertTrue(Atom("!!=d/b-1", eapi=2) > Atom("!=d/b-1"))
+ self.assertTrue(Atom("!=d/b-1") < Atom("!!=d/b-1"))
+ self.assertEqual(Atom("!=d/b-1"), Atom("!=d/b-1"))
+
+ def test_intersects(self):
+ for this, that, result in [
+ ('cat/pkg', 'pkg/cat', False),
+ ('cat/pkg', 'cat/pkg', True),
+ ('cat/pkg:1', 'cat/pkg:1', True),
+ ('cat/pkg:1', 'cat/pkg:2', False),
+ ('cat/pkg:1', 'cat/pkg[foo]', True),
+ ('cat/pkg[foo]', 'cat/pkg[-bar]', True),
+ ('cat/pkg[foo]', 'cat/pkg[-foo]', False),
+ ('>cat/pkg-3', '>cat/pkg-1', True),
+ ('>cat/pkg-3', '<cat/pkg-3', False),
+ ('>=cat/pkg-3', '<cat/pkg-3', False),
+ ('>cat/pkg-2', '=cat/pkg-2*', True),
+ # Portage vercmp disagrees with this one:
+ #('<cat/pkg-2_alpha1', '=cat/pkg-2*', True),
+ ('=cat/pkg-2', '=cat/pkg-2', True),
+ ('=cat/pkg-3', '=cat/pkg-2', False),
+ ('=cat/pkg-2', '>cat/pkg-2', False),
+ ('=cat/pkg-2', '>=cat/pkg-2', True),
+ ('~cat/pkg-2', '~cat/pkg-2', True),
+ ('~cat/pkg-2', '~cat/pkg-2.1', False),
+ ('=cat/pkg-2*', '=cat/pkg-2.3*', True),
+ ('>cat/pkg-2.4', '=cat/pkg-2*', True),
+ ('<cat/pkg-2.4', '=cat/pkg-2*', True),
+ ('<cat/pkg-1', '=cat/pkg-2*', False),
+ ('~cat/pkg-2', '>cat/pkg-2-r1', True),
+ ('~cat/pkg-2', '<=cat/pkg-2', True),
+ ('=cat/pkg-2-r2*', '<=cat/pkg-2-r20', True),
+ ('=cat/pkg-2-r2*', '<cat/pkg-2-r20', True),
+ ('=cat/pkg-2-r2*', '<=cat/pkg-2-r2', True),
+ ('~cat/pkg-2', '<cat/pkg-2', False),
+ ('=cat/pkg-1-r10*', '~cat/pkg-1', True),
+ ('=cat/pkg-1-r1*', '<cat/pkg-1-r1', False),
+ ('=cat/pkg-1*', '>cat/pkg-2', False),
+ ('>=cat/pkg-8.4', '=cat/pkg-8.3.4*', False),
+ # Repos not yet supported by Portage
+ #('cat/pkg::gentoo', 'cat/pkg', True),
+ #('cat/pkg::gentoo', 'cat/pkg::foo', False),
+ ('=sys-devel/gcc-4.1.1-r3', '=sys-devel/gcc-3.3*', False),
+ ('=sys-libs/db-4*', '~sys-libs/db-4.3.29', True),
+ ]:
+ this_atom = Atom(this)
+ that_atom = Atom(that)
+ self.assertEqual(
+ result, this_atom.intersects(that_atom),
+ '%s intersecting %s should be %s' % (this, that, result))
+ self.assertEqual(
+ result, that_atom.intersects(this_atom),
+ '%s intersecting %s should be %s' % (that, this, result))
+
+
+def test_main():
+ test_support.run_unittest(TestGentoolkitAtom)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/gentoolkit/pym/gentoolkit/test/test_cpv.py b/gentoolkit/pym/gentoolkit/test/test_cpv.py
new file mode 100644
index 0000000..833fd49
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/test_cpv.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+#
+# Copyright(c) 2009-2010, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+import unittest
+from test import test_support
+
+from gentoolkit.cpv import *
+
+class TestGentoolkitCPV(unittest.TestCase):
+
+ def assertEqual2(self, o1, o2):
+ # logic bugs hidden behind short circuiting comparisons for metadata
+ # is why we test the comparison *both* ways.
+ self.assertEqual(o1, o2)
+ c = cmp(o1, o2)
+ self.assertEqual(c, 0,
+ msg="checking cmp for %r, %r, aren't equal: got %i" % (o1, o2, c))
+ self.assertEqual(o2, o1)
+ c = cmp(o2, o1)
+ self.assertEqual(c, 0,
+ msg="checking cmp for %r, %r,aren't equal: got %i" % (o2, o1, c))
+
+ def assertNotEqual2(self, o1, o2):
+ # is why we test the comparison *both* ways.
+ self.assertNotEqual(o1, o2)
+ c = cmp(o1, o2)
+ self.assertNotEqual(c, 0,
+ msg="checking cmp for %r, %r, not supposed to be equal, got %i"
+ % (o1, o2, c))
+ self.assertNotEqual(o2, o1)
+ c = cmp(o2, o1)
+ self.assertNotEqual(c, 0,
+ msg="checking cmp for %r, %r, not supposed to be equal, got %i"
+ % (o2, o1, c))
+
+ def test_comparison(self):
+ self.assertEqual2(CPV('pkg'), CPV('pkg'))
+ self.assertNotEqual2(CPV('pkg'), CPV('pkg1'))
+ self.assertEqual2(CPV('cat/pkg'), CPV('cat/pkg'))
+ self.assertNotEqual2(CPV('cat/pkg'), CPV('cat/pkgb'))
+ self.assertNotEqual2(CPV('cata/pkg'), CPV('cat/pkg'))
+ self.assertEqual2(CPV('cat/pkg-0.1'), CPV('cat/pkg-0.1'))
+ self.assertNotEqual2(CPV('cat/pkg-1.0'), CPV('cat/pkg-1'))
+ self.assertEqual2(CPV('cat/pkg-0'), CPV('cat/pkg-0'))
+ self.assertEqual2(CPV('cat/pkg-1-r1'), CPV('cat/pkg-1-r1'))
+ self.assertNotEqual2(CPV('cat/pkg-2-r1'), CPV('cat/pkg-2-r10'))
+ self.assertEqual2(CPV('cat/pkg-1_rc2'), CPV('cat/pkg-1_rc2'))
+ self.assertNotEqual2(CPV('cat/pkg-2_rc2-r1'), CPV('cat/pkg-2_rc1-r1'))
+
+def test_main():
+ test_support.run_unittest(TestGentoolkitCPV)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/gentoolkit/pym/gentoolkit/test/test_helpers.py b/gentoolkit/pym/gentoolkit/test/test_helpers.py
new file mode 100644
index 0000000..2291efd
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/test_helpers.py
@@ -0,0 +1,152 @@
+import os
+import unittest
+import warnings
+from tempfile import NamedTemporaryFile
+from test import test_support
+
+from gentoolkit import helpers
+
+
+class TestChangeLog(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_split_changelog(self):
+ changelog = """
+*portage-2.1.6.2 (20 Dec 2008)
+
+ 20 Dec 2008; Zac Medico <zmedico@gentoo.org> +portage-2.1.6.2.ebuild:
+ 2.1.6.2 bump. This fixes bug #251591 (repoman inherit.autotools false
+ positives) and bug #251616 (performance issue in build log search regex
+ makes emerge appear to hang). Bug #216231 tracks all bugs fixed since
+ 2.1.4.x.
+
+ 20 Dec 2008; Zac Medico <zmedico@gentoo.org> -portage-2.1.6.ebuild,
+ -portage-2.1.6.1.ebuild, -portage-2.2_rc17.ebuild:
+ Remove old versions.
+
+
+*portage-2.1.6.1 (12 Dec 2008)
+
+ 12 Dec 2008; Zac Medico <zmedico@gentoo.org> +portage-2.1.6.1.ebuild:
+ 2.1.6.1 bump. This fixes bug #250148 (emerge hangs with selinux if ebuild
+ spawns a daemon), bug #250166 (trigger download when generating manifest
+ if file size differs from existing entry), and bug #250212 (new repoman
+ upstream.workaround category for emake -j1 warnings). Bug #216231 tracks
+ all bugs fixed since 2.1.4.x.
+
+
+*portage-2.1.6 (07 Dec 2008)
+
+ 07 Dec 2008; Zac Medico <zmedico@gentoo.org> +portage-2.1.6.ebuild:
+ 2.1.6 final release. This fixes bug #249586. Bug #216231 tracks all bugs
+ fixed since 2.1.4.x.
+
+ 07 Dec 2008; Zac Medico <zmedico@gentoo.org> -portage-2.1.6_rc1.ebuild,
+ -portage-2.1.6_rc2.ebuild, -portage-2.1.6_rc3.ebuild,
+ -portage-2.2_rc16.ebuild:
+ Remove old versions.
+ """
+
+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 TestGentoolkitHelpers(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/gentoolkit/pym/gentoolkit/test/test_syntax.py b/gentoolkit/pym/gentoolkit/test/test_syntax.py
new file mode 100644
index 0000000..bb7dcb4
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/test/test_syntax.py
@@ -0,0 +1,33 @@
+import os
+import os.path as osp
+import unittest
+import py_compile
+
+"""Does a basic syntax check by compiling all modules. From Portage."""
+
+pym_dirs = os.walk(osp.dirname(osp.dirname(osp.dirname(__file__))))
+blacklist_dirs = frozenset(('.svn', 'test'))
+
+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()
diff --git a/gentoolkit/pym/gentoolkit/textwrap_.py b/gentoolkit/pym/gentoolkit/textwrap_.py
new file mode 100644
index 0000000..845ae9d
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/textwrap_.py
@@ -0,0 +1,99 @@
+"""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
+prevent the splitting of ANSI colors as well as package names and versions."""
+
+import re
+import textwrap
+
+class TextWrapper(textwrap.TextWrapper):
+ """Ignore ANSI escape codes while wrapping text"""
+
+ def _split(self, text):
+ """_split(text : string) -> [string]
+
+ Split the text to wrap into indivisible chunks.
+ """
+ # 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 = [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()
+
+ # 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:
+
+ # 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
+
+ # 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]
+
+ while chunks:
+ # Ignore ANSI escape codes.
+ chunk_len = len(re.sub(ansi_re, '', chunks[-1]))
+
+ # 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
+
+ # 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 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).
+ if cur_line:
+ lines.append(indent + ''.join(cur_line))
+
+ return lines
+
+# vim: set ts=4 sw=4 tw=79:
diff --git a/gentoolkit/pym/gentoolkit/versionmatch.py b/gentoolkit/pym/gentoolkit/versionmatch.py
new file mode 100644
index 0000000..c081de0
--- /dev/null
+++ b/gentoolkit/pym/gentoolkit/versionmatch.py
@@ -0,0 +1,134 @@
+#! /usr/bin/python
+#
+# Copyright 2009-2010 Gentoo Foundation
+# Licensed under the GNU General Public License, v2
+#
+# Copyright 2005-2007 Brian Harring <ferringb@gmail.com>
+# License: GPL2/BSD
+#
+# $Header$
+
+"""Gentoo version comparison object from pkgcore.ebuild.atom_restricts."""
+
+# =======
+# Imports
+# =======
+
+from portage.versions import vercmp
+
+from gentoolkit import errors
+from gentoolkit.cpv import CPV
+
+# =======
+# Classes
+# =======
+
+class VersionMatch(object):
+ """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.
+ """
+ _convert_op2int = {(-1,):"<", (-1, 0): "<=", (0,):"=",
+ (0, 1):">=", (1,):">"}
+
+ _convert_int2op = dict([(v, k) for k, v in _convert_op2int.iteritems()])
+
+ def __init__(self, cpv, op='='):
+ """Initialize a VersionMatch instance.
+
+ @type cpv: L{gentoolkit.cpv.CPV}
+ @param cpv: cpv object
+ @type op: str
+ @keyword op: operator
+ """
+
+ if not isinstance(cpv, (CPV, self.__class__)):
+ err = "cpv must be a gentoolkit.cpv.CPV "
+ err += "or gentoolkit.versionmatch.VersionMatch instance"
+ raise ValueError(err)
+ 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(
+ "invalid operator '%s'" % self.operator)
+
+ if self.operator == "~":
+ if not self.version:
+ raise errors.GentoolkitInvalidVersion(
+ "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, other):
+ """See whether a passed in VersionMatch or CPV instance matches self.
+
+ Example usage:
+ >>> from gentoolkit.versionmatch import VersionMatch
+ >>> from gentoolkit.cpv import CPV
+ >>> VersionMatch(CPV('foo/bar-1.5'), op='>').match(
+ ... VersionMatch(CPV('foo/bar-2.0')))
+ True
+
+ @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, other.version
+ else:
+ 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:
+ return "ver %s %s" % (operator, self.version)
+ return "ver-rev %s %s-%s" % (
+ operator, self.version, self.revision
+ )
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, str(self))
+
+ @staticmethod
+ def _convert_ops(inst):
+ if inst.droprevision:
+ return inst.values
+ return tuple(sorted(set((-1, 0, 1)).difference(inst.values)))
+
+ def __eq__(self, other):
+ if self is other:
+ return True
+ if isinstance(other, self.__class__):
+ if (self.droprevision != other.droprevision or
+ self.version != other.version or
+ self.revision != other.revision):
+ return False
+ return self._convert_ops(self) == self._convert_ops(other)
+
+ return False
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __hash__(self):
+ return hash((
+ self.droprevision,
+ self.version,
+ self.revision,
+ self.values
+ ))
+
+# vim: set ts=4 sw=4 tw=79: