commit bad8e5cf95af444faec5bcbdfc6d5583e3c00bd8
Author: James Antill <james@and.org>
Date:   Thu Nov 13 14:18:19 2008 -0500

     Add initial best providers implementation
      Adds functions/classes and metadata type to repos.
      Use best providers data in compare_providers.
       Also convert _bestPackageFromList() to use compare_providers, so
       "yum install smtpd" uses it too.

diff --git a/yum/__init__.py b/yum/__init__.py
index a207959..2867fa3 100644
--- a/yum/__init__.py
+++ b/yum/__init__.py
@@ -60,6 +60,7 @@ import plugins
 import logginglevels
 import yumRepo
 import callbacks
+import bestprov
 
 import warnings
 warnings.simplefilter("ignore", Errors.YumFutureDeprecationWarning)
@@ -109,6 +110,8 @@ class YumBase(depsolve.Depsolve):
 
         self.mediagrabber = None
 
+        self.__bestProviders = None # Atm. we don't want this public
+
     def __del__(self):
         self.close()
         self.closeRpmDB()
@@ -608,8 +611,19 @@ class YumBase(depsolve.Depsolve):
     comps = property(fget=lambda self: self._getGroups(),
                      fset=lambda self, value: self._setGroups(value),
                      fdel=lambda self: setattr(self, "_comps", None))
-    
-    
+
+    # Atm. we don't want this public, so add another _ to everything
+    def __getBestProviders(self):
+        if self.__bestProviders is None:
+            rootbprov = bestprov.BestProviders()
+            self.__bestProviders = {None: rootbprov}
+            for repo in self.repos.listEnabled():
+                bprov = bestprov.BestProviders(set([rootbprov]), repo)
+                self.__bestProviders[repo.id] = bprov
+        return self.__bestProviders
+
+    _bestProviders = property(fget=lambda self: self.__getBestProviders())
+
     def doSackFilelistPopulate(self):
         """convenience function to populate the repos with the filelist metadata
            it also is simply to only emit a log if anything actually gets populated"""
@@ -2194,8 +2208,9 @@ class YumBase(depsolve.Depsolve):
             pkglist = self.returnPackagesByDep(depstring)
         except Errors.YumBaseError:
             raise Errors.YumBaseError, _('No Package found for %s') % depstring
-        
-        result = self._bestPackageFromList(pkglist)
+
+        requirement = (depstring, None, None) # FIXME: depstring == name only?
+        result = self._bestPackageFromList(pkglist, requirement)
         if result is None:
             raise Errors.YumBaseError, _('No Package found for %s') % depstring
         
@@ -2226,7 +2241,7 @@ class YumBase(depsolve.Depsolve):
         
         return self.rpmdb.getProvides(depname, depflags, depver).keys()
 
-    def _bestPackageFromList(self, pkglist):
+    def _bestPackageFromList(self, pkglist, requirement=None):
         """take list of package objects and return the best package object.
            If the list is empty, return None. 
            
@@ -2248,30 +2263,12 @@ class YumBase(depsolve.Depsolve):
         # in nbestlist.
         nbestlist = packagesNewestByName(bestlist)
 
-        best = nbestlist[0]
-        nbestlist = set(nbestlist)
-        for pkg in bestlist:
-            if pkg == best:
-                continue
-            if pkg not in nbestlist:
-                continue
-
-            # This is basically _compare_providers() ... but without a reqpo
-            if len(pkg.name) < len(best.name): # shortest name silliness
-                best = pkg
-                continue
-            elif len(pkg.name) > len(best.name):
-                continue
-
-            # compare arch
-            arch = rpmUtils.arch.getBestArchFromList([pkg.arch, best.arch])
-            if arch == pkg.arch:
-                best = pkg
-                continue
-
+        bestpkgs = self._compare_providers(nbestlist, None, requirement)
+        best = bestpkgs[0][0]
         return best
 
-    def bestPackagesFromList(self, pkglist, arch=None, single_name=False):
+    def bestPackagesFromList(self, pkglist, arch=None, single_name=False,
+                             requirement=None):
         """Takes a list of packages, returns the best packages.
            This function is multilib aware so that it will not compare
            multilib to singlelib packages""" 
@@ -2292,9 +2289,9 @@ class YumBase(depsolve.Depsolve):
                 singleLib.append(po)
                 
         # we now have three lists.  find the best package(s) of each
-        multi = self._bestPackageFromList(multiLib)
-        single = self._bestPackageFromList(singleLib)
-        no = self._bestPackageFromList(noarch)
+        multi = self._bestPackageFromList(multiLib, requirement)
+        single = self._bestPackageFromList(singleLib, requirement)
+        no = self._bestPackageFromList(noarch, requirement)
 
         if single_name and multi and single and multi.name != single.name:
             # Sinlge _must_ match multi, if we want a single package name
@@ -2308,7 +2305,7 @@ class YumBase(depsolve.Depsolve):
         # if there's a noarch and it's newer than the multilib, we want
         # just the noarch.  otherwise, we want multi + single
         elif multi:
-            best = self._bestPackageFromList([multi,no])
+            best = self._bestPackageFromList([multi,no], requirement)
             if best.arch == "noarch":
                 returnlist.append(no)
             else:
@@ -2316,7 +2313,7 @@ class YumBase(depsolve.Depsolve):
                 if single: returnlist.append(single)
         # similar for the non-multilib case
         elif single:
-            best = self._bestPackageFromList([single,no])
+            best = self._bestPackageFromList([single,no], requirement)
             if best.arch == "noarch":
                 returnlist.append(no)
             else:
@@ -2429,8 +2426,10 @@ class YumBase(depsolve.Depsolve):
                         if mypkgs:
                             #  Dep. installs don't do wildcards, so we
                             # just want a single named package.
-                            mypkgs = self.bestPackagesFromList(mypkgs,
-                                                               single_name=True)
+                            requirement = (arg, None, None)
+                            mypkgs = self.bestPackagesFromList(mypkgs, None,
+                                                               True,
+                                                               requirement)
                             pkgs.extend(mypkgs)
                         
             else:
diff --git a/yum/bestprov.py b/yum/bestprov.py
new file mode 100755
index 0000000..c355d4e
--- /dev/null
+++ b/yum/bestprov.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python -t
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Copyright 2008 Red Hat
+#
+# James Antill <james@fedoraproject.org>
+
+"""
+Best providers (best_providers.xml) parsing.
+"""
+
+import os
+import sys
+import gzip
+
+import Errors
+from yum.yumRepo import YumRepository
+
+try:
+    from xml.etree import cElementTree
+except ImportError:
+    import cElementTree
+iterparse = cElementTree.iterparse
+
+# _best_provider_mdtype = 'best_provider'
+_best_provider_mdtype = 'best_provider'
+
+class BestProvidersErrorParseFail(Errors.YumBaseError):
+    """ An exception thrown for an unparsable best_provide file. """
+    pass
+
+class BestProviders(object):
+
+    def __init__(self, next=None, obj=None):
+        if next is None:
+            next = set()
+        self.next = next
+        self.providers = {}
+        if obj is not None:
+            self.parse(obj)
+
+    def _debug_dump(self, done=None):
+        """ Return a dump of the data, for quick "testing". """
+
+        if done is None:
+            done = set()
+        ret = "%s\n" % ('-' * 79)
+        for provide in sorted(self.providers):
+            if provide in done:
+                continue
+            done.add(provide)
+            ret += '  Provide: %s\n' % provide
+            pkgnames = self.providers[provide]
+            ret += '           %s\n' % ", ".join(sorted(pkgnames))
+        for bprov in self.next:
+            ret += bprov._debug_dump(done)
+        return ret
+
+    def add_provider(self, provide, pkgname):
+        """ Add a pkgname as a best provider of a requirement. """
+        self.providers.setdefault(provide, set()).add(pkgname)
+        for bprov in self.next:
+            bprov.add_provider(provide, pkgname)
+
+    def add_next(self, next):
+        """ Add another BestProviders lookup. """
+        self.next.add(next)
+
+    def get_providers(self, provide):
+        """ Given a provide, return all the pkgnames. """
+
+        if provide in self.providers:
+            return self.providers[provide]
+        ret = set()
+        for bprov in self.next:
+            ret.update(bprov.get_providers(provide))
+        return ret
+
+    def filter_providers(self, requirement, providers):
+        """ Given a list of providers, filter to thse that are best. """
+
+        best_providers = self.get_providers(requirement)
+        return set(providers).intersection(best_providers)
+
+    def parse(self, obj):
+        """ Given a repo, filename or file. Parse the best providers data. """
+        if type(obj) in (type(''), type(u'')):
+            infile = obj.endswith('.gz') and gzip.open(obj) or open(obj, 'rt')
+        elif isinstance(obj, YumRepository):
+            if _best_provider_mdtype not in obj.repoXML.repoData:
+                return None
+            infile = gzip.open(obj.retrieveMD(_best_provider_mdtype))
+        else:   # obj is a file object
+            infile = obj
+
+        added = 0
+        for event, elem in iterparse(infile):
+            if elem.tag == 'bestprovider':
+                provides = set()
+                pkgnames = set()
+                for child in elem:
+                    if child.tag == 'provide':
+                        if child.text in provides:
+                            msg = "Multiple provides of %s" % child.text
+                            raise BestProvidersErrorParseFail, msg
+                        provides.add(child.text)
+                    if child.tag == 'pkgname':
+                        if child.text in pkgnames:
+                            msg = "Multiple package names of %s" % child.text
+                            raise BestProvidersErrorParseFail, msg
+                        pkgnames.add(child.text)
+                if not provides:
+                    raise BestProvidersErrorParseFail, "No provides"
+                if not pkgnames:
+                    raise BestProvidersErrorParseFail, "No package names"
+                for provide in provides:
+                    for pkgname in pkgnames:
+                        added += 1
+                        self.add_provider(provide, pkgname)
+        return added
+
+def main():
+    """ BestProviders test function. """
+
+    def usage():
+        print >> sys.stderr, "Usage: %s <best_provider> ..." % sys.argv[0]
+        sys.exit(1)
+
+    if len(sys.argv) < 2:
+        usage()
+
+    last = BestProviders()
+    rest = []
+    for filename in sys.argv[1:]:
+        if not os.path.exists(filename):
+            print "No such file:", filename
+            continue
+
+        last = BestProviders(set([last]), filename)
+
+    for filename in reversed(sys.argv[1:]):
+        print "File:", filename
+        print last._debug_dump()
+        print ''
+        for last in last.next:
+            pass
+
+if __name__ == '__main__':
+    main()
diff --git a/yum/depsolve.py b/yum/depsolve.py
index 579e7a1..99e2bf8 100644
--- a/yum/depsolve.py
+++ b/yum/depsolve.py
@@ -67,7 +67,7 @@ class Depsolve(object):
     """
 
     def __init__(self):
-        packages.base = self
+        packages.base = self # FIXME: this is evil
         self._ts = None
         self._tsInfo = None
         self.dsCallback = None
@@ -547,7 +547,8 @@ class Depsolve(object):
         newest = provSack.returnNewestByNameArch()
         if len(newest) > 1: # there's no way this can be zero
                             
-            pkgresults = self._compare_providers(newest, requiringPo)
+            pkgresults = self._compare_providers(newest, requiringPo,
+                                                 requirement)
             # take the first one...
             best = pkgresults[0][0]                   
                 
@@ -983,7 +984,7 @@ class Depsolve(object):
         return installed
     _isPackageInstalled = isPackageInstalled
 
-    def _compare_providers(self, pkgs, reqpo):
+    def _compare_providers(self, pkgs, reqpo, requirement=None):
         """take the list of pkgs and score them based on the requesting package
            return a dictionary of po=score"""
         self.verbose_logger.log(logginglevels.DEBUG_4,
@@ -1027,12 +1028,25 @@ class Depsolve(object):
             if y_dist == x_dist:
                 return None
             return x
-            
+
         pkgresults = {}
 
         for pkg in pkgs:
             pkgresults[pkg] = 0
-            
+
+        # If we have any best_providers data, use that...
+        if requirement and hasattr(self, '_bestProviders'):
+            providers = set([pkg.name for pkg in pkgs])
+            reqrepoid = None
+            if reqpo is not None:
+                if reqpo.repoid in self._bestProviders:
+                    reqrepoid = reqpo.repoid
+            bprov = self._bestProviders[reqrepoid]
+            for pkgname in bprov.filter_providers(requirement[0], providers):
+                for pkg in pkgs:
+                    if pkg.name == pkgname:
+                        pkgresults[pkg] = 10000 # FIXME: $rand number
+
         # go through each pkg and compare to others
         # if it is same skip it
         # if the pkg is obsoleted by any other of the packages
