[Python-checkins] distutils2: merge upstream

tarek.ziade python-checkins at python.org
Sun Jul 4 11:48:39 CEST 2010


tarek.ziade pushed 50b239007c3b to distutils2:

http://hg.python.org/distutils2/rev/50b239007c3b
changeset:   289:50b239007c3b
parent:      288:a8087e4ed26c
parent:      188:b7fd258b33e7
user:        Alexis Metaireau <ametaireau at gmail.com>
date:        Tue Jun 01 09:28:45 2010 +0200
summary:     merge upstream
files:       src/distutils2/dist.py

diff --git a/src/CONTRIBUTORS.txt b/src/CONTRIBUTORS.txt
new file mode 100644
--- /dev/null
+++ b/src/CONTRIBUTORS.txt
@@ -0,0 +1,25 @@
+============
+Contributors
+============
+
+Distutils2 is a project that was started and that is maintained by
+Tarek Ziadé, and many people are contributing to the project.
+
+If you did, please add your name below in alphabetical order !
+
+Thanks to:
+
+- Pior Bastida
+- Titus Brown
+- Nicolas Cadou
+- Josip Djolonga
+- Yannick Gringas
+- Carl Meyer
+- Michael Mulich
+- George Peris
+- Sean Reifschneider
+- Erik Rose
+- Brian Rosner
+- Alexandre Vassalotti
+- Martin von Löwis
+
diff --git a/src/DEVNOTES.txt b/src/DEVNOTES.txt
new file mode 100644
--- /dev/null
+++ b/src/DEVNOTES.txt
@@ -0,0 +1,10 @@
+Notes for developers
+====================
+
+- Distutils2 runs from 2.4 to 3.2 (3.x not implemented yet), so
+  make sure you don't use a syntax that doesn't work under
+  a specific Python version.
+
+- Always run tests.sh before you push a change. This implies
+  that you have all Python versions installed.
+
diff --git a/src/distutils2/_backport/pkgutil.py b/src/distutils2/_backport/pkgutil.py
--- a/src/distutils2/_backport/pkgutil.py
+++ b/src/distutils2/_backport/pkgutil.py
@@ -11,16 +11,18 @@
 from types import ModuleType
 from distutils2.errors import DistutilsError
 from distutils2.metadata import DistributionMetadata
-from distutils2.version import suggest_normalized_version
+from distutils2.version import suggest_normalized_version, VersionPredicate
 
 __all__ = [
     'get_importer', 'iter_importers', 'get_loader', 'find_loader',
     'walk_packages', 'iter_modules',
     'ImpImporter', 'ImpLoader', 'read_code', 'extend_path',
-    'Distribution', 'distinfo_dirname', 'get_distributions',
-    'get_distribution', 'get_file_users', 
+    'Distribution', 'EggInfoDistribution', 'distinfo_dirname',
+    'get_distributions', 'get_distribution', 'get_file_users',
+    'provides_distribution', 'obsoletes_distribution',
 ]
 
+
 def read_code(stream):
     # This helper is needed in order for the PEP 302 emulation to
     # correctly handle compiled files
@@ -37,6 +39,7 @@
 def simplegeneric(func):
     """Make a trivial single-dispatch generic function"""
     registry = {}
+
     def wrapper(*args, **kw):
         ob = args[0]
         try:
@@ -47,6 +50,7 @@
             mro = cls.__mro__
         except AttributeError:
             try:
+
                 class cls(cls, object):
                     pass
                 mro = cls.__mro__[1:]
@@ -128,7 +132,7 @@
                 # don't traverse path items we've seen before
                 path = [p for p in path if not seen(p)]
 
-                for item in walk_packages(path, name+'.', onerror):
+                for item in walk_packages(path, name + '.', onerror):
                     yield item
 
 
@@ -206,7 +210,7 @@
 
         for fn in filenames:
             modname = inspect.getmodulename(fn)
-            if modname=='__init__' or modname in yielded:
+            if modname == '__init__' or modname in yielded:
                 continue
 
             path = os.path.join(self.path, fn)
@@ -216,7 +220,7 @@
                 modname = fn
                 for fn in os.listdir(path):
                     subname = inspect.getmodulename(fn)
-                    if subname=='__init__':
+                    if subname == '__init__':
                         ispkg = True
                         break
                 else:
@@ -255,7 +259,7 @@
     def _reopen(self):
         if self.file and self.file.closed:
             mod_type = self.etc[2]
-            if mod_type==imp.PY_SOURCE:
+            if mod_type == imp.PY_SOURCE:
                 self.file = open(self.filename, 'rU')
             elif mod_type in (imp.PY_COMPILED, imp.C_EXTENSION):
                 self.file = open(self.filename, 'rb')
@@ -270,22 +274,22 @@
 
     def is_package(self, fullname):
         fullname = self._fix_name(fullname)
-        return self.etc[2]==imp.PKG_DIRECTORY
+        return self.etc[2] == imp.PKG_DIRECTORY
 
     def get_code(self, fullname=None):
         fullname = self._fix_name(fullname)
         if self.code is None:
             mod_type = self.etc[2]
-            if mod_type==imp.PY_SOURCE:
+            if mod_type == imp.PY_SOURCE:
                 source = self.get_source(fullname)
                 self.code = compile(source, self.filename, 'exec')
-            elif mod_type==imp.PY_COMPILED:
+            elif mod_type == imp.PY_COMPILED:
                 self._reopen()
                 try:
                     self.code = read_code(self.file)
                 finally:
                     self.file.close()
-            elif mod_type==imp.PKG_DIRECTORY:
+            elif mod_type == imp.PKG_DIRECTORY:
                 self.code = self._get_delegate().get_code()
         return self.code
 
@@ -293,29 +297,28 @@
         fullname = self._fix_name(fullname)
         if self.source is None:
             mod_type = self.etc[2]
-            if mod_type==imp.PY_SOURCE:
+            if mod_type == imp.PY_SOURCE:
                 self._reopen()
                 try:
                     self.source = self.file.read()
                 finally:
                     self.file.close()
-            elif mod_type==imp.PY_COMPILED:
+            elif mod_type == imp.PY_COMPILED:
                 if os.path.exists(self.filename[:-1]):
                     f = open(self.filename[:-1], 'rU')
                     self.source = f.read()
                     f.close()
-            elif mod_type==imp.PKG_DIRECTORY:
+            elif mod_type == imp.PKG_DIRECTORY:
                 self.source = self._get_delegate().get_source()
         return self.source
 
-
     def _get_delegate(self):
         return ImpImporter(self.filename).find_module('__init__')
 
     def get_filename(self, fullname=None):
         fullname = self._fix_name(fullname)
         mod_type = self.etc[2]
-        if self.etc[2]==imp.PKG_DIRECTORY:
+        if self.etc[2] == imp.PKG_DIRECTORY:
             return self._get_delegate().get_filename()
         elif self.etc[2] in (imp.PY_SOURCE, imp.PY_COMPILED, imp.C_EXTENSION):
             return self.filename
@@ -339,16 +342,16 @@
 
             fn = fn[plen:].split(os.sep)
 
-            if len(fn)==2 and fn[1].startswith('__init__.py'):
+            if len(fn) == 2 and fn[1].startswith('__init__.py'):
                 if fn[0] not in yielded:
                     yielded[fn[0]] = 1
                     yield fn[0], True
 
-            if len(fn)!=1:
+            if len(fn) != 1:
                 continue
 
             modname = inspect.getmodulename(fn[0])
-            if modname=='__init__':
+            if modname == '__init__':
                 continue
 
             if modname and '.' not in modname and modname not in yielded:
@@ -436,6 +439,7 @@
     if '.' not in fullname:
         yield ImpImporter()
 
+
 def get_loader(module_or_name):
     """Get a PEP 302 "loader" object for module_or_name
 
@@ -461,6 +465,7 @@
         fullname = module_or_name
     return find_loader(fullname)
 
+
 def find_loader(fullname):
     """Find a PEP 302 "loader" object for fullname
 
@@ -551,6 +556,7 @@
 
     return path
 
+
 def get_data(package, resource):
     """Get a resource from a package.
 
@@ -594,6 +600,7 @@
 
 DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED',)
 
+
 class Distribution(object):
     """Created with the *path* of the ``.dist-info`` directory provided to the
     constructor. It reads the metadata contained in METADATA when it is
@@ -604,7 +611,7 @@
     name = ''
     """The name of the distribution."""
     metadata = None
-    """A :class:`distutils2.metadata.DistributionMetadata` instance loaded with 
+    """A :class:`distutils2.metadata.DistributionMetadata` instance loaded with
     the distribution's METADATA file."""
     requested = False
     """A boolean that indicates whether the REQUESTED metadata file is present
@@ -620,7 +627,7 @@
         RECORD = os.path.join(self.path, 'RECORD')
         record_reader = csv_reader(open(RECORD, 'rb'), delimiter=',')
         for row in record_reader:
-            path, md5, size = row[:] + [ None for i in xrange(len(row), 3) ]
+            path, md5, size = row[:] + [None for i in xrange(len(row), 3)]
             if local:
                 path = path.replace('/', os.sep)
                 path = os.path.join(sys.prefix, path)
@@ -635,7 +642,6 @@
 
         A local absolute path is an absolute path in which occurrences of
         ``'/'`` have been replaced by the system separator given by ``os.sep``.
-
         :parameter local: flag to say if the path should be returned a local
                           absolute path
         :type local: boolean
@@ -643,10 +649,9 @@
         """
         return self._get_records(local)
 
-
     def uses(self, path):
         """
-        Returns ``True`` if path is listed in RECORD. *path* can be a local 
+        Returns ``True`` if path is listed in RECORD. *path* can be a local
         absolute path or a relative ``'/'``-separated path.
 
         :rtype: boolean
@@ -663,8 +668,8 @@
         ``file`` instance for the file pointed by *path*.
 
         :parameter path: a ``'/'``-separated path relative to the ``.dist-info``
-                         directory or an absolute path; If *path* is an absolute 
-                         path and doesn't start with the ``.dist-info``
+                         directory or an absolute path; If *path* is an
+                         absolute path and doesn't start with the ``.dist-info``
                          directory path, a :class:`DistutilsError` is raised
         :type path: string
         :parameter binary: If *binary* is ``True``, opens the file in read-only
@@ -696,8 +701,8 @@
 
     def get_distinfo_files(self, local=False):
         """
-        Iterates over the RECORD entries and returns paths for each line if the 
-        path is pointing to a file located in the ``.dist-info`` directory or 
+        Iterates over the RECORD entries and returns paths for each line if the
+        path is pointing to a file located in the ``.dist-info`` directory or
         one of its subdirectories.
 
         :parameter local: If *local* is ``True``, each returned path is
@@ -710,16 +715,42 @@
             yield path
 
 
+class EggInfoDistribution(object):
+    """Created with the *path* of the ``.egg-info`` directory or file provided
+    to the constructor. It reads the metadata contained in the file itself, or
+    if the given path happens to be a directory, the metadata is read from the
+    file PKG-INFO under that directory."""
+
+    name = ''
+    """The name of the distribution."""
+    metadata = None
+    """A :class:`distutils2.metadata.DistributionMetadata` instance loaded with
+    the distribution's METADATA file."""
+
+    def __init__(self, path):
+        if os.path.isdir(path):
+            path = os.path.join(path, 'PKG-INFO')
+        self.metadata = DistributionMetadata(path=path)
+        self.name = self.metadata['name']
+
+    def get_installed_files(self, local=False):
+        return []
+
+    def uses(self, path):
+        return False
+
+
 def _normalize_dist_name(name):
     """Returns a normalized name from the given *name*.
     :rtype: string"""
     return name.replace('-', '_')
 
+
 def distinfo_dirname(name, version):
     """
     The *name* and *version* parameters are converted into their
     filename-escaped form, i.e. any ``'-'`` characters are replaced with ``'_'``
-    other than the one in ``'dist-info'`` and the one separating the name from 
+    other than the one in ``'dist-info'`` and the one separating the name from
     the version number.
 
     :parameter name: is converted to a standard distribution name by replacing
@@ -743,13 +774,16 @@
         normalized_version = version
     return '-'.join([name, normalized_version]) + file_extension
 
-def get_distributions():
+
+def get_distributions(use_egg_info=False):
     """
     Provides an iterator that looks for ``.dist-info`` directories in
     ``sys.path`` and returns :class:`Distribution` instances for each one of
-    them.
+    them. If the parameters *use_egg_info* is ``True``, then the ``.egg-info``
+    files and directores are iterated as well.
 
-    :rtype: iterator of :class:`Distribution` instances"""
+    :rtype: iterator of :class:`Distribution` and :class:`EggInfoDistribution`
+            instances"""
     for path in sys.path:
         realpath = os.path.realpath(path)
         if not os.path.isdir(realpath):
@@ -758,25 +792,117 @@
             if dir.endswith('.dist-info'):
                 dist = Distribution(os.path.join(realpath, dir))
                 yield dist
+            elif use_egg_info and dir.endswith('.egg-info'):
+                dist = EggInfoDistribution(os.path.join(realpath, dir))
+                yield dist
 
-def get_distribution(name):
+
+def get_distribution(name, use_egg_info=False):
     """
     Scans all elements in ``sys.path`` and looks for all directories ending with
-    ``.dist-info``. Returns a :class:`Distribution` corresponding to the 
+    ``.dist-info``. Returns a :class:`Distribution` corresponding to the
     ``.dist-info`` directory that contains the METADATA that matches *name* for
-    the *name* metadata.
+    the *name* metadata field.
+    If no distribution exists with the given *name* and the parameter
+    *use_egg_info* is set to ``True``, then all files and directories ending
+    with ``.egg-info`` are scanned. A :class:`EggInfoDistribution` instance is
+    returned if one is found that has metadata that matches *name* for the
+    *name* metadata field.
 
     This function only returns the first result founded, as no more than one
     value is expected. If the directory is not found, ``None`` is returned.
 
-    :rtype: :class:`Distribution` or None"""
+    :rtype: :class:`Distribution` or :class:`EggInfoDistribution: or None"""
     found = None
     for dist in get_distributions():
         if dist.name == name:
             found = dist
             break
+    if use_egg_info:
+        for dist in get_distributions(True):
+            if dist.name == name:
+                found = dist
+                break
     return found
 
+
+def obsoletes_distribution(name, version=None, use_egg_info=False):
+    """
+    Iterates over all distributions to find which distributions obsolete *name*.
+    If a *version* is provided, it will be used to filter the results.
+    If the argument *use_egg_info* is set to ``True``, then ``.egg-info``
+    distributions will be considered as well.
+
+    :type name: string
+    :type version: string
+    :parameter name:
+    """
+    for dist in get_distributions(use_egg_info):
+        obsoleted = dist.metadata['Obsoletes-Dist'] + dist.metadata['Obsoletes']
+        for obs in obsoleted:
+            o_components = obs.split(' ', 1)
+            if len(o_components) == 1 or version is None:
+                if name == o_components[0]:
+                    yield dist
+                    break
+            else:
+                try:
+                    predicate = VersionPredicate(obs)
+                except ValueError:
+                    raise DistutilsError(('Distribution %s has ill formed' +
+                                          ' obsoletes field') % (dist.name,))
+                if name == o_components[0] and predicate.match(version):
+                    yield dist
+                    break
+
+
+def provides_distribution(name, version=None, use_egg_info=False):
+    """
+    Iterates over all distributions to find which distributions provide *name*.
+    If a *version* is provided, it will be used to filter the results. Scans
+    all elements in ``sys.path``  and looks for all directories ending with
+    ``.dist-info``. Returns a :class:`Distribution`  corresponding to the
+    ``.dist-info`` directory that contains a ``METADATA`` that matches *name*
+    for the name metadata. If the argument *use_egg_info* is set to ``True``,
+    then all files and directories ending with ``.egg-info`` are considered
+    as well and returns an :class:`EggInfoDistribution` instance.
+
+    This function only returns the first result founded, since no more than
+    one values are expected. If the directory is not found, returns ``None``.
+
+    :parameter version: a version specifier that indicates the version
+                        required, conforming to the format in ``PEP-345``
+
+    :type name: string
+    :type version: string
+    """
+    predicate = None
+    if not version is None:
+        try:
+            predicate = VersionPredicate(name + ' (' + version + ')')
+        except ValueError:
+            raise DistutilsError('Invalid name or version')
+
+    for dist in get_distributions(use_egg_info):
+        provided = dist.metadata['Provides-Dist'] + dist.metadata['Provides']
+
+        for p in provided:
+            p_components = p.split(' ', 1)
+            if len(p_components) == 1 or predicate is None:
+                if name == p_components[0]:
+                    yield dist
+                    break
+            else:
+                p_name, p_ver = p_components
+                if len(p_ver) < 2 or p_ver[0] != '(' or p_ver[-1] != ')':
+                    raise DistutilsError(('Distribution %s has invalid ' +
+                                          'provides field') % (dist.name,))
+                p_ver = p_ver[1:-1] # trim off the parenthesis
+                if p_name == name and predicate.match(p_ver):
+                    yield dist
+                    break
+
+
 def get_file_users(path):
     """
     Iterates over all distributions to find out which distributions uses
diff --git a/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO b/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO
new file mode 100644
--- /dev/null
+++ b/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO
@@ -0,0 +1,5 @@
+Metadata-Version: 1.2
+Name: bacon
+Version: 0.1
+Provides-Dist: truffles (2.0)
+Obsoletes-Dist: truffles (>=0.9,<=1.5)
diff --git a/src/distutils2/_backport/tests/fake_dists/cheese-2.0.2.egg-info b/src/distutils2/_backport/tests/fake_dists/cheese-2.0.2.egg-info
new file mode 100644
--- /dev/null
+++ b/src/distutils2/_backport/tests/fake_dists/cheese-2.0.2.egg-info
@@ -0,0 +1,5 @@
+Metadata-Version: 1.2
+Name: cheese
+Version: 2.0.2
+Provides-Dist: truffles (1.0.2)
+Obsoletes-Dist: truffles (!=1.2,<=2.0)
diff --git a/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA b/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA
--- a/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA
+++ b/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA
@@ -4,3 +4,5 @@
 Summary: Chocolate with a kick!
 Requires-Dist: towel-stuff (0.1)
 Provides-Dist: truffles (1.0)
+Obsoletes-Dist: truffles (<=0.8,>=0.5)
+Obsoletes-Dist: truffles (<=0.9,>=0.6)
diff --git a/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA b/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
--- a/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
+++ b/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
@@ -1,3 +1,4 @@
 Metadata-Version: 1.2
 Name: grammar
 Version: 1.0a4
+Requires-Dist: truffles (>=1.2)
diff --git a/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA b/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA
--- a/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA
+++ b/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA
@@ -1,3 +1,5 @@
 Metadata-Version: 1.2
 Name: towel-stuff
 Version: 0.1
+Provides-Dist: truffles (1.1.2)
+Obsoletes-Dist: truffles (!=0.8,<1.0)
diff --git a/src/distutils2/_backport/tests/test_pkgutil.py b/src/distutils2/_backport/tests/test_pkgutil.py
--- a/src/distutils2/_backport/tests/test_pkgutil.py
+++ b/src/distutils2/_backport/tests/test_pkgutil.py
@@ -216,7 +216,9 @@
         found_dists = []
 
         # Import the function in question
-        from distutils2._backport.pkgutil import get_distributions, Distribution
+        from distutils2._backport.pkgutil import get_distributions, \
+                                                 Distribution, \
+                                                 EggInfoDistribution
 
         # Verify the fake dists have been found.
         dists = [ dist for dist in get_distributions() ]
@@ -231,13 +233,31 @@
         # Finally, test that we found all that we were looking for
         self.assertListEqual(sorted(found_dists), sorted(fake_dists))
 
+        # Now, test if the egg-info distributions are found correctly as well
+        fake_dists += [('bacon', '0.1'), ('cheese', '2.0.2')]
+        found_dists = []
+
+        dists = [ dist for dist in get_distributions(use_egg_info=True) ]
+        for dist in dists:
+            if not (isinstance(dist, Distribution) or \
+                    isinstance(dist, EggInfoDistribution)):
+                self.fail("item received was not a Distribution or "
+                          "EggInfoDistribution instance: %s" % type(dist))
+            if dist.name in dict(fake_dists).keys():
+                found_dists.append((dist.name, dist.metadata['version']))
+
+        self.assertListEqual(sorted(fake_dists), sorted(found_dists))
+
+
     def test_get_distribution(self):
         """Test for looking up a distribution by name."""
         # Test the lookup of the towel-stuff distribution
         name = 'towel-stuff' # Note: This is different from the directory name
 
         # Import the function in question
-        from distutils2._backport.pkgutil import get_distribution, Distribution
+        from distutils2._backport.pkgutil import get_distribution, \
+                                                 Distribution, \
+                                                 EggInfoDistribution
 
         # Lookup the distribution
         dist = get_distribution(name)
@@ -250,6 +270,21 @@
         # Verify partial name matching doesn't work
         self.assertEqual(None, get_distribution('towel'))
 
+        # Verify that it does not find egg-info distributions, when not
+        # instructed to
+        self.assertEqual(None, get_distribution('bacon'))
+        self.assertEqual(None, get_distribution('cheese'))
+
+        # Now check that it works well in both situations, when egg-info
+        # is a file and directory respectively.
+        dist = get_distribution('cheese', use_egg_info=True)
+        self.assertTrue(isinstance(dist, EggInfoDistribution))
+        self.assertEqual(dist.name, 'cheese')
+
+        dist = get_distribution('bacon', use_egg_info=True)
+        self.assertTrue(isinstance(dist, EggInfoDistribution))
+        self.assertEqual(dist.name, 'bacon')
+
     def test_get_file_users(self):
         """Test the iteration of distributions that use a file."""
         from distutils2._backport.pkgutil import get_file_users, Distribution
@@ -260,6 +295,80 @@
             self.assertTrue(isinstance(dist, Distribution))
             self.assertEqual(dist.name, name)
 
+    def test_provides(self):
+        """ Test for looking up distributions by what they provide """
+        from distutils2._backport.pkgutil import provides_distribution
+        from distutils2.errors import DistutilsError
+
+        checkLists = lambda x,y: self.assertListEqual(sorted(x), sorted(y))
+
+        l = [dist.name for dist in provides_distribution('truffles')]
+        checkLists(l, ['choxie', 'towel-stuff'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '1.0')]
+        checkLists(l, ['choxie'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '1.0',
+                                                         use_egg_info=True)]
+        checkLists(l, ['choxie', 'cheese'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '1.1.2')]
+        checkLists(l, ['towel-stuff'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '1.1')]
+        checkLists(l, ['towel-stuff'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '!=1.1,<=2.0')]
+        checkLists(l, ['choxie'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '!=1.1,<=2.0',
+                                                          use_egg_info=True)]
+        checkLists(l, ['choxie', 'bacon', 'cheese'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '>1.0')]
+        checkLists(l, ['towel-stuff'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '>1.5')]
+        checkLists(l, [])
+
+        l = [dist.name for dist in provides_distribution('truffles', '>1.5',
+                                                         use_egg_info=True)]
+        checkLists(l, ['bacon'])
+
+        l = [dist.name for dist in provides_distribution('truffles', '>=1.0')]
+        checkLists(l, ['choxie', 'towel-stuff'])
+
+    def test_obsoletes(self):
+        """ Test looking for distributions based on what they obsolete """
+        from distutils2._backport.pkgutil import obsoletes_distribution
+        from distutils2.errors import DistutilsError
+
+        checkLists = lambda x,y: self.assertListEqual(sorted(x), sorted(y))
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '1.0')]
+        checkLists(l, [])
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '1.0',
+                                                          use_egg_info=True)]
+        checkLists(l, ['cheese', 'bacon'])
+
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '0.8')]
+        checkLists(l, ['choxie'])
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '0.8',
+                                                          use_egg_info=True)]
+        checkLists(l, ['choxie', 'cheese'])
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '0.9.6')]
+        checkLists(l, ['choxie', 'towel-stuff'])
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '0.5.2.3')]
+        checkLists(l, ['choxie', 'towel-stuff'])
+
+        l = [dist.name for dist in obsoletes_distribution('truffles', '0.2')]
+        checkLists(l, ['towel-stuff'])
+
 
 def test_suite():
     suite = unittest2.TestSuite()
@@ -273,3 +382,16 @@
 
 if __name__ == "__main__":
     test_main()
+
+def test_suite():
+    suite = unittest2.TestSuite()
+    testcase_loader = unittest2.loader.defaultTestLoader.loadTestsFromTestCase
+    suite.addTest(testcase_loader(TestPkgUtilFunctions))
+    suite.addTest(testcase_loader(TestPkgUtilDistribution))
+    return suite
+
+def test_main():
+    run_unittest(test_suite())
+
+if __name__ == "__main__":
+    test_main()
diff --git a/src/distutils2/command/sdist.py b/src/distutils2/command/sdist.py
--- a/src/distutils2/command/sdist.py
+++ b/src/distutils2/command/sdist.py
@@ -121,6 +121,7 @@
         self.metadata_check = 1
         self.owner = None
         self.group = None
+        self.filelist = None
 
     def _check_archive_formats(self, formats):
         supported_formats = [name for name, desc in get_archive_formats()]
@@ -152,10 +153,14 @@
         if self.dist_dir is None:
             self.dist_dir = "dist"
 
+        if self.filelist is None:
+            self.filelist = Manifest()
+
+
     def run(self):
         # 'filelist' contains the list of files that will make up the
         # manifest
-        self.filelist = Manifest()
+        self.filelist.clear()
 
         # Run sub commands
         for cmd_name in self.get_sub_commands():
@@ -200,7 +205,7 @@
         if self.use_defaults:
             self.add_defaults()
         if template_exists:
-            self.read_template()
+            self.filelist.read_template(self.template)
         if self.prune:
             self.prune_file_list()
 
diff --git a/src/distutils2/converter/fixers/fix_imports.py b/src/distutils2/converter/fixers/fix_imports.py
--- a/src/distutils2/converter/fixers/fix_imports.py
+++ b/src/distutils2/converter/fixers/fix_imports.py
@@ -20,6 +20,9 @@
         if node.type != syms.import_from:
             return
 
+        if not hasattr(imp, "next_sibling"):
+            imp.next_sibling = imp.get_next_sibling()
+
         while not hasattr(imp, 'value'):
             imp = imp.children[0]
 
@@ -34,6 +37,8 @@
             next = imp.next_sibling
             while next is not None:
                 pattern.append(next.value)
+                if not hasattr(next, "next_sibling"):
+                    next.next_sibling = next.get_next_sibling()
                 next = next.next_sibling
             if pattern == ['import', 'setup']:
                 imp.value = 'distutils2.core'
diff --git a/src/distutils2/converter/fixers/fix_setup_options.py b/src/distutils2/converter/fixers/fix_setup_options.py
--- a/src/distutils2/converter/fixers/fix_setup_options.py
+++ b/src/distutils2/converter/fixers/fix_setup_options.py
@@ -41,6 +41,10 @@
 
     def _fix_name(self, argument, remove_list):
         name = argument.children[0]
+
+        if not hasattr(name, "next_sibling"):
+            name.next_sibling = name.get_next_sibling()
+
         sibling = name.next_sibling
         if sibling is None or sibling.type != token.EQUAL:
             return False
@@ -48,6 +52,8 @@
         if name.value in _OLD_NAMES:
             name.value = _OLD_NAMES[name.value]
             if name.value in _SEQUENCE_NAMES:
+                if not hasattr(sibling, "next_sibling"):
+                    sibling.next_sibling = sibling.get_next_sibling()
                 right_operand = sibling.next_sibling
                 # replacing string -> list[string]
                 if right_operand.type == token.STRING:
diff --git a/src/distutils2/depgraph.py b/src/distutils2/depgraph.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/depgraph.py
@@ -0,0 +1,181 @@
+"""
+A dependency graph generator. The graph is represented as an instance of
+:class:`DependencyGraph`, and DOT output is possible as well.
+"""
+
+from distutils2._backport import pkgutil
+from distutils2.errors import DistutilsError
+from distutils2.version import VersionPredicate
+
+__all__ = ['DependencyGraph', 'generate_graph']
+
+
+class DependencyGraph(object):
+    """
+    Represents a dependency graph between distributions.
+
+    The depedency relationships are stored in an *adjacency_list* that maps
+    distributions to a list of ``(other, label)`` tuples where  ``other``
+    is a distribution and the edge is labelled with ``label`` (i.e. the version
+    specifier, if such was provided). If any missing depencies are found,
+    they are stored in ``missing``. It maps distributions to a list of
+    requirements that were not provided by any other distributions.
+    """
+
+    def __init__(self):
+        self.adjacency_list = {}
+        self.missing = {}
+
+    def add_distribution(self, distribution):
+        """
+        Add distribution *x* to the graph.
+
+        :type distribution: :class:`pkgutil.Distribution` or
+                            :class:`pkgutil.EggInfoDistribution`
+        """
+        self.adjacency_list[distribution] = list()
+        self.missing[distribution] = list()
+
+    def add_edge(self, x, y, label=None):
+        """
+        Add an edge from distribution *x* to distribution *y* with the given
+        *label*.
+
+
+        :type x: :class:`pkgutil.Distribution` or
+                 :class:`pkgutil.EggInfoDistribution`
+        :type y: :class:`pkgutil.Distribution` or
+                 :class:`pkgutil.EggInfoDistribution`
+        :type label: ``str`` or ``None``
+        """
+        self.adjacency_list[x].append((y, label))
+
+    def add_missing(self, distribution, requirement):
+        """
+        Add a missing *requirement* for the given *distribution*.
+
+        :type distribution: :class:`pkgutil.Distribution` or
+                            :class:`pkgutil.EggInfoDistribution`
+        :type requirement: ``str``
+        """
+        self.missing[distribution].append(requirement)
+
+    def to_dot(self, f, skip_disconnected=True):
+        """
+        Writes a DOT output for the graph to the provided *file*.
+        If *skip_disconnected* is set to ``True``, then all distributions
+        that are not dependent on any other distributions are skipped.
+
+        :type f: ``file``
+        ;type skip_disconnected: ``bool``
+        """
+        if not isinstance(f, file):
+            raise TypeError('the argument has to be of type file')
+
+        disconnected = []
+
+        f.write("digraph dependencies {\n")
+        for dist, adjs in self.adjacency_list.iteritems():
+            if len(adjs) == 0 and not skip_disconnected:
+                disconnected.append(dist)
+            for (other, label) in adjs:
+                if not label is None:
+                    f.write('"%s" -> "%s" [label="%s"]\n' %
+                                                (dist.name, other.name, label))
+                else:
+                    f.write('"%s" -> "%s"\n' % (dist.name, other.name))
+        if not skip_disconnected and len(disconnected) > 0:
+            f.write('subgraph disconnected {\n')
+            f.write('label = "Disconnected"\n')
+            f.write('bgcolor = red\n')
+
+            for dist in disconnected:
+                f.write('"%s"' % dist.name)
+                f.write('\n')
+            f.write('}\n')
+        f.write('}\n')
+
+
+def generate_graph(dists):
+    """
+    Generates a dependency graph from the given distributions.
+
+    :parameter dists: a list of distributions
+    :type dists: list of :class:`pkgutil.Distribution` and
+                         :class:`pkgutil.EggInfoDistribution` instances
+    :rtype: an :class:`DependencyGraph` instance
+    """
+    graph = DependencyGraph()
+    provided = {} # maps names to lists of (version, dist) tuples
+    dists = list(dists) # maybe use generator_tools in future
+
+    # first, build the graph and find out the provides
+    for dist in dists:
+        graph.add_distribution(dist)
+        provides = dist.metadata['Provides-Dist'] + dist.metadata['Provides']
+
+        for p in provides:
+            comps = p.split(" ", 1)
+            name = comps[0]
+            version = None
+            if len(comps) == 2:
+                version = comps[1]
+                if len(version) < 3 or version[0] != '(' or version[-1] != ')':
+                    raise DistutilsError('Distribution %s has ill formed' \
+                                         'provides field: %s' % (dist.name, p))
+                version = version[1:-1] # trim off parenthesis
+            if not name in provided:
+                provided[name] = []
+            provided[name].append((version, dist))
+
+    # now make the edges
+    for dist in dists:
+        requires = dist.metadata['Requires-Dist'] + dist.metadata['Requires']
+        for req in requires:
+            predicate = VersionPredicate(req)
+            comps = req.split(" ", 1)
+            name = comps[0]
+
+            if not name in provided:
+                graph.add_missing(dist, req)
+            else:
+                for (version, provider) in provided[name]:
+                    if predicate.match(version):
+                        graph.add_edge(dist, provider, req)
+
+    return graph
+
+
+def dependent_dists(dists, dist):
+    """
+    Recursively generate a list of distributions from *dists* that are
+    dependent on *dist*.
+
+    :param dists: a list of distributions
+    :param dist: a distribution, member of *dists* for which we are interested
+    """
+    if not dist in dists:
+        raise ValueError('The given distribution is not a member of the list')
+    graph = generate_graph(dists)
+
+    dep = [dist]
+    fringe = [dist] # list of nodes we should expand
+    while not len(fringe) == 0:
+        next = graph.adjacency_list[fringe.pop()]
+        for (dist, label) in next:
+            if not dist in dep: # avoid infinite loops
+                dep.append(dist)
+                fringe.append(dist)
+
+    dep.pop()
+    return dep
+
+if __name__ == '__main__':
+    dists = list(pkgutil.get_distributions(use_egg_info=True))
+    graph = generate_graph(dists)
+    for dist, reqs in graph.missing.iteritems():
+        if len(reqs) > 0:
+            print("Missing dependencies for %s: %s" % (dist.name,
+                                                       ", ".join(reqs)))
+    f = open('output.dot', 'w')
+    graph.to_dot(f, True)
diff --git a/src/distutils2/manifest.py b/src/distutils2/manifest.py
--- a/src/distutils2/manifest.py
+++ b/src/distutils2/manifest.py
@@ -54,6 +54,12 @@
         for sort_tuple in sortable_files:
             self.files.append(os.path.join(*sort_tuple))
 
+    def clear(self):
+        """Clear all collected files."""
+        self.files = []
+        if self.allfiles is not None:
+            self.allfiles = []
+
     def remove_duplicates(self):
         # Assumes list has been sorted!
         for i in range(len(self.files) - 1, 0, -1):
diff --git a/src/distutils2/tests/test_converter.py b/src/distutils2/tests/test_converter.py
--- a/src/distutils2/tests/test_converter.py
+++ b/src/distutils2/tests/test_converter.py
@@ -36,4 +36,4 @@
     return unittest2.makeSuite(ConverterTestCase)
 
 if __name__ == '__main__':
-    run_unittest(test_suite())
+    unittest2.main(defaultTest="test_suite")
diff --git a/src/distutils2/tests/test_core.py b/src/distutils2/tests/test_core.py
--- a/src/distutils2/tests/test_core.py
+++ b/src/distutils2/tests/test_core.py
@@ -60,6 +60,19 @@
         distutils2.core.run_setup(
             self.write_setup(setup_using___file__))
 
+    def test_run_setup_stop_after(self):
+        f = self.write_setup(setup_using___file__)
+        for s in ['init', 'config', 'commandline', 'run']:
+            distutils2.core.run_setup(f, stop_after=s)
+        self.assertRaises(ValueError, distutils2.core.run_setup, 
+                          f, stop_after='bob')
+
+    def test_run_setup_args(self):
+        f = self.write_setup(setup_using___file__)
+        d = distutils2.core.run_setup(f, script_args=["--help"], 
+                                        stop_after="init")
+        self.assertEqual(['--help'], d.script_args)
+
     def test_run_setup_uses_current_dir(self):
         # This tests that the setup script is run with the current directory
         # as its own current directory; this was temporarily broken by a
diff --git a/src/distutils2/tests/test_sdist.py b/src/distutils2/tests/test_sdist.py
--- a/src/distutils2/tests/test_sdist.py
+++ b/src/distutils2/tests/test_sdist.py
@@ -343,6 +343,19 @@
         finally:
             archive.close()
 
+    def test_get_file_list(self):
+        dist, cmd = self.get_cmd()
+        cmd.finalize_options()
+        cmd.template = os.path.join(self.tmp_dir, 'MANIFEST.in')
+        f = open(cmd.template, 'w')
+        try:
+            f.write('include MANIFEST.in\n')
+        finally:
+            f.close()
+
+        cmd.get_file_list()
+        self.assertIn('MANIFEST.in', cmd.filelist.files)
+
 def test_suite():
     return unittest2.makeSuite(SDistTestCase)
 
diff --git a/src/distutils2/tests/test_version.py b/src/distutils2/tests/test_version.py
--- a/src/distutils2/tests/test_version.py
+++ b/src/distutils2/tests/test_version.py
@@ -139,13 +139,17 @@
         for predicate in predicates:
             v = VersionPredicate(predicate)
 
-        assert VersionPredicate('Hey (>=2.5,<2.7)').match('2.6')
-        assert VersionPredicate('Ho').match('2.6')
-        assert not VersionPredicate('Hey (>=2.5,!=2.6,<2.7)').match('2.6')
-        assert VersionPredicate('Ho (<3.0)').match('2.6')
-        assert VersionPredicate('Ho (<3.0,!=2.5)').match('2.6.0')
-        assert not VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.0')
-
+        self.assertTrue(VersionPredicate('Hey (>=2.5,<2.7)').match('2.6'))
+        self.assertTrue(VersionPredicate('Ho').match('2.6'))
+        self.assertFalse(VersionPredicate('Hey (>=2.5,!=2.6,<2.7)').match('2.6'))
+        self.assertTrue(VersionPredicate('Ho (<3.0)').match('2.6'))
+        self.assertTrue(VersionPredicate('Ho (<3.0,!=2.5)').match('2.6.0'))
+        self.assertFalse(VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.0'))
+        self.assertTrue(VersionPredicate('Ho (2.5)').match('2.5.4'))
+        self.assertFalse(VersionPredicate('Ho (!=2.5)').match('2.5.2'))
+        self.assertTrue(VersionPredicate('Hey (<=2.5)').match('2.5.9'))
+        self.assertFalse(VersionPredicate('Hey (<=2.5)').match('2.6.0'))
+        self.assertTrue(VersionPredicate('Hey (>=2.5)').match('2.5.1'))
 
         # XXX need to silent the micro version in this case
         #assert not VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.3')
diff --git a/src/distutils2/version.py b/src/distutils2/version.py
--- a/src/distutils2/version.py
+++ b/src/distutils2/version.py
@@ -330,10 +330,11 @@
 
     _operators = {"<": lambda x, y: x < y,
                   ">": lambda x, y: x > y,
-                  "<=": lambda x, y: x <= y,
-                  ">=": lambda x, y: x >= y,
-                  "==": lambda x, y: x == y,
-                  "!=": lambda x, y: x != y}
+                  "<=": lambda x, y: str(x).startswith(str(y)) or x < y,
+                  ">=": lambda x, y: str(x).startswith(str(y)) or x > y,
+                  "==": lambda x, y: str(x).startswith(str(y)),
+                  "!=": lambda x, y: not str(x).startswith(str(y)),
+                  }
 
     def __init__(self, predicate):
         predicate = predicate.strip()
diff --git a/src/runtests.py b/src/runtests.py
--- a/src/runtests.py
+++ b/src/runtests.py
@@ -6,7 +6,7 @@
 
 def test_main():
     import distutils2.tests
-    from distutils2.tests import run_unittest, reap_children
+    from distutils2.tests import run_unittest, reap_children, TestFailed
     from distutils2._backport.tests import test_suite as btest_suite
     # just supporting -q right now
     # to enable detailed/quiet output
@@ -14,10 +14,15 @@
         verbose = sys.argv[-1] != '-q'
     else:
         verbose = 1
-
-    run_unittest([distutils2.tests.test_suite(), btest_suite()],
-                 verbose_=verbose)
-    reap_children()
+    try:
+        try:
+            run_unittest([distutils2.tests.test_suite(), btest_suite()],
+                    verbose_=verbose)
+            return 0
+        except TestFailed:
+            return 1
+    finally:
+        reap_children()
 
 if __name__ == "__main__":
     try:
@@ -26,4 +31,5 @@
         print('!!! You need to install unittest2')
         sys.exit(1)
 
-    test_main()
+    sys.exit(test_main())
+
diff --git a/src/tests.sh b/src/tests.sh
--- a/src/tests.sh
+++ b/src/tests.sh
@@ -1,12 +1,28 @@
 #!/bin/sh
-echo Testing with Python 2.4....
-python2.4 runtests.py -q
+echo -n "Running tests for Python 2.4..."
+python2.4 runtests.py -q > /dev/null 2> /dev/null
+if [ $? -ne 0 ];then
+    echo "Failed"
+    exit $1
+else
+    echo "Success"
+fi
 
-echo
-echo Testing with Python 2.5....
-python2.5 runtests.py -q
+echo -n "Running tests for Python 2.5..."
+python2.5 runtests.py -q > /dev/null 2> /dev/null
+if [ $? -ne 0 ];then
+    echo "Failed"
+    exit $1
+else
+    echo "Success"
+fi
 
-echo
-echo Testing with Python 2.6....
-python2.6 runtests.py -q
+echo -n "Running tests for Python 2.6..."
+python2.6 runtests.py -q > /dev/null 2> /dev/null
+if [ $? -ne 0 ];then
+    echo "Failed"
+    exit $1
+else
+    echo "Success"
+fi
 

--
Repository URL: http://hg.python.org/distutils2


More information about the Python-checkins mailing list