[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#733489: python-apt: Improve 'Dependency' and 'BaseDependency' to get target package versions that satisfy dependencies



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


Reply to: