[Python-checkins] distutils2: merge

tarek.ziade python-checkins at python.org
Sun Sep 19 10:20:21 CEST 2010


tarek.ziade pushed bc408018ca27 to distutils2:

http://hg.python.org/distutils2/rev/bc408018ca27
changeset:   609:bc408018ca27
parent:      608:10da588ede3e
parent:      521:e51ba85f26cd
user:        Konrad Delong <konryd at gmail.com>
date:        Mon Aug 09 18:22:43 2010 +0200
summary:     merge
files:       src/distutils2/dist.py

diff --git a/docs/source/projects-index.dist.rst b/docs/source/projects-index.dist.rst
--- a/docs/source/projects-index.dist.rst
+++ b/docs/source/projects-index.dist.rst
@@ -84,26 +84,27 @@
     {'url': 'http://example.org/foobar-1.0.tar.gz', 'hashname': None, 'hashval':
     None, 'is_external': True}
  
-Attributes Lazy loading
------------------------
+Getting attributes from the dist objects 
+-----------------------------------------
 
 To abstract a maximum the way of querying informations to the indexes,
-attributes and releases informations can be retrieved "on demand", in a "lazy"
-way.
+attributes and releases informations can be retrieved directly from the objects
+returned by the indexes. 
 
 For instance, if you have a release instance that does not contain the metadata
-attribute, it can be build directly when accedded::
+attribute, it can be fetched by using the "fetch_metadata" method::
 
     >>> r = Release("FooBar", "1.1")
-    >>> print r._metadata 
+    >>> print r.metadata 
     None # metadata field is actually set to "None"
-    >>> r.metadata
+    >>> r.fetch_metadata()
     <Metadata for FooBar 1.1>
 
-Like this, it's possible to retrieve project's releases, releases metadata and 
-releases distributions informations. 
+Like this, it's possible to retrieve project's releases (`fetch_releases`), 
+releases metadata (`fetch_metadata` and releases distributions
+(`fetch_distributions` informations). 
 
 Internally, this is possible because while retrieving for the first time
 informations about projects, releases or distributions, a reference to the
-client used is stored in the objects. Then, while trying to access undefined
-fields, it will be used if necessary.
+client used is stored in the objects (can be accessed using the object 
+`_index` attribute. 
diff --git a/docs/source/projects-index.xmlrpc.rst b/docs/source/projects-index.xmlrpc.rst
--- a/docs/source/projects-index.xmlrpc.rst
+++ b/docs/source/projects-index.xmlrpc.rst
@@ -126,24 +126,3 @@
 
 As you see, this does not return a list of distributions, but a release, 
 because a release can be used like a list of distributions. 
-
-Lazy load information from project, releases and distributions.
-----------------------------------------------------------------
-
-.. note:: The lazy loading feature is not currently available !
-
-As :mod:`distutils2.index.dist` classes support "lazy" loading of 
-informations, you can use it while retrieving informations from XML-RPC.
-
-For instance, it's possible to get all the releases for a project, and to access
-directly the metadata of each release, without making
-:class:`distutils2.index.xmlrpc.Client` directly (they will be made, but they're
-invisible to the you)::
-
-    >>> client = xmlrpc.Client()
-    >>> releases = client.get_releases("FooBar")
-    >>> releases.get_release("1.1").metadata
-    <Metadata for FooBar 1.1>
-
-Refer to the :mod:`distutils2.index.dist` documentation for more information
-about attributes lazy loading.
diff --git a/src/DEVNOTES.txt b/src/DEVNOTES.txt
--- a/src/DEVNOTES.txt
+++ b/src/DEVNOTES.txt
@@ -17,6 +17,5 @@
 
   - DistributionMetadata > Metadata or ReleaseMetadata
   - pkgutil > pkgutil.__init__ + new pkgutil.database (or better name)
-  - pypi > index
   - RationalizedVersion > Version
   - suggest_rationalized_version > suggest
diff --git a/src/distutils2/__init__.py b/src/distutils2/__init__.py
--- a/src/distutils2/__init__.py
+++ b/src/distutils2/__init__.py
@@ -20,7 +20,7 @@
 __version__ = "1.0a2"
 
 
-# when set to True, converts doctests by default too 
+# when set to True, converts doctests by default too
 run_2to3_on_doctests = True
 # Standard package names for fixer packages
 lib2to3_fixer_packages = ['lib2to3.fixes']
diff --git a/src/distutils2/command/__init__.py b/src/distutils2/command/__init__.py
--- a/src/distutils2/command/__init__.py
+++ b/src/distutils2/command/__init__.py
@@ -5,7 +5,8 @@
 
 __revision__ = "$Id: __init__.py 71473 2009-04-11 14:55:07Z tarek.ziade $"
 
-__all__ = ['build',
+__all__ = ['check',
+           'build',
            'build_py',
            'build_ext',
            'build_clib',
@@ -16,17 +17,11 @@
            'install_headers',
            'install_scripts',
            'install_data',
+           'install_distinfo',
            'sdist',
-           'register',
            'bdist',
            'bdist_dumb',
            'bdist_wininst',
+           'register',
            'upload',
-           'check',
-           # These two are reserved for future use:
-           #'bdist_sdux',
-           #'bdist_pkgtool',
-           # Note:
-           # bdist_packager is not included because it only provides
-           # an abstract base class
           ]
diff --git a/src/distutils2/command/build_py.py b/src/distutils2/command/build_py.py
--- a/src/distutils2/command/build_py.py
+++ b/src/distutils2/command/build_py.py
@@ -371,7 +371,12 @@
         return modules
 
     def get_source_files(self):
-        return [module[-1] for module in self.find_all_modules()]
+        sources = [module[-1] for module in self.find_all_modules()]
+        sources += [
+            os.path.join(src_dir, filename)
+            for package, src_dir, build_dir, filenames in self.data_files
+            for filename in filenames]
+        return sources
 
     def get_module_outfile(self, build_dir, package, module):
         outfile_path = [build_dir] + list(package) + [module + ".py"]
@@ -393,8 +398,7 @@
         outputs += [
             os.path.join(build_dir, filename)
             for package, src_dir, build_dir, filenames in self.data_files
-            for filename in filenames
-            ]
+            for filename in filenames]
 
         return outputs
 
diff --git a/src/distutils2/command/install.py b/src/distutils2/command/install.py
--- a/src/distutils2/command/install.py
+++ b/src/distutils2/command/install.py
@@ -18,6 +18,7 @@
 from distutils2.util import convert_path, change_root, get_platform
 from distutils2.errors import DistutilsOptionError
 
+
 class install(Command):
 
     description = "install everything from build directory"
@@ -31,7 +32,7 @@
         ('home=', None,
          "(Unix only) home directory to install under"),
 
-        # Or, just set the base director(y|ies)
+        # Or just set the base director(y|ies)
         ('install-base=', None,
          "base installation directory (instead of --prefix or --home)"),
         ('install-platbase=', None,
@@ -40,7 +41,7 @@
         ('root=', None,
          "install everything relative to this alternate root directory"),
 
-        # Or, explicitly set the installation scheme
+        # Or explicitly set the installation scheme
         ('install-purelib=', None,
          "installation directory for pure Python module distributions"),
         ('install-platlib=', None,
@@ -62,8 +63,8 @@
         ('compile', 'c', "compile .py to .pyc [default]"),
         ('no-compile', None, "don't compile .py files"),
         ('optimize=', 'O',
-         "also compile with optimization: -O1 for \"python -O\", "
-         "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
+         'also compile with optimization: -O1 for "python -O", '
+         '-O2 for "python -OO", and -O0 to disable [default: -O0]'),
 
         # Miscellaneous control options
         ('force', 'f',
@@ -77,34 +78,45 @@
         #('install-html=', None, "directory for HTML documentation"),
         #('install-info=', None, "directory for GNU info files"),
 
+        # XXX use a name that makes clear this is the old format
         ('record=', None,
-         "filename in which to record list of installed files"),
+         "filename in which to record a list of installed files "
+         "(not PEP 376-compliant)"),
 
         # .dist-info related arguments, read by install_dist_info
-        ('no-distinfo', None, 'do not create a .dist-info directory'),
-        ('installer=', None, 'the name of the installer'),
-        ('requested', None, 'generate a REQUESTED file'),
-        ('no-requested', None, 'do not generate a REQUESTED file'),
-        ('no-record', None, 'do not generate a RECORD file'),
+        ('no-distinfo', None,
+         "do not create a .dist-info directory"),
+        ('installer=', None,
+         "the name of the installer"),
+        ('requested', None,
+         "generate a REQUESTED file (i.e."),
+        ('no-requested', None,
+         "do not generate a REQUESTED file"),
+        ('no-record', None,
+         "do not generate a RECORD file"),
         ]
 
-    boolean_options = ['compile', 'force', 'skip-build', 'no-dist-info',
-                       'requested', 'no-dist-record',]
+    boolean_options = ['compile', 'force', 'skip-build', 'no-distinfo',
+                       'requested', 'no-record']
 
-    user_options.append(('user', None,
-                        "install in user site-package '%s'" % \
-                            get_path('purelib', '%s_user' % os.name)))
-    boolean_options.append('user')
-    negative_opt = {'no-compile' : 'compile', 'no-requested': 'requested'}
+    if sys.version >= '2.6':
+        user_options.append(
+            ('user', None,
+             "install in user site-package [%s]" %
+             get_path('purelib', '%s_user' % os.name)))
 
+        boolean_options.append('user')
+
+    negative_opt = {'no-compile': 'compile', 'no-requested': 'requested'}
 
     def initialize_options(self):
-        """Initializes options."""
         # High-level options: these select both an installation base
         # and scheme.
         self.prefix = None
         self.exec_prefix = None
         self.home = None
+        # This attribute is used all over the place, so it's best to
+        # define it even in < 2.6
         self.user = 0
 
         # These select only the installation base; it's up to the user to
@@ -174,7 +186,6 @@
         self.requested = None
         self.no_record = None
 
-
     # -- Option finalizing methods -------------------------------------
     # (This is rather more involved than for most commands,
     # because this is where the policy for installing third-
@@ -182,7 +193,6 @@
     # array of user input is decided.  Yes, it's quite complex!)
 
     def finalize_options(self):
-        """Finalizes options."""
         # This method (and its pliant slaves, like 'finalize_unix()',
         # 'finalize_other()', and 'select_scheme()') is where the default
         # installation directories for modules, extension modules, and
@@ -199,18 +209,19 @@
 
         if ((self.prefix or self.exec_prefix or self.home) and
             (self.install_base or self.install_platbase)):
-            raise DistutilsOptionError, \
-                  ("must supply either prefix/exec-prefix/home or " +
-                   "install-base/install-platbase -- not both")
+            raise DistutilsOptionError(
+                "must supply either prefix/exec-prefix/home or "
+                "install-base/install-platbase -- not both")
 
         if self.home and (self.prefix or self.exec_prefix):
-            raise DistutilsOptionError, \
-                  "must supply either home or prefix/exec-prefix -- not both"
+            raise DistutilsOptionError(
+                "must supply either home or prefix/exec-prefix -- not both")
 
         if self.user and (self.prefix or self.exec_prefix or self.home or
-                self.install_base or self.install_platbase):
-            raise DistutilsOptionError("can't combine user with with prefix/"
-                                       "exec_prefix/home or install_(plat)base")
+                          self.install_base or self.install_platbase):
+            raise DistutilsOptionError(
+                "can't combine user with prefix/exec_prefix/home or "
+                "install_base/install_platbase")
 
         # Next, stuff that's wrong (or dubious) only on certain platforms.
         if os.name != "posix":
@@ -245,18 +256,19 @@
                                                       'srcdir')
 
         metadata = self.distribution.metadata
-        self.config_vars = {'dist_name': metadata['Name'],
-                            'dist_version': metadata['Version'],
-                            'dist_fullname': metadata.get_fullname(),
-                            'py_version': py_version,
-                            'py_version_short': py_version[0:3],
-                            'py_version_nodot': py_version[0] + py_version[2],
-                            'sys_prefix': prefix,
-                            'prefix': prefix,
-                            'sys_exec_prefix': exec_prefix,
-                            'exec_prefix': exec_prefix,
-                            'srcdir': srcdir,
-                           }
+        self.config_vars = {
+            'dist_name': metadata['Name'],
+            'dist_version': metadata['Version'],
+            'dist_fullname': metadata.get_fullname(),
+            'py_version': py_version,
+            'py_version_short': py_version[:3],
+            'py_version_nodot': py_version[:3:2],
+            'sys_prefix': prefix,
+            'prefix': prefix,
+            'sys_exec_prefix': exec_prefix,
+            'exec_prefix': exec_prefix,
+            'srcdir': srcdir,
+            }
 
         self.config_vars['userbase'] = self.install_userbase
         self.config_vars['usersite'] = self.install_usersite
@@ -284,12 +296,11 @@
         # module distribution is pure or not.  Of course, if the user
         # already specified install_lib, use their selection.
         if self.install_lib is None:
-            if self.distribution.ext_modules: # has extensions: non-pure
+            if self.distribution.ext_modules:  # has extensions: non-pure
                 self.install_lib = self.install_platlib
             else:
                 self.install_lib = self.install_purelib
 
-
         # Convert directories from Unix /-separated syntax to the local
         # convention.
         self.convert_paths('lib', 'purelib', 'platlib',
@@ -301,7 +312,7 @@
         # non-packagized module distributions (hello, Numerical Python!) to
         # get their own directories.
         self.handle_extra_path()
-        self.install_libbase = self.install_lib # needed for .pth file
+        self.install_libbase = self.install_lib  # needed for .pth file
         self.install_lib = os.path.join(self.install_lib, self.extra_dirs)
 
         # If a new root directory was supplied, make all the installation
@@ -321,25 +332,8 @@
         if self.no_distinfo is None:
             self.no_distinfo = False
 
-    def dump_dirs(self, msg):
-        """Dumps the list of user options."""
-        from distutils2.fancy_getopt import longopt_xlate
-        log.debug(msg + ":")
-        for opt in self.user_options:
-            opt_name = opt[0]
-            if opt_name[-1] == "=":
-                opt_name = opt_name[0:-1]
-            if opt_name in self.negative_opt:
-                opt_name = self.negative_opt[opt_name]
-                opt_name = opt_name.translate(longopt_xlate)
-                val = not getattr(self, opt_name)
-            else:
-                opt_name = opt_name.translate(longopt_xlate)
-                val = getattr(self, opt_name)
-            log.debug("  %s: %s" % (opt_name, val))
-
     def finalize_unix(self):
-        """Finalizes options for posix platforms."""
+        """Finalize options for posix platforms."""
         if self.install_base is not None or self.install_platbase is not None:
             if ((self.install_lib is None and
                  self.install_purelib is None and
@@ -347,15 +341,15 @@
                 self.install_headers is None or
                 self.install_scripts is None or
                 self.install_data is None):
-                raise DistutilsOptionError, \
-                      ("install-base or install-platbase supplied, but "
-                      "installation scheme is incomplete")
+                raise DistutilsOptionError(
+                    "install-base or install-platbase supplied, but "
+                    "installation scheme is incomplete")
             return
 
         if self.user:
             if self.install_userbase is None:
                 raise DistutilsPlatformError(
-                    "User base directory is not specified")
+                    "user base directory is not specified")
             self.install_base = self.install_platbase = self.install_userbase
             self.select_scheme("posix_user")
         elif self.home is not None:
@@ -364,8 +358,8 @@
         else:
             if self.prefix is None:
                 if self.exec_prefix is not None:
-                    raise DistutilsOptionError, \
-                          "must not supply exec-prefix without prefix"
+                    raise DistutilsOptionError(
+                        "must not supply exec-prefix without prefix")
 
                 self.prefix = os.path.normpath(sys.prefix)
                 self.exec_prefix = os.path.normpath(sys.exec_prefix)
@@ -379,11 +373,11 @@
             self.select_scheme("posix_prefix")
 
     def finalize_other(self):
-        """Finalizes options for non-posix platforms"""
+        """Finalize options for non-posix platforms"""
         if self.user:
             if self.install_userbase is None:
                 raise DistutilsPlatformError(
-                    "User base directory is not specified")
+                    "user base directory is not specified")
             self.install_base = self.install_platbase = self.install_userbase
             self.select_scheme(os.name + "_user")
         elif self.home is not None:
@@ -397,11 +391,27 @@
             try:
                 self.select_scheme(os.name)
             except KeyError:
-                raise DistutilsPlatformError, \
-                      "I don't know how to install stuff on '%s'" % os.name
+                raise DistutilsPlatformError(
+                    "no support for installation on '%s'" % os.name)
+
+    def dump_dirs(self, msg):
+        """Dump the list of user options."""
+        log.debug(msg + ":")
+        for opt in self.user_options:
+            opt_name = opt[0]
+            if opt_name[-1] == "=":
+                opt_name = opt_name[0:-1]
+            if opt_name in self.negative_opt:
+                opt_name = self.negative_opt[opt_name]
+                opt_name = opt_name.replace('-', '_')
+                val = not getattr(self, opt_name)
+            else:
+                opt_name = opt_name.replace('-', '_')
+                val = getattr(self, opt_name)
+            log.debug("  %s: %s" % (opt_name, val))
 
     def select_scheme(self, name):
-        """Sets the install directories by applying the install schemes."""
+        """Set the install directories by applying the install schemes."""
         # it's the caller's problem if they supply a bad name!
         scheme = get_paths(name, expand=False)
         for key, value in scheme.items():
@@ -424,15 +434,14 @@
                 setattr(self, attr, val)
 
     def expand_basedirs(self):
-        """Calls `os.path.expanduser` on install_base, install_platbase and
-        root."""
+        """Call `os.path.expanduser` on install_{base,platbase} and root."""
         self._expand_attrs(['install_base', 'install_platbase', 'root'])
 
     def expand_dirs(self):
-        """Calls `os.path.expanduser` on install dirs."""
+        """Call `os.path.expanduser` on install dirs."""
         self._expand_attrs(['install_purelib', 'install_platlib',
                             'install_lib', 'install_headers',
-                            'install_scripts', 'install_data',])
+                            'install_scripts', 'install_data'])
 
     def convert_paths(self, *names):
         """Call `convert_path` over `names`."""
@@ -454,9 +463,9 @@
             elif len(self.extra_path) == 2:
                 path_file, extra_dirs = self.extra_path
             else:
-                raise DistutilsOptionError, \
-                      ("'extra_path' option must be a list, tuple, or "
-                      "comma-separated string with 1 or 2 elements")
+                raise DistutilsOptionError(
+                    "'extra_path' option must be a list, tuple, or "
+                    "comma-separated string with 1 or 2 elements")
 
             # convert to local form in case Unix notation used (as it
             # should be in setup scripts)
@@ -542,7 +551,6 @@
         else:
             self.warn("path file '%s' not created" % filename)
 
-
     # -- Reporting methods ---------------------------------------------
 
     def get_outputs(self):
@@ -597,10 +605,10 @@
 
     # 'sub_commands': a list of commands this command might have to run to
     # get its work done.  See cmd.py for more info.
-    sub_commands = [('install_lib',     has_lib),
+    sub_commands = [('install_lib', has_lib),
                     ('install_headers', has_headers),
                     ('install_scripts', has_scripts),
-                    ('install_data',    has_data),
+                    ('install_data', has_data),
                     # keep install_distinfo last, as it needs the record
                     # with files to be completely generated
                     ('install_distinfo', lambda self: not self.no_distinfo),
diff --git a/src/distutils2/command/install_data.py b/src/distutils2/command/install_data.py
--- a/src/distutils2/command/install_data.py
+++ b/src/distutils2/command/install_data.py
@@ -72,6 +72,21 @@
                         (out, _) = self.copy_file(data, dir)
                         self.outfiles.append(out)
 
+    def get_source_files(self):
+        sources = []
+        for item in self.data_files:
+            if isinstance(item, str): # plain file
+                item = convert_path(item)
+                if os.path.isfile(item):
+                    sources.append(item)
+            else:    # a (dirname, filenames) tuple
+                dirname, filenames = item
+                for f in filenames:
+                    f = convert_path(f)
+                    if os.path.isfile(f):
+                        sources.append(f)
+        return sources
+
     def get_inputs(self):
         return self.data_files or []
 
diff --git a/src/distutils2/command/install_distinfo.py b/src/distutils2/command/install_distinfo.py
--- a/src/distutils2/command/install_distinfo.py
+++ b/src/distutils2/command/install_distinfo.py
@@ -31,18 +31,18 @@
 
     user_options = [
         ('distinfo-dir=', None,
-                           'directory where the the .dist-info directory will '
-                           'be installed'),
-        ('installer=', None, 'the name of the installer'),
-        ('requested', None, 'generate a REQUESTED file'),
-        ('no-requested', None, 'do not generate a REQUESTED file'),
-        ('no-record', None, 'do not generate a RECORD file'),
+         "directory where the the .dist-info directory will be installed"),
+        ('installer=', None,
+         "the name of the installer"),
+        ('requested', None,
+         "generate a REQUESTED file"),
+        ('no-requested', None,
+         "do not generate a REQUESTED file"),
+        ('no-record', None,
+         "do not generate a RECORD file"),
     ]
 
-    boolean_options = [
-        'requested',
-        'no-record',
-    ]
+    boolean_options = ['requested', 'no-record']
 
     negative_opt = {'no-requested': 'requested'}
 
@@ -54,14 +54,14 @@
 
     def finalize_options(self):
         self.set_undefined_options('install',
-                                   ('installer', 'installer'),
-                                   ('requested', 'requested'),
-                                   ('no_record', 'no_record'))
+                                   'installer', 'requested', 'no_record')
 
         self.set_undefined_options('install_lib',
                                    ('install_dir', 'distinfo_dir'))
 
         if self.installer is None:
+            # FIXME distutils or distutils2?
+            # + document default in the option help text above and in install
             self.installer = 'distutils'
         if self.requested is None:
             self.requested = True
@@ -144,10 +144,7 @@
         return self.outputs
 
 
-# The following routines are taken from setuptools' pkg_resources module and
-# can be replaced by importing them from pkg_resources once it is included
-# in the stdlib.
-
+# The following functions are taken from setuptools' pkg_resources module.
 
 def safe_name(name):
     """Convert an arbitrary string to a standard distribution name
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
@@ -170,14 +170,6 @@
         # or zipfile, or whatever.
         self.make_distribution()
 
-    def check_metadata(self):
-        """Deprecated API."""
-        warn("distutils.command.sdist.check_metadata is deprecated, \
-              use the check command instead", PendingDeprecationWarning)
-        check = self.distribution.get_command_obj('check')
-        check.ensure_finalized()
-        check.run()
-
     def get_file_list(self):
         """Figure out the list of files to include in the source
         distribution, and put it in 'self.filelist'.  This might involve
@@ -243,47 +235,9 @@
             if files:
                 self.filelist.extend(files)
 
-        # build_py is used to get:
-        #  - python modules
-        #  - files defined in package_data
-        build_py = self.get_finalized_command('build_py')
-
-        # getting python files
-        if self.distribution.has_pure_modules():
-            self.filelist.extend(build_py.get_source_files())
-
-        # getting package_data files
-        # (computed in build_py.data_files by build_py.finalize_options)
-        for pkg, src_dir, build_dir, filenames in build_py.data_files:
-            for filename in filenames:
-                self.filelist.append(os.path.join(src_dir, filename))
-
-        # getting distribution.data_files
-        if self.distribution.has_data_files():
-            for item in self.distribution.data_files:
-                if isinstance(item, str): # plain file
-                    item = convert_path(item)
-                    if os.path.isfile(item):
-                        self.filelist.append(item)
-                else:    # a (dirname, filenames) tuple
-                    dirname, filenames = item
-                    for f in filenames:
-                        f = convert_path(f)
-                        if os.path.isfile(f):
-                            self.filelist.append(f)
-
-        if self.distribution.has_ext_modules():
-            build_ext = self.get_finalized_command('build_ext')
-            self.filelist.extend(build_ext.get_source_files())
-
-        if self.distribution.has_c_libraries():
-            build_clib = self.get_finalized_command('build_clib')
-            self.filelist.extend(build_clib.get_source_files())
-
-        if self.distribution.has_scripts():
-            build_scripts = self.get_finalized_command('build_scripts')
-            self.filelist.extend(build_scripts.get_source_files())
-
+        for cmd_name in self.distribution.get_command_names():
+            cmd_obj = self.get_finalized_command(cmd_name)
+            self.filelist.extend(cmd_obj.get_source_files())
 
     def prune_file_list(self):
         """Prune off branches that might slip into the file list as created
diff --git a/src/distutils2/core.py b/src/distutils2/core.py
--- a/src/distutils2/core.py
+++ b/src/distutils2/core.py
@@ -33,6 +33,7 @@
    or: %(script)s cmd --help
 """
 
+
 def gen_usage(script_name):
     script = os.path.basename(script_name)
     return USAGE % {'script': script}
@@ -59,6 +60,7 @@
                       'extra_objects', 'extra_compile_args', 'extra_link_args',
                       'swig_opts', 'export_symbols', 'depends', 'language')
 
+
 def setup(**attrs):
     """The gateway to the Distutils: do everything your setup script needs
     to do, in a highly flexible and user-driven way.  Briefly: create a
diff --git a/src/distutils2/depgraph.py b/src/distutils2/depgraph.py
--- a/src/distutils2/depgraph.py
+++ b/src/distutils2/depgraph.py
@@ -1,5 +1,5 @@
-"""Analyse the relationships between the distributions in the system and generate
-a dependency graph.
+"""Analyse the relationships between the distributions in the system
+and generate a dependency graph.
 """
 
 from distutils2.errors import DistutilsError
@@ -135,8 +135,7 @@
         requires = dist.metadata['Requires-Dist'] + dist.metadata['Requires']
         for req in requires:
             predicate = VersionPredicate(req)
-            comps = req.strip().rsplit(" ", 1)
-            name = comps[0]
+            name = predicate.name
 
             if not name in provided:
                 graph.add_missing(dist, req)
diff --git a/src/distutils2/dist.py b/src/distutils2/dist.py
--- a/src/distutils2/dist.py
+++ b/src/distutils2/dist.py
@@ -6,12 +6,10 @@
 
 __revision__ = "$Id: dist.py 77717 2010-01-24 00:33:32Z tarek.ziade $"
 
-import sys, os, re
-
-try:
-    import warnings
-except ImportError:
-    warnings = None
+import sys
+import os
+import re
+import warnings
 
 from ConfigParser import RawConfigParser
 
@@ -26,7 +24,8 @@
 # the same as a Python NAME -- I don't allow leading underscores.  The fact
 # that they're very similar is no coincidence; the default naming scheme is
 # to look for a Python module named after the command.
-command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
+command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
+
 
 class Distribution(object):
     """The core of the Distutils.  Most of the work hiding behind 'setup'
@@ -42,7 +41,6 @@
     See the code for 'setup()', in core.py, for details.
     """
 
-
     # 'global_options' describes the command-line options that may be
     # supplied to the setup script prior to any actual commands.
     # Eg. "./setup.py -n" or "./setup.py --quiet" both take advantage of
@@ -125,10 +123,8 @@
     # negative options are options that exclude other options
     negative_opt = {'quiet': 'verbose'}
 
-
     # -- Creation/initialization methods -------------------------------
-
-    def __init__ (self, attrs=None):
+    def __init__(self, attrs=None):
         """Construct a new Distribution instance: initialize all the
         attributes of a Distribution, and then use 'attrs' (a dictionary
         mapping attribute names to values) to assign some of those
@@ -193,18 +189,18 @@
         # These options are really the business of various commands, rather
         # than of the Distribution itself.  We provide aliases for them in
         # Distribution as a convenience to the developer.
-        self.packages = None
+        self.packages = []
         self.package_data = {}
         self.package_dir = None
-        self.py_modules = None
-        self.libraries = None
-        self.headers = None
-        self.ext_modules = None
+        self.py_modules = []
+        self.libraries = []
+        self.headers = []
+        self.ext_modules = []
         self.ext_package = None
-        self.include_dirs = None
+        self.include_dirs = []
         self.extra_path = None
-        self.scripts = None
-        self.data_files = None
+        self.scripts = []
+        self.data_files = []
         self.password = ''
         self.use_2to3 = False
         self.convert_2to3_doctests = []
@@ -253,10 +249,7 @@
                     setattr(self, key, val)
                 else:
                     msg = "Unknown distribution option: %r" % key
-                    if warnings is not None:
-                        warnings.warn(msg)
-                    else:
-                        sys.stderr.write(msg + "\n")
+                    warnings.warn(msg)
 
         # no-user-cfg is handled before other command line args
         # because other args override the config files, and this
@@ -282,10 +275,10 @@
         and return the new dictionary; otherwise, return the existing
         option dictionary.
         """
-        dict = self.command_options.get(command)
-        if dict is None:
-            dict = self.command_options[command] = {}
-        return dict
+        d = self.command_options.get(command)
+        if d is None:
+            d = self.command_options[command] = {}
+        return d
 
     def get_fullname(self):
         return self.metadata.get_fullname()
@@ -381,7 +374,7 @@
 
                 for opt in options:
                     if opt != '__name__':
-                        val = parser.get(section,opt)
+                        val = parser.get(section, opt)
                         opt = opt.replace('-', '_')
 
                         # ... although practicality beats purity :(
@@ -410,12 +403,12 @@
                 try:
                     if alias:
                         setattr(self, alias, not strtobool(val))
-                    elif opt in ('verbose', 'dry_run'): # ugh!
+                    elif opt in ('verbose', 'dry_run'):  # ugh!
                         setattr(self, opt, strtobool(val))
                     else:
                         setattr(self, opt, val)
                 except ValueError, msg:
-                    raise DistutilsOptionError, msg
+                    raise DistutilsOptionError(msg)
 
     # -- Command-line parsing methods ----------------------------------
 
@@ -481,7 +474,7 @@
 
         # Oops, no commands found -- an end-user error
         if not self.commands:
-            raise DistutilsArgError, "no commands supplied"
+            raise DistutilsArgError("no commands supplied")
 
         # All is well: return true
         return 1
@@ -512,7 +505,7 @@
         # Pull the current command from the head of the command line
         command = args[0]
         if not command_re.match(command):
-            raise SystemExit, "invalid command name '%s'" % command
+            raise SystemExit("invalid command name '%s'" % command)
         self.commands.append(command)
 
         # Dig up the command class that implements this command, so we
@@ -521,22 +514,21 @@
         try:
             cmd_class = self.get_command_class(command)
         except DistutilsModuleError, msg:
-            raise DistutilsArgError, msg
+            raise DistutilsArgError(msg)
 
         # Require that the command class be derived from Command -- want
         # to be sure that the basic "command" interface is implemented.
         if not issubclass(cmd_class, Command):
-            raise DistutilsClassError, \
-                  "command class %s must subclass Command" % cmd_class
+            raise DistutilsClassError(
+                  "command class %s must subclass Command" % cmd_class)
 
         # Also make sure that the command object provides a list of its
         # known options.
         if not (hasattr(cmd_class, 'user_options') and
                 isinstance(cmd_class.user_options, list)):
-            raise DistutilsClassError, \
-                  ("command class %s must provide " +
-                   "'user_options' attribute (a list of tuples)") % \
-                  cmd_class
+            raise DistutilsClassError(
+                  ("command class %s must provide "
+                   "'user_options' attribute (a list of tuples)") % cmd_class)
 
         # If the command class has a list of negative alias options,
         # merge it in with the global negative aliases.
@@ -553,7 +545,6 @@
         else:
             help_options = []
 
-
         # All commands support the global options too, just by adding
         # in 'global_options'.
         parser.set_option_table(self.global_options +
@@ -567,10 +558,10 @@
 
         if (hasattr(cmd_class, 'help_options') and
             isinstance(cmd_class.help_options, list)):
-            help_option_found=0
+            help_option_found = 0
             for (help_option, short, desc, func) in cmd_class.help_options:
                 if hasattr(opts, parser.get_attr_name(help_option)):
-                    help_option_found=1
+                    help_option_found = 1
                     if hasattr(func, '__call__'):
                         func()
                     else:
@@ -708,25 +699,26 @@
 
             print("  %-*s  %s" % (max_length, cmd, description))
 
+    def _get_command_groups(self):
+        """Helper function to retrieve all the command class names divided
+        into standard commands (listed in distutils2.command.__all__)
+        and extra commands (given in self.cmdclass and not standard
+        commands).
+        """
+        from distutils2.command import __all__ as std_commands
+        extra_commands = [cmd for cmd in self.cmdclass
+                          if cmd not in std_commands]
+        return std_commands, extra_commands
+
     def print_commands(self):
         """Print out a help message listing all available commands with a
-        description of each.  The list is divided into "standard commands"
-        (listed in distutils2.command.__all__) and "extra commands"
-        (mentioned in self.cmdclass, but not a standard command).  The
+        description of each.  The list is divided into standard commands
+        (listed in distutils2.command.__all__) and extra commands
+        (given in self.cmdclass and not standard commands).  The
         descriptions come from the command class attribute
         'description'.
         """
-        import distutils2.command
-        std_commands = distutils2.command.__all__
-        is_std = {}
-        for cmd in std_commands:
-            is_std[cmd] = 1
-
-        extra_commands = []
-        for cmd in self.cmdclass.keys():
-            if not is_std.get(cmd):
-                extra_commands.append(cmd)
-
+        std_commands, extra_commands = self._get_command_groups()
         max_length = 0
         for cmd in (std_commands + extra_commands):
             if len(cmd) > max_length:
@@ -743,30 +735,17 @@
 
     def get_command_list(self):
         """Get a list of (command, description) tuples.
-        The list is divided into "standard commands" (listed in
-        distutils2.command.__all__) and "extra commands" (mentioned in
-        self.cmdclass, but not a standard command).  The descriptions come
+
+        The list is divided into standard commands (listed in
+        distutils2.command.__all__) and extra commands (given in
+        self.cmdclass and not standard commands).  The descriptions come
         from the command class attribute 'description'.
         """
         # Currently this is only used on Mac OS, for the Mac-only GUI
         # Distutils interface (by Jack Jansen)
 
-        import distutils2.command
-        std_commands = distutils2.command.__all__
-        is_std = {}
-        for cmd in std_commands:
-            is_std[cmd] = 1
-
-        extra_commands = []
-        for cmd in self.cmdclass.keys():
-            if not is_std.get(cmd):
-                extra_commands.append(cmd)
-
         rv = []
-        for cmd in (std_commands + extra_commands):
-            cls = self.cmdclass.get(cmd)
-            if not cls:
-                cls = self.get_command_class(cmd)
+        for cls in self.get_command_classes():
             try:
                 description = cls.description
             except AttributeError:
@@ -788,6 +767,23 @@
             self.command_packages = pkgs
         return pkgs
 
+    def get_command_names(self):
+        """Return a list of all command names."""
+        return [getattr(cls, 'command_name', cls.__name__)
+                for cls in self.get_command_classes()]
+
+    def get_command_classes(self):
+        """Return a list of all command classes."""
+        std_commands, extra_commands = self._get_command_groups()
+        classes = []
+        for cmd in (std_commands + extra_commands):
+            try:
+                cls = self.cmdclass[cmd]
+            except KeyError:
+                cls = self.get_command_class(cmd)
+            classes.append(cls)
+        return classes
+
     def get_command_class(self, command):
         """Return the class that implements the Distutils command named by
         'command'.  First we check the 'cmdclass' dictionary; if the
@@ -809,7 +805,7 @@
             class_name = command
 
             try:
-                __import__ (module_name)
+                __import__(module_name)
                 module = sys.modules[module_name]
             except ImportError:
                 continue
@@ -817,16 +813,15 @@
             try:
                 cls = getattr(module, class_name)
             except AttributeError:
-                raise DistutilsModuleError, \
-                      "invalid command '%s' (no class '%s' in module '%s')" \
-                      % (command, class_name, module_name)
+                raise DistutilsModuleError(
+                      "invalid command '%s' (no class '%s' in module '%s')" %
+                      (command, class_name, module_name))
 
             self.cmdclass[command] = cls
             return cls
 
         raise DistutilsModuleError("invalid command '%s'" % command)
 
-
     def get_command_obj(self, command, create=1):
         """Return the command object for 'command'.  Normally this object
         is cached on a previous call to 'get_command_obj()'; if no command
@@ -889,11 +884,11 @@
                 elif hasattr(command_obj, option):
                     setattr(command_obj, option, value)
                 else:
-                    raise DistutilsOptionError, \
-                          ("error in %s: command '%s' has no such option '%s'"
-                           % (source, command_name, option))
+                    raise DistutilsOptionError(
+                        "error in %s: command '%s' has no such option '%s'" %
+                        (source, command_name, option))
             except ValueError, msg:
-                raise DistutilsOptionError, msg
+                raise DistutilsOptionError(msg)
 
     def get_reinitialized_command(self, command, reinit_subcommands=0):
         """Reinitializes a command to the state it was in when first
@@ -969,7 +964,6 @@
         self.run_command_hooks(cmd_obj, 'post_hook')
         self.have_run[command] = 1
 
-
     def run_command_hooks(self, cmd_obj, hook_kind):
         hooks = getattr(cmd_obj, hook_kind)
         if hooks is None:
@@ -979,7 +973,6 @@
             hook_func(cmd_obj)
 
     # -- Distribution query methods ------------------------------------
-
     def has_pure_modules(self):
         return len(self.packages or self.py_modules or []) > 0
 
@@ -1006,13 +999,8 @@
                 not self.has_ext_modules() and
                 not self.has_c_libraries())
 
-    # -- Metadata query methods ----------------------------------------
 
-    # If you're looking for 'get_name()', 'get_version()', and so forth,
-    # they are defined in a sneaky way: the constructor binds self.get_XXX
-    # to self.metadata.get_XXX.  The actual code is in the
-    # DistributionMetadata class, below.
-
+# XXX keep for compat or remove?
 
 def fix_help_options(options):
     """Convert a 4-tuple 'help_options' list as found in various command
diff --git a/src/distutils2/errors.py b/src/distutils2/errors.py
--- a/src/distutils2/errors.py
+++ b/src/distutils2/errors.py
@@ -10,31 +10,38 @@
 
 __revision__ = "$Id: errors.py 75901 2009-10-28 06:45:18Z tarek.ziade $"
 
+
 class DistutilsError(Exception):
     """The root of all Distutils evil."""
 
+
 class DistutilsModuleError(DistutilsError):
     """Unable to load an expected module, or to find an expected class
     within some module (in particular, command modules and classes)."""
 
+
 class DistutilsClassError(DistutilsError):
     """Some command class (or possibly distribution class, if anyone
     feels a need to subclass Distribution) is found not to be holding
     up its end of the bargain, ie. implementing some part of the
     "command "interface."""
 
+
 class DistutilsGetoptError(DistutilsError):
     """The option table provided to 'fancy_getopt()' is bogus."""
 
+
 class DistutilsArgError(DistutilsError):
     """Raised by fancy_getopt in response to getopt.error -- ie. an
     error in the command line usage."""
 
+
 class DistutilsFileError(DistutilsError):
     """Any problems in the filesystem: expected file not found, etc.
     Typically this is for problems that we detect before IOError or
     OSError could be raised."""
 
+
 class DistutilsOptionError(DistutilsError):
     """Syntactic/semantic errors in command options, such as use of
     mutually conflicting options, or inconsistent options,
@@ -43,60 +50,76 @@
     files, or what-have-you -- but if we *know* something originated in
     the setup script, we'll raise DistutilsSetupError instead."""
 
+
 class DistutilsSetupError(DistutilsError):
     """For errors that can be definitely blamed on the setup script,
     such as invalid keyword arguments to 'setup()'."""
 
+
 class DistutilsPlatformError(DistutilsError):
     """We don't know how to do something on the current platform (but
     we do know how to do it on some platform) -- eg. trying to compile
     C files on a platform not supported by a CCompiler subclass."""
 
+
 class DistutilsExecError(DistutilsError):
     """Any problems executing an external program (such as the C
     compiler, when compiling C files)."""
 
+
 class DistutilsInternalError(DistutilsError):
     """Internal inconsistencies or impossibilities (obviously, this
     should never be seen if the code is working!)."""
 
+
 class DistutilsTemplateError(DistutilsError):
     """Syntax error in a file list template."""
 
+
 class DistutilsByteCompileError(DistutilsError):
     """Byte compile error."""
 
+
 # Exception classes used by the CCompiler implementation classes
 class CCompilerError(Exception):
     """Some compile/link operation failed."""
 
+
 class PreprocessError(CCompilerError):
     """Failure to preprocess one or more C/C++ files."""
 
+
 class CompileError(CCompilerError):
     """Failure to compile one or more C/C++ source files."""
 
+
 class LibError(CCompilerError):
     """Failure to create a static library from one or more C/C++ object
     files."""
 
+
 class LinkError(CCompilerError):
     """Failure to link one or more C/C++ object files into an executable
     or shared library file."""
 
+
 class UnknownFileError(CCompilerError):
     """Attempt to process an unknown file type."""
 
+
 class MetadataConflictError(DistutilsError):
     """Attempt to read or write metadata fields that are conflictual."""
 
+
 class MetadataUnrecognizedVersionError(DistutilsError):
     """Unknown metadata version number."""
 
+
 class IrrationalVersionError(Exception):
     """This is an irrational version."""
     pass
 
+
 class HugeMajorVersionNumError(IrrationalVersionError):
     """An irrational version because the major version number is huge
     (often because a year or date was used).
@@ -105,4 +128,3 @@
     This guard can be disabled by setting that option False.
     """
     pass
-
diff --git a/src/distutils2/extension.py b/src/distutils2/extension.py
--- a/src/distutils2/extension.py
+++ b/src/distutils2/extension.py
@@ -17,6 +17,7 @@
 # import that large-ish module (indirectly, through distutils.core) in
 # order to do anything.
 
+
 class Extension(object):
     """Just a collection of attributes that describes an extension
     module and everything needed to build it (hopefully in a portable
@@ -84,7 +85,7 @@
 
     # When adding arguments to this constructor, be sure to update
     # setup_keywords in core.py.
-    def __init__ (self, name, sources,
+    def __init__(self, name, sources,
                   include_dirs=None,
                   define_macros=None,
                   undef_macros=None,
@@ -95,11 +96,11 @@
                   extra_compile_args=None,
                   extra_link_args=None,
                   export_symbols=None,
-                  swig_opts = None,
+                  swig_opts=None,
                   depends=None,
                   language=None,
                   optional=None,
-                  **kw                      # To catch unknown keywords
+                  **kw # To catch unknown keywords
                  ):
         if not isinstance(name, str):
             raise AssertionError("'name' must be a string")
@@ -134,4 +135,3 @@
             options = ', '.join(sorted(options))
             msg = "Unknown Extension options: %s" % options
             warnings.warn(msg)
-
diff --git a/src/distutils2/fancy_getopt.py b/src/distutils2/fancy_getopt.py
--- a/src/distutils2/fancy_getopt.py
+++ b/src/distutils2/fancy_getopt.py
@@ -30,6 +30,7 @@
 # (for use as attributes of some object).
 longopt_xlate = string.maketrans('-', '_')
 
+
 class FancyGetopt(object):
     """Wrapper around the standard 'getopt()' module that provides some
     handy extra functionality:
@@ -42,7 +43,7 @@
         on the command line sets 'verbose' to false
     """
 
-    def __init__ (self, option_table=None):
+    def __init__(self, option_table=None):
 
         # The option table is (currently) a list of tuples.  The
         # tuples may have 3 or four values:
@@ -180,7 +181,8 @@
             self.long_opts.append(long)
 
             if long[-1] == '=':             # option takes an argument?
-                if short: short = short + ':'
+                if short:
+                    short = short + ':'
                 long = long[0:-1]
                 self.takes_arg[long] = 1
             else:
diff --git a/src/distutils2/index/base.py b/src/distutils2/index/base.py
--- a/src/distutils2/index/base.py
+++ b/src/distutils2/index/base.py
@@ -1,4 +1,3 @@
-from distutils2.version import VersionPredicate
 from distutils2.index.dist import ReleasesList
 
 
@@ -10,14 +9,6 @@
         self._prefer_source = prefer_source
         self._index = self
 
-    def _get_version_predicate(self, requirements):
-        """Return a VersionPredicate object, from a string or an already
-        existing object.
-        """
-        if isinstance(requirements, str):
-            requirements = VersionPredicate(requirements)
-        return requirements
-
     def _get_prefer_final(self, prefer_final=None):
         """Return the prefer_final internal parameter or the specified one if
         provided"""
diff --git a/src/distutils2/index/dist.py b/src/distutils2/index/dist.py
--- a/src/distutils2/index/dist.py
+++ b/src/distutils2/index/dist.py
@@ -26,7 +26,8 @@
 from distutils2.errors import IrrationalVersionError
 from distutils2.index.errors import (HashDoesNotMatch, UnsupportedHashName,
                                      CantParseArchiveName)
-from distutils2.version import suggest_normalized_version, NormalizedVersion
+from distutils2.version import (suggest_normalized_version, NormalizedVersion,
+                                get_version_predicate)
 from distutils2.metadata import DistributionMetadata
 from distutils2.util import untar_file, unzip_file, splitext
 
@@ -38,6 +39,7 @@
 
 
 class IndexReference(object):
+    """Mixin used to store the index reference"""
     def set_index(self, index=None):
         self._index = index
 
@@ -64,10 +66,10 @@
         self._version = None
         self.version = version
         if metadata:
-            self._metadata = DistributionMetadata(mapping=metadata)
+            self.metadata = DistributionMetadata(mapping=metadata)
         else:
-            self._metadata = None
-        self._dists = {}
+            self.metadata = None
+        self.dists = {}
         self.hidden = hidden
 
         if 'dist_type' in kwargs:
@@ -89,25 +91,23 @@
 
     version = property(get_version, set_version)
 
-    @property
-    def metadata(self):
+    def fetch_metadata(self):
         """If the metadata is not set, use the indexes to get it"""
-        if not self._metadata:
+        if not self.metadata:
             self._index.get_metadata(self.name, '%s' % self.version)
-        return self._metadata
+        return self.metadata
 
     @property
     def is_final(self):
         """proxy to version.is_final"""
         return self.version.is_final
     
-    @property
-    def dists(self):
-        if self._dists is None:
+    def fetch_distributions(self):
+        if self.dists is None:
             self._index.get_distributions(self.name, '%s' % self.version)
-            if self._dists is None:
-                self._dists = {}
-        return self._dists
+            if self.dists is None:
+                self.dists = {}
+        return self.dists
 
     def add_distribution(self, dist_type='sdist', python_version=None, **params):
         """Add distribution informations to this release.
@@ -122,12 +122,12 @@
         if dist_type not in DIST_TYPES:
             raise ValueError(dist_type)
         if dist_type in self.dists:
-            self._dists[dist_type].add_url(**params)
+            self.dists[dist_type].add_url(**params)
         else:
-            self._dists[dist_type] = DistInfo(self, dist_type,
+            self.dists[dist_type] = DistInfo(self, dist_type,
                                              index=self._index, **params)
         if python_version:
-            self._dists[dist_type].python_version = python_version
+            self.dists[dist_type].python_version = python_version 
 
     def get_distribution(self, dist_type=None, prefer_source=True):
         """Return a distribution.
@@ -163,9 +163,9 @@
                    .download(path=temp_path)
 
     def set_metadata(self, metadata):
-        if not self._metadata:
-            self._metadata = DistributionMetadata()
-        self._metadata.update(metadata)
+        if not self.metadata:
+            self.metadata = DistributionMetadata()
+        self.metadata.update(metadata)
 
     def __getitem__(self, item):
         """distributions are available using release["sdist"]"""
@@ -351,18 +351,12 @@
     """
     def __init__(self, name, releases=None, contains_hidden=False, index=None):
         self.set_index(index)
-        self._releases = []
+        self.releases = [] 
         self.name = name
         self.contains_hidden = contains_hidden
         if releases:
             self.add_releases(releases)
 
-    @property
-    def releases(self):
-        if not self._releases:
-            self.fetch_releases()
-        return self._releases
-    
     def fetch_releases(self):
         self._index.get_releases(self.name)
         return self.releases
@@ -374,12 +368,13 @@
                                         if predicate.match(release.version)],
                                         index=self._index)
 
-    def get_last(self, predicate, prefer_final=None):
+    def get_last(self, requirements, prefer_final=None):
         """Return the "last" release, that satisfy the given predicates.
 
         "last" is defined by the version number of the releases, you also could
         set prefer_final parameter to True or False to change the order results
         """
+        predicate = get_version_predicate(requirements)
         releases = self.filter(predicate)
         releases.sort_releases(prefer_final, reverse=True)
         return releases[0]
@@ -412,16 +407,16 @@
                 
             if not version in self.get_versions():
                 # append only if not already exists
-                self._releases.append(release)
+                self.releases.append(release)
             for dist in release.dists.values():
                 for url in dist.urls:
                     self.add_release(version, dist.dist_type, **url)
         else:
-            matches = [r for r in self._releases if '%s' % r.version == version
+            matches = [r for r in self.releases if '%s' % r.version == version
                                                  and r.name == self.name]
             if not matches:
                 release = ReleaseInfo(self.name, version, index=self._index)
-                self._releases.append(release)
+                self.releases.append(release)
             else:
                 release = matches[0]
 
@@ -459,7 +454,7 @@
 
     def get_versions(self):
         """Return a list of releases versions contained"""
-        return ["%s" % r.version for r in self._releases]
+        return ["%s" % r.version for r in self.releases]
 
     def __getitem__(self, key):
         return self.releases[key]
diff --git a/src/distutils2/index/simple.py b/src/distutils2/index/simple.py
--- a/src/distutils2/index/simple.py
+++ b/src/distutils2/index/simple.py
@@ -22,6 +22,7 @@
                                      ReleaseNotFound, ProjectNotFound)
 from distutils2.index.mirrors import get_mirrors
 from distutils2.metadata import DistributionMetadata
+from distutils2.version import get_version_predicate
 from distutils2 import __version__ as __distutils2_version__
 
 __all__ = ['Crawler', 'DEFAULT_SIMPLE_INDEX_URL']
@@ -158,7 +159,7 @@
         """Search for releases and return a ReleaseList object containing
         the results.
         """
-        predicate = self._get_version_predicate(requirements)
+        predicate = get_version_predicate(requirements)
         if self._projects.has_key(predicate.name.lower()) and not force_update:
             return self._projects.get(predicate.name.lower())
         prefer_final = self._get_prefer_final(prefer_final)
@@ -173,7 +174,7 @@
 
     def get_release(self, requirements, prefer_final=None):
         """Return only one release that fulfill the given requirements"""
-        predicate = self._get_version_predicate(requirements)
+        predicate = get_version_predicate(requirements)
         release = self.get_releases(predicate, prefer_final)\
                       .get_last(predicate)
         if not release:
diff --git a/src/distutils2/index/xmlrpc.py b/src/distutils2/index/xmlrpc.py
--- a/src/distutils2/index/xmlrpc.py
+++ b/src/distutils2/index/xmlrpc.py
@@ -3,8 +3,10 @@
 
 from distutils2.errors import IrrationalVersionError
 from distutils2.index.base import BaseClient
-from distutils2.index.errors import ProjectNotFound, InvalidSearchField
+from distutils2.index.errors import (ProjectNotFound, InvalidSearchField,
+                                     ReleaseNotFound)
 from distutils2.index.dist import ReleaseInfo
+from distutils2.version import get_version_predicate
 
 __all__ = ['Client', 'DEFAULT_XMLRPC_INDEX_URL']
 
@@ -41,7 +43,7 @@
         related informations.
         """
         prefer_final = self._get_prefer_final(prefer_final)
-        predicate = self._get_version_predicate(requirements)
+        predicate = get_version_predicate(requirements)
         releases = self.get_releases(predicate.name)
         release = releases.get_last(predicate, prefer_final)
         self.get_metadata(release.name, "%s" % release.version)
@@ -72,7 +74,7 @@
         def get_versions(project_name, show_hidden):
             return self.proxy.package_releases(project_name, show_hidden)
 
-        predicate = self._get_version_predicate(requirements)
+        predicate = get_version_predicate(requirements)
         prefer_final = self._get_prefer_final(prefer_final)
         project_name = predicate.name
         if not force_update and (project_name.lower() in self._projects):
@@ -96,6 +98,8 @@
                                               index=self._index)
                                   for version in versions])
         project = project.filter(predicate)
+        if len(project) == 0:
+            raise ReleaseNotFound("%s" % predicate)
         project.sort_releases(prefer_final)
         return project
 
diff --git a/src/distutils2/install_with_deps.py b/src/distutils2/install_with_deps.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/install_with_deps.py
@@ -0,0 +1,171 @@
+import logging
+from distutils2.index import wrapper
+from distutils2.index.errors import ProjectNotFound, ReleaseNotFound
+from distutils2.depgraph import generate_graph
+from distutils2._backport.pkgutil import get_distributions
+
+
+"""Provides installations scripts.
+
+The goal of this script is to install a release from the indexes (eg.
+PyPI), including the dependencies of the releases if needed.
+
+It uses the work made in pkgutil and by the index crawlers to browse the
+installed distributions, and rely on the instalation commands to install.
+"""
+
+
+def get_deps(requirements, index):
+    """Return the dependencies of a project, as a depgraph object.
+
+    Build a :class depgraph.DependencyGraph: for the given requirements
+
+    If the project does not uses Metadata < 1.1, dependencies can't be handled
+    from here, so it returns an empty list of dependencies.
+
+    :param requirements: is a string containing the version predicate to take
+                         the project name and version specifier from.
+    :param index: the index to use for making searches.
+    """
+    deps = []
+    release = index.get_release(requirements)
+    requires = release.metadata['Requires-Dist'] + release.metadata['Requires']
+    deps.append(release) # include the release we are computing deps.
+    for req in requires:
+        deps.extend(get_deps(req, index))
+    return deps
+
+
+def install(requirements, index=None, interactive=True, upgrade=True,
+            prefer_source=True, prefer_final=True):
+    """Given a list of distributions to install, a list of distributions to
+    remove, and a list of conflicts, proceed and do what's needed to be done.
+
+    :param requirements: is a *string* containing the requirements for this
+                         project (for instance "FooBar 1.1" or "BarBaz (<1.2)
+    :param index: If an index is specified, use this one, otherwise, use
+                  :class index.ClientWrapper: to get project metadatas.
+    :param interactive: if set to True, will prompt the user for interactions
+                        of needed. If false, use the default values.
+    :param upgrade: If a project exists in a newer version, does the script
+                    need to install the new one, or keep the already installed
+                    version.
+    :param prefer_source: used to tell if the user prefer source distributions
+                          over built dists.
+    :param prefer_final: if set to true, pick up the "final" versions (eg.
+                         stable) over the beta, alpha (not final) ones.
+    """
+    # get the default index if none is specified
+    if not index:
+        index = wrapper.WrapperClient()
+
+    # check if the project is already installed.
+    installed_release = get_installed_release(requirements)
+
+    # if a version that satisfy the requirements is already installed
+    if installed_release and (interactive or upgrade):
+        new_releases = index.get_releases(requirements)
+        if (new_releases.get_last(requirements).version >
+            installed_release.version):
+            if interactive:
+                # prompt the user to install the last version of the package.
+                # set upgrade here.
+                print "You want to install a package already installed on your"
+                "system. A new version exists, you could just use the version"
+                "you have, or upgrade to the latest version"
+
+                upgrade = raw_input("Do you want to install the most recent one ? (Y/n)") or "Y"
+                if upgrade in ('Y', 'y'):
+                    upgrade = True
+                else:
+                    upgrade = False
+            if not upgrade:
+                return
+
+    # create the depgraph from the dependencies of the release we want to
+    # install
+    graph = generate_graph(get_deps(requirements, index))
+    from ipdb import set_trace
+    set_trace()
+    installed = [] # to uninstall on errors
+    try:
+        for release in graph.adjacency_list:
+            dist = release.get_distribution()
+            install(dist)
+            installed.append(dist)
+            print "%s have been installed on your system" % requirements
+    except:
+        print "an error has occured, uninstalling"
+        for dist in installed:
+            uninstall_dist(dist)
+
+class InstallationException(Exception):
+    pass
+
+def get_install_info(requirements, index=None, already_installed=None):
+    """Return the informations on what's going to be installed and upgraded.
+
+    :param requirements: is a *string* containing the requirements for this
+                         project (for instance "FooBar 1.1" or "BarBaz (<1.2)")
+    :param index: If an index is specified, use this one, otherwise, use
+                  :class index.ClientWrapper: to get project metadatas.
+    :param already_installed: a list of already installed distributions.
+
+    The results are returned in a dict. For instance::
+
+        >>> get_install_info("FooBar (<=1.2)")
+        {'install': [<FooBar 1.1>], 'remove': [], 'conflict': []}
+
+    Conflict contains all the conflicting distributions, if there is a
+    conflict.
+
+    """
+    def update_infos(new_infos, infos):
+        for key, value in infos.items():
+            if key in new_infos:
+                infos[key].extend(new_infos[key])
+        return new_infos
+
+    if not index:
+        index = wrapper.ClientWrapper()
+    logging.info("searching releases for %s" % requirements)
+
+    # 1. get all the releases that match the requirements
+    try:
+        releases = index.get_releases(requirements)
+    except (ReleaseNotFound, ProjectNotFound), e:
+       raise InstallationException('Release not found: "%s"' % requirements)
+
+    # 2. pick up a release, and try to get the dependency tree
+    release = releases.get_last(requirements)
+    metadata = release.fetch_metadata()
+
+    # 3. get the distributions already_installed on the system
+    # 4. and add the one we want to install
+    if not already_installed:
+        already_installed = get_distributions()
+
+    logging.info("fetching %s %s dependencies" % (
+                 release.name, release.version))
+    distributions = already_installed + [release]
+    depgraph = generate_graph(distributions)
+
+    # store all the already_installed packages in a list, in case of rollback.
+    infos = {'install':[], 'remove': [], 'conflict': []}
+
+    # 5. get what the missing deps are
+    for dists in depgraph.missing.values():
+        if dists:
+            logging.info("missing dependencies found, installing them")
+            # we have missing deps
+            for dist in dists:
+                update_infos(get_install_info(dist, index, already_installed),
+                             infos)
+
+    # 6. fill in the infos
+    existing = [d for d in already_installed if d.name == release.name]
+    if existing:
+        infos['remove'].append(existing[0])
+        infos['conflict'].extend(depgraph.reverse_list[existing[0]])
+    infos['install'].append(release)
+    return infos
diff --git a/src/distutils2/manifest.py b/src/distutils2/manifest.py
--- a/src/distutils2/manifest.py
+++ b/src/distutils2/manifest.py
@@ -22,7 +22,7 @@
 
 # a \ followed by some spaces + EOL
 _COLLAPSE_PATTERN = re.compile('\\\w*\n', re.M)
-_COMMENTED_LINE = re.compile('#.*?(?=\n)|^\w*\n|\n(?=$)', re.M|re.S)
+_COMMENTED_LINE = re.compile('#.*?(?=\n)|^\w*\n|\n(?=$)', re.M | re.S)
 
 class Manifest(object):
     """A list of files built by on exploring the filesystem and filtered by
diff --git a/src/distutils2/metadata.py b/src/distutils2/metadata.py
--- a/src/distutils2/metadata.py
+++ b/src/distutils2/metadata.py
@@ -3,10 +3,10 @@
 Supports all metadata formats (1.0, 1.1, 1.2).
 """
 
-import re
 import os
 import sys
 import platform
+import re
 from StringIO import StringIO
 from email import message_from_file
 from tokenize import tokenize, NAME, OP, STRING, ENDMARKER
@@ -52,12 +52,12 @@
 PKG_INFO_PREFERRED_VERSION = '1.0'
 
 _LINE_PREFIX = re.compile('\n       \|')
-_241_FIELDS = ('Metadata-Version',  'Name', 'Version', 'Platform',
+_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
                'Summary', 'Description',
                'Keywords', 'Home-page', 'Author', 'Author-email',
                'License')
 
-_314_FIELDS = ('Metadata-Version',  'Name', 'Version', 'Platform',
+_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
                'Supported-Platform', 'Summary', 'Description',
                'Keywords', 'Home-page', 'Author', 'Author-email',
                'License', 'Classifier', 'Download-URL', 'Obsoletes',
@@ -65,7 +65,7 @@
 
 _314_MARKERS = ('Obsoletes', 'Provides', 'Requires')
 
-_345_FIELDS = ('Metadata-Version',  'Name', 'Version', 'Platform',
+_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
                'Supported-Platform', 'Summary', 'Description',
                'Keywords', 'Home-page', 'Author', 'Author-email',
                'Maintainer', 'Maintainer-email', 'License',
@@ -92,6 +92,7 @@
         return _345_FIELDS
     raise MetadataUnrecognizedVersionError(version)
 
+
 def _best_version(fields):
     """Detect the best version depending on the fields used."""
     def _has_marker(keys, markers):
@@ -349,7 +350,7 @@
         Empty values (e.g. None and []) are not setted this way.
         """
         def _set(key, value):
-            if value not in ([], None) and key in _ATTR2FIELD:
+            if value not in ([], None, '') and key in _ATTR2FIELD:
                 self.set(self._convert_name(key), value)
 
         if other is None:
@@ -387,13 +388,16 @@
             for v in value:
                 # check that the values are valid predicates
                 if not is_valid_predicate(v.split(';')[0]):
-                    warn('"%s" is not a valid predicate' % v)
+                    warn('"%s" is not a valid predicate (field "%s")' %
+                         (v, name))
         elif name in _VERSIONS_FIELDS and value is not None:
             if not is_valid_versions(value):
-                warn('"%s" is not a valid predicate' % value)
+                warn('"%s" is not a valid version (field "%s")' %
+                     (value, name))
         elif name in _VERSION_FIELDS and value is not None:
             if not is_valid_version(value):
-                warn('"%s" is not a valid version' % value)
+                warn('"%s" is not a valid version (field "%s")' %
+                     (value, name))
 
         if name in _UNICODEFIELDS:
             value = self._encode_field(value)
@@ -448,7 +452,7 @@
                 missing.append(attr)
 
         if _HAS_DOCUTILS:
-            warnings = self._check_rst_data(self['Description'])
+            warnings.extend(self._check_rst_data(self['Description']))
 
         # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
         if self['Metadata-Version'] != '1.2':
@@ -497,6 +501,7 @@
               'in': lambda x, y: x in y,
               'not in': lambda x, y: x not in y}
 
+
 def _operate(operation, x, y):
     return _OPERATORS[operation](x, y)
 
@@ -508,6 +513,7 @@
          'platform.version': platform.version(),
          'platform.machine': platform.machine()}
 
+
 class _Operation(object):
 
     def __init__(self, execution_context=None):
@@ -568,6 +574,7 @@
         right = self._convert(self.right)
         return _operate(self.op, left, right)
 
+
 class _OR(object):
     def __init__(self, left, right=None):
         self.left = left
@@ -597,6 +604,7 @@
     def __call__(self):
         return self.left() and self.right()
 
+
 class _CHAIN(object):
 
     def __init__(self, execution_context=None):
@@ -658,6 +666,7 @@
                 return False
         return True
 
+
 def _interpret(marker, execution_context=None):
     """Interpret a marker and return a result depending on environment."""
     marker = marker.strip()
diff --git a/src/distutils2/mkpkg.py b/src/distutils2/mkpkg.py
--- a/src/distutils2/mkpkg.py
+++ b/src/distutils2/mkpkg.py
@@ -30,7 +30,11 @@
 #
 #    Detect scripts (not sure how.  #! outside of package?)
 
-import sys, os, re, shutil, ConfigParser
+import sys
+import os
+import re
+import shutil
+import ConfigParser
 
 
 helpText = {
@@ -629,19 +633,20 @@
         print '\nERROR: You must select "Y" or "N".\n'
 
 
-def ask(question, default = None, helptext = None, required = True,
-        lengthy = False, multiline = False):
-    prompt = '%s: ' % ( question, )
+def ask(question, default=None, helptext=None, required=True,
+        lengthy=False, multiline=False):
+    prompt = '%s: ' % (question,)
     if default:
-        prompt = '%s [%s]: ' % ( question, default )
+        prompt = '%s [%s]: ' % (question, default)
         if default and len(question) + len(default) > 70:
-            prompt = '%s\n    [%s]: ' % ( question, default )
+            prompt = '%s\n    [%s]: ' % (question, default)
     if lengthy or multiline:
         prompt += '\n   >'
 
-    if not helptext: helptext = 'No additional help available.'
-    if helptext[0] == '\n': helptext = helptext[1:]
-    if helptext[-1] == '\n': helptext = helptext[:-1]
+    if not helptext:
+        helptext = 'No additional help available.'
+
+    helptext = helptext.strip("\n")
 
     while True:
         sys.stdout.write(prompt)
@@ -653,12 +658,14 @@
             print helptext
             print '=' * 70
             continue
-        if default and not line: return(default)
+        if default and not line:
+            return(default)
         if not line and required:
             print '*' * 70
             print 'This value cannot be empty.'
             print '==========================='
-            if helptext: print helptext
+            if helptext:
+                print helptext
             print '*' * 70
             continue
         return(line)
@@ -669,7 +676,8 @@
     for key in troveList:
         subDict = dict
         for subkey in key.split(' :: '):
-            if not subkey in subDict: subDict[subkey] = {}
+            if not subkey in subDict:
+                subDict[subkey] = {}
             subDict = subDict[subkey]
     return(dict)
 troveDict = buildTroveDict(troveList)
@@ -687,7 +695,8 @@
 
 
     def lookupOption(self, key):
-        if not self.config.has_option('DEFAULT', key): return(None)
+        if not self.config.has_option('DEFAULT', key):
+            return(None)
         return(self.config.get('DEFAULT', key))
 
 
@@ -706,7 +715,8 @@
                 self.config.set('DEFAULT', compareKey,
                     self.setupData[compareKey])
 
-        if not valuesDifferent: return
+        if not valuesDifferent:
+            return
 
         self.config.write(open(os.path.expanduser('~/.pygiver'), 'w'))
 
@@ -718,7 +728,7 @@
     def inspectFile(self, path):
         fp = open(path, 'r')
         try:
-            for line in [ fp.readline() for x in range(10) ]:
+            for line in [fp.readline() for x in range(10)]:
                 m = re.match(r'^#!.*python((?P<major>\d)(\.\d+)?)?$', line)
                 if m:
                     if m.group('major') == '3':
@@ -761,16 +771,16 @@
               self.setupData.get('version'), helpText['version'])
         self.setupData['description'] = ask('Package description',
               self.setupData.get('description'), helpText['description'],
-              lengthy = True)
+              lengthy=True)
         self.setupData['author'] = ask('Author name',
               self.setupData.get('author'), helpText['author'])
         self.setupData['author_email'] = ask('Author e-mail address',
               self.setupData.get('author_email'), helpText['author_email'])
         self.setupData['url'] = ask('Project URL',
-              self.setupData.get('url'), helpText['url'], required = False)
+              self.setupData.get('url'), helpText['url'], required=False)
 
         if (askYn('Do you want to set Trove classifiers?',
-                helptext = helpText['do_classifier']) == 'y'):
+                helptext=helpText['do_classifier']) == 'y'):
             self.setTroveClassifier()
 
 
@@ -781,8 +791,10 @@
 
 
     def setTroveOther(self, classifierDict):
-        if askYn('Do you want to set other trove identifiers', 'n',
-                helpText['trove_generic']) != 'y': return
+        if askYn('Do you want to set other trove identifiers',
+                'n',
+                helpText['trove_generic']) != 'y':
+            return
 
         self.walkTrove(classifierDict, [troveDict], '')
 
@@ -799,7 +811,7 @@
                 continue
 
             if askYn('Do you want to set items under\n   "%s" (%d sub-items)'
-                    % ( key, len(trove[key]) ), 'n',
+                    % (key, len(trove[key])), 'n',
                     helpText['trove_generic']) == 'y':
                 self.walkTrove(classifierDict, trovePath + [trove[key]],
                         desc + ' :: ' + key)
@@ -808,15 +820,18 @@
     def setTroveLicense(self, classifierDict):
         while True:
             license = ask('What license do you use',
-                  helptext = helpText['trove_license'], required = False)
-            if not license: return
+                        helptext=helpText['trove_license'],
+                        required=False)
+            if not license:
+                return
 
             licenseWords = license.lower().split(' ')
 
             foundList = []
             for index in range(len(troveList)):
                 troveItem = troveList[index]
-                if not troveItem.startswith('License :: '): continue
+                if not troveItem.startswith('License :: '):
+                    continue
                 troveItem = troveItem[11:].lower()
 
                 allMatch = True
@@ -824,17 +839,20 @@
                     if not word in troveItem:
                         allMatch = False
                         break
-                if allMatch: foundList.append(index)
+                if allMatch:
+                    foundList.append(index)
 
             question = 'Matching licenses:\n\n'
             for i in xrange(1, len(foundList) + 1):
-                question += '   %s) %s\n' % ( i, troveList[foundList[i - 1]] )
+                question += '   %s) %s\n' % (i, troveList[foundList[i - 1]])
             question += ('\nType the number of the license you wish to use or '
                      '? to try again:')
-            troveLicense = ask(question, required = False)
+            troveLicense = ask(question, required=False)
 
-            if troveLicense == '?': continue
-            if troveLicense == '': return
+            if troveLicense == '?':
+                continue
+            if troveLicense == '':
+                return
             foundIndex = foundList[int(troveLicense) - 1]
             classifierDict[troveList[foundIndex]] = 1
             try:
@@ -856,7 +874,7 @@
 6 - Mature
 7 - Inactive
 
-Status''', required = False)
+Status''', required=False)
             if devStatus:
                 try:
                     key = {
@@ -884,7 +902,8 @@
         return modified_pkgs
 
     def writeSetup(self):
-        if os.path.exists('setup.py'): shutil.move('setup.py', 'setup.py.old')
+        if os.path.exists('setup.py'):
+            shutil.move('setup.py', 'setup.py.old')
 
         fp = open('setup.py', 'w')
         try:
diff --git a/src/distutils2/tests/pypi_server.py b/src/distutils2/tests/pypi_server.py
--- a/src/distutils2/tests/pypi_server.py
+++ b/src/distutils2/tests/pypi_server.py
@@ -1,13 +1,37 @@
-"""Mocked PyPI Server implementation, to use in tests.
+"""Mock PyPI Server implementation, to use in tests.
 
 This module also provides a simple test case to extend if you need to use
-the PyPIServer all along your test case. Be sure to read the documentation 
+the PyPIServer all along your test case. Be sure to read the documentation
 before any use.
+
+XXX TODO:
+
+The mock server can handle simple HTTP request (to simulate a simple index) or
+XMLRPC requests, over HTTP. Both does not have the same intergface to deal
+with, and I think it's a pain.
+
+A good idea could be to re-think a bit the way dstributions are handled in the
+mock server. As it should return malformed HTML pages, we need to keep the
+static behavior.
+
+I think of something like that:
+
+    >>> server = PyPIMockServer()
+    >>> server.startHTTP()
+    >>> server.startXMLRPC()
+
+Then, the server must have only one port to rely on, eg.
+
+    >>> server.fulladress()
+    "http://ip:port/"
+
+It could be simple to have one HTTP server, relaying the requests to the two
+implementations (static HTTP and XMLRPC over HTTP).
 """
 
-import os
 import Queue
 import SocketServer
+import os.path
 import select
 import socket
 import threading
@@ -29,7 +53,7 @@
     return use_pypi_server(*server_args, **server_kwargs)
 
 def use_pypi_server(*server_args, **server_kwargs):
-    """Decorator to make use of the PyPIServer for test methods, 
+    """Decorator to make use of the PyPIServer for test methods,
     just when needed, and not for the entire duration of the testcase.
     """
     def wrapper(func):
@@ -51,8 +75,8 @@
         self.pypi.start()
 
     def tearDown(self):
+        super(PyPIServerTestCase, self).tearDown()
         self.pypi.stop()
-        super(PyPIServerTestCase, self).tearDown()
 
 class PyPIServer(threading.Thread):
     """PyPI Mocked server.
@@ -65,8 +89,8 @@
                  static_filesystem_paths=["default"],
                  static_uri_paths=["simple"], serve_xmlrpc=False) :
         """Initialize the server.
-        
-        Default behavior is to start the HTTP server. You can either start the 
+
+        Default behavior is to start the HTTP server. You can either start the
         xmlrpc server by setting xmlrpc to True. Caution: Only one server will
         be started.
 
@@ -80,6 +104,7 @@
         self._run = True
         self._serve_xmlrpc = serve_xmlrpc
 
+        #TODO allow to serve XMLRPC and HTTP static files at the same time.
         if not self._serve_xmlrpc:
             self.server = HTTPServer(('', 0), PyPIRequestHandler)
             self.server.RequestHandlerClass.pypi_server = self
@@ -89,7 +114,7 @@
             self.default_response_status = 200
             self.default_response_headers = [('Content-type', 'text/plain')]
             self.default_response_data = "hello"
-            
+
             # initialize static paths / filesystems
             self.static_uri_paths = static_uri_paths
             if test_static_path is not None:
@@ -97,7 +122,7 @@
             self.static_filesystem_paths = [PYPI_DEFAULT_STATIC_PATH + "/" + path
                 for path in static_filesystem_paths]
         else:
-            # xmlrpc server
+            # XMLRPC server
             self.server = PyPIXMLRPCServer(('', 0))
             self.xmlrpc = XMLRPCMockIndex()
             # register the xmlrpc methods
@@ -176,7 +201,7 @@
         # serve the content from local disc if we request an URL beginning
         # by a pattern defined in `static_paths`
         url_parts = self.path.split("/")
-        if (len(url_parts) > 1 and 
+        if (len(url_parts) > 1 and
                 url_parts[1] in self.pypi_server.static_uri_paths):
             data = None
             # always take the last first.
@@ -214,7 +239,7 @@
             try:
                 status = int(status)
             except ValueError:
-                # we probably got something like YYY Codename. 
+                # we probably got something like YYY Codename.
                 # Just get the first 3 digits
                 status = int(status[:3])
 
@@ -233,7 +258,7 @@
         self.server_port = port
 
 class MockDist(object):
-    """Fake distribution, used in the Mock PyPI Server""" 
+    """Fake distribution, used in the Mock PyPI Server"""
     def __init__(self, name, version="1.0", hidden=False, url="http://url/",
              type="sdist", filename="", size=10000,
              digest="123456", downloads=7, has_sig=False,
@@ -252,7 +277,7 @@
         self.name = name
         self.version = version
         self.hidden = hidden
-        
+
         # URL infos
         self.url = url
         self.digest = digest
@@ -261,7 +286,7 @@
         self.python_version = python_version
         self.comment = comment
         self.type = type
-        
+
         # metadata
         self.author = author
         self.author_email = author_email
@@ -280,7 +305,7 @@
         self.cheesecake_documentation_id = documentation_id
         self.cheesecake_code_kwalitee_id = code_kwalitee_id
         self.cheesecake_installability_id = installability_id
-        
+
         self.obsoletes = obsoletes
         self.obsoletes_dist = obsoletes_dist
         self.provides = provides
@@ -289,7 +314,7 @@
         self.requires_dist = requires_dist
         self.requires_external = requires_external
         self.requires_python = requires_python
-        
+
     def url_infos(self):
         return {
             'url': self.url,
@@ -331,11 +356,12 @@
             'summary': self.summary,
             'home_page': self.homepage,
             'stable_version': self.stable_version,
-            'provides_dist': self.provides_dist,
+            'provides_dist': self.provides_dist or "%s (%s)" % (self.name,
+                                                              self.version),
             'requires': self.requires,
             'cheesecake_installability_id': self.cheesecake_installability_id,
         }
-    
+
     def search_result(self):
         return {
             '_pypi_ordering': 0,
@@ -346,7 +372,7 @@
 
 class XMLRPCMockIndex(object):
     """Mock XMLRPC server"""
-    
+
     def __init__(self, dists=[]):
         self._dists = dists
 
@@ -385,7 +411,7 @@
             # return only un-hidden
             return [d.version for d in self._dists if d.name == package_name
                     and not d.hidden]
-    
+
     def release_urls(self, package_name, version):
         return [d.url_infos() for d in self._dists
                 if d.name == package_name and d.version == version]
diff --git a/src/distutils2/tests/support.py b/src/distutils2/tests/support.py
--- a/src/distutils2/tests/support.py
+++ b/src/distutils2/tests/support.py
@@ -1,7 +1,7 @@
 """Support code for distutils2 test cases.
 
 Always import unittest from this module, it will be the right version
-(standard library unittest for 2.7 and higher, third-party unittest2
+(standard library unittest for 3.2 and higher, third-party unittest2
 release for older versions).
 
 Three helper classes are provided: LoggingSilencer, TempdirManager and
@@ -17,10 +17,10 @@
 tearDown):
 
         def setUp(self):
-            super(self.__class__, self).setUp()
+            super(SomeTestCase, self).setUp()
             ... # other setup code
 
-Read each class' docstring to see their purpose and usage.
+Read each class' docstring to see its purpose and usage.
 
 Also provided is a DummyCommand class, useful to mock commands in the
 tests of another command that needs them (see docstring).
diff --git a/src/distutils2/tests/test_dist.py b/src/distutils2/tests/test_dist.py
--- a/src/distutils2/tests/test_dist.py
+++ b/src/distutils2/tests/test_dist.py
@@ -153,6 +153,27 @@
         my_file2 = os.path.join(tmp_dir, 'f2')
         dist.metadata.write_file(open(my_file, 'w'))
 
+    def test_bad_attr(self):
+        cls = Distribution
+
+        # catching warnings
+        warns = []
+        def _warn(msg):
+            warns.append(msg)
+
+        old_warn = warnings.warn
+        warnings.warn = _warn
+        try:
+            dist = cls(attrs={'author': 'xxx',
+                              'name': 'xxx',
+                              'version': 'xxx',
+                              'url': 'xxxx',
+                              'badoptname': 'xxx'})
+        finally:
+            warnings.warn = old_warn
+
+        self.assertTrue(len(warns)==1 and "Unknown distribution" in warns[0])
+
     def test_empty_options(self):
         # an empty options dictionary should not stay in the
         # list of attributes
@@ -176,6 +197,21 @@
 
         self.assertEqual(len(warns), 0)
 
+    def test_non_empty_options(self):
+        # TODO: how to actually use options is not documented except
+        # for a few cryptic comments in dist.py.  If this is to stay
+        # in the public API, it deserves some better documentation.
+
+        # Here is an example of how it's used out there: 
+        # http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html#specifying-customizations
+        cls = Distribution
+        dist = cls(attrs={'author': 'xxx',
+                          'name': 'xxx',
+                          'version': 'xxx',
+                          'url': 'xxxx',
+                          'options': dict(sdist=dict(owner="root"))})
+        self.assertTrue("owner" in dist.get_option_dict("sdist"))
+
     def test_finalize_options(self):
 
         attrs = {'keywords': 'one,two',
diff --git a/src/distutils2/tests/test_install.py b/src/distutils2/tests/test_install.py
--- a/src/distutils2/tests/test_install.py
+++ b/src/distutils2/tests/test_install.py
@@ -202,6 +202,9 @@
         finally:
             f.close()
 
+        # XXX test that fancy_getopt is okay with options named
+        # record and no-record but unrelated
+
     def _test_debug_mode(self):
         # this covers the code called when DEBUG is set
         old_logs_len = len(self.logs)
diff --git a/src/distutils2/tests/test_install_with_deps.py b/src/distutils2/tests/test_install_with_deps.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/tests/test_install_with_deps.py
@@ -0,0 +1,152 @@
+"""Tests for the distutils2.index.xmlrpc module."""
+
+from distutils2.tests.pypi_server import use_xmlrpc_server
+from distutils2.tests import run_unittest
+from distutils2.tests.support import unittest
+from distutils2.index.xmlrpc import Client
+from distutils2.install_with_deps import (get_install_info, 
+                                          InstallationException)
+from distutils2.metadata import DistributionMetadata
+
+class FakeDist(object):
+    """A fake distribution object, for tests"""
+    def __init__(self, name, version, deps):
+        self.name = name
+        self.version = version
+        self.metadata = DistributionMetadata()
+        self.metadata['Requires-Dist'] = deps
+        self.metadata['Provides-Dist'] = ['%s (%s)' % (name, version)]
+
+    def __repr__(self):
+        return '<FakeDist %s>' % self.name
+
+def get_fake_dists(dists):
+    objects = []
+    for (name, version, deps) in dists:
+       objects.append(FakeDist(name, version, deps))
+    return objects
+
+class TestInstallWithDeps(unittest.TestCase):
+    def _get_client(self, server, *args, **kwargs):
+        return Client(server.full_address, *args, **kwargs)
+
+    @use_xmlrpc_server()
+    def test_existing_deps(self, server):
+        # Test that the installer get the dependencies from the metadatas
+        # and ask the index for this dependencies.
+        # In this test case, we have choxie that is dependent from towel-stuff
+        # 0.1, which is in-turn dependent on bacon <= 0.2:
+        # choxie -> towel-stuff -> bacon.
+        # Each release metadata is not provided in metadata 1.2.
+        client = self._get_client(server)
+        archive_path = '%s/distribution.tar.gz' % server.full_address
+        server.xmlrpc.set_distributions([
+            {'name':'choxie',
+             'version': '2.0.0.9',
+             'requires_dist': ['towel-stuff (0.1)',],
+             'url': archive_path},
+            {'name':'towel-stuff',
+             'version': '0.1',
+             'requires_dist': ['bacon (<= 0.2)',],
+             'url': archive_path},
+            {'name':'bacon',
+             'version': '0.1',
+             'requires_dist': [],
+             'url': archive_path},
+            ])
+        installed = get_fake_dists([('bacon', '0.1', []),])
+        output = get_install_info("choxie", index=client, 
+                                  already_installed=installed)
+
+        # we dont have installed bacon as it's already installed on the system.
+        self.assertEqual(0, len(output['remove']))
+        self.assertEqual(2, len(output['install']))
+        readable_output = [(o.name, '%s' % o.version) 
+                           for o in output['install']]
+        self.assertIn(('towel-stuff', '0.1'), readable_output)
+        self.assertIn(('choxie', '2.0.0.9'), readable_output)
+   
+    @use_xmlrpc_server()
+    def test_upgrade_existing_deps(self, server):
+        # Tests that the existing distributions can be upgraded if needed.
+        client = self._get_client(server)
+        archive_path = '%s/distribution.tar.gz' % server.full_address
+        server.xmlrpc.set_distributions([
+            {'name':'choxie',
+             'version': '2.0.0.9',
+             'requires_dist': ['towel-stuff (0.1)',],
+             'url': archive_path},
+            {'name':'towel-stuff',
+             'version': '0.1',
+             'requires_dist': ['bacon (>= 0.2)',],
+             'url': archive_path},
+            {'name':'bacon',
+             'version': '0.2',
+             'requires_dist': [],
+             'url': archive_path},
+            ])
+        
+        output = get_install_info("choxie", index=client, already_installed= 
+                         get_fake_dists([('bacon', '0.1', []),]))
+        installed = [(o.name, '%s' % o.version) for o in output['install']]
+        
+        # we need bacon 0.2, but 0.1 is installed.
+        # So we expect to remove 0.1 and to install 0.2 instead.
+        remove = [(o.name, '%s' % o.version) for o in output['remove']]
+        self.assertIn(('choxie', '2.0.0.9'), installed)
+        self.assertIn(('towel-stuff', '0.1'), installed)
+        self.assertIn(('bacon', '0.2'), installed)
+        self.assertIn(('bacon', '0.1'), remove)
+        self.assertEqual(0, len(output['conflict']))
+
+    @use_xmlrpc_server()
+    def test_conflicts(self, server):
+        # Tests that conflicts are detected 
+        client = self._get_client(server)
+        archive_path = '%s/distribution.tar.gz' % server.full_address
+        server.xmlrpc.set_distributions([
+            {'name':'choxie',
+             'version': '2.0.0.9',
+             'requires_dist': ['towel-stuff (0.1)',],
+             'url': archive_path},
+            {'name':'towel-stuff',
+             'version': '0.1',
+             'requires_dist': ['bacon (>= 0.2)',],
+             'url': archive_path},
+            {'name':'bacon',
+             'version': '0.2',
+             'requires_dist': [],
+             'url': archive_path},
+            ])
+        already_installed = [('bacon', '0.1', []), 
+                             ('chicken', '1.1', ['bacon (0.1)'])] 
+        output = get_install_info("choxie", index=client, already_installed= 
+                         get_fake_dists(already_installed))
+        
+        # we need bacon 0.2, but 0.1 is installed.
+        # So we expect to remove 0.1 and to install 0.2 instead.
+        installed = [(o.name, '%s' % o.version) for o in output['install']]
+        remove = [(o.name, '%s' % o.version) for o in output['remove']]
+        conflict = [(o.name, '%s' % o.version) for o in output['conflict']]
+        self.assertIn(('choxie', '2.0.0.9'), installed)
+        self.assertIn(('towel-stuff', '0.1'), installed)
+        self.assertIn(('bacon', '0.2'), installed)
+        self.assertIn(('bacon', '0.1'), remove)
+        self.assertIn(('chicken', '1.1'), conflict)
+
+    @use_xmlrpc_server()
+    def test_installation_unexisting_project(self, server):
+        # Test that the isntalled raises an exception if the project does not
+        # exists.
+        client = self._get_client(server)
+        self.assertRaises(InstallationException, get_install_info, 
+                          'unexistant project', index=client)
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestInstallWithDeps))
+    return suite
+
+if __name__ == '__main__':
+    run_unittest(test_suite())
diff --git a/src/distutils2/version.py b/src/distutils2/version.py
--- a/src/distutils2/version.py
+++ b/src/distutils2/version.py
@@ -349,6 +349,7 @@
                   }
 
     def __init__(self, predicate):
+        self._string = predicate
         predicate = predicate.strip()
         match = _PREDICATE.match(predicate)
         if match is None:
@@ -374,6 +375,9 @@
                 return False
         return True
 
+    def __repr__(self):
+        return self._string
+
 
 class _Versions(VersionPredicate):
     def __init__(self, predicate):
@@ -418,3 +422,12 @@
         return False
     else:
         return True
+
+
+def get_version_predicate(requirements):
+    """Return a VersionPredicate object, from a string or an already
+    existing object.
+    """
+    if isinstance(requirements, str):
+        requirements = VersionPredicate(requirements)
+    return requirements
diff --git a/src/runtests-cov.py b/src/runtests-cov.py
--- a/src/runtests-cov.py
+++ b/src/runtests-cov.py
@@ -5,9 +5,19 @@
 """
 
 import sys
-from os.path import dirname, islink, realpath
+from os.path import dirname, islink, realpath, join, abspath
 from optparse import OptionParser
 
+COVERAGE_FILE = join(dirname(abspath(__file__)), '.coverage')
+
+def get_coverage():
+    """ Return a usable coverage object. """
+    # deferred import because coverage is optional
+    import coverage
+    cov = getattr(coverage, "the_coverage", None)
+    if not cov:
+        cov = coverage.coverage(COVERAGE_FILE)
+    return cov
 
 def ignore_prefixes(module):
     """ Return a list of prefixes to ignore in the coverage report if
@@ -44,10 +54,17 @@
 
 
 def coverage_report(opts):
-    import coverage
     from distutils2.tests.support import unittest
-    cov = coverage.coverage()
-    cov.load()
+    cov = get_coverage()
+    if hasattr(cov, "load"):
+        # running coverage 3.x
+        cov.load()
+        morfs = None
+    else:
+        # running coverage 2.x
+        cov.cache = COVERAGE_FILE
+        cov.restore()
+        morfs = [m for m in cov.cexecuted.keys() if "distutils2" in m]
 
     prefixes = ["runtests", "distutils2/tests", "distutils2/_backport"]
     prefixes += ignore_prefixes(unittest)
@@ -66,7 +83,7 @@
         # that module is also completely optional
         pass
 
-    cov.report(omit_prefixes=prefixes, show_missing=opts.show_missing)
+    cov.report(morfs, omit_prefixes=prefixes, show_missing=opts.show_missing)
 
 
 def test_main():
@@ -74,11 +91,8 @@
     verbose = not opts.quiet
     ret = 0
 
-    if opts.coverage or opts.report:
-        import coverage
-
     if opts.coverage:
-        cov = coverage.coverage()
+        cov = get_coverage()
         cov.erase()
         cov.start()
     if not opts.report:
diff --git a/src/tests.sh b/src/tests.sh
--- a/src/tests.sh
+++ b/src/tests.sh
@@ -4,36 +4,37 @@
 python2.4 setup.py build_ext -f -q 2> /dev/null > /dev/null
 python2.4 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
-    echo "Failed"
+    echo Failed
     rm -f distutils2/_backport/_hashlib.so
     exit 1
 else
-    echo "Success"
+    echo Success
 fi
 
 echo -n "Running tests for Python 2.5... "
 python2.5 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
-    echo "Failed"
+    echo Failed
     exit 1
 else
-    echo "Success"
+    echo Success
 fi
 
 echo -n "Running tests for Python 2.6... "
 python2.6 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
-    echo "Failed"
+    echo Failed
     exit 1
 else
-    echo "Success"
+    echo Success
 fi
 
 echo -n "Running tests for Python 2.7... "
 python2.7 -Wd -bb -3 runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
-    echo "Failed"
+    echo Failed
     exit 1
 else
-    echo "Success"
+    echo Success
 fi
+echo Good job, commit now!

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


More information about the Python-checkins mailing list