diff options
Diffstat (limited to 'pym')
32 files changed, 3714 insertions, 2159 deletions
diff --git a/pym/gentoolkit/__init__.py b/pym/gentoolkit/__init__.py index 35d89ac..03382b7 100644 --- a/pym/gentoolkit/__init__.py +++ b/pym/gentoolkit/__init__.py @@ -1,42 +1,23 @@ #!/usr/bin/python # # Copyright 2003-2004 Karl Trygve Kalleberg -# Copyright 2003-2009 Gentoo Technologies, Inc. +# Copyright 2003-2009 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # # $Header$ -# ======= -# Imports -# ======= +"""Gentoolkit is a collection of administration scripts for Gentoo""" -import portage -try: - from threading import Lock -except ImportError: - # If we don't have thread support, we don't need to worry about - # locking the global settings object. So we define a "null" Lock. - class Lock(object): - def acquire(self): - pass - def release(self): - pass +import sys -# ======= -# Globals -# ======= - -PORTDB = portage.db[portage.root]["porttree"].dbapi -VARDB = portage.db[portage.root]["vartree"].dbapi -VIRTUALS = portage.db[portage.root]["virtuals"] - -Config = { - "verbosityLevel": 3 +CONFIG = { + # Color handling: -1: Use Portage settings, 0: Force off, 1: Force on + 'color': -1, + # Guess piping output: + 'piping': False if sys.stdout.isatty() else True, + # Set some defaults: + 'quiet': False, + 'debug': False } -try: - settingslock = Lock() - settings = portage.config(clone=portage.settings) -except portage.exception.PermissionDenied, err: - sys.stderr.write("Permission denied: '%s'\n" % str(err)) - sys.exit(e.errno) +# vim: set ts=8 sw=4 tw=79: diff --git a/pym/gentoolkit/atom.py b/pym/gentoolkit/atom.py new file mode 100644 index 0000000..ff150c0 --- /dev/null +++ b/pym/gentoolkit/atom.py @@ -0,0 +1,195 @@ +#!/usr/bin/python +# +# Copyright(c) 2009, Gentoo Foundation +# +# Licensed under the GNU General Public License, v2 +# +# $Header$ + +"""Subclasses portage.dep.Atom to provide methods on a Gentoo atom string.""" + +__all__ = ('Atom',) + +# ======= +# Imports +# ======= + +import weakref + +import portage + +from gentoolkit.cpv import CPV +from gentoolkit.versionmatch import VersionMatch +from gentoolkit import errors + +# ======= +# Classes +# ======= + +class Atom(portage.dep.Atom, CPV): + """Portage's Atom class with an improved intersects method from pkgcore. + + portage.dep.Atom provides the following instance variables: + + @type operator: str + @ivar operator: one of ('=', '=*', '<', '>', '<=', '>=', '~', None) + @type cp: str + @ivar cp: cat/pkg + @type cpv: str + @ivar cpv: cat/pkg-ver (if ver) + @type slot: str or None + @ivar slot: slot passed in as cpv:# + """ + + # Necessary for Portage versions < 2.1.7 + _atoms = weakref.WeakValueDictionary() + + def __init__(self, atom): + self.atom = atom + + try: + portage.dep.Atom.__init__(self, atom) + except portage.exception.InvalidAtom, err: + raise errors.GentoolkitInvalidAtom(err) + + # Make operator compatible with intersects + if self.operator is None: + self.operator = '=' + + self.cpv = CPV(self.cpv) + + # use_conditional is USE flag condition for this Atom to be required: + # For: !build? ( >=sys-apps/sed-4.0.5 ), use_conditional = '!build' + self.use_conditional = None + + def __repr__(self): + uc = self.use_conditional + uc = "%s? " % uc if uc is not None else '' + return "<%s %r>" % (self.__class__.__name__, "%s%s" % (uc, self.atom)) + + def __setattr__(self, name, value): + object.__setattr__(self, name, value) + + #R0911:121:Atom.intersects: Too many return statements (20/6) + #R0912:121:Atom.intersects: Too many branches (23/12) + # pylint: disable-msg=R0911,R0912 + def intersects(self, other): + """Check if a passed in package atom "intersects" this atom. + + Lifted from pkgcore. + + Two atoms "intersect" if a package can be constructed that + matches both: + - if you query for just "dev-lang/python" it "intersects" both + "dev-lang/python" and ">=dev-lang/python-2.4" + - if you query for "=dev-lang/python-2.4" it "intersects" + ">=dev-lang/python-2.4" and "dev-lang/python" but not + "<dev-lang/python-2.3" + + @type other: Any "Intersectable" object + @param other: other package to compare + @see: L{pkgcore.ebuild.atom} + """ + # Our "cp" (cat/pkg) must match exactly: + if self.cpv.cp != other.cpv.cp: + # Check to see if one is name only: + # Avoid slow partitioning if we're definitely not matching + # (yes, this is hackish, but it's faster): + if self.cpv.cp[-1:] != other.cpv.cp[-1:]: + return False + + if ((not self.cpv.category and self.cpv.name == other.cpv.name) or + (not other.cpv.category and other.cpv.name == self.cpv.name)): + return True + return False + + # If one of us is unversioned we intersect: + if not self.operator or not other.operator: + return True + + # If we are both "unbounded" in the same direction we intersect: + if (('<' in self.operator and '<' in other.operator) or + ('>' in self.operator and '>' in other.operator)): + return True + + # If one of us is an exact match we intersect if the other matches it: + if self.operator == '=': + if other.operator == '=*': + return self.cpv.fullversion.startswith(other.cpv.fullversion) + return VersionMatch(other.cpv, op=other.operator).match(self.cpv) + if other.operator == '=': + if self.operator == '=*': + return other.cpv.fullversion.startswith(self.cpv.fullversion) + return VersionMatch(self.cpv, op=self.operator).match(other.cpv) + + # If we are both ~ matches we match if we are identical: + if self.operator == other.operator == '~': + return (self.cpv.version == other.cpv.version and + self.cpv.revision == other.cpv.revision) + + # If we are both glob matches we match if one of us matches the other. + if self.operator == other.operator == '=*': + return (self.cpv.fullversion.startswith(other.cpv.fullversion) or + other.cpv.fullversion.startswith(self.cpv.fullversion)) + + # If one of us is a glob match and the other a ~ we match if the glob + # matches the ~ (ignoring a revision on the glob): + if self.operator == '=*' and other.operator == '~': + return other.cpv.fullversion.startswith(self.cpv.version) + if other.operator == '=*' and self.operator == '~': + return self.cpv.fullversion.startswith(other.cpv.version) + + # If we get here at least one of us is a <, <=, > or >=: + if self.operator in ('<', '<=', '>', '>='): + ranged, other = self, other + ranged.operator = self.operator + else: + ranged, other = other, self + ranged.operator = other.operator + + if '<' in other.operator or '>' in other.operator: + # We are both ranged, and in the opposite "direction" (or + # we would have matched above). We intersect if we both + # match the other's endpoint (just checking one endpoint + # is not enough, it would give a false positive on <=2 vs >2) + return (VersionMatch(other.cpv, op=other.operator).match(ranged) and + VersionMatch(ranged.cpv, op=ranged.operator).match(other.cpv)) + + if other.operator == '~': + # Other definitely matches its own version. If ranged also + # does we're done: + if VersionMatch(ranged.cpv, op=ranged.operator).match(other.cpv): + return True + # The only other case where we intersect is if ranged is a + # > or >= on other's version and a nonzero revision. In + # that case other will match ranged. Be careful not to + # give a false positive for ~2 vs <2 here: + return (ranged.operator in ('>', '>=') and + VersionMatch(other.cpv, op=other.operator).match(ranged.cpv)) + + if other.operator == '=*': + # a glob match definitely matches its own version, so if + # ranged does too we're done: + if VersionMatch(ranged.cpv, op=ranged.operator).match(other.cpv): + return True + if '<' in ranged.operator: + # If other.revision is not defined then other does not + # match anything smaller than its own fullversion: + if not other.cpv.revision: + return False + + # If other.revision is defined then we can always + # construct a package smaller than other.fullversion by + # tagging e.g. an _alpha1 on. + return ranged.cpv.fullversion.startswith(other.cpv.version) + else: + # Remaining cases where this intersects: there is a + # package greater than ranged.fullversion and + # other.fullversion that they both match. + return ranged.cpv.fullversion.startswith(other.cpv.version) + + # Handled all possible ops. + raise NotImplementedError( + 'Someone added an operator without adding it to intersects') + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/cpv.py b/pym/gentoolkit/cpv.py new file mode 100644 index 0000000..7dc54e5 --- /dev/null +++ b/pym/gentoolkit/cpv.py @@ -0,0 +1,144 @@ +#!/usr/bin/python +# +# Copyright(c) 2009, Gentoo Foundation +# +# Licensed under the GNU General Public License, v2 +# +# $Header$ + +"""Provides attributes and methods for a category/package-version string.""" + +__all__ = ('CPV',) + +# ======= +# Imports +# ======= + +from portage.versions import catpkgsplit, vercmp + +from gentoolkit import errors + +# ======= +# Classes +# ======= + +class CPV(object): + """Provides methods on a category/package-version string. + + Will also correctly split just a package or package-version string. + + Example usage: + >>> from gentoolkit.cpv import CPV + >>> cpv = CPV('sys-apps/portage-2.2-r1') + >>> cpv.category, cpv.name, cpv.fullversion + ('sys-apps', 'portage', '2.2-r1') + >>> str(cpv) + 'sys-apps/portage-2.2-r1' + >>> # An 'rc' (release candidate) version is less than non 'rc' version: + ... CPV('sys-apps/portage-2') > CPV('sys-apps/portage-2_rc10') + """ + + def __init__(self, cpv): + if not cpv: + raise errors.GentoolkitInvalidCPV(cpv) + self.scpv = cpv + + values = split_cpv(cpv) + self.category = values[0] + self.name = values[1] + self.version = values[2] + self.revision = values[3] + del values + + if not self.name: + raise errors.GentoolkitInvalidCPV(cpv) + + sep = '/' if self.category else '' + self.cp = sep.join((self.category, self.name)) + + sep = '-' if self.revision else '' + self.fullversion = sep.join((self.version, self.revision)) + del sep + + def __eq__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("other isn't of %s type, is %s" % ( + self.__class__, other.__class__) + ) + return self.scpv == other.scpv + + def __ne__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("other isn't of %s type, is %s" % ( + self.__class__, other.__class__) + ) + return not self == other + + def __lt__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("other isn't of %s type, is %s" % ( + self.__class__, other.__class__) + ) + + if self.category != other.category: + return self.category < other.category + elif self.name != other.name: + return self.name < other.name + else: + # FIXME: this cmp() hack is for vercmp not using -1,0,1 + # See bug 266493; this was fixed in portage-2.2_rc31 + #return vercmp(self.fullversion, other.fullversion) + result = cmp(vercmp(self.fullversion, other.fullversion), 0) + if result == -1: + return True + else: + return False + + def __gt__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("other isn't of %s type, is %s" % ( + self.__class__, other.__class__) + ) + return not self < other and not self == other + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, str(self)) + + def __str__(self): + return self.scpv + + +# ========= +# Functions +# ========= + +def split_cpv(cpv): + """Split a cpv into category, name, version and revision. + + Inlined from helpers because of circular imports. + + @type cpv: str + @param cpv: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom or regex + @rtype: tuple + @return: (category, pkg_name, version, revision) + Each tuple element is a string or empty string (""). + """ + + result = catpkgsplit(cpv) + + if result: + result = list(result) + if result[0] == 'null': + result[0] = '' + if result[3] == 'r0': + result[3] = '' + else: + result = cpv.split("/") + if len(result) == 1: + result = ['', cpv, '', ''] + else: + result = result + ['', ''] + + return tuple(result) + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/dbapi.py b/pym/gentoolkit/dbapi.py new file mode 100644 index 0000000..2866214 --- /dev/null +++ b/pym/gentoolkit/dbapi.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# +# Copyright 2009 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# $Header$ + +"""Provides access to Portage database api""" + +import portage + +#bindb = portage.db[portage.root]["bintree"].dbapi +PORTDB = portage.db[portage.root]["porttree"].dbapi +VARDB = portage.db[portage.root]["vartree"].dbapi +#virtuals = portage.db[portage.root]["virtuals"] + +# vim: set ts=8 sw=4 tw=79: diff --git a/pym/gentoolkit/dependencies.py b/pym/gentoolkit/dependencies.py new file mode 100644 index 0000000..632ca1e --- /dev/null +++ b/pym/gentoolkit/dependencies.py @@ -0,0 +1,317 @@ +# Copyright(c) 2009, Gentoo Foundation +# +# Licensed under the GNU General Public License, v2 +# +# $Header: $ + +"""Provides a class for easy calculating dependencies for a given CPV.""" + +__docformat__ = 'epytext' +__all__ = ('Dependencies',) + +# ======= +# Imports +# ======= + +import portage +from portage.dep import paren_reduce + +from gentoolkit import errors +from gentoolkit.atom import Atom +from gentoolkit.cpv import CPV +from gentoolkit.helpers import find_best_match, uniqify +from gentoolkit.dbapi import PORTDB, VARDB + +# ======= +# Classes +# ======= + +class Dependencies(CPV): + """Access a package's dependencies and reverse dependencies. + + Example usage: + >>> from gentoolkit.dependencies import Dependencies + >>> portage = Dependencies('sys-apps/portage-2.1.6.13') + >>> portage + <Dependencies 'sys-apps/portage-2.1.6.13'> + >>> # All methods return gentoolkit.atom.Atom instances + ... portage.get_depend() + [<Atom '>=dev-lang/python-2.5'>, <Atom '<dev-lang/python-3.0'>, ...] + + """ + def __init__(self, cpv, op='', parser=None): + if isinstance(cpv, CPV): + self.cpv = cpv + else: + self.cpv = CPV(cpv) + + self.operator = op + self.atom = self.operator + str(self.cpv) + self.use = [] + self.depatom = str() + + # Allow a custom parser function: + self.parser = parser if parser else self._parser + + def __eq__(self, other): + if self.atom != other.atom: + return False + else: + return True + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.atom, self.depatom, tuple(self.use))) + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.atom) + + def get_env_vars(self, envvars): + """Returns predefined env vars DEPEND, SRC_URI, etc.""" + + # Try to use the Portage tree first, since emerge only uses the tree + # when calculating dependencies + try: + result = PORTDB.aux_get(str(self.cpv), envvars) + except KeyError: + result = VARDB.aux_get(str(self.cpv), envvars) + return result + + def get_depend(self): + """Get the contents of DEPEND and parse it with self.parser.""" + + try: + return self.parser(self.get_env_vars(('DEPEND',))[0]) + except portage.exception.InvalidPackageName, err: + raise errors.GentoolkitInvalidCPV(err) + + def get_pdepend(self): + """Get the contents of PDEPEND and parse it with self.parser.""" + + try: + return self.parser(self.get_env_vars(('PDEPEND',))[0]) + except portage.exception.InvalidPackageName, err: + raise errors.GentoolkitInvalidCPV(err) + + def get_rdepend(self): + """Get the contents of RDEPEND and parse it with self.parser.""" + + try: + return self.parser(self.get_env_vars(('RDEPEND',))[0]) + except portage.exception.InvalidPackageName, err: + raise errors.GentoolkitInvalidCPV(err) + + def get_all_depends(self): + """Get the contents of ?DEPEND and parse it with self.parser.""" + + env_vars = ('DEPEND', 'PDEPEND', 'RDEPEND') + try: + return self.parser(' '.join(self.get_env_vars(env_vars))) + except portage.exception.InvalidPackageName, err: + raise errors.GentoolkitInvalidCPV(err) + + def graph_depends( + self, + max_depth=1, + printer_fn=None, + # The rest of these are only used internally: + depth=0, + seen=None, + depcache=None, + result=None + ): + """Graph direct dependencies for self. + + Optionally gather indirect dependencies. + + @type max_depth: int + @param max_depth: Maximum depth to recurse if. + <1 means no maximum depth + >0 means recurse only this depth; + @type printer_fn: callable + @param printer_fn: If None, no effect. If set, it will be applied to + each result. + @rtype: list + @return: [(depth, pkg), ...] + """ + if seen is None: + seen = set() + if depcache is None: + depcache = dict() + if result is None: + result = list() + + pkgdep = None + deps = self.get_all_depends() + for dep in deps: + if dep.atom in depcache: + continue + try: + pkgdep = depcache[dep.atom] + except KeyError: + pkgdep = find_best_match(dep.atom) + depcache[dep.atom] = pkgdep + if pkgdep and str(pkgdep.cpv) in seen: + continue + if depth < max_depth or max_depth <= 0: + + if printer_fn is not None: + printer_fn(depth, pkgdep, dep) + if not pkgdep: + continue + + seen.add(str(pkgdep.cpv)) + result.append(( + depth, + pkgdep.deps.graph_depends( + max_depth=max_depth, + printer_fn=printer_fn, + # The rest of these are only used internally: + depth=depth+1, + seen=seen, + depcache=depcache, + result=result + ) + )) + + if depth == 0: + return result + return pkgdep + + def graph_reverse_depends( + self, + pkgset=None, + max_depth=-1, + only_direct=True, + printer_fn=None, + # The rest of these are only used internally: + depth=0, + depcache=None, + seen=None, + result=None + ): + """Graph direct reverse dependencies for self. + + Example usage: + >>> from gentoolkit.dependencies import Dependencies + >>> ffmpeg = Dependencies('media-video/ffmpeg-0.5_p20373') + >>> # I only care about installed packages that depend on me: + ... from gentoolkit.helpers import get_installed_cpvs + >>> # I want to pass in a sorted list. We can pass strings or + ... # Package or Atom types, so I'll use Package to sort: + ... from gentoolkit.package import Package + >>> installed = sorted(Package(x) for x in get_installed_cpvs()) + >>> deptree = ffmpeg.graph_reverse_depends( + ... only_direct=False, # Include indirect revdeps + ... pkgset=installed) # from installed pkgset + >>> len(deptree) + 44 + + @type pkgset: iterable + @param pkgset: sorted pkg cpv strings or any 'intersectable' objects to + use for calculate our revdep graph. + @type max_depth: int + @param max_depth: Maximum depth to recurse if only_direct=False. + -1 means no maximum depth; + 0 is the same as only_direct=True; + >0 means recurse only this many times; + @type only_direct: bool + @param only_direct: to recurse or not to recurse + @type printer_fn: callable + @param printer_fn: If None, no effect. If set, it will be applied to + each L{gentoolkit.atom.Atom} object as it is added to + the results. + @rtype: list + @return: L{gentoolkit.dependencies.Dependencies} objects + """ + if not pkgset: + err = ("%s kwarg 'pkgset' must be set. " + "Can be list of cpv strings or any 'intersectable' object.") + raise errors.GentoolkitFatalError(err % (self.__class__.__name__,)) + + if depcache is None: + depcache = dict() + if seen is None: + seen = set() + if result is None: + result = list() + + if depth == 0: + pkgset = tuple(Dependencies(x) for x in pkgset) + + pkgdep = None + for pkgdep in pkgset: + try: + all_depends = depcache[pkgdep] + except KeyError: + all_depends = uniqify(pkgdep.get_all_depends()) + depcache[pkgdep] = all_depends + + dep_is_displayed = False + for dep in all_depends: + # TODO: Add ability to determine if dep is enabled by USE flag. + # Check portage.dep.use_reduce + if dep.intersects(self): + pkgdep.depth = depth + pkgdep.matching_dep = dep + if printer_fn is not None: + printer_fn(pkgdep, dep_is_displayed=dep_is_displayed) + result.append(pkgdep) + dep_is_displayed = True + + # if --indirect specified, call ourselves again with the dep + # Do not call if we have already called ourselves. + if ( + dep_is_displayed and not only_direct and + str(pkgdep.cpv) not in seen and + (depth < max_depth or max_depth == -1) + ): + + seen.add(str(pkgdep.cpv)) + result.append( + pkgdep.graph_reverse_depends( + pkgset=pkgset, + max_depth=max_depth, + only_direct=only_direct, + printer_fn=printer_fn, + depth=depth+1, + depcache=depcache, + seen=seen, + result=result + ) + ) + + if depth == 0: + return result + return pkgdep + + def _parser(self, deps, use_conditional=None, depth=0): + """?DEPEND file parser. + + @rtype: list + @return: L{gentoolkit.atom.Atom} objects + """ + result = [] + + if depth == 0: + deps = paren_reduce(deps) + for tok in deps: + if tok == '||': + continue + if tok[-1] == '?': + use_conditional = tok[:-1] + continue + if isinstance(tok, list): + asdf = self._parser(tok, use_conditional, depth=depth+1) + result.extend(asdf) + continue + atom = Atom(tok) + if use_conditional is not None: + atom.use_conditional = use_conditional + result.append(atom) + + return result + +# vim: set ts=4 sw=4 tw=0: diff --git a/pym/gentoolkit/deprecated/helpers.py b/pym/gentoolkit/deprecated/helpers.py new file mode 100644 index 0000000..df158f2 --- /dev/null +++ b/pym/gentoolkit/deprecated/helpers.py @@ -0,0 +1,179 @@ +#!/usr/bin/python2 +# +# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org> +# Copyright(c) 2009, Gentoo Foundation +# +# Licensed under the GNU General Public License, v2 +# +# $Header$ + +"""The functions in this module have been deprecated and are not even +guaranteed to work. Improved functions can be found in helpers2.py""" + +import warnings + +import portage +from gentoolkit import * +from package import * +from pprinter import warn +try: + from portage.util import unique_array +except ImportError: + from portage_util import unique_array + +def find_packages(search_key, masked=False): + """Returns a list of Package objects that matched the search key.""" + warnings.warn("Deprecated. Use helpers2.find_packages.", DeprecationWarning) + try: + if masked: + t = portage.db["/"]["porttree"].dbapi.xmatch("match-all", search_key) + t += portage.db["/"]["vartree"].dbapi.match(search_key) + else: + t = portage.db["/"]["porttree"].dbapi.match(search_key) + t += portage.db["/"]["vartree"].dbapi.match(search_key) + # catch the "amgigous package" Exception + except ValueError, e: + if isinstance(e[0],list): + t = [] + for cp in e[0]: + if masked: + t += portage.db["/"]["porttree"].dbapi.xmatch("match-all", cp) + t += portage.db["/"]["vartree"].dbapi.match(cp) + else: + t += portage.db["/"]["porttree"].dbapi.match(cp) + t += portage.db["/"]["vartree"].dbapi.match(cp) + else: + raise ValueError(e) + except portage_exception.InvalidAtom, e: + print warn("Invalid Atom: '%s'" % str(e)) + return [] + # Make the list of packages unique + t = unique_array(t) + t.sort() + return [Package(x) for x in t] + +def find_installed_packages(search_key, masked=False): + """Returns a list of Package objects that matched the search key.""" + warnings.warn("Deprecated. Use helpers2.find_installed_packages.", + DeprecationWarning) + try: + t = portage.db["/"]["vartree"].dbapi.match(search_key) + # catch the "amgigous package" Exception + except ValueError, e: + if isinstance(e[0],list): + t = [] + for cp in e[0]: + t += portage.db["/"]["vartree"].dbapi.match(cp) + else: + raise ValueError(e) + except portage_exception.InvalidAtom, e: + print warn("Invalid Atom: '%s'" % str(e)) + return [] + return [Package(x) for x in t] + +def find_best_match(search_key): + """Returns a Package object for the best available candidate that + matched the search key.""" + warnings.warn("Deprecated. Use helpers2.find_best_match.", + DeprecationWarning) + t = portage.db["/"]["porttree"].dep_bestmatch(search_key) + if t: + return Package(t) + return None + +def find_system_packages(prefilter=None): + """Returns a tuple of lists, first list is resolved system packages, + second is a list of unresolved packages.""" + pkglist = settings.packages + resolved = [] + unresolved = [] + for x in pkglist: + cpv = x.strip() + if len(cpv) and cpv[0] == "*": + pkg = find_best_match(cpv) + if pkg: + resolved.append(pkg) + else: + unresolved.append(cpv) + return (resolved, unresolved) + +def find_world_packages(prefilter=None): + """Returns a tuple of lists, first list is resolved world packages, + seond is unresolved package names.""" + f = open(portage.root+portage.WORLD_FILE) + pkglist = f.readlines() + resolved = [] + unresolved = [] + for x in pkglist: + cpv = x.strip() + if len(cpv) and cpv[0] != "#": + pkg = find_best_match(cpv) + if pkg: + resolved.append(pkg) + else: + unresolved.append(cpv) + return (resolved,unresolved) + +def find_all_installed_packages(prefilter=None): + """Returns a list of all installed packages, after applying the prefilter + function""" + warnings.warn("Deprecated. Use helpers2.get_installed_cpvs.", + DeprecationWarning) + t = vartree.dbapi.cpv_all() + if prefilter: + t = filter(prefilter,t) + return [Package(x) for x in t] + +def find_all_uninstalled_packages(prefilter=None): + """Returns a list of all uninstalled packages, after applying the prefilter + function""" + warnings.warn("Deprecated. Use helpers2.get_uninstalled_cpvs.", + DeprecationWarning) + alist = find_all_packages(prefilter) + return [x for x in alist if not x.is_installed()] + +def find_all_packages(prefilter=None): + """Returns a list of all known packages, installed or not, after applying + the prefilter function""" + warnings.warn("Deprecated. Use helpers2.get_cpvs.", DeprecationWarning) + t = porttree.dbapi.cp_all() + t += vartree.dbapi.cp_all() + if prefilter: + t = filter(prefilter,t) + t = unique_array(t) + t2 = [] + for x in t: + t2 += porttree.dbapi.cp_list(x) + t2 += vartree.dbapi.cp_list(x) + t2 = unique_array(t2) + return [Package(x) for x in t2] + +def split_package_name(name): + """Returns a list on the form [category, name, version, revision]. Revision will + be 'r0' if none can be inferred. Category and version will be empty, if none can + be inferred.""" + warnings.warn("Deprecated. Just use portage.catpkgsplit or apply " + "gentoolkit.package.Package to access pkg.category, pkg.revision, etc.", + DeprecationWarning) + r = portage.catpkgsplit(name) + if not r: + r = name.split("/") + if len(r) == 1: + return ["", name, "", "r0"] + else: + return r + ["", "r0"] + else: + r = list(r) + if r[0] == 'null': + r[0] = '' + return r + +# XXX: Defunct: use helpers2.compare_package_strings +#def sort_package_list(pkglist): +# """Returns the list ordered in the same way portage would do with lowest version +# at the head of the list.""" +# pkglist.sort(Package.compare_version) +# return pkglist + +if __name__ == "__main__": + print "This module is for import only" diff --git a/pym/gentoolkit/equery/__init__.py b/pym/gentoolkit/equery/__init__.py index c205938..13ff6ba 100644 --- a/pym/gentoolkit/equery/__init__.py +++ b/pym/gentoolkit/equery/__init__.py @@ -15,27 +15,51 @@ __all__ = ( 'mod_usage' ) __docformat__ = 'epytext' +# version is dynamically set by distutils sdist +__version__ = "svn" # ======= # Imports # ======= import errno +import os import sys import time from getopt import getopt, GetoptError -from portage import exception +import portage import gentoolkit -import gentoolkit.pprinter as pp -from gentoolkit import settings, Config +from gentoolkit import CONFIG +from gentoolkit import errors +from gentoolkit import pprinter as pp from gentoolkit.textwrap_ import TextWrapper __productname__ = "equery" -__authors__ = """\ -Karl Trygve Kalleberg - Original author -Douglas Anderson - Modular redesign; author of meta, changes""" +__authors__ = ( + 'Karl Trygve Kalleberg - Original author', + 'Douglas Anderson - Modular redesign; author of meta, changes' +) + +# ======= +# Globals +# ======= + +NAME_MAP = { + 'b': 'belongs', + 'c': 'changes', + 'k': 'check', + 'd': 'depends', + 'g': 'depgraph', + 'f': 'files', + 'h': 'hasuse', + 'l': 'list_', + 'm': 'meta', + 's': 'size', + 'u': 'uses', + 'w': 'which' +} # ========= # Functions @@ -48,7 +72,7 @@ def print_help(with_description=True): """ if with_description: - print __doc__ + print __doc__ print main_usage() print print pp.globaloption("global options") @@ -78,30 +102,15 @@ def print_help(with_description=True): def expand_module_name(module_name): - """Returns one of the values of name_map or raises KeyError""" - - name_map = { - 'b': 'belongs', - 'c': 'changes', - 'k': 'check', - 'd': 'depends', - 'g': 'depgraph', - 'f': 'files', - 'h': 'hasuse', - 'l': 'list_', - 'm': 'meta', - 's': 'size', - 'u': 'uses', - 'w': 'which' - } + """Returns one of the values of NAME_MAP or raises KeyError""" if module_name == 'list': # list is a Python builtin type, so we must rename our module return 'list_' - elif module_name in name_map.values(): + elif module_name in NAME_MAP.values(): return module_name else: - return name_map[module_name] + return NAME_MAP[module_name] def format_options(options): @@ -114,21 +123,21 @@ def format_options(options): """ result = [] - twrap = TextWrapper(width=Config['termWidth']) + twrap = TextWrapper(width=CONFIG['termWidth']) opts = (x[0] for x in options) descs = (x[1] for x in options) for opt, desc in zip(opts, descs): twrap.initial_indent = pp.emph(opt.ljust(25)) twrap.subsequent_indent = " " * 25 result.append(twrap.fill(desc)) - + return '\n'.join(result) def format_filetype(path, fdesc, show_type=False, show_md5=False, show_timestamp=False): """Format a path for printing. - + @type path: str @param path: the path @type fdesc: list @@ -141,7 +150,7 @@ def format_filetype(path, fdesc, show_type=False, show_md5=False, @type show_md5: bool @param show_md5: if True, append MD5 sum to the formatted string @type show_timestamp: bool - @param show_timestamp: if True, append time-of-creation after pathname + @param show_timestamp: if True, append time-of-creation after pathname @rtype: str @return: formatted pathname with optional added information """ @@ -160,7 +169,7 @@ def format_filetype(path, fdesc, show_type=False, show_md5=False, ftype = "sym" stamp = format_timestamp(fdesc[1]) tgt = fdesc[2].split()[0] - if Config["piping"]: + if CONFIG["piping"]: fpath = path else: fpath = pp.path_symlink(path + " -> " + tgt) @@ -168,7 +177,9 @@ def format_filetype(path, fdesc, show_type=False, show_md5=False, ftype = "dev" fpath = path else: - pp.print_error("%s has unknown type: %s" % (path, fdesc[0])) + sys.stderr.write( + pp.error("%s has unknown type: %s" % (path, fdesc[0])) + ) result = "" if show_type: @@ -198,24 +209,14 @@ def initialize_configuration(): term_width = 80 # Terminal size, minus a 1-char margin for text wrapping - Config['termWidth'] = term_width - 1 - - # Color handling: -1: Use Portage settings, 0: Force off, 1: Force on - Config['color'] = -1 - - Config['quiet'] = False + CONFIG['termWidth'] = term_width - 1 # Guess color output - if (Config['color'] == -1 and (not sys.stdout.isatty() or - settings["NOCOLOR"] in ("yes", "true")) or - Config['color'] == 0): + if (CONFIG['color'] == -1 and (not sys.stdout.isatty() or + os.getenv("NOCOLOR") in ("yes", "true")) or CONFIG['color'] == 0): pp.output.nocolor() - # Guess piping output - if not sys.stdout.isatty(): - Config["piping"] = True - else: - Config["piping"] = False + CONFIG['verbose'] = not CONFIG['piping'] def main_usage(): @@ -232,7 +233,7 @@ def main_usage(): def mod_usage(mod_name="module", arg="pkgspec", optional=False): """Provide a consistant usage message to the calling module. - + @type arg: string @param arg: what kind of argument the module takes (pkgspec, filename, etc) @type optional: bool @@ -262,88 +263,88 @@ def parse_global_options(global_opts, args): print_help() sys.exit(0) elif opt in ('-q','--quiet'): - Config["quiet"] = True + CONFIG['quiet'] = True elif opt in ('-C', '--no-color', '--nocolor'): - Config['color'] = 0 + CONFIG['color'] = 0 pp.output.nocolor() elif opt in ('-N', '--no-pipe'): - Config["piping"] = False + CONFIG['piping'] = False elif opt in ('-V', '--version'): print_version() sys.exit(0) - + elif opt in ('--debug'): + CONFIG['debug'] = True + return need_help - + def print_version(): """Print the version of this tool to the console.""" - try: - with open('/usr/share/gentoolkit/VERSION') as gentoolkit_version: - version = gentoolkit_version.read().strip() - except IOError, err: - pp.die(2, str(err)) - print "%(product)s (%(version)s) - %(docstring)s" % { "product": pp.productname(__productname__), - "version": version, + "version": __version__, "docstring": __doc__ } def split_arguments(args): """Separate module name from module arguments""" - + return args.pop(0), args def main(): """Parse input and run the program.""" - initialize_configuration() - short_opts = "hqCNV" - long_opts = ('help', 'quiet', 'nocolor', 'no-color', 'no-pipe', 'version') + long_opts = ( + 'help', 'quiet', 'nocolor', 'no-color', 'no-pipe', 'version', 'debug' + ) + + initialize_configuration() try: global_opts, args = getopt(sys.argv[1:], short_opts, long_opts) except GetoptError, err: - pp.print_error("Global %s" % err) + sys.stderr.write(pp.error("Global %s" % err)) print_help(with_description=False) sys.exit(2) # Parse global options need_help = parse_global_options(global_opts, args) + # FIXME: There are a few places that make use of both quiet and verbose. + # Consider combining. + if CONFIG['quiet']: + CONFIG['verbose'] = False + try: module_name, module_args = split_arguments(args) except IndexError: print_help() sys.exit(2) - + if need_help: module_args.append('--help') - if Config['piping'] or Config['quiet']: - Config['verbose'] = False - else: - Config['verbose'] = True - try: expanded_module_name = expand_module_name(module_name) except KeyError: - pp.print_error("Unknown module '%s'" % module_name) + sys.stderr.write(pp.error("Unknown module '%s'" % module_name)) print_help(with_description=False) sys.exit(2) try: - loaded_module = __import__(expanded_module_name, globals(), - locals(), [], -1) + loaded_module = __import__( + expanded_module_name, globals(), locals(), [], -1 + ) loaded_module.main(module_args) - except exception.AmbiguousPackageName, err: - pp.print_error("Ambiguous package name. Use one of: ") - while err[0]: - print " " + err[0].pop() + except portage.exception.AmbiguousPackageName, err: + raise errors.GentoolkitAmbiguousPackage(err) except IOError, err: if err.errno != errno.EPIPE: raise + +if __name__ == '__main__': + main() diff --git a/pym/gentoolkit/equery/belongs.py b/pym/gentoolkit/equery/belongs.py index d4da36f..e1367cf 100644 --- a/pym/gentoolkit/equery/belongs.py +++ b/pym/gentoolkit/equery/belongs.py @@ -16,15 +16,13 @@ __docformat__ = 'epytext' # Imports # ======= -import re import sys from getopt import gnu_getopt, GetoptError import gentoolkit.pprinter as pp -from gentoolkit.equery import format_filetype, format_options, mod_usage, \ - Config -from gentoolkit.helpers2 import get_installed_cpvs -from gentoolkit.package import Package +from gentoolkit.equery import (format_filetype, format_options, mod_usage, + CONFIG) +from gentoolkit.helpers import FileOwner # ======= # Globals @@ -36,27 +34,60 @@ QUERY_OPTS = { "nameOnly": False } +# ======= +# Classes +# ======= + +class BelongsPrinter(object): + """Outputs a formatted list of packages that claim to own a files.""" + + def __init__(self, verbose=True, name_only=False): + if verbose: + self.print_fn = self.print_verbose + else: + self.print_fn = self.print_quiet + + self.name_only = name_only + + def __call__(self, pkg, cfile): + self.print_fn(pkg, cfile) + + # W0613: *Unused argument %r* + # pylint: disable-msg=W0613 + def print_quiet(self, pkg, cfile): + "Format for minimal output." + if self.name_only: + name = pkg.cpv.cp + else: + name = str(pkg.cpv) + print name + + def print_verbose(self, pkg, cfile): + "Format for full output." + file_str = pp.path(format_filetype(cfile, pkg.get_contents()[cfile])) + if self.name_only: + name = pkg.cpv.cp + else: + name = str(pkg.cpv) + print pp.cpv(name), "(" + file_str + ")" + + # ========= # Functions # ========= def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) for opt in opts: if opt in ('-h','--help'): print_help() sys.exit(0) - elif opt in ('-c', '--category'): - # Remove this warning after a reasonable amount of time - # (djanderson, 2/2009) - pp.print_warn("Module option -c, --category not implemented") - print elif opt in ('-e', '--early-out', '--earlyout'): if opt == '--earlyout': - pp.print_warn("Use of --earlyout is deprecated.") - pp.print_warn("Please use --early-out.") + sys.stderr.write(pp.warn("Use of --earlyout is deprecated.")) + sys.stderr.write(pp.warn("Please use --early-out.")) print QUERY_OPTS['earlyOut'] = True elif opt in ('-f', '--full-regex'): @@ -65,31 +96,9 @@ def parse_module_options(module_opts): QUERY_OPTS['nameOnly'] = True -def prepare_search_regex(queries): - """Create a regex out of the queries""" - - if QUERY_OPTS["fullRegex"]: - result = queries - else: - result = [] - # Trim trailing and multiple slashes from queries - slashes = re.compile('/+') - for query in queries: - query = slashes.sub('/', query).rstrip('/') - if query.startswith('/'): - query = "^%s$" % re.escape(query) - else: - query = "/%s$" % re.escape(query) - result.append(query) - - result = "|".join(result) - - return re.compile(result) - - def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -111,15 +120,14 @@ def print_help(with_description=True): def main(input_args): """Parse input and run the program""" - # -c, --category is not implemented - short_opts = "hc:fen" - long_opts = ('help', 'category=', 'full-regex', 'early-out', 'earlyout', + short_opts = "h:fen" + long_opts = ('help', 'full-regex', 'early-out', 'earlyout', 'name-only') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) @@ -130,31 +138,19 @@ def main(input_args): print_help() sys.exit(2) - query_re = prepare_search_regex(queries) - - if Config['verbose']: - pp.print_info(3, " * Searching for %s ... " - % (pp.regexpquery(",".join(queries)))) - - matches = get_installed_cpvs() - - # Print matches to screen or pipe - found_match = False - for pkg in [Package(x) for x in matches]: - files = pkg.get_contents() - for cfile in files: - if query_re.search(cfile): - if QUERY_OPTS["nameOnly"]: - pkg_str = pkg.key - else: - pkg_str = pkg.cpv - if Config['verbose']: - file_str = pp.path(format_filetype(cfile, files[cfile])) - print "%s (%s)" % (pkg_str, file_str) - else: - print pkg_str - - found_match = True - - if found_match and QUERY_OPTS["earlyOut"]: - break + if CONFIG['verbose']: + print " * Searching for %s ... " % (pp.regexpquery(",".join(queries))) + + printer_fn = BelongsPrinter( + verbose=CONFIG['verbose'], name_only=QUERY_OPTS['nameOnly'] + ) + + find_owner = FileOwner( + is_regex=QUERY_OPTS['fullRegex'], + early_out=QUERY_OPTS['earlyOut'], + printer_fn=printer_fn + ) + + find_owner(queries) + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/changes.py b/pym/gentoolkit/equery/changes.py index 604bb45..09cccca 100644 --- a/pym/gentoolkit/equery/changes.py +++ b/pym/gentoolkit/equery/changes.py @@ -4,14 +4,13 @@ # # $Header: $ -"""Display the Gentoo ChangeLog entry for the latest installable version of a -given package +"""Display the Gentoo ChangeLog entry for the latest installable version of a +given package. """ # Move to Imports sections when Python 2.6 is stable from __future__ import with_statement -__author__ = 'Douglas Anderson' __docformat__ = 'epytext' # ======= @@ -22,14 +21,11 @@ import os import sys from getopt import gnu_getopt, GetoptError -from portage.versions import pkgsplit - import gentoolkit.pprinter as pp from gentoolkit import errors +from gentoolkit.atom import Atom from gentoolkit.equery import format_options, mod_usage -from gentoolkit.helpers2 import find_best_match, find_packages -from gentoolkit.package import Package -from gentoolkit.versionmatch import VersionMatch +from gentoolkit.helpers import ChangeLog, find_best_match, find_packages # ======= # Globals @@ -49,7 +45,7 @@ QUERY_OPTS = { def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -78,71 +74,26 @@ def print_help(with_description=True): )) -def get_logpath(pkg): - """Test that the package's ChangeLog path is valid and readable, else - die. - - @type pkg: gentoolkit.package.Package - @param pkg: package to find logpath for - @rtype: str - @return: a path to a readable ChangeLog - """ - - logpath = os.path.join(pkg.get_package_path(), 'ChangeLog') - if not os.path.isfile(logpath) or not os.access(logpath, os.R_OK): - raise errors.GentoolkitFatalError("%s does not exist or is unreadable" - % pp.path(logpath)) - - return logpath - - def get_match(query): - """Find a valid package to get the ChangeLog path from or raise - GentoolkitNoMatches. + """Find a valid package from which to get the ChangeLog path. + + @raise GentoolkitNoMatches: if no matches found """ match = matches = None match = find_best_match(query) - + if not match: matches = find_packages(query, include_masked=True) else: matches = [match] if not matches: - pp.print_warn("Try using an unversioned query with " - "--from and --to.") raise errors.GentoolkitNoMatches(query) return matches[0] -def index_changelog(entries): - """Convert the list from split_changelog into a dict with VersionMatch - instance as the index. - - @todo: UPDATE THIS - @type entries: list - @param entries: output of split_changelog - @rtype: dict - @return: dict with gentoolkit.package.Package instances as keys and the - corresponding ChangeLog entree as its value - """ - - result = [] - for entry in entries: - # Extract the package name from the entry, ex: - # *xterm-242 (07 Mar 2009) => xterm-242 - pkg_name = entry.split(' ', 1)[0].lstrip('*') - if not pkg_name.strip(): - continue - pkg_split = pkgsplit(pkg_name) - result.append( - (VersionMatch(op="=", ver=pkg_split[1], rev=pkg_split[2]), entry)) - - return result - - def is_ranged(atom): """Return True if an atom string appears to be ranged, else False.""" @@ -150,7 +101,7 @@ def is_ranged(atom): def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) posargs = (x[1] for x in module_opts) @@ -165,126 +116,39 @@ def parse_module_options(module_opts): elif opt in ('--limit',): set_limit(posarg) elif opt in ('--from',): - set_from(posarg) + QUERY_OPTS['from'] = posarg elif opt in ('--to',): - set_to(posarg) - - -def print_matching_entries(indexed_entries, pkg, first_run): - """Print only the entries which interect with the pkg version.""" - - from_restriction = QUERY_OPTS['from'] - to_restriction = QUERY_OPTS['to'] - - for entry_set in indexed_entries: - i, entry = entry_set - # a little hackery, since versionmatch doesn't store the - # package key, but intersects checks that it matches. - i.key = pkg.key - if from_restriction or to_restriction: - if from_restriction and not from_restriction.match(i): - continue - if to_restriction and not to_restriction.match(i): - continue - elif not pkg.intersects(i): - continue - - if not first_run: - print "\n" - print entry.strip() - first_run = False - - return first_run + QUERY_OPTS['to'] = posarg -def set_from(posarg): - """Set a starting version to filter the ChangeLog with or die if posarg - is not a valid version. - """ - - pkg_split = pkgsplit('null-%s' % posarg) +def print_entries(entries): + """Print entries and strip trailing whitespace from the last entry.""" - if pkg_split and not is_ranged(posarg): - ver_match = VersionMatch( - op=">=", - ver=pkg_split[1], - rev=pkg_split[2] if pkg_split[2] != 'r0' else '') - QUERY_OPTS['from'] = ver_match - else: - err = "Module option --from requires valid unranged version (got '%s')" - pp.print_error(err % posarg) - print - print_help(with_description=False) - sys.exit(2) + len_entries = len(entries) + for i, entry in enumerate(entries): # , start=1): in py2.6 + i += 1 + if i < len_entries: + print entry + else: + print entry.strip() def set_limit(posarg): - """Set a limit in QUERY_OPTS on how many ChangeLog entries to display or - die if posarg is not an integer. + """Set a limit in QUERY_OPTS on how many ChangeLog entries to display. + + Die if posarg is not an integer. """ if posarg.isdigit(): QUERY_OPTS['limit'] = int(posarg) else: err = "Module option --limit requires integer (got '%s')" - pp.print_error(err % posarg) + sys.stderr.write(pp.error(err % posarg)) print print_help(with_description=False) sys.exit(2) -def set_to(posarg): - """Set an ending version to filter the ChangeLog with or die if posarg - is not a valid version. - """ - - pkg_split = pkgsplit('null-%s' % posarg) - if pkg_split and not is_ranged(posarg): - ver_match = VersionMatch( - op="<=", - ver=pkg_split[1], - rev=pkg_split[2] if pkg_split[2] != 'r0' else '') - QUERY_OPTS['to'] = ver_match - else: - err = "Module option --to requires valid unranged version (got '%s')" - pp.print_error(err % posarg) - print - print_help(with_description=False) - sys.exit(2) - - -def split_changelog(logpath): - """Split the changelog up into individual entries. - - @type logpath: str - @param logpath: valid path to ChangeLog file - @rtype: list - @return: individual ChangeLog entrees - """ - - result = [] - partial_entries = [] - with open(logpath) as log: - for line in log: - if line.startswith('#'): - continue - elif line.startswith('*'): - # Append last entry to result... - entry = ''.join(partial_entries) - if entry and not entry.isspace(): - result.append(entry) - # ... and start a new entry - partial_entries = [line] - else: - partial_entries.append(line) - else: - # Append the final entry - entry = ''.join(partial_entries) - result.append(entry) - - return result - - def main(input_args): """Parse input and run the program""" @@ -294,7 +158,7 @@ def main(input_args): try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) @@ -310,36 +174,34 @@ def main(input_args): if not first_run: print - ranged_query = None - if is_ranged(query): - # Raises GentoolkitInvalidCPV here if invalid - ranged_query = Package(query) - - pkg = get_match(query) - logpath = get_logpath(pkg) - log_entries = split_changelog(logpath) - if not any(log_entries): - raise errors.GentoolkitFatalError( - "%s exists but doesn't contain entries." % pp.path(logpath)) - indexed_entries = index_changelog(log_entries) + match = get_match(query) + changelog_path = os.path.join(match.get_package_path(), 'ChangeLog') + changelog = ChangeLog(changelog_path) # # Output # - if QUERY_OPTS['onlyLatest']: - print log_entries[0].strip() - elif QUERY_OPTS['showFullLog']: - end = QUERY_OPTS['limit'] or len(log_entries) - for entry in log_entries[:end]: - print entry - first_run = False - elif log_entries and not indexed_entries: - # We can't match anything, so just print latest: - print log_entries[0].strip() + if (QUERY_OPTS['onlyLatest'] or ( + changelog.entries and not changelog.indexed_entries + )): + print changelog.latest.strip() else: - if ranged_query: - pkg = ranged_query - first_run = print_matching_entries(indexed_entries, pkg, first_run) + end = QUERY_OPTS['limit'] or len(changelog.indexed_entries) + if QUERY_OPTS['to'] or QUERY_OPTS['from']: + print_entries( + changelog.entries_matching_range( + from_ver=QUERY_OPTS['from'], + to_ver=QUERY_OPTS['to'] + )[:end] + ) + elif QUERY_OPTS['showFullLog']: + print_entries(changelog.full[:end]) + else: + # Raises GentoolkitInvalidAtom here if invalid + atom = Atom(query) if is_ranged(query) else '=' + str(match.cpv) + print_entries(changelog.entries_matching_atom(atom)[:end]) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/check.py b/pym/gentoolkit/equery/check.py index 2531970..011fd5d 100644 --- a/pym/gentoolkit/equery/check.py +++ b/pym/gentoolkit/equery/check.py @@ -4,7 +4,7 @@ # # $Header: $ -"""Check timestamps and MD5sums for files owned by a given installed package""" +"""Check timestamps and MD5 sums for files owned by a given installed package""" __docformat__ = 'epytext' @@ -14,43 +14,166 @@ __docformat__ = 'epytext' import os import sys +from functools import partial from getopt import gnu_getopt, GetoptError -try: - import portage.checksum as checksum -except ImportError: - import portage_checksum as checksum +import portage.checksum as checksum import gentoolkit.pprinter as pp -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import do_lookup +from gentoolkit import errors +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import do_lookup # ======= # Globals # ======= QUERY_OPTS = { - "categoryFilter": None, - "includeInstalled": False, + "includeInstalled": True, "includeOverlayTree": False, "includePortTree": False, "checkMD5sum": True, "checkTimestamp" : True, "isRegex": False, - "matchExact": True, + "onlyFailures": False, "printMatchInfo": False, "showSummary" : True, "showPassedFiles" : False, "showFailedFiles" : True } +# ======= +# Classes +# ======= + +class VerifyContents(object): + """Verify installed packages' CONTENTS files. + + The CONTENTS file contains timestamps and MD5 sums for each file owned + by a package. + """ + def __init__(self, printer_fn=None): + """Create a VerifyObjects instance. + + @type printer_fn: callable + @param printer_fn: if defined, will be applied to each result as found + """ + self.check_sums = True + self.check_timestamps = True + self.printer_fn = printer_fn + + self.is_regex = False + + def __call__( + self, + pkgs, + is_regex=False, + check_sums=True, + check_timestamps=True + ): + self.is_regex = is_regex + self.check_sums = check_sums + self.check_timestamps = check_timestamps + + result = {} + for pkg in pkgs: + # _run_checks returns tuple(n_passed, n_checked, err) + check_results = self._run_checks(pkg.get_contents()) + result[pkg.cpv] = check_results + if self.printer_fn is not None: + self.printer_fn(pkg.cpv, check_results) + + return result + + def _run_checks(self, files): + """Run some basic sanity checks on a package's contents. + + If the file type (ftype) is not a directory or symlink, optionally + verify MD5 sums or mtimes via L{self._verify_obj}. + + @see: gentoolkit.packages.get_contents() + @type files: dict + @param files: in form {'PATH': ['TYPE', 'TIMESTAMP', 'MD5SUM']} + @rtype: tuple + @return: + n_passed (int): number of files that passed all checks + n_checked (int): number of files checked + errs (list): check errors' descriptions + """ + n_checked = 0 + n_passed = 0 + errs = [] + for cfile in files: + n_checked += 1 + ftype = files[cfile][0] + if not os.path.exists(cfile): + errs.append("%s does not exist" % cfile) + continue + elif ftype == "dir": + if not os.path.isdir(cfile): + err = "%(cfile)s exists, but is not a directory" + errs.append(err % locals()) + continue + elif ftype == "obj": + obj_errs = self._verify_obj(files, cfile, errs) + if len(obj_errs) > len(errs): + errs = obj_errs[:] + continue + elif ftype == "sym": + target = files[cfile][2].strip() + if not os.path.islink(cfile): + err = "%(cfile)s exists, but is not a symlink" + errs.append(err % locals()) + continue + tgt = os.readlink(cfile) + if tgt != target: + err = "%(cfile)s does not point to %(target)s" + errs.append(err % locals()) + continue + else: + err = "%(cfile)s has unknown type %(ftype)s" + errs.append(err % locals()) + continue + n_passed += 1 + + return n_passed, n_checked, errs + + def _verify_obj(self, files, cfile, errs): + """Verify the MD5 sum and/or mtime and return any errors.""" + + obj_errs = errs[:] + if self.check_sums: + md5sum = files[cfile][2] + try: + cur_checksum = checksum.perform_md5(cfile, calc_prelink=1) + except IOError: + err = "Insufficient permissions to read %(cfile)s" + obj_errs.append(err % locals()) + return obj_errs + if cur_checksum != md5sum: + err = "%(cfile)s has incorrect MD5sum" + obj_errs.append(err % locals()) + return obj_errs + if self.check_timestamps: + mtime = int(files[cfile][1]) + st_mtime = int(os.lstat(cfile).st_mtime) + if st_mtime != mtime: + err = ( + "%(cfile)s has wrong mtime (is %(st_mtime)d, should be " + "%(mtime)d)" + ) + obj_errs.append(err % locals()) + return obj_errs + + return obj_errs + # ========= # Functions # ========= def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -60,9 +183,14 @@ def print_help(with_description=True): print # Deprecation warning added by djanderson, 12/2008 - pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.") - pp.print_warn("Use globbing to simulate the old behavior (see man equery).") - pp.print_warn("Use '*' to check all installed packages.") + depwarning = ( + "Default action for this module has changed in Gentoolkit 0.3.", + "Use globbing to simulate the old behavior (see man equery).", + "Use '*' to check all installed packages.", + "Use 'foo-bar/*' to filter by category." + ) + for line in depwarning: + sys.stderr.write(pp.warn(line)) print print mod_usage(mod_name="check") @@ -70,135 +198,73 @@ def print_help(with_description=True): print pp.command("options") print format_options(( (" -h, --help", "display this help message"), - (" -c, --category CAT", "only check files from packages in CAT"), (" -f, --full-regex", "query is a regular expression"), + (" -o, --only-failures", "only display packages that do not pass"), )) +def checks_printer(cpv, data, verbose=True, only_failures=False): + """Output formatted results of pkg file(s) checks""" + seen = [] + + n_passed, n_checked, errs = data + n_failed = n_checked - n_passed + if only_failures and not n_failed: + return + else: + if verbose: + if not cpv in seen: + print "* Checking %s ..." % (pp.emph(str(cpv))) + seen.append(cpv) + else: + print "%s:" % cpv, + + if verbose: + for err in errs: + sys.stderr.write(pp.error(err)) + + if verbose: + n_passed = pp.number(str(n_passed)) + n_checked = pp.number(str(n_checked)) + info = " %(n_passed)s out of %(n_checked)s files passed" + print info % locals() + else: + print "failed(%s)" % n_failed + + def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) - posargs = (x[1] for x in module_opts) - for opt, posarg in zip(opts, posargs): + for opt in opts: if opt in ('-h', '--help'): print_help() sys.exit(0) - elif opt in ('-c', '--category'): - QUERY_OPTS['categoryFilter'] = posarg elif opt in ('-f', '--full-regex'): QUERY_OPTS['isRegex'] = True - - -def run_checks(files): - """Run some basic sanity checks on a package's contents. - - If the file type (ftype) is not a directory or symlink, optionally - verify MD5 sums or mtimes via verify_obj(). - - @see: gentoolkit.packages.get_contents() - @type files: dict - @param files: in form {'PATH': ['TYPE', 'TIMESTAMP', 'MD5SUM']} - @rtype: tuple - @return: - passed (int): number of files that passed all checks - checked (int): number of files checked - errs (list): check errors' descriptions - """ - - checked = 0 - passed = 0 - errs = [] - for cfile in files: - checked += 1 - ftype = files[cfile][0] - if not os.path.exists(cfile): - errs.append("%s does not exist" % cfile) - continue - elif ftype == "dir": - if not os.path.isdir(cfile): - err = "%(cfile)s exists, but is not a directory" - errs.append(err % locals()) - continue - elif ftype == "obj": - new_errs = verify_obj(files, cfile, errs) - if new_errs != errs: - errs = new_errs - continue - elif ftype == "sym": - target = files[cfile][2].strip() - if not os.path.islink(cfile): - err = "%(cfile)s exists, but is not a symlink" - errs.append(err % locals()) - continue - tgt = os.readlink(cfile) - if tgt != target: - err = "%(cfile)s does not point to %(target)s" - errs.append(err % locals()) - continue - else: - err = "%(cfile)s has unknown type %(ftype)s" - errs.append(err % locals()) - continue - passed += 1 - - return passed, checked, errs - - -def verify_obj(files, cfile, errs): - """Verify the MD5 sum and/or mtime and return any errors.""" - - if QUERY_OPTS["checkMD5sum"]: - md5sum = files[cfile][2] - try: - cur_checksum = checksum.perform_md5(cfile, calc_prelink=1) - except IOError: - err = "Insufficient permissions to read %(cfile)s" - errs.append(err % locals()) - return errs - if cur_checksum != md5sum: - err = "%(cfile)s has incorrect MD5sum" - errs.append(err % locals()) - return errs - if QUERY_OPTS["checkTimestamp"]: - mtime = int(files[cfile][1]) - st_mtime = os.lstat(cfile).st_mtime - if st_mtime != mtime: - err = "%(cfile)s has wrong mtime (is %(st_mtime)d, " + \ - "should be %(mtime)d)" - errs.append(err % locals()) - return errs - - return errs + elif opt in ('-o', '--only-failures'): + QUERY_OPTS['onlyFailures'] = True def main(input_args): """Parse input and run the program""" - short_opts = "hac:f" - long_opts = ('help', 'all', 'category=', 'full-regex') + short_opts = "hof" + long_opts = ('help', 'only-failures', 'full-regex') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) parse_module_options(module_opts) - - if not queries and not QUERY_OPTS["includeInstalled"]: + + if not queries: print_help() sys.exit(2) - elif queries and not QUERY_OPTS["includeInstalled"]: - QUERY_OPTS["includeInstalled"] = True - elif QUERY_OPTS["includeInstalled"]: - queries = ["*"] - - # - # Output - # first_run = True for query in queries: @@ -208,25 +274,18 @@ def main(input_args): matches = do_lookup(query, QUERY_OPTS) if not matches: - pp.print_error("No package found matching %s" % query) + raise errors.GentoolkitNoMatches(query, in_installed=True) matches.sort() - for pkg in matches: - if Config['verbose']: - print " * Checking %s ..." % pp.emph(pkg.cpv) - else: - print "%s:" % pkg.cpv - - passed, checked, errs = run_checks(pkg.get_contents()) - - if Config['verbose']: - for err in errs: - pp.print_error(err) + printer = partial( + checks_printer, + verbose=CONFIG['verbose'], + only_failures=QUERY_OPTS['onlyFailures'] + ) + check = VerifyContents(printer_fn=printer) + check(matches) - passed = pp.number(str(passed)) - checked = pp.number(str(checked)) - info = " %(passed)s out of %(checked)s files passed" - print info % locals() + first_run = False - first_run = False +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/depends.py b/pym/gentoolkit/equery/depends.py index a1a0d20..df2de9f 100644 --- a/pym/gentoolkit/equery/depends.py +++ b/pym/gentoolkit/equery/depends.py @@ -4,7 +4,7 @@ # # $Header: $ -"""List all direct dependencies matching a given query""" +"""List all packages that depend on a given query""" __docformat__ = 'epytext' @@ -15,34 +15,82 @@ __docformat__ = 'epytext' import sys from getopt import gnu_getopt, GetoptError -from portage.util import unique_array - import gentoolkit.pprinter as pp -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import compare_package_strings, do_lookup, \ - find_packages, get_cpvs, get_installed_cpvs -from gentoolkit.package import Package +from gentoolkit.dependencies import Dependencies +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import (get_cpvs, get_installed_cpvs, + compare_package_strings) # ======= # Globals # ======= QUERY_OPTS = { - "categoryFilter": None, - "includeInstalled": True, - "includePortTree": False, - "includeOverlayTree": False, - "isRegex": False, + "includeMasked": False, "onlyDirect": True, - "printMatchInfo": (not Config['quiet']), - "indentLevel": 0, - "depth": -1 + "maxDepth": -1, } -# Used to cache and detect looping -PKGSEEN = set() -PKGDEPS = {} -DEPPKGS = {} +# ======= +# Classes +# ======= + +class DependPrinter(object): + """Output L{gentoolkit.dependencies.Dependencies} objects.""" + def __init__(self, verbose=True): + if verbose: + self.print_fn = self.print_verbose + else: + self.print_fn = self.print_quiet + + def __call__(self, dep, dep_is_displayed=False): + self.format_depend(dep, dep_is_displayed) + + @staticmethod + def print_verbose(indent, cpv, use_conditional, depatom): + """Verbosely prints a set of dep strings.""" + + sep = ' ? ' if (depatom and use_conditional) else '' + print indent + pp.cpv(cpv), "(" + use_conditional + sep + depatom + ")" + + # W0613: *Unused argument %r* + # pylint: disable-msg=W0613 + @staticmethod + def print_quiet(indent, cpv, use_conditional, depatom): + """Quietly prints a subset set of dep strings.""" + + print indent + pp.cpv(cpv) + + def format_depend(self, dep, dep_is_displayed): + """Format a dependency for printing. + + @type dep: L{gentoolkit.dependencies.Dependencies} + @param dep: the dependency to display + """ + + depth = getattr(dep, 'depth', 0) + indent = " " * depth + mdep = dep.matching_dep + use_conditional = "" + if mdep.use_conditional: + use_conditional = " & ".join( + pp.useflag(u) for u in mdep.use_conditional.split() + ) + if mdep.operator == '=*': + formatted_dep = '=%s*' % str(mdep.cpv) + else: + formatted_dep = mdep.operator + str(mdep.cpv) + if mdep.slot: + formatted_dep += pp.emph(':') + pp.slot(mdep.slot) + if mdep.use: + useflags = pp.useflag(','.join(mdep.use.tokens)) + formatted_dep += (pp.emph('[') + useflags + pp.emph(']')) + + if dep_is_displayed: + indent = indent + " " * len(str(dep.cpv)) + self.print_fn(indent, '', use_conditional, formatted_dep) + else: + self.print_fn(indent, str(dep.cpv), use_conditional, formatted_dep) # ========= # Functions @@ -50,7 +98,7 @@ DEPPKGS = {} def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -63,125 +111,16 @@ def print_help(with_description=True): print pp.command("options") print format_options(( (" -h, --help", "display this help message"), - (" -a, --all-packages", - "include packages that are not installed (slow)"), + (" -a, --all-packages", + "include dependencies that are not installed (slow)"), (" -D, --indirect", "search both direct and indirect dependencies"), (" --depth=N", "limit indirect dependency tree to specified depth") )) -def cache_package_list(pkg_cache=None): - """Ensure that the package cache is set.""" - - if not pkg_cache: - if QUERY_OPTS['includePortTree']: - packages = [Package(x) for x in get_cpvs()] - else: - packages = [Package(x) for x in get_installed_cpvs()] - packages.sort() - pkg_cache = packages - else: - packages = pkg_cache - - return packages - - -def display_dependencies(cpv_is_displayed, dependency, cpv): - """Output dependencies calculated by find_dependencies. - - @type cpv_is_displayed: bool - @param cpv_is_displayed: if True, the cpv has already been printed - @see: gentoolkit.package.get_*_deps() - @type dependency: tuple - @param dependency: (comparator, [use flags], cpv) - @type cpv: string - @param cpv: cat/pkg-ver - """ - - atom = pp.pkgquery(dependency[0] + dependency[2]) - indent = " " * (QUERY_OPTS["indentLevel"] * 2) - useflags = pp.useflag(" & ".join(dependency[1])) - - if not cpv_is_displayed: - if dependency[1]: - if Config['verbose']: - print indent + pp.cpv(cpv), - print "(" + useflags + " ? " + atom + ")" - else: - print indent + cpv - else: - if Config['verbose']: - print indent + pp.cpv(cpv), - print "(" + atom + ")" - else: - print indent + cpv - elif Config['verbose']: - indent = indent + " " * len(cpv) - if dependency[1]: - print indent + " (" + useflags + " ? " + atom + ")" - else: - print indent + " (" + atom + ")" - - -def find_dependencies(matches, pkg_cache): - """Find dependencies for the packaged named in queries. - - @type queries: list - @param queries: packages to find the dependencies for - """ - - for pkg in cache_package_list(pkg_cache): - if not pkg.cpv in PKGDEPS: - try: - deps = pkg.get_runtime_deps() + pkg.get_compiletime_deps() - deps.extend(pkg.get_postmerge_deps()) - except KeyError: - # If the ebuild is not found... - continue - # Remove duplicate deps - deps = unique_array(deps) - PKGDEPS[pkg.cpv] = deps - else: - deps = PKGDEPS[pkg.cpv] - - cpv_is_displayed = False - for dependency in deps: - # TODO: (old) determine if dependency is enabled by USE flag - # Find all packages matching the dependency - depstr = dependency[0] + dependency[2] - if not depstr in DEPPKGS: - depcpvs = find_packages(depstr, - include_masked=QUERY_OPTS["includePortTree"]) - DEPPKGS[depstr] = depcpvs - else: - depcpvs = DEPPKGS[depstr] - - for depcpv in depcpvs: - is_match = False - if depcpv in matches: - is_match = True - - if is_match: - display_dependencies(cpv_is_displayed, dependency, pkg.cpv) - cpv_is_displayed = True - break - - # if --indirect specified, call ourselves again with the dependency - # Do not call if we have already called ourselves. - if (cpv_is_displayed and not QUERY_OPTS["onlyDirect"] and - pkg not in PKGSEEN and - (QUERY_OPTS["indentLevel"] < QUERY_OPTS["depth"] or - QUERY_OPTS["depth"] == -1)): - - PKGSEEN.add(pkg) - QUERY_OPTS["indentLevel"] += 1 - find_dependencies([pkg], pkg_cache) - QUERY_OPTS["indentLevel"] -= 1 - - def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) posargs = (x[1] for x in module_opts) @@ -190,9 +129,7 @@ def parse_module_options(module_opts): print_help() sys.exit(0) elif opt in ('-a', '--all-packages'): - QUERY_OPTS['includePortTree'] = True - elif opt in ('-d', '--direct'): - continue + QUERY_OPTS['includeMasked'] = True elif opt in ('-D', '--indirect'): QUERY_OPTS['onlyDirect'] = False elif opt in ('--depth'): @@ -200,29 +137,28 @@ def parse_module_options(module_opts): depth = int(posarg) else: err = "Module option --depth requires integer (got '%s')" - pp.print_error(err % posarg) + sys.stdout.write(pp.error(err % posarg)) print print_help(with_description=False) sys.exit(2) - QUERY_OPTS["depth"] = depth + QUERY_OPTS["maxDepth"] = depth def main(input_args): """Parse input and run the program""" - short_opts = "hadD" # -d, --direct was old option for default action long_opts = ('help', 'all-packages', 'direct', 'indirect', 'depth=') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) parse_module_options(module_opts) - + if not queries: print_help() sys.exit(2) @@ -231,21 +167,27 @@ def main(input_args): # Output # + dep_print = DependPrinter(verbose=CONFIG['verbose']) first_run = True for query in queries: if not first_run: print - matches = do_lookup(query, QUERY_OPTS) - - if matches: - find_dependencies(matches, None) + pkg = Dependencies(query) + if QUERY_OPTS['includeMasked']: + pkggetter = get_cpvs else: - if QUERY_OPTS['includePortTree']: - pp.print_error("No matching package found for %s" % query) - else: - pp.print_error( - "No matching package or all versions masked for %s" % query - ) + pkggetter = get_installed_cpvs + + if CONFIG['verbose']: + print " * These packages depend on %s:" % pp.emph(str(pkg.cpv)) + pkg.graph_reverse_depends( + pkgset=sorted(pkggetter(), cmp=compare_package_strings), + max_depth=QUERY_OPTS["maxDepth"], + only_direct=QUERY_OPTS["onlyDirect"], + printer_fn=dep_print + ) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/depgraph.py b/pym/gentoolkit/equery/depgraph.py index b9cd0a1..18d19ba 100644 --- a/pym/gentoolkit/equery/depgraph.py +++ b/pym/gentoolkit/equery/depgraph.py @@ -4,7 +4,7 @@ # # $Header: $ -"""Display a dependency graph for a given package""" +"""Display a direct dependency graph for a given package""" __docformat__ = 'epytext' @@ -13,30 +13,30 @@ __docformat__ = 'epytext' # ======= import sys +from functools import partial from getopt import gnu_getopt, GetoptError -import gentoolkit import gentoolkit.pprinter as pp from gentoolkit import errors -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import do_lookup, find_best_match +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import do_lookup # ======= # Globals # ======= QUERY_OPTS = { - "categoryFilter": None, - "depth": 0, - "displayUseflags": True, - "fancyFormat": True, + "depth": 1, + "noAtom": False, + "noIndent": False, + "noUseflags": False, "includeInstalled": True, "includePortTree": True, "includeOverlayTree": True, "includeMasked": True, "isRegex": False, "matchExact": True, - "printMatchInfo": (not Config['quiet']) + "printMatchInfo": (not CONFIG['quiet']) } # ========= @@ -45,7 +45,7 @@ QUERY_OPTS = { def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -53,66 +53,22 @@ def print_help(with_description=True): if with_description: print __doc__.strip() print + print "Default depth is set to 1 (direct only). Use --depth=0 for no max." + print print mod_usage(mod_name="depgraph") print print pp.command("options") print format_options(( (" -h, --help", "display this help message"), + (" -A, --no-atom", "do not show dependency atom"), (" -U, --no-useflags", "do not show USE flags"), - (" -l, --linear", "do not use fancy formatting"), + (" -l, --linear", "do not format the graph by indenting dependencies"), (" --depth=N", "limit dependency graph to specified depth") )) -def display_graph(pkg, stats, level=0, seen_pkgs=None, suffix=""): - """Display a dependency graph for a package - - @type pkg: gentoolkit.package.Package - @param pkg: package to check dependencies of - @type level: int - @param level: current depth level - @type seen_pkgs: set - @param seen_pkgs: a set of all packages that have had their deps graphed - """ - - if not seen_pkgs: - seen_pkgs = set() - - stats["packages"] += 1 - stats["maxdepth"] = max(stats["maxdepth"], level) - - pfx = "" - if QUERY_OPTS["fancyFormat"]: - pfx = (level * " ") + "`-- " - pp.print_info(0, pfx + pkg.cpv + suffix) - - seen_pkgs.add(pkg.cpv) - - deps = pkg.get_runtime_deps() + pkg.get_compiletime_deps() - deps.extend(pkg.get_postmerge_deps()) - for dep in deps: - suffix = "" - depcpv = dep[2] - deppkg = find_best_match(dep[0] + depcpv) - if not deppkg: - print (pfx + dep[0] + depcpv), - print "(unable to resolve: package masked or removed)" - continue - if deppkg.get_cpv() in seen_pkgs: - continue - if depcpv.find("virtual") == 0: - suffix += " (%s)" % pp.cpv(depcpv) - if dep[1] and QUERY_OPTS["displayUseflags"]: - suffix += " [%s]" % pp.useflagon(' '.join(dep[1])) - if (level < QUERY_OPTS["depth"] or QUERY_OPTS["depth"] <= 0): - seen_pkgs, stats = display_graph(deppkg, stats, level+1, - seen_pkgs, suffix) - - return seen_pkgs, stats - - def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) posargs = (x[1] for x in module_opts) @@ -120,38 +76,112 @@ def parse_module_options(module_opts): if opt in ('-h', '--help'): print_help() sys.exit(0) + if opt in ('-A', '--no-atom'): + QUERY_OPTS["noAtom"] = True if opt in ('-U', '--no-useflags'): - QUERY_OPTS["displayUseflags"] = False + QUERY_OPTS["noUseflags"] = True if opt in ('-l', '--linear'): - QUERY_OPTS["fancyFormat"] = False + QUERY_OPTS["noIndent"] = True if opt in ('--depth'): if posarg.isdigit(): depth = int(posarg) else: - err = "Module option --depth requires integer (got '%s')" - pp.print_error(err % posarg) + err = "Module option --depth requires integer (got '%s')" + sys.stderr.write(pp.error(err % posarg)) print print_help(with_description=False) sys.exit(2) QUERY_OPTS["depth"] = depth +def depgraph_printer( + depth, + pkg, + dep, + no_use=False, + no_atom=False, + no_indent=False, + initial_pkg=False +): + """Display L{gentoolkit.dependencies.Dependencies.graph_depends} results. + + @type depth: int + @param depth: depth of indirection, used to calculate indent + @type pkg: L{gentoolkit.package.Package} + @param pkg: "best match" package matched by B{dep} + @type dep: L{gentoolkit.atom.Atom} + @param dep: dependency that matched B{pkg} + @type no_use: bool + @param no_use: don't output USE flags + @type no_atom: bool + @param no_atom: don't output dep atom + @type no_indent: bool + @param no_indent: don't output indent based on B{depth} + @type initial_pkg: bool + @param initial_pkg: somewhat of a hack used to print the root package of + the graph with absolutely no indent + """ + indent = '' if no_indent or initial_pkg else ' ' + (' ' * depth) + decorator = '[%3d] ' % depth if no_indent else '`-- ' + use = '' + try: + atom = '' if no_atom else ' (%s)' % dep.atom + if not no_use and dep is not None and dep.use: + use = ' [%s]' % ' '.join( + pp.useflag(x, enabled=True) for x in dep.use.tokens + ) + except AttributeError: + # 'NoneType' object has no attribute 'atom' + atom = '' + try: + print ''.join((indent, decorator, pp.cpv(str(pkg.cpv)), atom, use)) + except AttributeError: + # 'NoneType' object has no attribute 'cpv' + print ''.join((indent, decorator, "(no match for %r)" % dep.atom)) + + +def make_depgraph(pkg, printer_fn): + """Create and display depgraph for each package.""" + + if CONFIG['verbose']: + print " * direct dependency graph for %s:" % pp.cpv(str(pkg.cpv)) + else: + print "%s:" % str(pkg.cpv) + + # Print out the first package + printer_fn(0, pkg, None, initial_pkg=True) + + deps = pkg.deps.graph_depends( + max_depth=QUERY_OPTS['depth'], + printer_fn=printer_fn, + # Use this to set this pkg as the graph's root; better way? + result=[(0, pkg)] + ) + + if CONFIG['verbose']: + pkgname = pp.cpv(str(pkg.cpv)) + n_packages = pp.number(str(len(deps))) + max_seen = pp.number(str(max(x[0] for x in deps))) + info = "[ %s stats: packages (%s), max depth (%s) ]" + print info % (pkgname, n_packages, max_seen) + + def main(input_args): """Parse input and run the program""" - short_opts = "hUl" - long_opts = ('help', 'no-useflags', 'depth=') + short_opts = "hAUl" + long_opts = ('help', 'no-atom', 'no-useflags', 'depth=') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) parse_module_options(module_opts) - + if not queries: print_help() sys.exit(2) @@ -168,22 +198,26 @@ def main(input_args): matches = do_lookup(query, QUERY_OPTS) if not matches: - errors.GentoolkitNoMatches(query) + raise errors.GentoolkitNoMatches(query) + + if CONFIG['verbose']: + printer = partial( + depgraph_printer, + no_atom=QUERY_OPTS['noAtom'], + no_indent=QUERY_OPTS['noIndent'], + no_use=QUERY_OPTS['noUseflags'] + ) + else: + printer = partial( + depgraph_printer, + no_atom=True, + no_indent=True, + no_use=True + ) for pkg in matches: - stats = {"maxdepth": 0, "packages": 0} - - if Config['verbose']: - pp.print_info(3, " * dependency graph for %s:" % pp.cpv(pkg.cpv)) - else: - pp.print_info(0, "%s:" % pkg.cpv) - - stats = display_graph(pkg, stats)[1] - - if Config['verbose']: - info = ''.join(["[ ", pp.cpv(pkg.cpv), " stats: packages (", - pp.number(str(stats["packages"])), "), max depth (", - pp.number(str(stats["maxdepth"])), ") ]"]) - pp.print_info(0, info) + make_depgraph(pkg, printer) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/files.py b/pym/gentoolkit/equery/files.py index 80caf1d..4f588bb 100644 --- a/pym/gentoolkit/equery/files.py +++ b/pym/gentoolkit/equery/files.py @@ -16,11 +16,12 @@ import os import sys from getopt import gnu_getopt, GetoptError -import gentoolkit +import portage + import gentoolkit.pprinter as pp -from gentoolkit.equery import format_filetype, format_options, mod_usage, \ - Config -from gentoolkit.helpers2 import do_lookup +from gentoolkit.equery import (format_filetype, format_options, mod_usage, + CONFIG) +from gentoolkit.helpers import do_lookup # ======= # Globals @@ -35,15 +36,16 @@ QUERY_OPTS = { "isRegex": False, "matchExact": True, "outputTree": False, - "printMatchInfo": (not Config['quiet']), + "printMatchInfo": (not CONFIG['quiet']), "showType": False, "showTimestamp": False, "showMD5": False, "typeFilter": None } -FILTER_RULES = ('dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc', - 'man', 'info') +FILTER_RULES = ( + 'dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc', 'man', 'info' +) # ========= # Functions @@ -51,7 +53,7 @@ FILTER_RULES = ('dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc', def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -69,12 +71,14 @@ def print_help(with_description=True): (" -t, --type", "include file type in output"), (" --tree", "display results in a tree (turns off other options)"), (" -f, --filter=RULES", "filter output by file type"), - (" RULES", + (" RULES", "a comma-separated list (no spaces); choose from:") )) print " " * 24, ', '.join(pp.emph(x) for x in FILTER_RULES) +# R0912: *Too many branches (%s/%s)* +# pylint: disable-msg=R0912 def display_files(contents): """Display the content of an installed package. @@ -89,40 +93,43 @@ def display_files(contents): for name in filenames: if QUERY_OPTS["outputTree"]: - basename = name.split("/")[1:] + dirdepth = name.count('/') + indent = " " + if dirdepth == 2: + indent = " " + elif dirdepth > 2: + indent = " " * (dirdepth - 1) + + basename = name.rsplit("/", dirdepth - 1) if contents[name][0] == "dir": if len(last) == 0: last = basename - print pp.path(" /" + basename[0]) + print pp.path(indent + basename[0]) continue - numol = 0 for i, directory in enumerate(basename): try: if directory in last[i]: - numol = i + 1 continue - # W0704: Except doesn't do anything - # pylint: disable-msg=W0704 except IndexError: pass last = basename if len(last) == 1: - print pp.path(" " + last[0]) + print pp.path(indent + last[0]) continue - ind = " " * (numol * 3) - print pp.path(ind + "> " + "/" + last[-1]) + print pp.path(indent + "> /" + last[-1]) elif contents[name][0] == "sym": - print pp.path(" " * (len(last) * 3) + "+"), + print pp.path(indent + "+"), print pp.path_symlink(basename[-1] + " -> " + contents[name][2]) - else: - print pp.path(" " * (len(last) * 3) + "+ ") + basename[-1] + else: + print pp.path(indent + "+ ") + basename[-1] else: - pp.print_info(0, format_filetype( + print format_filetype( name, contents[name], show_type=QUERY_OPTS["showType"], show_md5=QUERY_OPTS["showMD5"], - show_timestamp=QUERY_OPTS["showTimestamp"])) + show_timestamp=QUERY_OPTS["showTimestamp"] + ) def filter_by_doc(contents, content_filter): @@ -136,7 +143,7 @@ def filter_by_doc(contents, content_filter): for path in contents: if contents[path][0] == 'obj' and path.startswith(docpath): filtered_content[path] = contents[path] - + return filtered_content @@ -150,7 +157,7 @@ def filter_by_command(contents): if (contents[path][0] in ['obj', 'sym'] and os.path.dirname(path) in userpath): filtered_content[path] = contents[path] - + return filtered_content @@ -172,7 +179,7 @@ def filter_by_path(contents): if check_subdirs: while (paths and paths[-1].startswith(basepath)): paths.pop() - + return filtered_content @@ -180,9 +187,9 @@ def filter_by_conf(contents): """Return a copy of content filtered by configuration files.""" filtered_content = {} - conf_path = gentoolkit.settings["CONFIG_PROTECT"].split() + conf_path = portage.settings["CONFIG_PROTECT"].split() conf_path = tuple(os.path.normpath(x) for x in conf_path) - conf_mask_path = gentoolkit.settings["CONFIG_PROTECT_MASK"].split() + conf_mask_path = portage.settings["CONFIG_PROTECT_MASK"].split() conf_mask_path = tuple(os.path.normpath(x) for x in conf_mask_path) for path in contents: if contents[path][0] == 'obj' and path.startswith(conf_path): @@ -206,7 +213,7 @@ def filter_contents(contents): content_filter = QUERY_OPTS['typeFilter'] else: return contents - + filtered_content = {} if frozenset(('dir', 'obj', 'sym', 'dev')).intersection(content_filter): # Filter elements by type (as recorded in CONTENTS) @@ -221,12 +228,12 @@ def filter_contents(contents): filtered_content.update(filter_by_conf(contents)) if frozenset(('doc' ,'man' ,'info')).intersection(content_filter): filtered_content.update(filter_by_doc(contents, content_filter)) - + return filtered_content def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" content_filter = [] opts = (x[0] for x in module_opts) @@ -250,7 +257,9 @@ def parse_module_options(module_opts): content_filter.extend(x.lstrip('=') for x in f_split) for rule in content_filter: if not rule in FILTER_RULES: - pp.print_error("Invalid filter rule '%s'" % rule) + sys.stderr.write( + pp.error("Invalid filter rule '%s'" % rule) + ) print print_help(with_description=False) sys.exit(2) @@ -260,20 +269,21 @@ def parse_module_options(module_opts): def main(input_args): """Parse input and run the program""" + # -e, --exact-name is legacy option. djanderson '09 short_opts = "hemstf:" long_opts = ('help', 'exact-name', 'md5sum', 'timestamp', 'type', 'tree', - 'filter=') + 'filter=') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) parse_module_options(module_opts) - + if not queries: print_help() sys.exit(2) @@ -294,13 +304,17 @@ def main(input_args): matches = do_lookup(query, QUERY_OPTS) if not matches: - pp.print_error("No matching packages found for %s" % query) + sys.stderr.write( + pp.error("No matching packages found for %s" % query) + ) for pkg in matches: - if Config['verbose']: - print " * Contents of %s:" % pp.cpv(pkg.cpv) + if CONFIG['verbose']: + print " * Contents of %s:" % pp.cpv(str(pkg.cpv)) contents = pkg.get_contents() display_files(filter_contents(contents)) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/hasuse.py b/pym/gentoolkit/equery/hasuse.py index c110a22..82b2e29 100644 --- a/pym/gentoolkit/equery/hasuse.py +++ b/pym/gentoolkit/equery/hasuse.py @@ -15,11 +15,11 @@ __docformat__ = 'epytext' import sys from getopt import gnu_getopt, GetoptError -import gentoolkit import gentoolkit.pprinter as pp -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import do_lookup, get_installed_cpvs, print_sequence -from gentoolkit.package import Package, PackageFormatter +from gentoolkit import errors +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import do_lookup +from gentoolkit.package import PackageFormatter # ======= # Globals @@ -41,7 +41,7 @@ QUERY_OPTS = { def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -61,8 +61,42 @@ def print_help(with_description=True): )) +def display_useflags(query, pkg): + """Display USE flag information for a given package.""" + + try: + useflags = [x.lstrip("+-") for x in pkg.get_env_var("IUSE").split()] + except errors.GentoolkitFatalError: + # aux_get KeyError or other unexpected result + return + + if query not in useflags: + return + + if CONFIG['verbose']: + fmt_pkg = PackageFormatter(pkg, do_format=True) + else: + fmt_pkg = PackageFormatter(pkg, do_format=False) + + if (QUERY_OPTS["includeInstalled"] and + not QUERY_OPTS["includePortTree"] and + not QUERY_OPTS["includeOverlayTree"]): + if not 'I' in fmt_pkg.location: + return + if (QUERY_OPTS["includePortTree"] and + not QUERY_OPTS["includeOverlayTree"]): + if not 'P' in fmt_pkg.location: + return + if (QUERY_OPTS["includeOverlayTree"] and + not QUERY_OPTS["includePortTree"]): + if not 'O' in fmt_pkg.location: + return + print fmt_pkg + + + def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" # Parse module options opts = (x[0] for x in module_opts) @@ -89,7 +123,7 @@ def main(input_args): try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) @@ -112,33 +146,11 @@ def main(input_args): if not first_run: print - if Config['verbose']: + if CONFIG['verbose']: print " * Searching for USE flag %s ... " % pp.emph(query) for pkg in matches: - - useflags = [x.lstrip("+-") for x in pkg.get_env_var("IUSE").split()] - if query not in useflags: - continue - - if Config['verbose']: - pkgstr = PackageFormatter(pkg, format=True) - else: - pkgstr = PackageFormatter(pkg, format=False) - - if (QUERY_OPTS["includeInstalled"] and - not QUERY_OPTS["includePortTree"] and - not QUERY_OPTS["includeOverlayTree"]): - if not 'I' in pkgstr.location: - continue - if (QUERY_OPTS["includePortTree"] and - not QUERY_OPTS["includeOverlayTree"]): - if not 'P' in pkgstr.location: - continue - if (QUERY_OPTS["includeOverlayTree"] and - not QUERY_OPTS["includePortTree"]): - if not 'O' in pkgstr.location: - continue - print pkgstr - + display_useflags(query, pkg) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/list_.py b/pym/gentoolkit/equery/list_.py index dd13029..32f8eff 100644 --- a/pym/gentoolkit/equery/list_.py +++ b/pym/gentoolkit/equery/list_.py @@ -17,8 +17,8 @@ from getopt import gnu_getopt, GetoptError import gentoolkit import gentoolkit.pprinter as pp -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import do_lookup, get_installed_cpvs +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import do_lookup, get_installed_cpvs from gentoolkit.package import Package, PackageFormatter # ======= @@ -26,14 +26,14 @@ from gentoolkit.package import Package, PackageFormatter # ======= QUERY_OPTS = { - "categoryFilter": None, "duplicates": False, "includeInstalled": True, "includePortTree": False, "includeOverlayTree": False, "includeMasked": True, + "includeMaskReason": False, "isRegex": False, - "printMatchInfo": (not Config['quiet']) + "printMatchInfo": (not CONFIG['quiet']) } # ========= @@ -42,7 +42,7 @@ QUERY_OPTS = { def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -50,11 +50,16 @@ def print_help(with_description=True): if with_description: print __doc__.strip() print - # Deprecation warning added 04/09: djanderson - pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.") - pp.print_warn("-e, --exact-name is now the default behavior.") - pp.print_warn("Use globbing to simulate the old behavior (see man equery).") - pp.print_warn("Use '*' to check all installed packages.") + + # Deprecation warning added by djanderson, 12/2008 + depwarning = ( + "Default action for this module has changed in Gentoolkit 0.3.", + "Use globbing to simulate the old behavior (see man equery).", + "Use '*' to check all installed packages.", + "Use 'foo-bar/*' to filter by category." + ) + for line in depwarning: + sys.stderr.write(pp.warn(line)) print print mod_usage(mod_name="list") @@ -62,9 +67,9 @@ def print_help(with_description=True): print pp.command("options") print format_options(( (" -h, --help", "display this help message"), - (" -c, --category CAT", "only search in the category CAT"), (" -d, --duplicates", "list only installed duplicate packages"), (" -f, --full-regex", "query is a regular expression"), + (" -m, --mask-reason", "include reason for package mask"), (" -I, --exclude-installed", "exclude installed packages from output"), (" -o, --overlay-tree", "list packages in overlays"), @@ -78,10 +83,10 @@ def get_duplicates(matches): dups = {} result = [] for pkg in matches: - if pkg.key in dups: - dups[pkg.key].append(pkg) + if pkg.cp in dups: + dups[pkg.cp].append(pkg) else: - dups[pkg.key] = [pkg] + dups[pkg.cp] = [pkg] for cpv in dups.values(): if len(cpv) > 1: @@ -91,7 +96,7 @@ def get_duplicates(matches): def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) posargs = (x[1] for x in module_opts) @@ -99,10 +104,6 @@ def parse_module_options(module_opts): if opt in ('-h', '--help'): print_help() sys.exit(0) - elif opt in ('-a', '--all'): - QUERY_OPTS['listAllPackages'] = True - elif opt in ('-c', '--category'): - QUERY_OPTS['categoryFilter'] = posarg elif opt in ('-I', '--exclude-installed'): QUERY_OPTS['includeInstalled'] = False elif opt in ('-p', '--portage-tree'): @@ -111,9 +112,13 @@ def parse_module_options(module_opts): QUERY_OPTS['includeOverlayTree'] = True elif opt in ('-f', '--full-regex'): QUERY_OPTS['isRegex'] = True + elif opt in ('-m', '--mask-reason'): + QUERY_OPTS['includeMaskReason'] = True elif opt in ('-e', '--exact-name'): - pp.print_warn("-e, --exact-name is now default.") - pp.print_warn("Use globbing to simulate the old behavior.") + sys.stderr.write(pp.warn("-e, --exact-name is now default.")) + sys.stderr.write( + pp.warn("Use globbing to simulate the old behavior.") + ) print elif opt in ('-d', '--duplicates'): QUERY_OPTS['duplicates'] = True @@ -122,18 +127,20 @@ def parse_module_options(module_opts): def main(input_args): """Parse input and run the program""" - short_opts = "hc:defiIop" # -i, -e were options for default actions + short_opts = "hdefiImop" # -i, -e were options for default actions # 04/09: djanderson + # --all is no longer needed. Kept for compatibility. # --installed is no longer needed. Kept for compatibility. # --exact-name is no longer needed. Kept for compatibility. - long_opts = ('help', 'all', 'category=', 'installed', 'exclude-installed', - 'portage-tree', 'overlay-tree', 'full-regex', 'exact-name', 'duplicates') + long_opts = ('help', 'all', 'installed', 'exclude-installed', + 'mask-reason', 'portage-tree', 'overlay-tree', 'full-regex', 'exact-name', + 'duplicates') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) @@ -145,6 +152,7 @@ def main(input_args): QUERY_OPTS["includeInstalled"] = True QUERY_OPTS["includePortTree"] = False QUERY_OPTS["includeOverlayTree"] = False + QUERY_OPTS["includeMaskReason"] = False if not queries: print_help() @@ -168,10 +176,10 @@ def main(input_args): # for pkg in matches: - if Config['verbose']: - pkgstr = PackageFormatter(pkg, format=True) + if CONFIG['verbose']: + pkgstr = PackageFormatter(pkg, do_format=True) else: - pkgstr = PackageFormatter(pkg, format=False) + pkgstr = PackageFormatter(pkg, do_format=False) if (QUERY_OPTS["includeInstalled"] and not QUERY_OPTS["includePortTree"] and @@ -188,4 +196,29 @@ def main(input_args): continue print pkgstr + if QUERY_OPTS["includeMaskReason"]: + ms_int, ms_orig = pkgstr.format_mask_status() + if not ms_int > 2: + # ms_int is a number representation of mask level. + # Only 2 and above are "hard masked" and have reasons. + continue + mask_reason = pkg.get_mask_reason() + if not mask_reason: + # Package not on system or not masked + continue + elif not any(mask_reason): + print " * No mask reason given" + else: + status = ', '.join(ms_orig) + explanation = mask_reason[0] + mask_location = mask_reason[1] + print " * Masked by %r" % status + print " * %s:" % mask_location + print '\n'.join( + [' * %s' % line.lstrip(' #') + for line in explanation.splitlines()] + ) + first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/meta.py b/pym/gentoolkit/equery/meta.py index d847f56..fc38bff 100644 --- a/pym/gentoolkit/equery/meta.py +++ b/pym/gentoolkit/equery/meta.py @@ -9,7 +9,6 @@ # Move to Imports section after Python-2.6 is stable from __future__ import with_statement -__author__ = "Douglas Anderson" __docformat__ = 'epytext' # ======= @@ -17,18 +16,14 @@ __docformat__ = 'epytext' # ======= import os -import re +import re import sys -import xml.etree.cElementTree as ET from getopt import gnu_getopt, GetoptError -from portage import settings - import gentoolkit.pprinter as pp from gentoolkit import errors -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import find_packages, print_sequence, print_file, \ - uniqify +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import find_packages, print_sequence, print_file from gentoolkit.textwrap_ import TextWrapper # ======= @@ -40,28 +35,23 @@ from gentoolkit.textwrap_ import TextWrapper # pylint: disable-msg=E1101 QUERY_OPTS = { - "current": False, - "description": False, - "herd": False, - "maintainer": False, - "useflags": False, - "upstream": False, - "xml": False -} - -# Get the location of the main Portage tree -PORTDIR = [settings["PORTDIR"] or os.path.join(os.sep, "usr", "portage")] -# Check for overlays -if settings["PORTDIR_OVERLAY"]: - PORTDIR.extend(settings["PORTDIR_OVERLAY"].split()) + 'current': False, + 'description': False, + 'herd': False, + 'keywords': False, + 'maintainer': False, + 'useflags': False, + 'upstream': False, + 'xml': False +} # ========= # Functions # ========= -def print_help(with_description=True): +def print_help(with_description=True, with_usage=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -69,83 +59,207 @@ def print_help(with_description=True): if with_description: print __doc__.strip() print - print mod_usage(mod_name="meta") - print + if with_usage: + print mod_usage(mod_name="meta") + print print pp.command("options") print format_options(( (" -h, --help", "display this help message"), - (" -c, --current", "parse metadata.xml in the current directory"), (" -d, --description", "show an extended package description"), (" -H, --herd", "show the herd(s) for the package"), + (" -k, --keywords", "show keywords for all matching package versions"), (" -m, --maintainer", "show the maintainer(s) for the package"), (" -u, --useflags", "show per-package USE flag descriptions"), (" -U, --upstream", "show package's upstream information"), - (" -x, --xml", "show the plain XML file") + (" -x, --xml", "show the plain metadata.xml file") )) -def call_get_functions(metadata_path, package_dir, QUERY_OPTS): +def format_herds(herds): + """Format herd information for display.""" + + result = [] + for herd in herds: + herdstr = '' + email = "(%s)" % herd[1] if herd[1] else '' + herdstr = herd[0] + if CONFIG['verbose']: + herdstr += " %s" % (email,) + result.append(herdstr) + + return result + + +def format_maintainers(maints): + """Format maintainer information for display.""" + + result = [] + for maint in maints: + maintstr = '' + maintstr = maint.email + if CONFIG['verbose']: + maintstr += " (%s)" % (maint.name,) if maint.name else '' + maintstr += "\n%s" % (maint.description,) \ + if maint.description else '' + result.append(maintstr) + + return result + + +def format_upstream(upstream): + """Format upstream information for display.""" + + def _format_upstream_docs(docs): + result = [] + for doc in docs: + doc_location = doc[0] + doc_lang = doc[1] + docstr = doc_location + if doc_lang is not None: + docstr += " (%s)" % (doc_lang,) + result.append(docstr) + return result + + def _format_upstream_ids(ids): + result = [] + for id_ in ids: + site = id_[0] + proj_id = id_[1] + idstr = "%s ID: %s" % (site, proj_id) + result.append(idstr) + return result + + result = [] + for up in upstream: + upmaints = format_maintainers(up.maintainers) + for upmaint in upmaints: + result.append(format_line(upmaint, "Maintainer: ", " " * 13)) + + for upchange in up.changelogs: + result.append(format_line(upchange, "ChangeLog: ", " " * 13)) + + updocs = _format_upstream_docs(up.docs) + for updoc in updocs: + result.append(format_line(updoc, "Docs: ", " " * 13)) + + for upbug in up.bugtrackers: + result.append(format_line(upbug, "Bugs-to: ", " " * 13)) + + upids = _format_upstream_ids(up.remoteids) + for upid in upids: + result.append(format_line(upid, "Remote-ID: ", " " * 13)) + + return result + + +def format_useflags(useflags): + """Format USE flag information for display.""" + + result = [] + for flag in useflags: + result.append(pp.useflag(flag.name)) + result.append(flag.description) + result.append("") + + return result + + +def format_keywords(match): + """Format keywords information for display.""" + + kwsplit = match.get_env_var('KEYWORDS').split() + ver = match.cpv.fullversion + keywords = '' + for kw in kwsplit: + if kw.startswith('~'): + keywords += " %s" % pp.useflag(kw, enabled=True) + else: + keywords += " %s" % pp.useflag(kw, enabled=False) + + if CONFIG['verbose']: + result = format_line( + keywords, "%s: " % pp.cpv(ver), " " * (len(ver) + 2) + ) + else: + result = "%s:%s" % (ver, keywords) + + return result + +# R0912: *Too many branches (%s/%s)* +# pylint: disable-msg=R0912 +def call_format_functions(matches): """Call information gathering functions and display the results.""" - - if Config['verbose']: - print get_overlay_name(package_dir) - try: - xml_tree = ET.parse(metadata_path) - except IOError: - pp.print_error("No metadata available") - first_run = False - return + # Choose a good package to reference metadata from + ref_pkg = get_reference_pkg(matches) + + if CONFIG['verbose']: + repo = ref_pkg.get_repo_name() + print " * %s [%s]" % (pp.cpv(ref_pkg.cpv.cp), pp.section(repo)) got_opts = False - if (QUERY_OPTS["herd"] or QUERY_OPTS["description"] or - QUERY_OPTS["useflags"] or QUERY_OPTS["maintainer"] or - QUERY_OPTS["upstream"] or QUERY_OPTS["xml"]): + if any(QUERY_OPTS.values()): # Specific information requested, less formatting got_opts = True + if not got_opts: + pkg_loc = ref_pkg.get_package_path() + print format_line(pkg_loc, "Location: ", " " * 13) + if QUERY_OPTS["herd"] or not got_opts: - herd = get_herd(xml_tree) + herds = format_herds(ref_pkg.metadata.get_herds(include_email=True)) if QUERY_OPTS["herd"]: - herd = format_list(herd) + print_sequence(format_list(herds)) else: - herd = format_list(herd, "Herd: ", " " * 13) - print_sequence(herd) + for herd in herds: + print format_line(herd, "Herd: ", " " * 13) if QUERY_OPTS["maintainer"] or not got_opts: - maint = get_maitainer(xml_tree) + maints = format_maintainers(ref_pkg.metadata.get_maintainers()) if QUERY_OPTS["maintainer"]: - maint = format_list(maint) + print_sequence(format_list(maints)) else: - maint = format_list(maint, "Maintainer: ", " " * 13) - print_sequence(maint) + if not maints: + print format_line([], "Maintainer: ", " " * 13) + else: + for maint in maints: + print format_line(maint, "Maintainer: ", " " * 13) if QUERY_OPTS["upstream"] or not got_opts: - upstream = get_upstream(xml_tree) + upstream = format_upstream(ref_pkg.metadata.get_upstream()) if QUERY_OPTS["upstream"]: upstream = format_list(upstream) else: upstream = format_list(upstream, "Upstream: ", " " * 13) print_sequence(upstream) + if QUERY_OPTS["keywords"] or not got_opts: + for match in matches: + kwds = format_keywords(match) + if QUERY_OPTS["keywords"]: + print kwds + else: + indent = " " * (15 + len(match.cpv.fullversion)) + print format_line(kwds, "Keywords: ", indent) + if QUERY_OPTS["description"]: - desc = get_description(xml_tree) + desc = ref_pkg.metadata.get_descriptions() print_sequence(format_list(desc)) if QUERY_OPTS["useflags"]: - useflags = get_useflags(xml_tree) + useflags = format_useflags(ref_pkg.metadata.get_useflags()) print_sequence(format_list(useflags)) if QUERY_OPTS["xml"]: - print_file(metadata_path) + print_file(os.path.join(ref_pkg.get_package_path(), 'metadata.xml')) def format_line(line, first="", subsequent="", force_quiet=False): """Wrap a string at word boundaries and optionally indent the first line and/or subsequent lines with custom strings. - Preserve newlines if the longest line is not longer than - Config['termWidth']. To force the preservation of newlines and indents, + Preserve newlines if the longest line is not longer than + CONFIG['termWidth']. To force the preservation of newlines and indents, split the string into a list and feed it to format_line via format_list. @see: format_list() @@ -161,7 +275,7 @@ def format_line(line, first="", subsequent="", force_quiet=False): """ if line: - line = line.expandtabs().strip("\n").splitlines() + line = line.expandtabs().strip("\n").splitlines() else: if force_quiet: return @@ -172,18 +286,18 @@ def format_line(line, first="", subsequent="", force_quiet=False): wider_indent = first else: wider_indent = subsequent - + widest_line_len = len(max(line, key=len)) + len(wider_indent) - - if widest_line_len > Config['termWidth']: - twrap = TextWrapper(width=Config['termWidth'], expand_tabs=False, + + if widest_line_len > CONFIG['termWidth']: + twrap = TextWrapper(width=CONFIG['termWidth'], expand_tabs=False, initial_indent=first, subsequent_indent=subsequent) line = " ".join(line) line = re.sub("\s+", " ", line) line = line.lstrip() result = twrap.fill(line) else: - # line will fit inside Config['termWidth'], so preserve whitespace and + # line will fit inside CONFIG['termWidth'], so preserve whitespace and # newlines line[0] = first + line[0] # Avoid two newlines if len == 1 @@ -212,7 +326,7 @@ def format_list(lst, first="", subsequent="", force_quiet=False): @type subsequent: string @param subsequent: text to prepend to subsequent lines @rtype: list - @return: list with element text wrapped at Config['termWidth'] + @return: list with element text wrapped at CONFIG['termWidth'] """ result = [] @@ -229,7 +343,7 @@ def format_list(lst, first="", subsequent="", force_quiet=False): # We don't want to send a blank line to format_line() result.append("") else: - if Config['verbose']: + if CONFIG['verbose']: if force_quiet: result = None else: @@ -239,238 +353,34 @@ def format_list(lst, first="", subsequent="", force_quiet=False): return result -def get_herd(xml_tree): - """Return a list of text nodes for <herd>.""" - - result = [] - for elem in xml_tree.findall("herd"): - herd_mail = get_herd_email(elem.text) - if herd_mail and Config['verbose']: - result.append("%s (%s)" % (elem.text, herd_mail)) - else: - result.append(elem.text) +def get_reference_pkg(matches): + """Find a package in the Portage tree to reference.""" - return result - - -def get_herd_email(herd): - """Return the email of the given herd if it's in herds.xml, else None.""" - - herds_path = os.path.join(PORTDIR[0], "metadata/herds.xml") - - try: - herds_tree = ET.parse(herds_path) - except IOError, err: - pp.print_error(str(err)) - return None - - # Some special herds are not listed in herds.xml - if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'): - return None - - for node in herds_tree.getiterator("herd"): - if node.findtext("name") == herd: - return node.findtext("email") - - -def get_description(xml_tree): - """Return a list of text nodes for <longdescription>. - - @todo: Support the `lang' attribute - """ - - return [e.text for e in xml_tree.findall("longdescription")] - - -def get_maitainer(xml_tree): - """Return a parsable tree of all maintainer elements and sub-elements.""" - - first_run = True - result = [] - for node in xml_tree.findall("maintainer"): - if not first_run: - result.append("") - restrict = node.get("restrict") - if restrict: - result.append("(%s %s)" % - (pp.emph("Restrict to"), pp.output.green(restrict))) - result.extend(e.text for e in node) - first_run = False - - return result - - -def get_overlay_name(p_dir): - """Determine the overlay name and return a formatted string.""" - - result = [] - cat_pkg = '/'.join(p_dir.split('/')[-2:]) - result.append(" * %s" % pp.cpv(cat_pkg)) - o_dir = '/'.join(p_dir.split('/')[:-2]) - if o_dir != PORTDIR[0]: - # o_dir is an overlay - o_name = o_dir.split('/')[-1] - o_name = ("[", o_name, "]") - result.append(pp.output.turquoise("".join(o_name))) - - return ' '.join(result) - - -def get_package_directory(query): - """Find a package's portage directory.""" - - matches = find_packages(query, include_masked=True) - # Prefer a package that's in the Portage tree over one in an - # overlay. Start with oldest first. pkg = None while list(reversed(matches)): pkg = matches.pop() if not pkg.is_overlay(): break - - return pkg.get_package_path() if pkg else None - - -def get_useflags(xml_tree): - """Return a list of formatted <useflag> lines, including blank elements - where blank lines should be printed.""" - - first_run = True - result = [] - for node in xml_tree.getiterator("flag"): - if not first_run: - result.append("") - flagline = pp.useflag(node.get("name")) - restrict = node.get("restrict") - if restrict: - result.append("%s (%s %s)" % - (flagline, pp.emph("Restrict to"), pp.output.green(restrict))) - else: - result.append(flagline) - # ElementTree handles nested element text in a funky way. - # So we need to dump the raw XML and parse it manually. - flagxml = ET.tostring(node) - flagxml = re.sub("\s+", " ", flagxml) - flagxml = re.sub("\n\t", "", flagxml) - flagxml = re.sub("<(pkg|cat)>(.*?)</(pkg|cat)>", - pp.cpv(r"\2"), flagxml) - flagtext = re.sub("<.*?>", "", flagxml) - result.append(flagtext) - first_run = False - - return result - - -def _get_upstream_bugtracker(node): - """Extract and format upstream bugtracker information.""" - - bt_loc = [e.text for e in node.findall("bugs-to")] - - return format_list(bt_loc, "Bugs to: ", " " * 12, force_quiet=True) - - -def _get_upstream_changelog(node): - """Extract and format upstream changelog information.""" - - cl_paths = [e.text for e in node.findall("changelog")] - - return format_list(cl_paths, "Changelog: ", " " * 12, force_quiet=True) - - -def _get_upstream_documentation(node): - """Extract and format upstream documentation information.""" - - doc = [] - for elem in node.findall("doc"): - lang = elem.get("lang") - if lang: - lang = "(%s)" % pp.output.yellow(lang) - else: - lang = "" - doc.append(" ".join([elem.text, lang])) - - return format_list(doc, "Docs: ", " " * 12, force_quiet=True) - -def _get_upstream_maintainer(node): - """Extract and format upstream maintainer information.""" - - maintainer = node.findall("maintainer") - maint = [] - for elem in maintainer: - if elem.find("name") != None: - maint.append(elem.find("name").text) - if elem.find("email") != None: - maint.append(elem.find("email").text) - if elem.get("status") == "active": - maint.append("(%s)" % pp.output.green("active")) - elif elem.get("status") == "inactive": - maint.append("(%s)" % pp.output.red("inactive")) - elif elem.get("status") != None: - maint.append("(" + elem.get("status") + ")") - - return format_list(maint, "Maintainer: ", " " * 12, force_quiet=True) - - -def _get_upstream_remoteid(node): - """Extract and format upstream remote ID.""" - - r_id = [e.get("type") + ": " + e.text for e in node.findall("remote-id")] - - return format_list(r_id, "Remote ID: ", " " * 12, force_quiet=True) - - -def get_upstream(xml_tree): - """Return a list of formatted <upstream> lines, including blank elements - where blank lines should be printed.""" - - first_run = True - result = [] - for node in xml_tree.findall("upstream"): - if not first_run: - result.append("") - - maint = _get_upstream_maintainer(node) - if maint: - result.append("\n".join(maint)) - - changelog = _get_upstream_changelog(node) - if changelog: - result.append("\n".join(changelog)) - - documentation = _get_upstream_documentation(node) - if documentation: - result.append("\n".join(documentation)) - - bugs_to = _get_upstream_bugtracker(node) - if bugs_to: - result.append("\n".join(bugs_to)) - - remote_id = _get_upstream_remoteid(node) - if remote_id: - result.append("\n".join(remote_id)) - - first_run = False - - return result + return pkg def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) for opt in opts: if opt in ('-h', '--help'): print_help() sys.exit(0) - elif opt in ('-c', '--current'): - QUERY_OPTS["current"] = True elif opt in ('-d', '--description'): QUERY_OPTS["description"] = True elif opt in ('-H', '--herd'): QUERY_OPTS["herd"] = True elif opt in ('-m', '--maintainer'): QUERY_OPTS["maintainer"] = True + elif opt in ('-k', '--keywords'): + QUERY_OPTS["keywords"] = True elif opt in ('-u', '--useflags'): QUERY_OPTS["useflags"] = True elif opt in ('-U', '--upstream'): @@ -482,44 +392,37 @@ def parse_module_options(module_opts): def main(input_args): """Parse input and run the program.""" - short_opts = "hcdHmuUx" - long_opts = ('help', 'current', 'description', 'herd', 'maintainer', + short_opts = "hdHkmuUx" + long_opts = ('help', 'description', 'herd', 'keywords', 'maintainer', 'useflags', 'upstream', 'xml') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) parse_module_options(module_opts) - + # Find queries' Portage directory and throw error if invalid - if not queries and not QUERY_OPTS["current"]: + if not queries: print_help() sys.exit(2) - - if QUERY_OPTS["current"]: - package_dir = os.getcwd() - metadata_path = os.path.join(package_dir, "metadata.xml") - call_get_functions(metadata_path, package_dir, QUERY_OPTS) - else: - first_run = True - for query in queries: - package_dir = get_package_directory(query) - if not package_dir: - raise errors.GentoolkitNoMatches(query) - metadata_path = os.path.join(package_dir, "metadata.xml") - - # -------------------------------- - # Check options and call functions - # -------------------------------- - - if not first_run: - print - - call_get_functions(metadata_path, package_dir, QUERY_OPTS) - - first_run = False + + first_run = True + for query in queries: + matches = find_packages(query, include_masked=True) + if not matches: + raise errors.GentoolkitNoMatches(query) + + if not first_run: + print + + matches.sort() + call_format_functions(matches) + + first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/size.py b/pym/gentoolkit/equery/size.py index 9cb6bc9..8570a6a 100644 --- a/pym/gentoolkit/equery/size.py +++ b/pym/gentoolkit/equery/size.py @@ -16,16 +16,15 @@ import sys from getopt import gnu_getopt, GetoptError import gentoolkit.pprinter as pp -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import do_lookup +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import do_lookup # ======= # Globals # ======= QUERY_OPTS = { - "categoryFilter": None, - "includeInstalled": False, + "includeInstalled": True, "includePortTree": False, "includeOverlayTree": False, "includeMasked": True, @@ -41,7 +40,7 @@ QUERY_OPTS = { def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -50,11 +49,15 @@ def print_help(with_description=True): print __doc__.strip() print - # Deprecation warning added 04/09: djanderson - pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.") - pp.print_warn("-e, --exact-name is now the default behavior.") - pp.print_warn("Use globbing to simulate the old behavior (see man equery).") - pp.print_warn("Use '*' to check all installed packages.") + # Deprecation warning added by djanderson, 12/2008 + depwarning = ( + "Default action for this module has changed in Gentoolkit 0.3.", + "Use globbing to simulate the old behavior (see man equery).", + "Use '*' to check all installed packages.", + "Use 'foo-bar/*' to filter by category." + ) + for line in depwarning: + sys.stderr.write(pp.warn(line)) print print mod_usage(mod_name="size") @@ -63,7 +66,6 @@ def print_help(with_description=True): print format_options(( (" -h, --help", "display this help message"), (" -b, --bytes", "report size in bytes"), - (" -c, --category CAT", "only search in the category CAT"), (" -f, --full-regex", "query is a regular expression") )) @@ -76,14 +78,14 @@ def display_size(match_set): """ for pkg in match_set: - (size, files, uncounted) = pkg.size() + size, files, uncounted = pkg.get_size() - if Config['verbose']: - print " * %s" % pp.cpv(pkg.cpv) + if CONFIG['verbose']: + print " * %s" % pp.cpv(str(pkg.cpv)) print "Total files : %s".rjust(25) % pp.number(str(files)) if uncounted: - pp.print_info(0, "Inaccessible files : %s".rjust(25) % + print ("Inaccessible files : %s".rjust(25) % pp.number(str(uncounted))) if QUERY_OPTS["sizeInBytes"]: @@ -91,10 +93,10 @@ def display_size(match_set): else: size_str = "%s %s" % format_bytes(size) - pp.print_info(0, "Total size : %s".rjust(25) % size_str) + print "Total size : %s".rjust(25) % size_str else: info = "%s: total(%d), inaccessible(%d), size(%s)" - print info % (pkg.cpv, files, uncounted, size) + print info % (str(pkg.cpv), files, uncounted, size) def format_bytes(bytes_, precision=2): @@ -134,21 +136,19 @@ def format_bytes(bytes_, precision=2): def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) - posargs = (x[1] for x in module_opts) - for opt, posarg in zip(opts, posargs): + for opt in opts: if opt in ('-h', '--help'): print_help() sys.exit(0) elif opt in ('-b', '--bytes'): QUERY_OPTS["sizeInBytes"] = True - elif opt in ('-c', '--category'): - QUERY_OPTS['categoryFilter'] = posarg elif opt in ('-e', '--exact-name'): - pp.print_warn("-e, --exact-name is now default.") - pp.print_warn("Use globbing to simulate the old behavior.") + sys.stderr.write(pp.warn("-e, --exact-name is now default.")) + warning = pp.warn("Use globbing to simulate the old behavior.") + sys.stderr.write(warning) print elif opt in ('-f', '--full-regex'): QUERY_OPTS['isRegex'] = True @@ -159,30 +159,22 @@ def main(input_args): # -e, --exact-name is no longer needed. Kept for compatibility. # 04/09 djanderson - short_opts = "hbc:fe" - long_opts = ('help', 'bytes', 'category=', 'full-regex', 'exact-name') + short_opts = "hbfe" + long_opts = ('help', 'bytes', 'full-regex', 'exact-name') try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) parse_module_options(module_opts) - - if not queries and not QUERY_OPTS["includeInstalled"]: + + if not queries: print_help() sys.exit(2) - elif queries and not QUERY_OPTS["includeInstalled"]: - QUERY_OPTS["includeInstalled"] = True - elif QUERY_OPTS["includeInstalled"]: - queries = ["*"] - - # - # Output - # first_run = True for query in queries: @@ -192,8 +184,10 @@ def main(input_args): matches = do_lookup(query, QUERY_OPTS) if not matches: - pp.print_error("No package found matching %s" % query) + sys.stderr.write(pp.error("No package found matching %s" % query)) display_size(matches) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/uses.py b/pym/gentoolkit/equery/uses.py index 56fc78b..88a7a87 100644 --- a/pym/gentoolkit/equery/uses.py +++ b/pym/gentoolkit/equery/uses.py @@ -16,20 +16,17 @@ __docformat__ = 'epytext' # ======= import os -import re import sys +from functools import partial from getopt import gnu_getopt, GetoptError from glob import glob -import xml.etree.cElementTree as ET -from portage.util import unique_array +from portage import settings -import gentoolkit import gentoolkit.pprinter as pp from gentoolkit import errors -from gentoolkit.equery import format_options, mod_usage, Config -from gentoolkit.helpers2 import compare_package_strings, find_best_match, \ - find_packages +from gentoolkit.equery import format_options, mod_usage, CONFIG +from gentoolkit.helpers import find_best_match, find_packages, uniqify from gentoolkit.textwrap_ import TextWrapper # ======= @@ -44,7 +41,7 @@ QUERY_OPTS = {"allVersions" : False} def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -66,29 +63,29 @@ def display_useflags(output): @type output: list @param output: [(inuse, inused, flag, desc, restrict), ...] - inuse (int) = 0 or 1; if 1, flag is set in make.conf - inused (int) = 0 or 1; if 1, package is installed with flag enabled - flag (str) = the name of the USE flag - desc (str) = the flag's description - restrict (str) = corresponds to the text of restrict in metadata + inuse (int) = 0 or 1; if 1, flag is set in make.conf + inused (int) = 0 or 1; if 1, package is installed with flag enabled + flag (str) = the name of the USE flag + desc (str) = the flag's description + restrict (str) = corresponds to the text of restrict in metadata """ maxflag_len = len(max([t[2] for t in output], key=len)) twrap = TextWrapper() - twrap.width = Config['termWidth'] + twrap.width = CONFIG['termWidth'] twrap.subsequent_indent = " " * (maxflag_len + 8) markers = ("-", "+") - color = [pp.useflagoff, pp.useflagon] + color = [pp.useflag, partial(pp.useflag, enabled=True)] for in_makeconf, in_installed, flag, desc, restrict in output: - if Config['verbose']: + if CONFIG['verbose']: flag_name = "" if in_makeconf != in_installed: - flag_name += pp.emph(" %s %s" % + flag_name += pp.emph(" %s %s" % (markers[in_makeconf], markers[in_installed])) else: - flag_name += (" %s %s" % + flag_name += (" %s %s" % (markers[in_makeconf], markers[in_installed])) flag_name += " " + color[in_makeconf](flag.ljust(maxflag_len)) @@ -96,7 +93,7 @@ def display_useflags(output): # print description if restrict: - restrict = "(%s %s)" % (pp.emph("Restricted to"), + restrict = "(%s %s)" % (pp.emph("Restricted to"), pp.cpv(restrict)) twrap.initial_indent = flag_name print twrap.fill(restrict) @@ -128,8 +125,7 @@ def get_global_useflags(): global_usedesc = {} # Get global USE flag descriptions try: - path = os.path.join(gentoolkit.settings["PORTDIR"], 'profiles', - 'use.desc') + path = os.path.join(settings["PORTDIR"], 'profiles', 'use.desc') with open(path) as open_file: for line in open_file: if line.startswith('#'): @@ -139,13 +135,16 @@ def get_global_useflags(): if len(fields) == 2: global_usedesc[fields[0]] = fields[1].rstrip() except IOError: - pp.print_warn("Could not load USE flag descriptions from %s" % - pp.path(path)) + sys.stderr.write( + pp.warn( + "Could not load USE flag descriptions from %s" % pp.path(path) + ) + ) del path, open_file # Add USE_EXPANDED variables to usedesc hash -- Bug #238005 - for path in glob(os.path.join(gentoolkit.settings["PORTDIR"], - 'profiles', 'desc', '*.desc')): + for path in glob(os.path.join(settings["PORTDIR"], + 'profiles', 'desc', '*.desc')): try: with open(path) as open_file: for line in open_file: @@ -157,49 +156,13 @@ def get_global_useflags(): (path.split("/")[-1][0:-5], fields[0]) global_usedesc[expanded_useflag] = fields[1] except IOError: - pp.print_warn("Could not load USE flag descriptions from %s" % - path) + sys.stderr.write( + pp.warn("Could not load USE flag descriptions from %s" % path) + ) return global_usedesc -def get_local_useflags(pkg): - """Parse package-specific flag descriptions from a package's metadata.xml. - - @see: http://www.gentoo.org/proj/en/glep/glep-0056.html - @type pkg: gentoolkit.package.Package - @param pkg: the package to find useflags for - @rtype: dict - @return: {string: tuple} - string = flag's name - tuple = (description, restrictions) - """ - - result = {} - - metadata = os.path.join(pkg.get_package_path(), 'metadata.xml') - try: - xml_tree = ET.parse(metadata) - except IOError: - pp.print_error("Could not open %s" % metadata) - return result - - for node in xml_tree.getiterator("flag"): - name = node.get("name") - restrict = node.get("restrict") - # ElementTree handles nested element text in a funky way. - # So we need to dump the raw XML and parse it manually. - flagxml = ET.tostring(node) - flagxml = re.sub("\s+", " ", flagxml) - flagxml = re.sub("\n\t", "", flagxml) - flagxml = re.sub("<(pkg|cat)>([^<]*)</(pkg|cat)>", - pp.cpv("%s" % r"\2"), flagxml) - flagtext = re.sub("<.*?>", "", flagxml) - result[name] = (flagtext, restrict) - - return result - - def get_matches(query): """Get packages matching query.""" @@ -208,24 +171,24 @@ def get_matches(query): if None in matches: matches = find_packages(query, include_masked=False) if matches: - matches = sorted(matches, compare_package_strings)[-1:] + matches.sort() else: matches = find_packages(query, include_masked=True) if not matches: raise errors.GentoolkitNoMatches(query) - + return matches def get_output_descriptions(pkg, global_usedesc): """Prepare descriptions and usage information for each USE flag.""" - local_usedesc = get_local_useflags(pkg) + local_usedesc = pkg.metadata.get_useflags() iuse = pkg.get_env_var("IUSE") if iuse: - usevar = unique_array([x.lstrip('+-') for x in iuse.split()]) + usevar = uniqify([x.lstrip('+-') for x in iuse.split()]) usevar.sort() else: usevar = [] @@ -233,23 +196,32 @@ def get_output_descriptions(pkg, global_usedesc): if pkg.is_installed(): used_flags = pkg.get_use_flags().split() else: - used_flags = gentoolkit.settings["USE"].split() + used_flags = settings["USE"].split() # store (inuse, inused, flag, desc, restrict) output = [] for flag in usevar: inuse = False inused = False + + local_use = None + for use in local_usedesc: + if use.name == flag: + local_use = use + break + try: - desc = local_usedesc[flag][0] - except KeyError: + desc = local_use.description + except AttributeError: try: desc = global_usedesc[flag] except KeyError: desc = "" + try: - restrict = local_usedesc[flag][1] - except KeyError: + restrict = local_use.restrict + restrict = restrict if restrict is not None else "" + except AttributeError: restrict = "" if flag in pkg.get_settings("USE").split(): @@ -258,12 +230,12 @@ def get_output_descriptions(pkg, global_usedesc): inused = True output.append((inuse, inused, flag, desc, restrict)) - + return output def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) for opt in opts: @@ -274,13 +246,13 @@ def parse_module_options(module_opts): QUERY_OPTS['allVersions'] = True -def print_legend(query): +def print_legend(): """Print a legend to explain the output format.""" print "[ Legend : %s - flag is set in make.conf ]" % pp.emph("U") print "[ : %s - package is installed with flag ]" % pp.emph("I") print "[ Colors : %s, %s ]" % ( - pp.useflagon("set"), pp.useflagoff("unset")) + pp.useflag("set", enabled=True), pp.useflag("unset", enabled=False)) def main(input_args): @@ -292,7 +264,7 @@ def main(input_args): try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) @@ -312,7 +284,7 @@ def main(input_args): if not first_run: print - if Config['verbose']: + if CONFIG['verbose']: print " * Searching for %s ..." % pp.pkgquery(query) matches = get_matches(query) @@ -323,15 +295,18 @@ def main(input_args): output = get_output_descriptions(pkg, global_usedesc) if output: - if Config['verbose']: - print_legend(query) + if CONFIG['verbose']: + print_legend() print (" * Found these USE flags for %s:" % - pp.cpv(pkg.cpv)) + pp.cpv(str(pkg.cpv))) print pp.emph(" U I") display_useflags(output) else: - if Config['verbose']: - pp.print_warn("No USE flags found for %s" % - pp.cpv(pkg.cpv)) + if CONFIG['verbose']: + sys.stderr.write( + pp.warn("No USE flags found for %s" % pp.cpv(pkg.cpv)) + ) first_run = False + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/equery/which.py b/pym/gentoolkit/equery/which.py index 828dae1..ada3fde 100644 --- a/pym/gentoolkit/equery/which.py +++ b/pym/gentoolkit/equery/which.py @@ -21,7 +21,7 @@ from getopt import gnu_getopt, GetoptError import gentoolkit.pprinter as pp from gentoolkit import errors from gentoolkit.equery import format_options, mod_usage -from gentoolkit.helpers2 import find_packages +from gentoolkit.helpers import find_packages # ======= # Globals @@ -35,7 +35,7 @@ QUERY_OPTS = {"includeMasked": False} def print_help(with_description=True): """Print description, usage and a detailed help message. - + @type with_description: bool @param with_description: if true, print module's __doc__ string """ @@ -53,7 +53,7 @@ def print_help(with_description=True): def parse_module_options(module_opts): - """Parse module options and update GLOBAL_OPTS""" + """Parse module options and update QUERY_OPTS""" opts = (x[0] for x in module_opts) for opt in opts: @@ -73,7 +73,7 @@ def main(input_args): try: module_opts, queries = gnu_getopt(input_args, short_opts, long_opts) except GetoptError, err: - pp.print_error("Module %s" % err) + sys.stderr.write(pp.error("Module %s" % err)) print print_help(with_description=False) sys.exit(2) @@ -93,6 +93,10 @@ def main(input_args): if ebuild_path: print os.path.normpath(ebuild_path) else: - pp.print_warn("No ebuilds to satisfy %s" % pkg.name) + sys.stderr.write( + pp.warn("No ebuilds to satisfy %s" % pkg.name) + ) else: raise errors.GentoolkitNoMatches(query) + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/errors.py b/pym/gentoolkit/errors.py index 9843b6b..988d91c 100644 --- a/pym/gentoolkit/errors.py +++ b/pym/gentoolkit/errors.py @@ -4,32 +4,25 @@ """Exception classes for gentoolkit""" -__all__ = [ - 'FatalError', +__all__ = ( 'GentoolkitException', + 'GentoolkitFatalError', + 'GentoolkitAmbiguousPackage', 'GentoolkitInvalidAtom', 'GentoolkitInvalidCategory', - 'GentoolkitInvalidPackageName', + 'GentoolkitInvalidPackage', 'GentoolkitInvalidCPV', 'GentoolkitInvalidRegex', 'GentoolkitInvalidVersion', 'GentoolkitNoMatches' -] - -# ======= -# Imports -# ======= - -import sys - -import gentoolkit.pprinter as pp +) # ========== # Exceptions # ========== class GentoolkitException(Exception): - """Base class for gentoolkit exceptions""" + """Base class for gentoolkit exceptions.""" def __init__(self): pass @@ -37,64 +30,85 @@ class GentoolkitException(Exception): class GentoolkitFatalError(GentoolkitException): """A fatal error occurred. Usually used to catch Portage exceptions.""" def __init__(self, err): - pp.print_error("Fatal error: %s" % err) - sys.exit(2) + self.err = err + + def __str__(self): + return "Fatal error: %s" % self.err + + +class GentoolkitAmbiguousPackage(GentoolkitException): + """Got an ambiguous package name.""" + def __init__(self, choices): + self.choices = choices + + def __str__(self): + choices = '\n'.join(" %s" % x for x in self.choices) + return '\n'.join(("Ambiguous package name. Choose from:", choices)) class GentoolkitInvalidAtom(GentoolkitException): - """Got a malformed package atom""" + """Got a malformed package atom.""" def __init__(self, atom): - pp.print_error("Invalid atom: '%s'" % atom) - sys.exit(2) + self.atom = atom + + def __str__(self): + return "Invalid atom: '%s'" % self.atom class GentoolkitInvalidCategory(GentoolkitException): - """The category was not listed in portage.settings.categories""" + """The category was not listed in portage.settings.categories.""" def __init__(self, category): - pp.print_error("Invalid category: '%s'" % category) - if not category: - pp.print_error("Try --category=cat1,cat2 with no spaces.") - sys.exit(2) + self.category = category + def __str__(self): + return "Invalid category: '%s'" % self.category -class GentoolkitInvalidPackageName(GentoolkitException): - """Got an unknown package name""" + +class GentoolkitInvalidPackage(GentoolkitException): + """Got an unknown or invalid package.""" def __init__(self, package): - pp.print_error("Invalid package name: '%s'" % package) - sys.exit(2) + self.package = package + + def __str__(self): + return "Invalid package: '%s'" % self.package class GentoolkitInvalidCPV(GentoolkitException): - """Got an unknown package name""" + """Got an invalid category/package-ver string.""" def __init__(self, cpv): - pp.print_error("Invalid CPV: '%s'" % cpv) - sys.exit(2) + self.cpv = cpv + + def __str__(self): + return "Invalid CPV: '%s'" % self.cpv class GentoolkitInvalidRegex(GentoolkitException): - """The regex could not be compiled""" + """The regex could not be compiled.""" def __init__(self, regex): - pp.print_error("Invalid regex: '%s'" % regex) - sys.exit(2) + self.regex = regex + + def __str__(self): + return "Invalid regex: '%s'" % self.regex class GentoolkitInvalidVersion(GentoolkitException): - """Got a malformed version""" + """Got a malformed version.""" def __init__(self, version): - pp.print_error("Malformed version: '%s'" % version) - sys.exit(2) + self.version = version + + def __str__(self): + return "Malformed version: '%s'" % self.version class GentoolkitNoMatches(GentoolkitException): - """No packages were found matching the search query""" - def __init__(self, query): - pp.print_error("No packages matching '%s'" % query) - sys.exit(2) - - -# XXX: Deprecated -class FatalError: - def __init__(self, s): - self._message = s - def get_message(self): - return self._message + """No packages were found matching the search query.""" + def __init__(self, query, in_installed=False): + self.query = query + self.in_installed = in_installed + + def __str__(self): + inst = 'installed ' if self.in_installed else '' + return "No %spackages matching '%s'" % (inst, self.query) + + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/glsa/__init__.py b/pym/gentoolkit/glsa/__init__.py index 2efb008..11b7dbe 100644 --- a/pym/gentoolkit/glsa/__init__.py +++ b/pym/gentoolkit/glsa/__init__.py @@ -33,7 +33,7 @@ except ImportError: import portage # Note: the space for rgt and rlt is important !! -opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=", +opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=", "rge": ">=~", "rle": "<=~", "rgt": " >~", "rlt": " <~"} NEWLINE_ESCAPE = "!;\\n" # some random string to mark newlines that should be preserved SPACE_ESCAPE = "!;_" # some random string to mark spaces that should be preserved @@ -42,7 +42,7 @@ def center(text, width): """ Returns a string containing I{text} that is padded with spaces on both sides. If C{len(text) >= width} I{text} is returned unchanged. - + @type text: String @param text: the text to be embedded @type width: Integer @@ -65,15 +65,15 @@ def center(text, width): def wrap(text, width, caption=""): """ Wraps the given text at column I{width}, optionally indenting - it so that no text is under I{caption}. It's possible to encode + it so that no text is under I{caption}. It's possible to encode hard linebreaks in I{text} with L{NEWLINE_ESCAPE}. - + @type text: String @param text: the text to be wrapped @type width: Integer @param width: the column at which the text should be wrapped @type caption: String - @param caption: this string is inserted at the beginning of the + @param caption: this string is inserted at the beginning of the return value and the paragraph is indented up to C{len(caption)}. @rtype: String @@ -84,7 +84,7 @@ def wrap(text, width, caption=""): text = text.replace(2*NEWLINE_ESCAPE, NEWLINE_ESCAPE+" "+NEWLINE_ESCAPE) words = text.split() indentLevel = len(caption)+1 - + for w in words: if line[-1] == "\n": rValue += line @@ -132,13 +132,13 @@ def get_glsa_list(repository, myconfig): Returns a list of all available GLSAs in the given repository by comparing the filelist there with the pattern described in the config. - + @type repository: String @param repository: The directory or an URL that contains GLSA files (Note: not implemented yet) @type myconfig: portage.config @param myconfig: a GLSA aware config instance (see L{checkconfig}) - + @rtype: List of Strings @return: a list of GLSA IDs in this repository """ @@ -151,7 +151,7 @@ def get_glsa_list(repository, myconfig): dirlist = os.listdir(repository) prefix = myconfig["GLSA_PREFIX"] suffix = myconfig["GLSA_SUFFIX"] - + for f in dirlist: try: if f[:len(prefix)] == prefix and f[-1*len(suffix):] == suffix: @@ -163,7 +163,7 @@ def get_glsa_list(repository, myconfig): def getListElements(listnode): """ Get all <li> elements for a given <ol> or <ul> node. - + @type listnode: xml.dom.Node @param listnode: <ul> or <ol> list to get the elements for @rtype: List of Strings @@ -184,7 +184,7 @@ def getText(node, format, textfd = None): parameter the text might be formatted by adding/removing newlines, tabs and spaces. This function is only useful for the GLSA DTD, it's not applicable for other DTDs. - + @type node: xml.dom.Node @param node: the root node to start with the parsing @type format: String @@ -251,7 +251,7 @@ def getMultiTagsText(rootnode, tagname, format): """ Returns a list with the text of all subnodes of type I{tagname} under I{rootnode} (which itself is not parsed) using the given I{format}. - + @type rootnode: xml.dom.Node @param rootnode: the node to search for I{tagname} @type tagname: String @@ -267,9 +267,9 @@ def getMultiTagsText(rootnode, tagname, format): def makeAtom(pkgname, versionNode): """ - creates from the given package name and information in the + creates from the given package name and information in the I{versionNode} a (syntactical) valid portage atom. - + @type pkgname: String @param pkgname: the name of the package for this atom @type versionNode: xml.dom.Node @@ -292,9 +292,9 @@ def makeAtom(pkgname, versionNode): def makeVersion(versionNode): """ - creates from the information in the I{versionNode} a + creates from the information in the I{versionNode} a version string (format <op><version>). - + @type versionNode: xml.dom.Node @param versionNode: a <vulnerable> or <unaffected> Node that contains the version information for this atom @@ -314,17 +314,17 @@ def makeVersion(versionNode): def match(atom, portdbname, match_type="default"): """ - wrapper that calls revisionMatch() or portage.dbapi.match() depending on + wrapper that calls revisionMatch() or portage.dbapi.match() depending on the given atom. - + @type atom: string @param atom: a <~ or >~ atom or a normal portage atom that contains the atom to match against @type portdb: portage.dbapi @param portdb: one of the portage databases to use as information source @type match_type: string - @param match_type: if != "default" passed as first argument to dbapi.xmatch + @param match_type: if != "default" passed as first argument to dbapi.xmatch to apply the wanted visibility filters - + @rtype: list of strings @return: a list with the matching versions """ @@ -341,15 +341,15 @@ def revisionMatch(revisionAtom, portdb, match_type="default"): handler for the special >~, >=~, <=~ and <~ atoms that are supposed to behave as > and < except that they are limited to the same version, the range only applies to the revision part. - + @type revisionAtom: string @param revisionAtom: a <~ or >~ atom that contains the atom to match against @type portdb: portage.dbapi @param portdb: one of the portage databases to use as information source @type match_type: string - @param match_type: if != "default" passed as first argument to portdb.xmatch + @param match_type: if != "default" passed as first argument to portdb.xmatch to apply the wanted visibility filters - + @rtype: list of strings @return: a list with the matching versions """ @@ -370,35 +370,34 @@ def revisionMatch(revisionAtom, portdb, match_type="default"): if eval(r1+" "+revisionAtom[0:2]+" "+r2): rValue.append(v) return rValue - + def getMinUpgrade(vulnerableList, unaffectedList, minimize=True): """ - Checks if the state of installed packages matches an atom in - I{vulnerableList} and returns an update path. - - Return value is: - * None if the system is not affected - * a list of tuples (a,b) where - a is a cpv describing an installed vulnerable atom - b is a cpv describing an uninstalled unaffected atom - in the same slot as a - OR the empty string ("") which means no upgrade - is possible - + Checks if the systemstate is matching an atom in + I{vulnerableList} and returns string describing + the lowest version for the package that matches an atom in + I{unaffectedList} and is greater than the currently installed + version. It will return an empty list if the system is affected, + and no upgrade is possible or None if the system is not affected. + Both I{vulnerableList} and I{unaffectedList} should have the + same base package. + @type vulnerableList: List of Strings @param vulnerableList: atoms matching vulnerable package versions @type unaffectedList: List of Strings @param unaffectedList: atoms matching unaffected package versions @type minimize: Boolean @param minimize: True for a least-change upgrade, False for emerge-like algorithm - - @rtype: List | None - @return: None if unaffected or a list of (vuln, upgrade) atoms. + + @rtype: String | None + @return: the lowest unaffected version that is greater than + the installed version. """ + rValue = "" v_installed = reduce(operator.add, [match(v, "vartree") for v in vulnerableList], []) u_installed = reduce(operator.add, [match(u, "vartree") for u in unaffectedList], []) - + # remove all unaffected atoms from vulnerable list v_installed = list(set(v_installed).difference(set(u_installed))) @@ -417,18 +416,13 @@ def getMinUpgrade(vulnerableList, unaffectedList, minimize=True): for vuln in v_installed: update = "" - # find the best update path for the vuln atom for c in avail_updates: c_pv = portage.catpkgsplit(c) i_pv = portage.catpkgsplit(vuln) - if portage.pkgcmp(c_pv[1:], i_pv[1:]) <= 0: - # c is less or equal than vuln - continue - if portage.db["/"]["porttree"].dbapi.aux_get(c, ["SLOT"]) != \ - portage.db["/"]["vartree"].dbapi.aux_get(vuln, ["SLOT"]): - # upgrade to a different slot - continue - if update == "" or (minimize ^ (portage.pkgcmp(c_pv[1:], portage.catpkgsplit(update)[1:]) > 0)): + if portage.pkgcmp(c_pv[1:], i_pv[1:]) > 0 \ + and (update == "" \ + or (minimize ^ (portage.pkgcmp(c_pv[1:], portage.catpkgsplit(update)[1:]) > 0))) \ + and portage.db["/"]["porttree"].dbapi.aux_get(c, ["SLOT"]) == portage.db["/"]["vartree"].dbapi.aux_get(vuln, ["SLOT"]): update = c_pv[0]+"/"+c_pv[1]+"-"+c_pv[2] if c_pv[3] != "r0": # we don't like -r0 for display update += "-"+c_pv[3] @@ -440,7 +434,7 @@ def format_date(datestr): """ Takes a date (announced, revised) date from a GLSA and formats it as readable text (i.e. "January 1, 2008"). - + @type date: String @param date: the date string to reformat @rtype: String @@ -450,16 +444,16 @@ def format_date(datestr): splitdate = datestr.split("-", 2) if len(splitdate) != 3: return datestr - + # This cannot raise an error as we use () instead of [] splitdate = (int(x) for x in splitdate) - + from datetime import date try: d = date(*splitdate) except ValueError: return datestr - + # TODO We could format to local date format '%x' here? return d.strftime("%B %d, %Y") @@ -470,7 +464,7 @@ class GlsaTypeException(Exception): class GlsaFormatException(Exception): pass - + class GlsaArgumentException(Exception): pass @@ -482,9 +476,9 @@ class Glsa: """ def __init__(self, myid, myconfig): """ - Simple constructor to set the ID, store the config and gets the + Simple constructor to set the ID, store the config and gets the XML data by calling C{self.read()}. - + @type myid: String @param myid: String describing the id for the GLSA object (standard GLSAs have an ID of the form YYYYMM-nn) or an existing @@ -506,7 +500,7 @@ class Glsa: """ Here we build the filename from the config and the ID and pass it to urllib to fetch it from the filesystem or a remote server. - + @rtype: None @return: None """ @@ -523,10 +517,10 @@ class Glsa: def parse(self, myfile): """ - This method parses the XML file and sets up the internal data + This method parses the XML file and sets up the internal data structures by calling the different helper functions in this module. - + @type myfile: String @param myfile: Filename to grab the XML data from @rtype: None @@ -549,7 +543,7 @@ class Glsa: self.title = getText(myroot.getElementsByTagName("title")[0], format="strip") self.synopsis = getText(myroot.getElementsByTagName("synopsis")[0], format="strip") self.announced = format_date(getText(myroot.getElementsByTagName("announced")[0], format="strip")) - + count = 1 # Support both formats of revised: # <revised>December 30, 2007: 02</revised> @@ -560,15 +554,15 @@ class Glsa: count = revisedEl.getAttribute("count") elif (self.revised.find(":") >= 0): (self.revised, count) = self.revised.split(":") - + self.revised = format_date(self.revised) - + try: self.count = int(count) except ValueError: # TODO should this rais a GlsaFormatException? self.count = 1 - + # now the optional and 0-n toplevel, #PCDATA tags and references try: self.access = getText(myroot.getElementsByTagName("access")[0], format="strip") @@ -576,7 +570,7 @@ class Glsa: self.access = "" self.bugs = getMultiTagsText(myroot, "bug", format="strip") self.references = getMultiTagsText(myroot.getElementsByTagName("references")[0], "uri", format="keep") - + # and now the formatted text elements self.description = getText(myroot.getElementsByTagName("description")[0], format="xml") self.workaround = getText(myroot.getElementsByTagName("workaround")[0], format="xml") @@ -586,7 +580,7 @@ class Glsa: try: self.background = getText(myroot.getElementsByTagName("background")[0], format="xml") except IndexError: - self.background = "" + self.background = "" # finally the interesting tags (product, affected, package) self.glsatype = myroot.getElementsByTagName("product")[0].getAttribute("type") @@ -611,10 +605,10 @@ class Glsa: def dump(self, outstream=sys.stdout, encoding="utf-8"): """ - Dumps a plaintext representation of this GLSA to I{outfile} or + Dumps a plaintext representation of this GLSA to I{outfile} or B{stdout} if it is ommitted. You can specify an alternate I{encoding} if needed (default is utf-8). - + @type outstream: File @param outfile: Stream that should be used for writing (defaults to sys.stdout) @@ -655,13 +649,13 @@ class Glsa: myreferences = " ".join(r.replace(" ", SPACE_ESCAPE)+NEWLINE_ESCAPE for r in self.references) outstream.write("\n"+wrap(myreferences, width, caption="References: ")) outstream.write("\n") - + def isVulnerable(self): """ Tests if the system is affected by this GLSA by checking if any vulnerable package versions are installed. Also checks for affected architectures. - + @rtype: Boolean @returns: True if the system is affected, False if not """ @@ -674,12 +668,12 @@ class Glsa: rValue = rValue \ or (None != getMinUpgrade([v,], path["unaff_atoms"])) return rValue - + def isInjected(self): """ Looks if the GLSA ID is in the GLSA checkfile to check if this GLSA should be marked as applied. - + @rtype: Boolean @returns: True if the GLSA is in the inject file, False if not """ @@ -691,7 +685,7 @@ class Glsa: def inject(self): """ Puts the ID of this GLSA into the GLSA checkfile, so it won't - show up on future checks. Should be called after a GLSA is + show up on future checks. Should be called after a GLSA is applied or on explicit user request. @rtype: None @@ -702,13 +696,13 @@ class Glsa: checkfile.write(self.nr+"\n") checkfile.close() return None - + def getMergeList(self, least_change=True): """ Returns the list of package-versions that have to be merged to - apply this GLSA properly. The versions are as low as possible + apply this GLSA properly. The versions are as low as possible while avoiding downgrades (see L{getMinUpgrade}). - + @type least_change: Boolean @param least_change: True if the smallest possible upgrade should be selected, False for an emerge-like algorithm diff --git a/pym/gentoolkit/helpers.py b/pym/gentoolkit/helpers.py index 6d272d3..277e41f 100644 --- a/pym/gentoolkit/helpers.py +++ b/pym/gentoolkit/helpers.py @@ -1,163 +1,718 @@ -#!/usr/bin/python2 -# -# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org> # Copyright(c) 2009, Gentoo Foundation # -# Licensed under the GNU General Public License, v2 +# Licensed under the GNU General Public License, v2 or higher # # $Header$ +"""Improved versions of the original helpers functions. + +As a convention, functions ending in '_packages' or '_match{es}' return +Package objects, while functions ending in 'cpvs' return a sequence of strings. +Functions starting with 'get_' return a set of packages by default and can be +filtered, while functions starting with 'find_' return nothing unless the +query matches one or more packages. +""" + +# Move to Imports section after Python 2.6 is stable +from __future__ import with_statement + +__all__ = ( + 'ChangeLog', + 'FileOwner', + 'compare_package_strings', + 'do_lookup', + 'find_best_match', + 'find_installed_packages', + 'find_packages', + 'get_cpvs', + 'get_installed_cpvs', + 'get_uninstalled_cpvs', + 'uniqify', + 'uses_globbing', + 'split_cpv' +) +__docformat__ = 'epytext' + +# ======= +# Imports +# ======= + +import fnmatch +import os +import re +from functools import partial +from itertools import chain + import portage -from gentoolkit import * -from package import * -from pprinter import print_warn -try: - from portage.util import unique_array -except ImportError: - from portage_util import unique_array - -def find_packages(search_key, masked=False): - """Returns a list of Package objects that matched the search key.""" - try: - if masked: - t = portage.db["/"]["porttree"].dbapi.xmatch("match-all", search_key) - t += portage.db["/"]["vartree"].dbapi.match(search_key) +from portage.versions import catpkgsplit, pkgcmp + +from gentoolkit import pprinter as pp +from gentoolkit import CONFIG +from gentoolkit import errors +from gentoolkit.atom import Atom +from gentoolkit.cpv import CPV +from gentoolkit.dbapi import PORTDB, VARDB +from gentoolkit.versionmatch import VersionMatch +# This has to be imported below to stop circular import. +#from gentoolkit.package import Package + +# ======= +# Classes +# ======= + +class ChangeLog(object): + """Provides methods for working with a Gentoo ChangeLog file. + + Example usage: + >>> from gentoolkit.helpers import ChangeLog + >>> portage = ChangeLog('/usr/portage/sys-apps/portage/ChangeLog') + >>> print portage.latest.strip() + *portage-2.2_rc50 (15 Nov 2009) + + 15 Nov 2009; Zac Medico <zmedico@gentoo.org> +portage-2.2_rc50.ebuild: + 2.2_rc50 bump. This includes all fixes in 2.1.7.5. + >>> len(portage.full) + 75 + >>> len(portage.entries_matching_range( + ... from_ver='2.2_rc40', + ... to_ver='2.2_rc50')) + 11 + + """ + def __init__(self, changelog_path, invalid_entry_is_fatal=False): + if not (os.path.isfile(changelog_path) and + os.access(changelog_path, os.R_OK)): + raise errors.GentoolkitFatalError( + "%s does not exist or is unreadable" % pp.path(changelog_path) + ) + self.changelog_path = changelog_path + self.invalid_entry_is_fatal = invalid_entry_is_fatal + + # Process the ChangeLog: + self.entries = self._split_changelog() + self.indexed_entries = self._index_changelog() + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.changelog_path) + + @property + def full(self): + """Return the output of L{self._split_changelog}.""" + return self.entries + + @property + def latest(self): + """Return the newest ChangeLog entry.""" + return self.entries[0] + + def entries_matching_atom(self, atom): + """Return entries whose header versions match atom's version. + + @type atom: L{gentoolkit.atom.Atom} or str + @param atom: a atom to find matching entries against + @rtype: list + @return: entries matching atom + @raise errors.GentoolkitInvalidAtom: if atom is a string and malformed + """ + result = [] + + if not isinstance(atom, Atom): + atom = Atom(atom) + + for entry_set in self.indexed_entries: + i, entry = entry_set + # VersionMatch doesn't store .cp, so we'll force it to match here: + i.cpv.cp = atom.cpv.cp + if atom.intersects(i): + result.append(entry) + + return result + + def entries_matching_range(self, from_ver=None, to_ver=None): + """Return entries whose header versions are within a range of versions. + + @type from_ver: str + @param from_ver: valid Gentoo version + @type to_ver: str + @param to_ver: valid Gentoo version + @rtype: list + @return: entries between from_ver and to_ver + @raise errors.GentoolkitFatalError: if neither vers are set + @raise errors.GentoolkitInvalidVersion: if either ver is invalid + """ + result = [] + + # Make sure we have at least one version set + if not (from_ver or to_ver): + raise errors.GentoolkitFatalError( + "Need to specifiy 'from_ver' or 'to_ver'" + ) + + # Create a VersionMatch instance out of from_ver + from_restriction = None + if from_ver: + try: + from_ver_rev = CPV("null-%s" % from_ver) + except errors.GentoolkitInvalidCPV: + raise errors.GentoolkitInvalidVersion(from_ver) + from_restriction = VersionMatch(from_ver_rev, op='>=') + + # Create a VersionMatch instance out of to_ver + to_restriction = None + if to_ver: + try: + to_ver_rev = CPV("null-%s" % to_ver) + except errors.GentoolkitInvalidCPV: + raise errors.GentoolkitInvalidVersion(to_ver) + to_restriction = VersionMatch(to_ver_rev, op='<=') + + # Add entry to result if version ranges intersect it + for entry_set in self.indexed_entries: + i, entry = entry_set + if from_restriction and not from_restriction.match(i): + continue + if to_restriction and not to_restriction.match(i): + continue + result.append(entry) + + return result + + def _index_changelog(self): + """Use the output of L{self._split_changelog} to create an index list + of L{gentoolkit.versionmatch.VersionMatch} objects. + + @rtype: list + @return: tuples containing a VersionMatch instance for the release + version of each entry header as the first item and the entire entry + as the second item + @raise ValueError: if self.invalid_entry_is_fatal is True and we hit an + invalid entry + """ + + result = [] + for entry in self.entries: + # Extract the package name from the entry header, ex: + # *xterm-242 (07 Mar 2009) => xterm-242 + pkg_name = entry.split(' ', 1)[0].lstrip('*') + if not pkg_name.strip(): + continue + try: + entry_ver = CPV(pkg_name) + except errors.GentoolkitInvalidCPV: + if self.invalid_entry_is_fatal: + raise ValueError(entry_ver) + continue + + result.append((VersionMatch(entry_ver, op='='), entry)) + + return result + + def _split_changelog(self): + """Split the ChangeLog into individual entries. + + @rtype: list + @return: individual ChangeLog entries + """ + + result = [] + partial_entries = [] + with open(self.changelog_path) as log: + for line in log: + if line.startswith('#'): + continue + elif line.startswith('*'): + # Append last entry to result... + entry = ''.join(partial_entries) + if entry and not entry.isspace(): + result.append(entry) + # ... and start a new entry + partial_entries = [line] + else: + partial_entries.append(line) + else: + # Append the final entry + entry = ''.join(partial_entries) + result.append(entry) + + return result + + +class FileOwner(object): + """Creates a function for locating the owner of filename queries. + + Example usage: + >>> from gentoolkit.helpers import FileOwner + >>> findowner = FileOwner() + >>> findowner(('/usr/bin/vim',)) + [(<Package app-editors/vim-7.2.182>, '/usr/bin/vim')] + """ + def __init__(self, is_regex=False, early_out=False, printer_fn=None): + """Instantiate function. + + @type is_regex: bool + @param is_regex: funtion args are regular expressions + @type early_out: bool + @param early_out: return when first result is found (safe) + @type printer_fn: callable + @param printer_fn: If defined, will be passed useful information for + printing each result as it is found. + """ + self.is_regex = is_regex + self.early_out = early_out + self.printer_fn = printer_fn + + def __call__(self, queries): + """Run the function. + + @type queries: iterable + @param queries: filepaths or filepath regexes + """ + query_re_string = self._prepare_search_regex(queries) + try: + query_re = re.compile(query_re_string) + except (TypeError, re.error), err: + raise errors.GentoolkitInvalidRegex(err) + + use_match = False + if ((self.is_regex or query_re_string.startswith('^\/')) + and '|' not in query_re_string ): + # If we were passed a regex or a single path starting with root, + # we can use re.match, else use re.search. + use_match = True + + return self.find_owners(query_re, use_match=use_match) + + def find_owners(self, query_re, use_match=False, pkgset=None): + """Find owners and feed data to supplied output function. + + @type query_re: _sre.SRE_Pattern + @param query_re: file regex + @type use_match: bool + @param use_match: use re.match or re.search + @type pkgset: iterable or None + @param pkgset: list of packages to look through + """ + # FIXME: Remove when lazyimport supports objects: + from gentoolkit.package import Package + + if use_match: + query_fn = query_re.match + else: + query_fn = query_re.search + + results = [] + found_match = False + if pkgset is None: + pkgset = get_installed_cpvs() + for pkg in sorted([Package(x) for x in pkgset]): + files = pkg.get_contents() + for cfile in files: + match = query_fn(cfile) + if match: + results.append((pkg, cfile)) + if self.printer_fn is not None: + self.printer_fn(pkg, cfile) + if self.early_out: + found_match = True + break + if found_match: + break + return results + + @staticmethod + def extend_realpaths(paths): + """Extend a list of paths with the realpaths for any symlinks. + + @type paths: list + @param paths: file path strs + @rtype: list + @return: the original list plus the realpaths for any symlinks + so long as the realpath doesn't already exist in the list + @raise AttributeError: if paths does not have attribute 'extend' + """ + + osp = os.path + paths.extend([osp.realpath(x) for x in paths + if osp.islink(x) and osp.realpath(x) not in paths]) + + return paths + + def _prepare_search_regex(self, queries): + """Create a regex out of the queries""" + + queries = list(queries) + if self.is_regex: + return '|'.join(queries) else: - t = portage.db["/"]["porttree"].dbapi.match(search_key) - t += portage.db["/"]["vartree"].dbapi.match(search_key) - # catch the "amgigous package" Exception - except ValueError, e: - if isinstance(e[0],list): - t = [] - for cp in e[0]: - if masked: - t += portage.db["/"]["porttree"].dbapi.xmatch("match-all", cp) - t += portage.db["/"]["vartree"].dbapi.match(cp) + result = [] + # Trim trailing and multiple slashes from queries + slashes = re.compile('/+') + queries = self.extend_realpaths(queries) + for query in queries: + query = slashes.sub('/', query).rstrip('/') + if query.startswith('/'): + query = "^%s$" % re.escape(query) else: - t += portage.db["/"]["porttree"].dbapi.match(cp) - t += portage.db["/"]["vartree"].dbapi.match(cp) + query = "/%s$" % re.escape(query) + result.append(query) + result = "|".join(result) + return result + +# ========= +# Functions +# ========= + +def compare_package_strings(pkg1, pkg2): + """Similar to the builtin cmp, but for package strings. Usually called + as: package_list.sort(compare_package_strings) + + An alternative is to use the Package descriptor from gentoolkit.package + >>> pkgs = [Package(x) for x in package_list] + >>> pkgs.sort() + + @see: >>> help(cmp) + """ + + pkg1 = catpkgsplit(pkg1) + pkg2 = catpkgsplit(pkg2) + if pkg1[0] != pkg2[0]: + return cmp(pkg1[0], pkg2[0]) + elif pkg1[1] != pkg2[1]: + return cmp(pkg1[1], pkg2[1]) + else: + return pkgcmp(pkg1[1:], pkg2[1:]) + + +def do_lookup(query, query_opts): + """A high-level wrapper around gentoolkit package-finder functions. + + @type query: str + @param query: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom, glob or regex + @type query_opts: dict + @param query_opts: user-configurable options from the calling module + Currently supported options are: + + includeInstalled = bool + includePortTree = bool + includeOverlayTree = bool + isRegex = bool + printMatchInfo = bool # Print info about the search + + @rtype: list + @return: Package objects matching query + """ + + if query_opts["includeInstalled"]: + if query_opts["includePortTree"] or query_opts["includeOverlayTree"]: + simple_package_finder = partial(find_packages, include_masked=True) + complex_package_finder = get_cpvs else: - raise ValueError(e) - except portage_exception.InvalidAtom, e: - print_warn("Invalid Atom: '%s'" % str(e)) + simple_package_finder = find_installed_packages + complex_package_finder = get_installed_cpvs + elif query_opts["includePortTree"] or query_opts["includeOverlayTree"]: + simple_package_finder = partial(find_packages, include_masked=True) + complex_package_finder = get_uninstalled_cpvs + else: + raise errors.GentoolkitFatalError( + "Not searching in installed, Portage tree, or overlay. " + "Nothing to do." + ) + + is_simple_query = True + if query_opts["isRegex"] or uses_globbing(query): + is_simple_query = False + + if is_simple_query: + matches = _do_simple_lookup(query, simple_package_finder, query_opts) + else: + matches = _do_complex_lookup(query, complex_package_finder, query_opts) + + return matches + + +def _do_complex_lookup(query, package_finder, query_opts): + """Find matches for a query which is a regex or includes globbing.""" + + # FIXME: Remove when lazyimport supports objects: + from gentoolkit.package import Package + + result = [] + + if query_opts["printMatchInfo"] and not CONFIG["piping"]: + print_query_info(query, query_opts) + + cat = split_cpv(query)[0] + + pre_filter = [] + # The "get_" functions can pre-filter against the whole package key, + # but since we allow globbing now, we run into issues like: + # >>> portage.dep.dep_getkey("sys-apps/portage-*") + # 'sys-apps/portage-' + # So the only way to guarantee we don't overrun the key is to + # prefilter by cat only. + if cat: + if query_opts["isRegex"]: + cat_re = cat + else: + cat_re = fnmatch.translate(cat) + # [::-1] reverses a sequence, so we're emulating an ".rreplace()" + # except we have to put our "new" string on backwards + cat_re = cat_re[::-1].replace('$', '*./', 1)[::-1] + predicate = lambda x: re.match(cat_re, x) + pre_filter = package_finder(predicate=predicate) + + # Post-filter + if query_opts["isRegex"]: + predicate = lambda x: re.search(query, x) + else: + if cat: + query_re = fnmatch.translate(query) + else: + query_re = fnmatch.translate("*/%s" % query) + predicate = lambda x: re.search(query_re, x) + if pre_filter: + result = [x for x in pre_filter if predicate(x)] + else: + result = package_finder(predicate=predicate) + + return [Package(x) for x in result] + + +def _do_simple_lookup(query, package_finder, query_opts): + """Find matches for a query which is an atom or string.""" + + result = [] + + if query_opts["printMatchInfo"] and CONFIG['verbose']: + print_query_info(query, query_opts) + + result = package_finder(query) + if not query_opts["includeInstalled"]: + result = [x for x in result if not x.is_installed()] + + return result + + +def find_best_match(query): + """Return the highest unmasked version of a package matching query. + + @type query: str + @param query: can be of the form: pkg, pkg-ver, cat/pkg, cat/pkg-ver, atom + @rtype: str or None + @raise portage.exception.InvalidAtom: if query is not valid input + """ + # FIXME: Remove when lazyimport supports objects: + from gentoolkit.package import Package + + try: + match = PORTDB.xmatch("bestmatch-visible", query) + except portage.exception.InvalidAtom, err: + raise errors.GentoolkitInvalidAtom(err) + + return Package(match) if match else None + + +def find_installed_packages(query): + """Return a list of Package objects that matched the search key.""" + # FIXME: Remove when lazyimport supports objects: + from gentoolkit.package import Package + + try: + matches = VARDB.match(query) + # catch the ambiguous package Exception + except portage.exception.AmbiguousPackageName, err: + matches = [] + for pkgkey in err[0]: + matches.extend(VARDB.match(pkgkey)) + except portage.exception.InvalidAtom, err: + raise errors.GentoolkitInvalidAtom(err) + + return [Package(x) for x in matches] + + +def find_packages(query, include_masked=False): + """Returns a list of Package objects that matched the query. + + @type query: str + @param query: can be of the form: pkg, pkg-ver, cat/pkg, cat/pkg-ver, atom + @type include_masked: bool + @param include_masked: include masked packages + @rtype: list + @return: matching Package objects + """ + # FIXME: Remove when lazyimport supports objects: + from gentoolkit.package import Package + + if not query: return [] - # Make the list of packages unique - t = unique_array(t) - t.sort() - return [Package(x) for x in t] -def find_installed_packages(search_key, masked=False): - """Returns a list of Package objects that matched the search key.""" try: - t = portage.db["/"]["vartree"].dbapi.match(search_key) - # catch the "amgigous package" Exception - except ValueError, e: - if isinstance(e[0],list): - t = [] - for cp in e[0]: - t += portage.db["/"]["vartree"].dbapi.match(cp) + if include_masked: + matches = PORTDB.xmatch("match-all", query) else: - raise ValueError(e) - except portage_exception.InvalidAtom, e: - print_warn("Invalid Atom: '%s'" % str(e)) - return [] - return [Package(x) for x in t] - -def find_best_match(search_key): - """Returns a Package object for the best available candidate that - matched the search key.""" - t = portage.db["/"]["porttree"].dep_bestmatch(search_key) - if t: - return Package(t) - return None - -def find_system_packages(prefilter=None): - """Returns a tuple of lists, first list is resolved system packages, - second is a list of unresolved packages.""" - pkglist = settings.packages - resolved = [] - unresolved = [] - for x in pkglist: - cpv = x.strip() - if len(cpv) and cpv[0] == "*": - pkg = find_best_match(cpv) - if pkg: - resolved.append(pkg) - else: - unresolved.append(cpv) - return (resolved, unresolved) - -def find_world_packages(prefilter=None): - """Returns a tuple of lists, first list is resolved world packages, - seond is unresolved package names.""" - f = open(portage.root+portage.WORLD_FILE) - pkglist = f.readlines() - resolved = [] - unresolved = [] - for x in pkglist: - cpv = x.strip() - if len(cpv) and cpv[0] != "#": - pkg = find_best_match(cpv) - if pkg: - resolved.append(pkg) - else: - unresolved.append(cpv) - return (resolved,unresolved) - -def find_all_installed_packages(prefilter=None): - """Returns a list of all installed packages, after applying the prefilter - function""" - t = vartree.dbapi.cpv_all() - if prefilter: - t = filter(prefilter,t) - return [Package(x) for x in t] - -def find_all_uninstalled_packages(prefilter=None): - """Returns a list of all uninstalled packages, after applying the prefilter - function""" - alist = find_all_packages(prefilter) - return [x for x in alist if not x.is_installed()] - -def find_all_packages(prefilter=None): - """Returns a list of all known packages, installed or not, after applying - the prefilter function""" - t = porttree.dbapi.cp_all() - t += vartree.dbapi.cp_all() - if prefilter: - t = filter(prefilter,t) - t = unique_array(t) - t2 = [] - for x in t: - t2 += porttree.dbapi.cp_list(x) - t2 += vartree.dbapi.cp_list(x) - t2 = unique_array(t2) - return [Package(x) for x in t2] - -def split_package_name(name): - """Returns a list on the form [category, name, version, revision]. Revision will - be 'r0' if none can be inferred. Category and version will be empty, if none can - be inferred.""" - r = portage.catpkgsplit(name) - if not r: - r = name.split("/") - if len(r) == 1: - return ["", name, "", "r0"] + matches = PORTDB.match(query) + matches.extend(VARDB.match(query)) + except portage.exception.InvalidAtom, err: + raise errors.GentoolkitInvalidAtom(str(err)) + + return [Package(x) for x in set(matches)] + + +def get_cpvs(predicate=None, include_installed=True): + """Get all packages in the Portage tree and overlays. Optionally apply a + predicate. + + Example usage: + >>> from gentoolkit.helpers import get_cpvs + >>> len(set(get_cpvs())) + 26065 + >>> fn = lambda x: x.startswith('app-portage') + >>> len(get_cpvs(fn, include_installed=False)) + 112 + + @type predicate: function + @param predicate: a function to filter the package list with + @type include_installed: bool + @param include_installed: + If True: Return the union of all_cpvs and all_installed_cpvs + If False: Return the difference of all_cpvs and all_installed_cpvs + @rtype: generator + @return: a generator that yields unsorted cat/pkg-ver strings from the + Portage tree + """ + + if predicate: + all_cps = iter(x for x in PORTDB.cp_all() if predicate(x)) + else: + all_cps = PORTDB.cp_all() + + all_cpvs = chain.from_iterable(PORTDB.cp_list(x) for x in all_cps) + all_installed_cpvs = get_installed_cpvs(predicate) + + if include_installed: + for cpv in chain(all_cpvs, all_installed_cpvs): + yield cpv + else: + # Consume the smaller pkg set: + installed_cpvs = set(all_installed_cpvs) + for cpv in all_cpvs: + if cpv not in installed_cpvs: + yield cpv + + +# pylint thinks this is a global variable +# pylint: disable-msg=C0103 +get_uninstalled_cpvs = partial(get_cpvs, include_installed=False) + + +def get_installed_cpvs(predicate=None): + """Get all installed packages. Optionally apply a predicate. + + @type predicate: function + @param predicate: a function to filter the package list with + @rtype: generator + @return: a generator that yields unsorted installed cat/pkg-ver strings + from VARDB + """ + + if predicate: + installed_cps = iter(x for x in VARDB.cp_all() if predicate(x)) + else: + installed_cps = VARDB.cp_all() + + for cpv in chain.from_iterable(VARDB.cp_list(x) for x in installed_cps): + yield cpv + + +def print_query_info(query, query_opts): + """Print info about the query to the screen.""" + + cat, pkg = split_cpv(query)[:2] + if cat and not query_opts["isRegex"]: + cat_str = "in %s " % pp.emph(cat.lstrip('><=~!')) + else: + cat_str = "" + + if query_opts["isRegex"]: + pkg_str = query + else: + pkg_str = pkg + + print " * Searching for %s %s..." % (pp.emph(pkg_str), cat_str) + + +def print_file(path): + """Display the contents of a file.""" + + with open(path) as open_file: + lines = open_file.read() + print lines.strip() + + +def print_sequence(seq): + """Print every item of a sequence.""" + + for item in seq: + print item + + +def split_cpv(query): + """Split a cpv into category, name, version and revision. + + @type query: str + @param query: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom or regex + @rtype: tuple + @return: (category, pkg_name, version, revision) + Each tuple element is a string or empty string (""). + """ + + result = catpkgsplit(query) + + if result: + result = list(result) + if result[0] == 'null': + result[0] = '' + if result[3] == 'r0': + result[3] = '' + else: + result = query.split("/") + if len(result) == 1: + result = ['', query, '', ''] else: - return r + ["", "r0"] + result = result + ['', ''] + + if len(result) != 4: + raise errors.GentoolkitInvalidPackageName(query) + + return tuple(result) + + +def uniqify(seq, preserve_order=True): + """Return a uniqified list. Optionally preserve order.""" + + if preserve_order: + seen = set() + result = [x for x in seq if x not in seen and not seen.add(x)] else: - r = list(r) - if r[0] == 'null': - r[0] = '' - return r + result = list(set(seq)) + + return result + + +def uses_globbing(query): + """Check the query to see if it is using globbing. -# XXX: Defunct: use helpers2.compare_package_strings -#def sort_package_list(pkglist): -# """Returns the list ordered in the same way portage would do with lowest version -# at the head of the list.""" -# pkglist.sort(Package.compare_version) -# return pkglist + @type query: str + @param query: user input package query + @rtype: bool + @return: True if query uses globbing, else False + """ -if __name__ == "__main__": - print "This module is for import only" + if set('!*?[]').intersection(query): + # Is query an atom such as '=sys-apps/portage-2.2*'? + if query[0] != '=': + return True + return False +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/metadata.py b/pym/gentoolkit/metadata.py new file mode 100644 index 0000000..9c65fd9 --- /dev/null +++ b/pym/gentoolkit/metadata.py @@ -0,0 +1,303 @@ +#!/usr/bin/python +# +# Copyright(c) 2009, Gentoo Foundation +# +# Licensed under the GNU General Public License, v2 +# +# $Header$ + +"""Provides an easy-to-use python interface to Gentoo's metadata.xml file. + + Example usage: + >>> from gentoolkit.metadata import MetaData + >>> pkg_md = MetaData('/usr/portage/app-misc/gourmet/metadata.xml') + >>> pkg_md + <MetaData '/usr/portage/app-misc/gourmet/metadata.xml'> + >>> pkg_md.get_herds() + ['no-herd'] + >>> for maint in pkg_md.get_maintainers(): + ... print "{0} ({1})".format(maint.email, maint.name) + ... + nixphoeni@gentoo.org (Joe Sapp) + >>> for flag in pkg_md.get_useflags(): + ... print flag.name, "->", flag.description + ... + rtf -> Enable export to RTF + gnome-print -> Enable printing support using gnome-print + >>> upstream = pkg_md.get_upstream() + >>> upstream + [<_Upstream {'docs': [], 'remoteid': [], 'maintainer': + [<_Maintainer 'Thomas_Hinkle@alumni.brown.edu'>], 'bugtracker': [], + 'changelog': []}>] + >>> upstream[0].maintainer[0].name + 'Thomas Mills Hinkle' +""" + +# Move to Imports section after Python-2.6 is stable +from __future__ import with_statement + +__all__ = ('MetaData',) +__docformat__ = 'epytext' + +# ======= +# Imports +# ======= + +import re +import os +import xml.etree.cElementTree as etree + +from portage import settings + +# ======= +# Classes +# ======= + +class _Maintainer(object): + """An object for representing one maintainer. + + @type email: str or None + @ivar email: Maintainer's email address. Used for both Gentoo and upstream. + @type name: str or None + @ivar name: Maintainer's name. Used for both Gentoo and upstream. + @type description: str or None + @ivar description: Description of what a maintainer does. Gentoo only. + @type restrict: str or None + @ivar restrict: e.g. >=portage-2.2 means only maintains versions + of Portage greater than 2.2. + @type status: str or None + @ivar status: If set, either 'active' or 'inactive'. Upstream only. + """ + + def __init__(self, node): + self.email = None + self.name = None + self.description = None + self.restrict = node.get('restrict') + self.status = node.get('status') + maint_attrs = node.getchildren() + for attr in maint_attrs: + setattr(self, attr.tag, attr.text) + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.email) + + +class _Useflag(object): + """An object for representing one USE flag. + + @todo: Is there any way to have a keyword option to leave in + <pkg> and <cat> for later processing? + @type name: str or None + @ivar name: USE flag + @type restrict: str or None + @ivar restrict: e.g. >=portage-2.2 means flag is only avaiable in + versions greater than 2.2 + @type description: str + @ivar description: description of the USE flag + """ + + def __init__(self, node): + self.name = node.get('name') + self.restrict = node.get('restrict') + _desc = '' + if node.text: + _desc = node.text + for child in node.getchildren(): + _desc += child.text if child.text else '' + _desc += child.tail if child.tail else '' + # This takes care of tabs and newlines left from the file + self.description = re.sub('\s+', ' ', _desc) + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.name) + + +class _Upstream(object): + """An object for representing one package's upstream. + + @type maintainers: list + @ivar maintainers: L{_Maintainer} objects for each upstream maintainer + @type changelogs: list + @ivar changelogs: URLs to upstream's ChangeLog file in str format + @type docs: list + @ivar docs: Sequence of tuples containing URLs to upstream documentation + in the first slot and 'lang' attribute in the second, e.g., + [('http.../docs/en/tut.html', None), ('http.../doc/fr/tut.html', 'fr')] + @type bugtrackers: list + @ivar bugtrackers: URLs to upstream's bugtracker. May also contain an email + address if prepended with 'mailto:' + @type remoteids: list + @ivar remoteids: Sequence of tuples containing the project's hosting site + name in the first slot and the project's ID name or number for that + site in the second, e.g., [('sourceforge', 'systemrescuecd')] + """ + + def __init__(self, node): + self.node = node + self.maintainers = self.get_upstream_maintainers() + self.changelogs = self.get_upstream_changelogs() + self.docs = self.get_upstream_documentation() + self.bugtrackers = self.get_upstream_bugtrackers() + self.remoteids = self.get_upstream_remoteids() + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.__dict__) + + def get_upstream_bugtrackers(self): + """Retrieve upstream bugtracker location from xml node.""" + return [e.text for e in self.node.findall('bugs-to')] + + def get_upstream_changelogs(self): + """Retrieve upstream changelog location from xml node.""" + return [e.text for e in self.node.findall('changelog')] + + def get_upstream_documentation(self): + """Retrieve upstream documentation location from xml node.""" + result = [] + for elem in self.node.findall('doc'): + lang = elem.get('lang') + result.append((elem.text, lang)) + return result + + def get_upstream_maintainers(self): + """Retrieve upstream maintainer information from xml node.""" + return [_Maintainer(m) for m in self.node.findall('maintainer')] + + def get_upstream_remoteids(self): + """Retrieve upstream remote ID from xml node.""" + return [(e.text, e.get('type')) for e in self.node.findall('remote-id')] + + +class MetaData(object): + """Access metadata.xml""" + + def __init__(self, metadata_path): + """Parse a valid metadata.xml file. + + @type metadata_path: str + @ivar metadata_path: path to a valid metadata.xml file + @raise IOError: if C{matadata_path} can not be read + """ + + self.metadata_path = metadata_path + self._xml_tree = etree.parse(metadata_path) + + # Used for caching + self._herdstree = None + self._descriptions = None + self._maintainers = None + self._useflags = None + self._upstream = None + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.metadata_path) + + def _get_herd_email(self, herd): + """Get a herd's email address. + + @type herd: str + @param herd: herd whose email you want + @rtype: str or None + @return: email address or None if herd is not in herds.xml + @raise IOError: if $PORTDIR/metadata/herds.xml can not be read + """ + + if self._herdstree is None: + herds_path = os.path.join(settings['PORTDIR'], 'metadata/herds.xml') + self._herdstree = etree.parse(herds_path) + + # Some special herds are not listed in herds.xml + if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'): + return None + + for node in self._herdstree.getiterator('herd'): + if node.findtext('name') == herd: + return node.findtext('email') + + def get_herds(self, include_email=False): + """Return a list of text nodes for <herd>. + + @type include_email: bool + @keyword include_email: if True, also look up the herd's email + @rtype: list + @return: if include_email is False, return a list of string; + if include_email is True, return a list of tuples containing: + [('herd1', 'herd1@gentoo.org'), ('no-herd', None); + """ + + result = [] + for elem in self._xml_tree.findall('herd'): + if include_email: + herd_mail = self._get_herd_email(elem.text) + result.append((elem.text, herd_mail)) + else: + result.append(elem.text) + + return result + + def get_descriptions(self): + """Return a list of text nodes for <longdescription>. + + @rtype: list + @return: package description in string format + @todo: Support the C{lang} attribute + """ + + if self._descriptions is not None: + return self._descriptions + + self._descriptions = [ + e.text for e in self._xml_tree.findall("longdescription") + ] + return self._descriptions + + def get_maintainers(self): + """Get maintainers' name, email and description. + + @rtype: list + @return: a sequence of L{_Maintainer} objects in document order. + """ + + if self._maintainers is not None: + return self._maintainers + + self._maintainers = [] + for node in self._xml_tree.findall('maintainer'): + self._maintainers.append(_Maintainer(node)) + + return self._maintainers + + def get_useflags(self): + """Get names and descriptions for USE flags defined in metadata. + + @rtype: list + @return: a sequence of L{_Useflag} objects in document order. + """ + + if self._useflags is not None: + return self._useflags + + self._useflags = [] + for node in self._xml_tree.getiterator('flag'): + self._useflags.append(_Useflag(node)) + + return self._useflags + + def get_upstream(self): + """Get upstream contact information. + + @rtype: list + @return: a sequence of L{_Upstream} objects in document order. + """ + + if self._upstream is not None: + return self._upstream + + self._upstream = [] + for node in self._xml_tree.findall('upstream'): + self._upstream.append(_Upstream(node)) + + return self._upstream + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/package.py b/pym/gentoolkit/package.py index 857470a..e348258 100644 --- a/pym/gentoolkit/package.py +++ b/pym/gentoolkit/package.py @@ -7,159 +7,198 @@ # # $Header$ +"""Provides classes for accessing Portage db information for a given package.""" + +__all__ = ( + 'Package', + 'PackageFormatter' +) + # ======= -# Imports +# Imports # ======= import os import portage -from portage.versions import catpkgsplit, vercmp +from portage import settings import gentoolkit.pprinter as pp -from gentoolkit import settings, settingslock, PORTDB, VARDB from gentoolkit import errors -from gentoolkit.versionmatch import VersionMatch +from gentoolkit.cpv import CPV +from gentoolkit.dbapi import PORTDB, VARDB +from gentoolkit.dependencies import Dependencies +from gentoolkit.metadata import MetaData # ======= # Classes # ======= -class Package(object): - """Package descriptor. Contains convenience functions for querying the - state of a package, its contents, name manipulation, ebuild info and - similar.""" - - def __init__(self, arg): - - self._cpv = arg - self.cpv = self._cpv - - if self.cpv[0] in ('<', '>'): - if self.cpv[1] == '=': - self.operator = self.cpv[:2] - self.cpv = self.cpv[2:] - else: - self.operator = self.cpv[0] - self.cpv = self.cpv[1:] - elif self.cpv[0] == '=': - if self.cpv[-1] == '*': - self.operator = '=*' - self.cpv = self.cpv[1:-1] - else: - self.cpv = self.cpv[1:] - self.operator = '=' - elif self.cpv[0] == '~': - self.operator = '~' - self.cpv = self.cpv[1:] +class Package(CPV): + """Provides methods for ascertaining the state of a given CPV.""" + + def __init__(self, cpv): + if isinstance(cpv, CPV): + self.cpv = cpv else: - self.operator = '=' - self._cpv = '=%s' % self._cpv + self.cpv = CPV(cpv) + del cpv - if not portage.dep.isvalidatom(self._cpv): - raise errors.GentoolkitInvalidCPV(self._cpv) + if not all(getattr(self.cpv, x) for x in ('category', 'version')): + # CPV allows some things that Package must not + raise errors.GentoolkitInvalidPackage(str(self.cpv)) - cpv_split = portage.catpkgsplit(self.cpv) - - try: - self.key = "/".join(cpv_split[:2]) - except TypeError: - # catpkgsplit returned None - raise errors.GentoolkitInvalidCPV(self._cpv) - - cpv_split = list(cpv_split) - if cpv_split[0] == 'null': - cpv_split[0] = '' - if cpv_split[3] == 'r0': - cpv_split[3] = '' - self.cpv_split = cpv_split - self._scpv = self.cpv_split # XXX: namespace compatability 03/09 - - self._db = None - self._settings = settings - self._settingslock = settingslock - self._portdir_path = os.path.realpath(settings["PORTDIR"]) - - self.category = self.cpv_split[0] - self.name = self.cpv_split[1] - self.version = self.cpv_split[2] - self.revision = self.cpv_split[3] - if not self.revision: - self.fullversion = self.version - else: - self.fullversion = "%s-%s" % (self.version, self.revision) + # Set dynamically + self._package_path = None + self._dblink = None + self._metadata = None + self._deps = None + self._portdir_path = None def __repr__(self): - return "<%s %s @%#8x>" % (self.__class__.__name__, self._cpv, id(self)) + return "<%s %r>" % (self.__class__.__name__, str(self.cpv)) def __eq__(self, other): - return hash(self) == hash(other) + if not hasattr(other, 'cpv'): + return False + return self.cpv == other.cpv def __ne__(self, other): - return hash(self) != hash(other) + return not self == other def __lt__(self, other): - if not isinstance(other, self.__class__): - raise TypeError("other isn't of %s type, is %s" % - (self.__class__, other.__class__)) - - if self.category != other.category: - return self.category < other.category - elif self.name != other.name: - return self.name < other.name - else: - # FIXME: this cmp() hack is for vercmp not using -1,0,1 - # See bug 266493; this was fixed in portage-2.2_rc31 - #return portage.vercmp(self.fullversion, other.fullversion) - result = cmp(portage.vercmp(self.fullversion, other.fullversion), 0) - if result == -1: - return True - else: - return False + return self.cpv < other.cpv def __gt__(self, other): - return not self.__lt__(other) + return self.cpv > other.cpv def __hash__(self): - return hash(self._cpv) + return hash(str(self.cpv)) def __contains__(self, key): - return key in self._cpv - + return key in str(self.cpv) + def __str__(self): - return self._cpv + return str(self.cpv) + + def _get_trees(self): + """Return dbapi objects for each repository that contains self.""" + + result = [] + if self.is_installed(): + result.append(VARDB) + if self.exists(): + result.append(PORTDB) + if not result: + raise errors.GentoolkitFatalError("Could not find package tree") + + return result + + @property + def metadata(self): + """Instantiate a L{gentoolkit.metadata.MetaData} object here.""" + + if self._metadata is None: + metadata_path = os.path.join( + self.get_package_path(), 'metadata.xml' + ) + self._metadata = MetaData(metadata_path) + + return self._metadata + + @property + def dblink(self): + """Instantiate a L{portage.dbapi.vartree.dblink} object here.""" + + if self._dblink is None: + self._dblink = portage.dblink( + self.cpv.category, + "%s-%s" % (self.cpv.name, self.cpv.fullversion), + settings["ROOT"], + settings + ) + + return self._dblink + + @property + def deps(self): + """Instantiate a L{gentoolkit.dependencies.Dependencies} object here.""" + + if self._deps is None: + self._deps = Dependencies(self.cpv) - def get_name(self): - """Returns base name of package, no category nor version""" - return self.name + return self._deps - def get_version(self): - """Returns version of package, with revision number""" - return self.fullversion + def exists(self): + """Return True if package exists in the Portage tree, else False""" - def get_category(self): - """Returns category of package""" - return self.category + return bool(PORTDB.cpv_exists(str(self.cpv))) - def get_settings(self, key): - """Returns the value of the given key for this package (useful + @staticmethod + def get_settings(key): + """Returns the value of the given key for this package (useful for package.* files.""" + + if settings.locked: + settings.unlock() try: - self._settingslock.acquire() - self._settings.setcpv(self.cpv) - result = self._settings[key] + result = settings[key] finally: - self._settingslock.release() + settings.lock() + return result + + def get_mask_status(self): + """Shortcut to L{portage.getmaskingstatus}. + + @rtype: None or list + @return: a list containing none or some of: + 'profile' + 'package.mask' + license(s) + "kmask" keyword + 'missing keyword' + """ + + if settings.locked: + settings.unlock() + try: + result = portage.getmaskingstatus(str(self.cpv), + settings=settings, + portdb=PORTDB) + except KeyError: + # getmaskingstatus doesn't support packages without ebuilds in the + # Portage tree. + result = None + return result - def get_cpv(self): - """Returns full Category/Package-Version string""" - return self.cpv + def get_mask_reason(self): + """Shortcut to L{portage.getmaskingreason}. + + @rtype: None or tuple + @return: empty tuple if pkg not masked OR + ('mask reason', 'mask location') + """ + + try: + result = portage.getmaskingreason(str(self.cpv), + settings=settings, + PORTDB=PORTDB, + return_location=True) + if result is None: + result = tuple() + except KeyError: + # getmaskingstatus doesn't support packages without ebuilds in the + # Portage tree. + result = None + + return result def get_provide(self): """Return a list of provides, if any""" + if self.is_installed(): - result = VARDB.get_provide(self.cpv) + result = VARDB.get_provide(str(self.cpv)) else: try: result = [self.get_env_var('PROVIDE')] @@ -167,289 +206,114 @@ class Package(object): result = [] return result - def get_dependants(self): - """Retrieves a list of CPVs for all packages depending on this one""" - raise NotImplementedError("Not implemented yet!") + def get_ebuild_path(self, in_vartree=False): + """Returns the complete path to the .ebuild file. - def get_runtime_deps(self): - """Returns a linearised list of first-level run time dependencies for - this package, on the form [(comparator, [use flags], cpv), ...] + Example usage: + >>> pkg.get_ebuild_path() + '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild' + >>> pkg.get_ebuild_path(in_vartree=True) + '/var/db/pkg/sys-apps/portage-2.1.6.13/portage-2.1.6.13.ebuild' """ - # Try to use the portage tree first, since emerge only uses the tree - # when calculating dependencies - try: - rdepends = self.get_env_var("RDEPEND", PORTDB).split() - except KeyError: - rdepends = self.get_env_var("RDEPEND", VARDB).split() - return self._parse_deps(rdepends)[0] - def get_compiletime_deps(self): - """Returns a linearised list of first-level compile time dependencies - for this package, on the form [(comparator, [use flags], cpv), ...] - """ - # Try to use the portage tree first, since emerge only uses the tree - # when calculating dependencies - try: - depends = self.get_env_var("DEPEND", PORTDB).split() - except KeyError: - depends = self.get_env_var("DEPEND", VARDB).split() - return self._parse_deps(depends)[0] + if in_vartree: + return VARDB.findname(str(self.cpv)) + return PORTDB.findname(str(self.cpv)) - def get_postmerge_deps(self): - """Returns a linearised list of first-level post merge dependencies - for this package, on the form [(comparator, [use flags], cpv), ...] - """ - # Try to use the portage tree first, since emerge only uses the tree - # when calculating dependencies - try: - postmerge_deps = self.get_env_var("PDEPEND", PORTDB).split() - except KeyError: - postmerge_deps = self.get_env_var("PDEPEND", VARDB).split() - return self._parse_deps(postmerge_deps)[0] + def get_package_path(self): + """Return the path to where the ebuilds and other files reside.""" - def intersects(self, other): - """Check if a passed in package atom "intersects" this atom. + if self._package_path is None: + path_split = self.get_ebuild_path().split(os.sep) + self._package_path = os.sep.join(path_split[:-1]) - Lifted from pkgcore. + return self._package_path - Two atoms "intersect" if a package can be constructed that - matches both: - - if you query for just "dev-lang/python" it "intersects" both - "dev-lang/python" and ">=dev-lang/python-2.4" - - if you query for "=dev-lang/python-2.4" it "intersects" - ">=dev-lang/python-2.4" and "dev-lang/python" but not - "<dev-lang/python-2.3" + def get_repo_name(self): + """Using the package path, determine the repo name. - @type other: L{gentoolkit.package.Package} - @param other: other package to compare - @see: pkgcore.ebuild.atom.py + @rtype: str + @return: /usr/<THIS>portage</THIS>/cat-egory/name/ """ - # Our "key" (cat/pkg) must match exactly: - if self.key != other.key: - return False - # If we are both "unbounded" in the same direction we intersect: - if (('<' in self.operator and '<' in other.operator) or - ('>' in self.operator and '>' in other.operator)): - return True - - # If one of us is an exact match we intersect if the other matches it: - if self.operator == '=': - if other.operator == '=*': - return self.fullversion.startswith(other.fullversion) - return VersionMatch(other).match(self) - if other.operator == '=': - if self.operator == '=*': - return other.fullversion.startswith(self.fullversion) - return VersionMatch(self).match(other) - - # If we are both ~ matches we match if we are identical: - if self.operator == other.operator == '~': - return (self.version == other.version and - self.revision == other.revision) - - # If we are both glob matches we match if one of us matches the other. - if self.operator == other.operator == '=*': - return (self.fullver.startswith(other.fullver) or - other.fullver.startswith(self.fullver)) - - # If one of us is a glob match and the other a ~ we match if the glob - # matches the ~ (ignoring a revision on the glob): - if self.operator == '=*' and other.operator == '~': - return other.fullversion.startswith(self.version) - if other.operator == '=*' and self.operator == '~': - return self.fullversion.startswith(other.version) - - # If we get here at least one of us is a <, <=, > or >=: - if self.operator in ('<', '<=', '>', '>='): - ranged, other = self, other - else: - ranged, other = other, self - - if '<' in other.operator or '>' in other.operator: - # We are both ranged, and in the opposite "direction" (or - # we would have matched above). We intersect if we both - # match the other's endpoint (just checking one endpoint - # is not enough, it would give a false positive on <=2 vs >2) - return ( - VersionMatch(other).match(ranged) and - VersionMatch(ranged).match(other)) - - if other.operator == '~': - # Other definitely matches its own version. If ranged also - # does we're done: - if VersionMatch(ranged).match(other): - return True - # The only other case where we intersect is if ranged is a - # > or >= on other's version and a nonzero revision. In - # that case other will match ranged. Be careful not to - # give a false positive for ~2 vs <2 here: - return ranged.operator in ('>', '>=') and VersionMatch( - other.operator, other.version, other.revision).match(ranged) - - if other.operator == '=*': - # a glob match definitely matches its own version, so if - # ranged does too we're done: - if VersionMatch( - ranged.operator, ranged.version, ranged.revision).match(other): - return True - if '<' in ranged.operator: - # If other.revision is not defined then other does not - # match anything smaller than its own fullver: - if not other.revision: - return False - - # If other.revision is defined then we can always - # construct a package smaller than other.fullver by - # tagging e.g. an _alpha1 on. - return ranged.fullversion.startswith(other.version) - else: - # Remaining cases where this intersects: there is a - # package greater than ranged.fullver and - # other.fullver that they both match. - return ranged.fullversion.startswith(other.version) - - # Handled all possible ops. - raise NotImplementedError( - 'Someone added an operator without adding it to intersects') - - - def _parse_deps(self,deps,curuse=[],level=0): - # store (comparator, [use predicates], cpv) - r = [] - comparators = ["~","<",">","=","<=",">="] - end = len(deps) - i = 0 - while i < end: - tok = deps[i] - if tok == ')': - return r,i - if tok[-1] == "?": - tok = tok.replace("?","") - sr,l = self._parse_deps(deps[i+2:],curuse=curuse+[tok],level=level+1) - r += sr - i += l + 3 - continue - if tok == "||": - sr,l = self._parse_deps(deps[i+2:],curuse,level=level+1) - r += sr - i += l + 3 - continue - # conjunction, like in "|| ( ( foo bar ) baz )" => recurse - if tok == "(": - sr,l = self._parse_deps(deps[i+1:],curuse,level=level+1) - r += sr - i += l + 2 - continue - # pkg block "!foo/bar" => ignore it - if tok[0] == "!": - i += 1 - continue - # pick out comparator, if any - cmp = "" - for c in comparators: - if tok.find(c) == 0: - cmp = c - tok = tok[len(cmp):] - r.append((cmp,curuse,tok)) - i += 1 - return r,i + return self.get_package_path().split(os.sep)[-3] - def is_installed(self): - """Returns True if this package is installed (merged)""" - return VARDB.cpv_exists(self.cpv) + def get_env_var(self, var, tree=None): + """Returns one of the predefined env vars DEPEND, SRC_URI, etc.""" - def is_overlay(self): - """Returns True if the package is in an overlay.""" - ebuild, tree = portage.portdb.findname2(self.cpv) - return tree != self._portdir_path + if tree is None: + tree = self._get_trees()[0] + try: + result = tree.aux_get(str(self.cpv), [var]) + if len(result) != 1: + raise errors.GentoolkitFatalError + except (KeyError, errors.GentoolkitFatalError): + err = "aux_get returned unexpected results" + raise errors.GentoolkitFatalError(err) + return result[0] - def is_masked(self): - """Returns true if this package is masked against installation. - Note: We blindly assume that the package actually exists on disk - somewhere.""" - unmasked = portage.portdb.xmatch("match-visible", self.cpv) - return self.cpv not in unmasked + def get_use_flags(self): + """Returns the USE flags active at time of installation.""" - def get_ebuild_path(self, in_vartree=False): - """Returns the complete path to the .ebuild file""" - if in_vartree: - return VARDB.getebuildpath(self.cpv) - return PORTDB.findname(self.cpv) + return self.dblink.getstring("USE") - def get_package_path(self): - """Returns the path to where the ChangeLog, Manifest, .ebuild files - reside""" - ebuild_path = self.get_ebuild_path() - path_split = ebuild_path.split("/") - if path_split: - return os.sep.join(path_split[:-1]) + def get_contents(self): + """Returns the parsed CONTENTS file. - def get_env_var(self, var, tree=None): - """Returns one of the predefined env vars DEPEND, RDEPEND, - SRC_URI,....""" - if tree == None: - tree = VARDB - if not self.is_installed(): - tree = PORTDB - result = tree.aux_get(self.cpv, [var]) - if not result: - raise errors.GentoolkitFatalError("Could not find the package tree") - if len(result) != 1: - raise errors.GentoolkitFatalError("Should only get one element!") - return result[0] + @rtype: dict + @return: {'/full/path/to/obj': ['type', 'timestamp', 'md5sum'], ...} + """ - def get_use_flags(self): - """Returns the USE flags active at time of installation""" - self._initdb() - if self.is_installed(): - return self._db.getfile("USE") + return self.dblink.getcontents() - def get_contents(self): - """Returns the full contents, as a dictionary, in the form - ['/bin/foo' : [ 'obj', '1052505381', '45ca8b89751...' ], ... ]""" - self._initdb() - if self.is_installed(): - return self._db.getcontents() - return {} + def get_size(self): + """Estimates the installed size of the contents of this package. - def size(self): - """Estimates the installed size of the contents of this package, - if possible. - Returns (size, number of files in total, number of uncounted files) + @rtype: tuple + @return: (size, number of files in total, number of uncounted files) """ + contents = self.get_contents() - size = 0 - uncounted = 0 - files = 0 - for x in contents: + size = n_uncounted = n_files = 0 + for cfile in contents: try: - size += os.lstat(x).st_size - files += 1 + size += os.lstat(cfile).st_size + n_files += 1 except OSError: - uncounted += 1 - return (size, files, uncounted) + n_uncounted += 1 + return (size, n_files, n_uncounted) - def _initdb(self): - """Internal helper function; loads package information from disk, - when necessary. - """ - if not self._db: - self._db = portage.dblink( - self.category, - "%s-%s" % (self.name, self.fullversion), - settings["ROOT"], - settings - ) + def is_installed(self): + """Returns True if this package is installed (merged)""" + + return VARDB.cpv_exists(str(self.cpv)) + + def is_overlay(self): + """Returns True if the package is in an overlay.""" + + ebuild, tree = PORTDB.findname2(str(self.cpv)) + if not ebuild: + return None + if self._portdir_path is None: + self._portdir_path = os.path.realpath(settings["PORTDIR"]) + return (tree and tree != self._portdir_path) + + def is_masked(self): + """Returns true if this package is masked against installation. + Note: We blindly assume that the package actually exists on disk + somewhere.""" + + unmasked = PORTDB.xmatch("match-visible", str(self.cpv)) + return str(self.cpv) not in unmasked class PackageFormatter(object): """When applied to a L{gentoolkit.package.Package} object, determine the location (Portage Tree vs. overlay), install status and masked status. That information can then be easily formatted and displayed. - + Example usage: - >>> from gentoolkit.helpers2 import find_packages + >>> from gentoolkit.helpers import find_packages >>> from gentoolkit.package import PackageFormatter >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')] >>> for pkg in pkgs: @@ -457,49 +321,45 @@ class PackageFormatter(object): ... # tree ... if set('IP').issubset(pkg.location): ... print pkg - ... + ... [IP-] [ ] sys-devel/gcc-4.3.2-r3 (4.3) @type pkg: L{gentoolkit.package.Package} @param pkg: package to format @type format: L{bool} - @param format: Whether to format the package name or not. + @param format: Whether to format the package name or not. Essentially C{format} should be set to False when piping or when - quiet output is desired. If C{format} is False, only the location + quiet output is desired. If C{do_format} is False, only the location attribute will be created to save time. """ - def __init__(self, pkg, format=True): - location = '' - maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-'] - + def __init__(self, pkg, do_format=True): self.pkg = pkg - self.format = format - if format: - self.arch = settings["ARCH"] - self.mask = maskmodes[self.get_mask_status()] - self.slot = pkg.get_env_var("SLOT") - self.location = self.get_package_location() + self.do_format = do_format + self.location = self.format_package_location() or '' def __repr__(self): return "<%s %s @%#8x>" % (self.__class__.__name__, self.pkg, id(self)) def __str__(self): - if self.format: + if self.do_format: + maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-', 'XX'] return "[%(location)s] [%(mask)s] %(package)s (%(slot)s)" % { 'location': self.location, - 'mask': pp.maskflag(self.mask), - 'package': pp.cpv(self.pkg.cpv), - 'slot': self.slot + 'mask': pp.maskflag(maskmodes[self.format_mask_status()[0]]), + 'package': pp.cpv(str(self.pkg.cpv)), + 'slot': self.pkg.get_env_var("SLOT") } else: return self.pkg.cpv - def get_package_location(self): - """Get the physical location of a package on disk. + def format_package_location(self): + """Get the install status (in /var/db/?) and origin (from and overlay + and the Portage tree?). @rtype: str @return: one of: + 'I--' : Installed but ebuild doesn't exist on system anymore '-P-' : Not installed and from the Portage tree '--O' : Not installed and from an overlay 'IP-' : Installed and from the Portage tree @@ -510,37 +370,47 @@ class PackageFormatter(object): if self.pkg.is_installed(): result[0] = 'I' - if self.pkg.is_overlay(): + + overlay = self.pkg.is_overlay() + if overlay is None: + pass + elif overlay: result[2] = 'O' else: result[1] = 'P' return ''.join(result) - def get_mask_status(self): - """Get the mask status of a given package. - - @type pkg: L{gentoolkit.package.Package} - @param pkg: pkg to get mask status of - @type arch: str - @param arch: output of gentoolkit.settings["ARCH"] - @rtype: int - @return: an index for this list: [" ", " ~", " -", "M ", "M~", "M-"] + def format_mask_status(self): + """Get the mask status of a given package. + + @rtype: tuple: (int, list) + @return: int = an index for this list: + [" ", " ~", " -", "M ", "M~", "M-", "XX"] 0 = not masked 1 = keyword masked 2 = arch masked 3 = hard masked 4 = hard and keyword masked, 5 = hard and arch masked + 6 = ebuild doesn't exist on system anymore + + list = original output of portage.getmaskingstatus """ - keywords = self.pkg.get_env_var("KEYWORDS").split() - mask_status = 0 - if self.pkg.is_masked(): - mask_status += 3 - if ("~%s" % self.arch) in keywords: - mask_status += 1 - elif ("-%s" % self.arch) in keywords or "-*" in keywords: - mask_status += 2 + result = 0 + masking_status = self.pkg.get_mask_status() + if masking_status is None: + return (6, []) + + if ("~%s keyword" % self.pkg.get_settings("ARCH")) in masking_status: + result += 1 + if "missing keyword" in masking_status: + result += 2 + if set(('profile', 'package.mask')).intersection(masking_status): + result += 3 + + return (result, masking_status) + - return mask_status +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/pprinter.py b/pym/gentoolkit/pprinter.py index ff92a26..db8a368 100644 --- a/pym/gentoolkit/pprinter.py +++ b/pym/gentoolkit/pprinter.py @@ -1,116 +1,131 @@ #!/usr/bin/python # # Copyright 2004 Karl Trygve Kalleberg <karltk@gentoo.org> -# Copyright 2004 Gentoo Foundation +# Copyright 2004-2009 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # # $Header$ +"""Provides a consistent color scheme for Gentoolkit scripts.""" + +__all__ = ( + 'command', + 'cpv', + 'die', + 'emph', + 'error', + 'globaloption', + 'installedflag', + 'localoption', + 'maskflag', + 'number', + 'path', + 'path_symlink', + 'pkgquery', + 'productname', + 'regexpquery', + 'section', + 'slot', + 'subsection', + 'useflag', + 'warn' +) + +# ======= +# Imports +# ======= + import sys -import gentoolkit -try: - import portage.output as output -except ImportError: - import output +import portage.output as output +# ========= +# Functions +# ========= -def print_error(s): - """Prints an error string to stderr.""" - sys.stderr.write(output.red("!!! ") + s + "\n") +# output creates color functions on the fly, which confuses pylint. +# E1101: *%s %r has no %r member* +# pylint: disable-msg=E1101 -def print_info(lv, s, line_break = True): - """Prints an informational string to stdout.""" - if gentoolkit.Config["verbosityLevel"] >= lv: - sys.stdout.write(s) - if line_break: - sys.stdout.write("\n") +def command(string): + """Print a program command string.""" + return output.green(string) -def print_warn(s): - """Print a warning string to stderr.""" - sys.stderr.write("!!! " + s + "\n") - -def die(err, s): +def cpv(string): + """Print a category/package-<version> string.""" + return output.green(string) + +def die(err, string): """Print an error string and die with an error code.""" - print_error(s) + sys.stderr.write(error(string)) sys.exit(err) -# Colour settings +def emph(string): + """Print a string as emphasized.""" + return output.bold(string) -def cpv(s): - """Print a category/package-<version> string.""" - return output.green(s) +def error(string): + """Prints an error string to stderr.""" + return output.red("!!! ") + string + "\n" -def slot(s): - """Print a slot string""" - return output.bold(s) - -def useflag(s): - """Print a USE flag strign""" - return output.blue(s) - -def useflagon(s): - """Print an enabled USE flag string""" - # FIXME: Collapse into useflag with parameter - return output.red(s) - -def useflagoff(s): - """Print a disabled USE flag string""" - # FIXME: Collapse into useflag with parameter - return output.blue(s) - -def maskflag(s): - """Print a masking flag string""" - return output.red(s) +def globaloption(string): + """Print a global option string, i.e. the program global options.""" + return output.yellow(string) -def installedflag(s): +def installedflag(string): """Print an installed flag string""" - return output.bold(s) - -def number(s): - """Print a number string""" - return output.turquoise(s) + return output.bold(string) -def pkgquery(s): - """Print a package query string.""" - return output.bold(s) +def localoption(string): + """Print a local option string, i.e. the program local options.""" + return output.green(string) -def regexpquery(s): - """Print a regular expression string""" - return output.bold(s) +def maskflag(string): + """Print a masking flag string""" + return output.red(string) + +def number(string): + """Print a number string""" + return output.turquoise(string) -def path(s): +def path(string): """Print a file or directory path string""" - return output.bold(s) + return output.bold(string) -def path_symlink(s): +def path_symlink(string): """Print a symlink string.""" - return output.turquoise(s) + return output.turquoise(string) + +def pkgquery(string): + """Print a package query string.""" + return output.bold(string) -def productname(s): +def productname(string): """Print a product name string, i.e. the program name.""" - return output.turquoise(s) - -def globaloption(s): - """Print a global option string, i.e. the program global options.""" - return output.yellow(s) + return output.turquoise(string) -def localoption(s): - """Print a local option string, i.e. the program local options.""" - return output.green(s) +def regexpquery(string): + """Print a regular expression string""" + return output.bold(string) -def command(s): - """Print a program command string.""" - return output.green(s) - -def section(s): +def section(string): """Print a string as a section header.""" - return output.turquoise(s) + return output.turquoise(string) + +def slot(string): + """Print a slot string""" + return output.bold(string) -def subsection(s): +def subsection(string): """Print a string as a subsection header.""" - return output.turquoise(s) - -def emph(s): - """Print a string as emphasized.""" - return output.bold(s) + return output.turquoise(string) + +def useflag(string, enabled=False): + """Print a USE flag string""" + return output.red(string) if enabled else output.blue(string) + +def warn(string): + """Print a warning string to stderr.""" + return "!!! " + string + "\n" + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/test/__init__.py b/pym/gentoolkit/test/__init__.py new file mode 100644 index 0000000..94423e9 --- /dev/null +++ b/pym/gentoolkit/test/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/python +# Copyright 2009 Gentoo Foundation +# +# Distributed under the terms of the GNU General Public License v2 +# +# $Header$ diff --git a/pym/gentoolkit/test/equery/__init__.py b/pym/gentoolkit/test/equery/__init__.py new file mode 100644 index 0000000..94423e9 --- /dev/null +++ b/pym/gentoolkit/test/equery/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/python +# Copyright 2009 Gentoo Foundation +# +# Distributed under the terms of the GNU General Public License v2 +# +# $Header$ diff --git a/pym/gentoolkit/test/equery/test_init.py b/pym/gentoolkit/test/equery/test_init.py new file mode 100644 index 0000000..d135aa5 --- /dev/null +++ b/pym/gentoolkit/test/equery/test_init.py @@ -0,0 +1,52 @@ +import unittest +from test import test_support + +from gentoolkit import equery + +class TestEqueryInit(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_expand_module_name(self): + # Test that module names are properly expanded + name_map = { + 'b': 'belongs', + 'c': 'changes', + 'k': 'check', + 'd': 'depends', + 'g': 'depgraph', + 'f': 'files', + 'h': 'hasuse', + 'l': 'list_', + 'm': 'meta', + 's': 'size', + 'u': 'uses', + 'w': 'which' + } + self.failUnlessEqual(equery.NAME_MAP, name_map) + for short_name, long_name in zip(name_map, name_map.values()): + self.failUnlessEqual(equery.expand_module_name(short_name), + long_name) + self.failUnlessEqual(equery.expand_module_name(long_name), + long_name) + unused_keys = set(map(chr, range(0, 256))).difference(name_map.keys()) + for key in unused_keys: + self.failUnlessRaises(KeyError, equery.expand_module_name, key) + + def test_format_timestamp(self): + # Test that a certain timetamp produces the correct formatted string + tstamp = 1257626685.6503389 + tstr = '2009-11-07 15:44:45' + self.failUnlessEqual(equery.format_timestamp(tstamp), tstr) + + +def test_main(): + test_support.run_unittest(TestEqueryInit) + + +if __name__ == '__main__': + test_main() diff --git a/pym/gentoolkit/test/test_helpers.py b/pym/gentoolkit/test/test_helpers.py new file mode 100644 index 0000000..22509b7 --- /dev/null +++ b/pym/gentoolkit/test/test_helpers.py @@ -0,0 +1,107 @@ +import os +import unittest +import warnings +from tempfile import NamedTemporaryFile +from test import test_support + +from gentoolkit import helpers + + +class TestFileOwner(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_extend_realpaths(self): + extend_realpaths = helpers.FileOwner.extend_realpaths + + # Test that symlinks's realpaths are extended + f1 = NamedTemporaryFile(prefix='equeryunittest') + f2 = NamedTemporaryFile(prefix='equeryunittest') + f3 = NamedTemporaryFile(prefix='equeryunittest') + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + sym1 = os.tmpnam() + os.symlink(f1.name, sym1) + sym2 = os.tmpnam() + os.symlink(f3.name, sym2) + # We've created 3 files and 2 symlinks for testing. We're going to pass + # in only the first two files and both symlinks. sym1 points to f1. + # Since f1 is already in the list, sym1's realpath should not be added. + # sym2 points to f3, but f3's not in our list, so sym2's realpath + # should be added to the list. + p = [f1.name, f2.name, sym1, sym2] + p_xr = extend_realpaths(p) + + self.failUnlessEqual(p_xr[0], f1.name) + self.failUnlessEqual(p_xr[1], f2.name) + self.failUnlessEqual(p_xr[2], sym1) + self.failUnlessEqual(p_xr[3], sym2) + self.failUnlessEqual(p_xr[4], f3.name) + + # Clean up + os.unlink(sym1) + os.unlink(sym2) + + # Make sure we raise an exception if we don't get acceptable input + self.failUnlessRaises(AttributeError, extend_realpaths, 'str') + self.failUnlessRaises(AttributeError, extend_realpaths, set()) + + +class TestGentoolkitHelpers2(unittest.TestCase): + + def test_compare_package_strings(self): + # Test ordering of package strings, Portage has test for vercmp, + # so just do the rest + version_tests = [ + # different categories + ('sys-apps/portage-2.1.6.8', 'sys-auth/pambase-20080318'), + # different package names + ('sys-apps/pkgcore-0.4.7.15-r1', 'sys-apps/portage-2.1.6.8'), + # different package versions + ('sys-apps/portage-2.1.6.8', 'sys-apps/portage-2.2_rc25') + ] + # Check less than + for vt in version_tests: + self.failUnless( + helpers.compare_package_strings(vt[0], vt[1]) == -1 + ) + # Check greater than + for vt in version_tests: + self.failUnless( + helpers.compare_package_strings(vt[1], vt[0]) == 1 + ) + # Check equal + vt = ('sys-auth/pambase-20080318', 'sys-auth/pambase-20080318') + self.failUnless( + helpers.compare_package_strings(vt[0], vt[1]) == 0 + ) + + def test_uses_globbing(self): + globbing_tests = [ + ('sys-apps/portage-2.1.6.13', False), + ('>=sys-apps/portage-2.1.6.13', False), + ('<=sys-apps/portage-2.1.6.13', False), + ('~sys-apps/portage-2.1.6.13', False), + ('=sys-apps/portage-2*', False), + ('sys-*/*-2.1.6.13', True), + ('sys-app?/portage-2.1.6.13', True), + ('sys-apps/[bp]ortage-2.1.6.13', True), + ('sys-apps/[!p]ortage*', True) + ] + + for gt in globbing_tests: + self.failUnless( + helpers.uses_globbing(gt[0]) == gt[1] + ) + + +def test_main(): + test_support.run_unittest(TestGentoolkitHelpers2) + + +if __name__ == '__main__': + test_main() diff --git a/pym/gentoolkit/test/test_syntax.py b/pym/gentoolkit/test/test_syntax.py new file mode 100644 index 0000000..5b00fc5 --- /dev/null +++ b/pym/gentoolkit/test/test_syntax.py @@ -0,0 +1,31 @@ +import os +import os.path as osp +import unittest +import py_compile + +pym_dirs = os.walk(osp.dirname(osp.dirname(osp.dirname(__file__)))) +blacklist_dirs = frozenset(('.svn', 'tests')) + +class TestForSyntaxErrors(unittest.TestCase): + + def test_compileability(self): + compileables = [] + for thisdir, subdirs, files in pym_dirs: + if os.path.basename(thisdir) in blacklist_dirs: + continue + compileables.extend([ + osp.join(thisdir, f) + for f in files + if osp.splitext(f)[1] == '.py' + ]) + + for c in compileables: + py_compile.compile(c, doraise=True) + + +def test_main(): + test_support.run_unittest(TestGentoolkitHelpers2) + + +if __name__ == '__main__': + test_main() diff --git a/pym/gentoolkit/textwrap_.py b/pym/gentoolkit/textwrap_.py index 6851402..845ae9d 100644 --- a/pym/gentoolkit/textwrap_.py +++ b/pym/gentoolkit/textwrap_.py @@ -1,5 +1,5 @@ """This modification of textwrap allows it to wrap ANSI colorized text as if -it weren't colorized. It also uses a much simpler word splitting regex to +it weren't colorized. It also uses a much simpler word splitting regex to prevent the splitting of ANSI colors as well as package names and versions.""" import re @@ -13,81 +13,81 @@ class TextWrapper(textwrap.TextWrapper): Split the text to wrap into indivisible chunks. """ - # Only split on whitespace to avoid mangling ANSI escape codes or + # Only split on whitespace to avoid mangling ANSI escape codes or # package names. wordsep_re = re.compile(r'(\s+)') chunks = wordsep_re.split(text) - chunks = filter(None, chunks) + chunks = [x for x in chunks if x is not None] return chunks - def _wrap_chunks(self, chunks): - """_wrap_chunks(chunks : [string]) -> [string] - - Wrap a sequence of text chunks and return a list of lines of - length 'self.width' or less. (If 'break_long_words' is false, - some lines may be longer than this.) Chunks correspond roughly - to words and the whitespace between them: each chunk is - indivisible (modulo 'break_long_words'), but a line break can - come between any two chunks. Chunks should not have internal - whitespace; ie. a chunk is either all whitespace or a "word". - Whitespace chunks will be removed from the beginning and end of - lines, but apart from that whitespace is preserved. - """ - lines = [] - if self.width <= 0: - raise ValueError("invalid width %r (must be > 0)" % self.width) - - # Arrange in reverse order so items can be efficiently popped - # from a stack of chunks. - chunks.reverse() + def _wrap_chunks(self, chunks): + """_wrap_chunks(chunks : [string]) -> [string] + + Wrap a sequence of text chunks and return a list of lines of + length 'self.width' or less. (If 'break_long_words' is false, + some lines may be longer than this.) Chunks correspond roughly + to words and the whitespace between them: each chunk is + indivisible (modulo 'break_long_words'), but a line break can + come between any two chunks. Chunks should not have internal + whitespace; ie. a chunk is either all whitespace or a "word". + Whitespace chunks will be removed from the beginning and end of + lines, but apart from that whitespace is preserved. + """ + lines = [] + if self.width <= 0: + raise ValueError("invalid width %r (must be > 0)" % self.width) + + # Arrange in reverse order so items can be efficiently popped + # from a stack of chunks. + chunks.reverse() # Regex to strip ANSI escape codes. It's only used for the # length calculations of indent and each chuck. ansi_re = re.compile('\x1b\[[0-9;]*m') - while chunks: + while chunks: - # Start the list of chunks that will make up the current line. - # cur_len is just the length of all the chunks in cur_line. - cur_line = [] - cur_len = 0 + # Start the list of chunks that will make up the current line. + # cur_len is just the length of all the chunks in cur_line. + cur_line = [] + cur_len = 0 - # Figure out which static string will prefix this line. - if lines: - indent = self.subsequent_indent - else: - indent = self.initial_indent + # Figure out which static string will prefix this line. + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent # Maximum width for this line. Ingore ANSI escape codes. width = self.width - len(re.sub(ansi_re, '', indent)) - # First chunk on line is whitespace -- drop it, unless this - # is the very beginning of the text (ie. no lines started yet). - if chunks[-1].strip() == '' and lines: - del chunks[-1] + # First chunk on line is whitespace -- drop it, unless this + # is the very beginning of the text (ie. no lines started yet). + if chunks[-1].strip() == '' and lines: + del chunks[-1] - while chunks: + while chunks: # Ignore ANSI escape codes. - l = len(re.sub(ansi_re, '', chunks[-1])) + chunk_len = len(re.sub(ansi_re, '', chunks[-1])) - # Can at least squeeze this chunk onto the current line. - if cur_len + l <= width: - cur_line.append(chunks.pop()) - cur_len += l + # Can at least squeeze this chunk onto the current line. + if cur_len + chunk_len <= width: + cur_line.append(chunks.pop()) + cur_len += chunk_len - # Nope, this line is full. - else: - break + # Nope, this line is full. + else: + break - # The current line is full, and the next chunk is too big to - # fit on *any* line (not just this one). + # The current line is full, and the next chunk is too big to + # fit on *any* line (not just this one). # Ignore ANSI escape codes. - if chunks and len(re.sub(ansi_re, '', chunks[-1])) > width: - self._handle_long_word(chunks, cur_line, cur_len, width) + if chunks and len(re.sub(ansi_re, '', chunks[-1])) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) - # If the last chunk on this line is all whitespace, drop it. - if cur_line and cur_line[-1].strip() == '': - del cur_line[-1] + # If the last chunk on this line is all whitespace, drop it. + if cur_line and cur_line[-1].strip() == '': + del cur_line[-1] # Convert current line back to a string and store it in list # of all lines (return value). @@ -95,3 +95,5 @@ class TextWrapper(textwrap.TextWrapper): lines.append(indent + ''.join(cur_line)) return lines + +# vim: set ts=4 sw=4 tw=79: diff --git a/pym/gentoolkit/versionmatch.py b/pym/gentoolkit/versionmatch.py index 4cafa00..12f6732 100644 --- a/pym/gentoolkit/versionmatch.py +++ b/pym/gentoolkit/versionmatch.py @@ -1,6 +1,6 @@ #! /usr/bin/python # -# Copyright(c) 2009, Gentoo Foundation +# Copyright(c) 2009 Gentoo Foundation # Licensed under the GNU General Public License, v2 # # Copyright: 2005-2007 Brian Harring <ferringb@gmail.com> @@ -8,52 +8,7 @@ # # $Header$ -"""Gentoo package version comparison object from pkgcore.ebuild.atom_restricts. - -The VersionMatch class allows you to compare package versions according to -Gentoo's versioning rules. - -The simplest way to use it is to test simple equality. In this example I've -passed in the keyword arguments op (operator), ver (version), and -rev (revision) explicitly: ->>> from gentoolkit.versionmatch import VersionMatch ->>> VersionMatch(op='=',ver='1',rev='') == VersionMatch(op='=',ver='1',rev='') -True ->>> VersionMatch(op='=',ver='1',rev='') == VersionMatch(op='=',ver='2',rev='') -False - -A more flexible way to use it is to pass it a single gentoolkit.package.Package -instance which it uses to determine op, ver and rev: ->>> from gentoolkit.package import Package ->>> from gentoolkit.versionmatch import VersionMatch ->>> pkg1 = Package('sys-apps/portage-2.2') ->>> pkg2 = Package('sys-apps/portage-1.6') ->>> VersionMatch(pkg1) == VersionMatch(pkg2) -False - -Simple equality tests aren't actually very useful because they don't understand -different operators: ->>> VersionMatch(op='>', ver='1.5', rev='') == \ -... VersionMatch(op='=', ver='2', rev='') -False - -For more complicated comparisons, we can use the match method: ->>> from gentoolkit.package import Package ->>> from gentoolkit.versionmatch import VersionMatch ->>> pkg1 = Package('>=sys-apps/portage-2.2') ->>> pkg2 = Package('=sys-apps/portage-2.2_rc30') ->>> # An "rc" (release candidate) version compares less than a non "rc" version -... VersionMatch(pkg1).match(pkg2) -False ->>> pkg2 = Package('=sys-apps/portage-2.2-r6') ->>> # But an "r" (revision) version compares greater than a non "r" version -... VersionMatch(pkg1).match(pkg2) -True - -@see: gentoolkit.equery.changes for examples of use in gentoolkit. -@see: gentoolkit.package.Package.intersects for a higher level version - comparison method. -""" +"""Gentoo version comparison object from pkgcore.ebuild.atom_restricts.""" # ======= # Imports @@ -61,15 +16,15 @@ True from portage.versions import vercmp -import gentoolkit from gentoolkit import errors +from gentoolkit.cpv import CPV # ======= # Classes # ======= class VersionMatch(object): - """Gentoo package version comparison object from pkgcore.ebuild.atom_restricts. + """Gentoo version comparison object from pkgcore.ebuild.atom_restricts. Any overriding of this class *must* maintain numerical order of self.vals, see intersect for reason why. vals also must be a tuple. @@ -78,41 +33,23 @@ class VersionMatch(object): (0, 1):">=", (1,):">"} _convert_int2op = dict([(v, k) for k, v in _convert_op2int.iteritems()]) - del k, v - def __init__(self, *args, **kwargs): - """This class will either create a VersionMatch instance out of - a Package instance, or from explicitly passed in operator, version, - and revision. + def __init__(self, cpv, op='='): + """Initialize a VersionMatch instance. - Takes EITHER one arg: - <gentoolkit.package.Package> instance - - OR - - three keyword args: - op=str: version comparison to do, - valid operators are ('<', '<=', '=', '>=', '>', '~') - ver=str: version to base comparison on - rev=str: revision to base comparison on + @type cpv: L{gentoolkit.cpv.CPV} + @param cpv: cpv object + @type op: str + @keyword op: operator """ - if args and isinstance(args[0], (gentoolkit.package.Package, - self.__class__)): - self.operator = args[0].operator - self.version = args[0].version - self.revision = args[0].revision - self.fullversion = args[0].fullversion - elif set(('op', 'ver', 'rev')) == set(kwargs): - self.operator = kwargs['op'] - self.version = kwargs['ver'] - self.revision = kwargs['rev'] - if not self.revision: - self.fullversion = self.version - else: - self.fullversion = "%s-%s" % (self.version, self.revision) - else: - raise TypeError('__init__() takes either a Package instance ' - 'argument or op=, ver= and rev= all passed in as keyword args') + + if not isinstance(cpv, CPV): + raise ValueError("cpv must be a gentoolkit.cpv.CPV instance") + self.cpv = cpv + self.operator = op + self.version = cpv.version + self.revision = cpv.revision + self.fullversion = cpv.fullversion if self.operator != "~" and self.operator not in self._convert_int2op: raise errors.GentoolkitInvalidVersion( @@ -121,16 +58,15 @@ class VersionMatch(object): if self.operator == "~": if not self.version: raise errors.GentoolkitInvalidVersion( - "for ~ op, version must be specified") + "for ~ op, ver must be specified") self.droprevision = True self.values = (0,) else: self.droprevision = False self.values = self._convert_int2op[self.operator] - def match(self, pkginst): - """See whether a passed in VersionMatch or Package instance matches - self. + def match(self, other): + """See whether a passed in VersionMatch or CPV instance matches self. Example usage: >>> from gentoolkit.versionmatch import VersionMatch @@ -138,37 +74,30 @@ class VersionMatch(object): ... VersionMatch(op='=',ver='2.0',rev='')) True - @type pkginst: gentoolkit.versionmatch.VersionMatch OR - gentoolkit.package.Package - @param pkginst: version to compare with self's version + @type other: gentoolkit.versionmatch.VersionMatch OR + gentoolkit.cpv.CPV + @param other: version to compare with self's version @rtype: bool """ if self.droprevision: - ver1, ver2 = self.version, pkginst.version + ver1, ver2 = self.version, other.version else: - ver1, ver2 = self.fullversion, pkginst.fullversion - - #print "== VersionMatch.match DEBUG START ==" - #print "ver1:", ver1 - #print "ver2:", ver2 - #print "vercmp(ver2, ver1):", vercmp(ver2, ver1) - #print "self.values:", self.values - #print "vercmp(ver2, ver1) in values?", - #print "vercmp(ver2, ver1) in self.values" - #print "== VersionMatch.match DEBUG END ==" + ver1, ver2 = self.fullversion, other.fullversion return vercmp(ver2, ver1) in self.values def __str__(self): operator = self._convert_op2int[self.values] - if self.droprevision or not self.revision: + if self.droprevision or not self.cpv.revision: return "ver %s %s" % (operator, self.version) - return "ver-rev %s %s-%s" % (operator, self.version, self.revision) + return "ver-rev %s %s-%s" % ( + operator, self.version, self.revision + ) def __repr__(self): - return "<%s %s @%#8x>" % (self.__class__.__name__, str(self), id(self)) + return "<%s %r>" % (self.__class__.__name__, str(self)) @staticmethod def _convert_ops(inst): @@ -188,6 +117,15 @@ class VersionMatch(object): return False + def __ne__(self, other): + return not self == other + def __hash__(self): - return hash((self.droprevision, self.version, self.revision, - self.values)) + return hash(( + self.droprevision, + self.version, + self.revision, + self.values + )) + +# vim: set ts=4 sw=4 tw=79: |
