diff options
Diffstat (limited to 'gentoolkit/pym/gentoolkit')
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. >=portage-2.2 means only maintains versions + of Portage greater than 2.2. Should be DEPEND string with < and > + converted to < and > 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. >=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: |