--- Begin Message ---
- To: Debian Bug Tracking System <submit@bugs.debian.org>
- Subject: python-apt: Improve 'Dependency' and 'BaseDependency' to get target package versions that satisfy dependencies
- From: Michael Schaller <michael@5challer.de>
- Date: Sun, 29 Dec 2013 12:06:18 +0100
- Message-id: <20131229110618.7902.57274.reportbug@black-box>
Package: python-apt
Version: 0.9.2
Severity: wishlist
Tags: patch
>From 0d295006a98769cd6151c78b3a078ad92d8047ee Mon Sep 17 00:00:00 2001
From: Michael Wisheu <michael@5challer.de>
Date: Sun, 29 Dec 2013 11:57:19 +0100
Subject: [PATCH]
* apt/cache.py:
- Fixed PEP8 linter and pyflakes issues
- Added 'InstalledFilter' to get a filtered cache that only contains the currently installed packages.
* apt/packages.py:
- Fixed PEP8 linter issues
- Removed special handling of 'collections' import as all supported distributions have Python 2.6 or newer by now.
- Replaced faulty 'BaseDependency.__dstr' with easier to read compat code.
- Added new properties to 'Dependency' and 'BaseDependency' to get the target package versions that could satisfy a dependency.
---
apt/cache.py | 52 ++++++-----
apt/package.py | 279 ++++++++++++++++++++++++++++++++++++++++++++-----------
debian/changelog | 16 +++-
3 files changed, 268 insertions(+), 79 deletions(-)
diff --git a/apt/cache.py b/apt/cache.py
index 43fb55d..897c2be 100644
--- a/apt/cache.py
+++ b/apt/cache.py
@@ -40,6 +40,7 @@ class FetchFailedException(IOError):
class LockFailedException(IOError):
"""Exception that is thrown when locking fails."""
+
class CacheClosedException(Exception):
"""Exception that is thrown when the cache is used after close()."""
@@ -53,7 +54,7 @@ class Cache(object):
list of available packages.
The cache can be used like a mapping from package names to Package
- objects (although only getting items is supported).
+ objects (although only getting items is supported).
Keyword arguments:
progress -- a OpProgress object
@@ -74,7 +75,7 @@ class Cache(object):
self._fullnameset = set()
self._changes_count = -1
self._sorted_set = None
-
+
self.connect("cache_post_open", self._inc_changes_count)
self.connect("cache_post_change", self._inc_changes_count)
if memonly:
@@ -86,17 +87,17 @@ class Cache(object):
apt_pkg.config.clear("APT")
apt_pkg.config.set("Dir", rootdir)
apt_pkg.init_config()
- if os.path.exists(rootdir+"/etc/apt/apt.conf"):
+ if os.path.exists(rootdir + "/etc/apt/apt.conf"):
apt_pkg.read_config_file(apt_pkg.config,
rootdir + "/etc/apt/apt.conf")
- if os.path.isdir(rootdir+"/etc/apt/apt.conf.d"):
+ if os.path.isdir(rootdir + "/etc/apt/apt.conf.d"):
apt_pkg.read_config_dir(apt_pkg.config,
rootdir + "/etc/apt/apt.conf.d")
apt_pkg.config.set("Dir::State::status",
rootdir + "/var/lib/dpkg/status")
# also set dpkg to the rootdir path so that its called for the
# --print-foreign-architectures call
- apt_pkg.config.set("Dir::bin::dpkg",
+ apt_pkg.config.set("Dir::bin::dpkg",
os.path.join(rootdir, "usr", "bin", "dpkg"))
# create required dirs/files when run with special rootdir
# automatically
@@ -105,7 +106,6 @@ class Cache(object):
# recognized (LP: #320665)
apt_pkg.init_system()
self.open(progress)
-
def _inc_changes_count(self):
"""Increase the number of changes"""
@@ -165,8 +165,8 @@ class Cache(object):
i = last = 0
size = len(self._cache.packages)
for pkg in self._cache.packages:
- if progress is not None and last+100 < i:
- progress.update(i/float(size)*100)
+ if progress is not None and last + 100 < i:
+ progress.update(i / float(size) * 100)
last = i
# drop stuff with no versions (cruft)
if pkg.has_versions:
@@ -289,16 +289,14 @@ class Cache(object):
# now check the result (this is the code from apt-get.cc)
failed = False
- transient = False
err_msg = ""
for item in fetcher.items:
if item.status == item.STAT_DONE:
continue
if item.STAT_IDLE:
- transient = True
continue
err_msg += "Failed to fetch %s %s\n" % (item.desc_uri,
- item.error_text)
+ item.error_text)
failed = True
# we raise a exception if the download failed or it was cancelt
@@ -349,7 +347,6 @@ class Cache(object):
if fetcher is None:
fetcher = apt_pkg.Acquire(progress)
-
return self._fetch_archives(fetcher,
apt_pkg.PackageManager(self._depcache))
@@ -362,12 +359,12 @@ class Cache(object):
else:
return bool(pkg.has_provides and not pkg.has_versions)
- def get_providing_packages(self, pkgname, candidate_only=True,
+ def get_providing_packages(self, pkgname, candidate_only=True,
include_nonvirtual=False):
"""Return a list of all packages providing a package.
-
+
Return a list of packages which provide the virtual package of the
- specified name.
+ specified name.
If 'candidate_only' is False, return all packages with at
least one version providing the virtual package. Otherwise,
@@ -378,7 +375,7 @@ class Cache(object):
packages providing pkgname, even if pkgname is not itself
a virtual pkg.
"""
-
+
providers = set()
get_candidate_ver = self._depcache.get_candidate_ver
try:
@@ -423,7 +420,8 @@ class Cache(object):
old_sources_list = apt_pkg.config.find("Dir::Etc::sourcelist")
old_sources_list_d = apt_pkg.config.find("Dir::Etc::sourceparts")
old_cleanup = apt_pkg.config.find("APT::List-Cleanup")
- apt_pkg.config.set("Dir::Etc::sourcelist", os.path.abspath(sources_list))
+ apt_pkg.config.set("Dir::Etc::sourcelist",
+ os.path.abspath(sources_list))
apt_pkg.config.set("Dir::Etc::sourceparts", "xxx")
apt_pkg.config.set("APT::List-Cleanup", "0")
slist = apt_pkg.SourceList()
@@ -559,9 +557,9 @@ class Cache(object):
@property
def dpkg_journal_dirty(self):
"""Return True if the dpkg was interrupted
-
+
All dpkg operations will fail until this is fixed, the action to
- fix the system if dpkg got interrupted is to run
+ fix the system if dpkg got interrupted is to run
'dpkg --configure -a' as root.
"""
dpkg_status_dir = os.path.dirname(
@@ -644,6 +642,19 @@ class Filter(object):
return True
+class InstalledFilter(Filter):
+ """ Filter that returns all installed packages
+
+ .. versionadded:: 0.9.2
+ """
+
+ def apply(self, pkg):
+ if pkg.is_installed:
+ return True
+ else:
+ return False
+
+
class MarkedChangesFilter(Filter):
""" Filter that returns all marked changes """
@@ -711,7 +722,6 @@ class FilteredCache(object):
#print "filterCachePostChange()"
self._reapply_filter()
-
# def connect(self, name, callback):
# self.cache.connect(name, callback)
@@ -750,8 +760,6 @@ def _test():
for pkg in changes:
assert pkg.name
-
-
# see if fetching works
for dirname in ["/tmp/pytest", "/tmp/pytest/partial"]:
if not os.path.exists(dirname):
diff --git a/apt/package.py b/apt/package.py
index 04d4ddd..7414fb2 100644
--- a/apt/package.py
+++ b/apt/package.py
@@ -19,6 +19,7 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
"""Functionality related to packages."""
+import collections
import httplib
import os
import sys
@@ -26,17 +27,6 @@ import re
import socket
import subprocess
import urllib2
-import warnings
-try:
- from collections import Mapping, Sequence
-except ImportError:
- # (for Python < 2.6) pylint: disable-msg=C0103
- Sequence = Mapping = object
-
-try:
- from collections import Sequence
-except ImportError:
- Sequence = object
import apt_pkg
import apt.progress.text
@@ -45,6 +35,14 @@ from apt_pkg import gettext as _
__all__ = ('BaseDependency', 'Dependency', 'Origin', 'Package', 'Record',
'Version', 'VersionList')
+# Replaces for compatibility '<' with '<=' and '>' with '>='. Furthermore it
+# can be used for relation validation. Source:
+# http://www.debian.org/doc/debian-policy/ch-relationships.html
+RELATION_COMPAT = {
+ '>': '>=', '<': '<=', # Translation of deprecated relations
+ '>>': '>>', '<<': '<<', '>=': '>=', '<=': '<=', '=': '=', '': '', # 1:1
+}
+
def _file_is_same(path, size, md5):
"""Return ``True`` if the file is the same."""
@@ -58,35 +56,135 @@ class FetchError(Exception):
class BaseDependency(object):
- """A single dependency.
+ """A single dependency."""
- Attributes defined here:
- name - The name of the dependency
- relation - The relation (>,>=,==,<,<=,)
- version - The version depended on
- rawtype - The type of the dependendy (e.g. 'Recommends')
- pre_depend - Boolean value whether this is a pre-dependency.
- """
+ def __init__(self, cache, dep):
+ self._cache = cache # apt.cache.Cache
+ self._dep = dep # apt_pkg.Dependency
+
+ @property
+ def installed_targets(self):
+ """`Version` object list of installed packages that satisfy the dep.
+
+ Returns a list with all `Version` objects of installed packages that
+ satisfy the dependency. The returned list will be empty if no installed
+ package satisfies the dependency.
+
+ .. versionadded:: 0.9.2
+ """
+ inst_targets = []
+ for target in self.targets: # apt.package.Version
+ if target.installed:
+ inst_targets.append(target)
+ return inst_targets
+
+ @property
+ def name(self):
+ """Return the name of the dependencies target package.
+
+ If the package is not part of the system's preferred architecture,
+ return the same as :attr:`fullname`, otherwise return the same
+ as :attr:`shortname`
+
+ .. versionchanged:: 0.9.2
+ As part of multi-arch, this field now may include architecture
+ information.
+ """
+ return self._dep.target_pkg.get_fullname(True)
+
+ @property
+ def fullname(self):
+ """Return the full name of the dependencies target package.
- class __dstr(str):
- """Helper to make > match >> and < match <<"""
+ .. versionadded:: 0.9.2
+ """
+ return self._dep.target_pkg.get_fullname(False)
- def __eq__(self, other):
- return str.__eq__(self, other) or str.__eq__(2 * self, other)
+ @property
+ def shortname(self):
+ """Return the short name of the dependencies target package.
- def __ne__(self, other):
- return str.__eq__(self, other) and str.__ne__(2 * self, other)
+ .. versionadded:: 0.9.2
+ """
+ return self._dep.target_pkg.name
- def __init__(self, name, rel, ver, pre, rawtype=None):
- self.name = name
- self.relation = len(rel) == 1 and self.__dstr(rel) or rel
- self.version = ver
- self.pre_depend = pre
- self.rawtype = rawtype
+ @property
+ def pre_depend(self):
+ """Return boolean whether this is a pre-dependency."""
+ return (self._dep.dep_type_untranslated == "PreDepends")
+
+ @property
+ def rawstr(self):
+ """Return the dependency as string.
+
+ The string is similar to how the dependency would be listed in the
+ control file of a package.
+
+ Examples:
+ package
+ package (> 1.0)
+ """
+ name = self.shortname
+ version = self.version
+ relation = self.relation
+ if not relation or not version:
+ return name
+ else:
+ return "{0} ({1} {2})".format(name, relation, version)
+
+ @property
+ def rawtype(self):
+ """Return the dependeny type as string.
+
+ Possible return values are:
+ 'Breaks', 'Conflicts', 'Depends', 'Enhances',
+ 'PreDepends', 'Recommends', 'Replaces', 'Suggests'
+ """
+ return self._dep.dep_type_untranslated
+
+ @property
+ def relation(self):
+ """The relation of the dependency as string.
+
+ In case of an unversioned dependency the empty string will be returned.
+
+ Possible return values are:
+ '', '>>', '>=', '=', '<<', '<='
+ """
+ return RELATION_COMPAT[self._dep.comp_type]
+
+ @property
+ def targets(self):
+ """`Version` object list of packages that could satisfy the dep.
+
+ Returns a list with all `Version` objects of packages that could satisfy
+ the dependency. The returned list will be empty if no available package
+ could satisfy the dependency.
+
+ .. versionadded:: 0.9.2
+ """
+ target_vers = []
+ for _target_ver in self._dep.all_targets(): # apt_pkg.Version
+ _target_pkg_name = _target_ver.parent_pkg.get_fullname(False)
+ _target_ver_str = _target_ver.ver_str
+ target_pkg = self._cache[_target_pkg_name] # apt.package.Package
+ pkg_vers = target_pkg.versions # apt.package.VersionList
+ target_ver = pkg_vers[_target_ver_str] # apt.package.Version
+ if target_ver not in target_vers:
+ target_vers.append(target_ver)
+ return target_vers
+
+ @property
+ def version(self):
+ """The version of the dependency as string.
+
+ In case of an unversioned dependency the empty string will be returned.
+ """
+ return self._dep.target_ver
def __repr__(self):
- return ('<BaseDependency: name:%r relation:%r version:%r preDepend:%r>'
- % (self.name, self.relation, self.version, self.pre_depend))
+ return ('<BaseDependency: name:%r relation:%r version:%r rawtype:%r>'
+ % (self.name, self.relation, self.version, self.rawtype))
class Dependency(list):
@@ -97,13 +195,73 @@ class Dependency(list):
"""
def __init__(self, alternatives):
+ assert alternatives, "Or-group without dependencies"
super(Dependency, self).__init__()
self.extend(alternatives)
@property
+ def installed_targets(self):
+ """`Version` object list of installed packages that satisfy the or-dep.
+
+ Returns a list with all `Version` objects of installed packages that
+ satisfy the Or-group of dependencies. The returned list will be empty if
+ no installed package satisfies the Or-group of dependencies.
+
+ .. versionadded:: 0.9.2
+ """
+ inst_targets = []
+ for dep in self: # apt.package.BaseDependency
+ for inst_target in dep.installed_targets: # apt.package.Version
+ if inst_target not in inst_targets:
+ inst_targets.append(inst_target)
+ return inst_targets
+
+ @property
def or_dependencies(self):
return self
+ @property
+ def rawstr(self):
+ """Return the Or-group of dependencies as string.
+
+ The string is similar to how the Or-group of dependencies would be
+ listed in the control file of a package.
+
+ Examples:
+ package2 | package1
+ libpackage (=> 1.2) | package (< 1.2)
+ """
+ return " | ".join(map(lambda base_dep: base_dep.rawstr, self))
+
+ @property
+ def rawtype(self):
+ """Return the Or-group dependencies type as string.
+
+ Possible return values are:
+ 'Breaks', 'Conflicts', 'Depends', 'Enhances',
+ 'PreDepends', 'Recommends', 'Replaces', 'Suggests'
+ """
+ # There is always at least one dependency (BaseDependency) in an
+ # Or-group and all dependencies in an Or-group have the same rawtype
+ return self[0].rawtype
+
+ @property
+ def targets(self):
+ """`Version` object list of packages that could satisfy the or-dep.
+
+ Returns a list with all `Version` objects of packages that could satisfy
+ the Or-group of dependencies. The returned list will be empty if no
+ available package could satisfy the Or-group of dependencies.
+
+ .. versionadded:: 0.9.2
+ """
+ targets = []
+ for dep in self: # apt.package.BaseDependency
+ for target in dep.targets: # apt.package.Version
+ if target not in targets:
+ targets.append(target)
+ return targets
+
class Origin(object):
"""The origin of a version.
@@ -140,7 +298,7 @@ class Origin(object):
self.site, self.trusted)
-class Record(Mapping):
+class Record(collections.Mapping):
"""Record in a Packages file
Represent a record as stored in a Packages file. You can use this like
@@ -208,8 +366,8 @@ class Version(object):
"""
def __init__(self, package, cand):
- self.package = package
- self._cand = cand
+ self.package = package # apt.package.Package()
+ self._cand = cand # apt_pkg.Version()
def _cmp(self, other):
try:
@@ -274,6 +432,14 @@ class Version(object):
return self.package._pcache._records
@property
+ def installed(self):
+ """Wether this version of the package is installed or not.
+
+ .. versionadded:: 0.9.2
+ """
+ return (self.package.installed == self)
+
+ @property
def installed_size(self):
"""Return the size of the package when installed."""
return self._cand.installed_size
@@ -407,28 +573,30 @@ class Version(object):
return Record(self._records.record)
def get_dependencies(self, *types):
- """Return a list of Dependency objects for the given types."""
+ """Return a list of Dependency objects for the given types.
+ The types can be:
+ 'Breaks', 'Conflicts', 'Depends', 'Enhances', 'PreDepends',
+ 'Recommends', 'Replaces', 'Suggests'
+ """
depends_list = []
depends = self._cand.depends_list
for type_ in types:
- try:
- for dep_ver_list in depends[type_]:
- base_deps = []
- for dep_or in dep_ver_list:
- base_deps.append(BaseDependency(dep_or.target_pkg.name,
- dep_or.comp_type, dep_or.target_ver,
- (type_ == "PreDepends"),
- rawtype=type_))
- depends_list.append(Dependency(base_deps))
- except KeyError:
- pass
+ dep_lists = depends.get(type_) # [[apt_pkg.Dependency, ...], ...]
+ if dep_lists is None:
+ continue # No dependencies of this type
+ for dep_list in dep_lists: # [apt_pkg.Dependency, ...]
+ base_deps = []
+ for dep in dep_list: # apt_pkg.Dependency
+ base_deps.append(
+ BaseDependency(self.package._pcache, dep))
+ depends_list.append(Dependency(base_deps))
return depends_list
@property
def provides(self):
""" Return a list of names that this version provides."""
return [p[0] for p in self._cand.provides_list]
-
+
@property
def enhances(self):
"""Return the list of enhances for the package version."""
@@ -612,7 +780,7 @@ class Version(object):
return os.path.abspath(dsc)
-class VersionList(Sequence):
+class VersionList(collections.Sequence):
"""Provide a mapping & sequence interface to all versions of a package.
This class can be used like a dictionary, where version strings are the
@@ -633,8 +801,8 @@ class VersionList(Sequence):
"""
def __init__(self, package, slice_=None):
- self._package = package # apt.package.Package()
- self._versions = package._pkg.version_list # [apt_pkg.Version(), ...]
+ self._package = package # apt.package.Package()
+ self._versions = package._pkg.version_list # [apt_pkg.Version(), ...]
if slice_:
self._versions = self._versions[slice_]
@@ -659,7 +827,7 @@ class VersionList(Sequence):
return (Version(self._package, ver) for ver in self._versions)
def __contains__(self, item):
- if isinstance(item, Version): # Sequence interface
+ if isinstance(item, Version): # Sequence interface
item = item.version
# Dictionary interface.
for ver in self._versions:
@@ -917,10 +1085,10 @@ class Package(object):
src_section = "main"
# use the section of the candidate as a starting point
section = self.candidate.section
-
+
# get the source version
src_ver = self.candidate.source_version
-
+
try:
# try to get the source version of the pkg, this differs
# for some (e.g. libnspr4 on ubuntu)
@@ -1173,7 +1341,6 @@ def _test():
print "homepage: %s" % pkg.candidate.homepage
print "rec: ", pkg.candidate.record
-
print cache["2vcard"].get_changelog()
for i in True, False:
print "Running install on random upgradable pkgs with AutoFix: %s " % i
diff --git a/debian/changelog b/debian/changelog
index 52a1000..b3783cc 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,10 +1,24 @@
python-apt (0.9.2) UNRELEASED; urgency=low
+ [ Michael Vogt ]
* apt/cache.py:
- when using apt.Cache(rootdir=/some/dir) only read the APT
configuration from this rootdir instead of /etc (closes: #728274)
- -- Michael Vogt <michael.vogt@ubuntu.com> Sat, 23 Nov 2013 08:49:51 +0100
+ [ Michael Schaller ]
+ * apt/cache.py:
+ - Fixed PEP8 linter and pyflakes issues
+ - Added 'InstalledFilter' to get a filtered cache that only contains the
+ currently installed packages.
+ * apt/packages.py:
+ - Fixed PEP8 linter issues
+ - Removed special handling of 'collections' import as all supported
+ distributions have Python 2.6 or newer by now.
+ - Replaced faulty 'BaseDependency.__dstr' with easier to read compat code.
+ - Added new properties to 'Dependency' and 'BaseDependency' to get the
+ target package versions that could satisfy a dependency.
+
+ -- Michael Schaller <michael@5challer.de> Sat, 28 Dec 2013 21:25:43 +0100
python-apt (0.9.1) unstable; urgency=low
--
1.8.3.2
--- End Message ---