[Python-checkins] distutils2: Merge upstream.

tarek.ziade python-checkins at python.org
Sun Aug 8 11:50:47 CEST 2010


tarek.ziade pushed 919bbd329ab3 to distutils2:

http://hg.python.org/distutils2/rev/919bbd329ab3
changeset:   504:919bbd329ab3
parent:      503:3d5db5aa326a
parent:      494:292c0b541b47
user:        Alexis Metaireau <ametaireau at gmail.com>
date:        Fri Aug 06 17:17:22 2010 +0200
summary:     Merge upstream.
files:       docs/source/new_commands.rst, src/Modules/_hashopenssl.c, src/Modules/md5.c, src/Modules/md5.h, src/Modules/md5module.c, src/Modules/sha256module.c, src/Modules/sha512module.c, src/Modules/shamodule.c, src/distutils2/command/install_egg_info.py, src/distutils2/depgraph.py, src/distutils2/index/dist.py, src/distutils2/index/simple.py, src/distutils2/index/xmlrpc.py, src/distutils2/metadata.py, src/distutils2/tests/pypi_server.py

diff --git a/.hgignore b/.hgignore
--- a/.hgignore
+++ b/.hgignore
@@ -1,9 +1,10 @@
 syntax: glob
 *.py[co]
 __pycache__/
+*.so
 configure.cache
 build/
 MANIFEST
 dist/
 *.swp
-.coverage
\ No newline at end of file
+.coverage
diff --git a/docs/design/pep-0376.txt b/docs/design/pep-0376.txt
--- a/docs/design/pep-0376.txt
+++ b/docs/design/pep-0376.txt
@@ -17,7 +17,7 @@
 This PEP proposes various enhancements for Distutils:
 
 - A new format for the .egg-info structure.
-- Some APIs to read the meta-data of a distribution.
+- Some APIs to read the metadata of a distribution.
 - A replacement PEP 262.
 - An uninstall feature.
 
diff --git a/docs/source/command_hooks.rst b/docs/source/command_hooks.rst
new file mode 100644
--- /dev/null
+++ b/docs/source/command_hooks.rst
@@ -0,0 +1,31 @@
+=============
+Command hooks
+=============
+
+Distutils2 provides a way of extending its commands by the use of pre- and
+post- command hooks. The hooks are simple Python functions (or any callable
+objects) and are specified in the config file using their full qualified names.
+The pre-hooks are run after the command is finalized (its options are
+processed), but before it is run. The post-hooks are run after the command
+itself. Both types of hooks receive an instance of the command object.
+
+Sample usage of hooks
+=====================
+
+Firstly, you need to make sure your hook is present in the path. This is usually
+done by dropping them to the same folder where `setup.py` file lives ::
+
+  # file: myhooks.py
+  def my_install_hook(install_cmd):
+      print "Oh la la! Someone is installing my project!"
+
+Then, you need to point to it in your `setup.cfg` file, under the appropriate
+command section ::
+
+  [install]
+  pre-hook.project = myhooks.my_install_hook
+
+The hooks defined in different config files (system-wide, user-wide and
+package-wide) do not override each other as long as they are specified with
+different aliases (additional names after the dot). The alias in the example
+above is ``project``.
diff --git a/docs/source/new_commands.rst b/docs/source/commands.rst
rename from docs/source/new_commands.rst
rename to docs/source/commands.rst
--- a/docs/source/new_commands.rst
+++ b/docs/source/commands.rst
@@ -6,6 +6,50 @@
 You might recognize some of them from other projects, like Distribute or
 Setuptools.
 
+``upload`` - Upload source and/or binary distributions to PyPI
+==============================================================
+
+The Python Package Index (PyPI) not only stores the package info, but also  the
+package data if the author of the package wishes to. The distutils command
+:command:`upload` pushes the distribution files to PyPI.
+
+The command is invoked immediately after building one or more distribution
+files.  For example, the command ::
+
+    python setup.py sdist bdist_wininst upload
+
+will cause the source distribution and the Windows installer to be uploaded to
+PyPI.  Note that these will be uploaded even if they are built using an earlier
+invocation of :file:`setup.py`, but that only distributions named on the command
+line for the invocation including the :command:`upload` command are uploaded.
+
+The :command:`upload` command uses the username, password, and repository URL
+from the :file:`$HOME/.pypirc` file . If a :command:`register` command was
+previously called in the same command, and if the password was entered in the
+prompt, :command:`upload` will reuse the entered password. This is useful if
+you do not want to store a clear text password in the :file:`$HOME/.pypirc`
+file.
+
+The ``upload`` command has a few options worth noting:
+
+``--sign, -s``
+    Sign each uploaded file using GPG (GNU Privacy Guard).  The ``gpg`` program
+    must be available for execution on the system ``PATH``.
+
+``--identity=NAME, -i NAME``
+    Specify the identity or key name for GPG to use when signing.  The value of
+    this option will be passed through the ``--local-user`` option of the
+    ``gpg`` program.
+
+``--show-response``
+    Display the full response text from server; this is useful for debugging
+    PyPI problems.
+
+``--repository=URL, -r URL``
+    The URL of the repository to upload to.  Defaults to
+    http://pypi.python.org/pypi (i.e., the main PyPI installation).
+
+
 ``upload_docs`` - Upload package documentation to PyPI
 ======================================================
 
@@ -40,7 +84,7 @@
 
     python setup.py upload_docs --upload-dir=docs/build/html
 
-As with any other ``setuptools`` based command, you can define useful
+As with any other command, you can define useful
 defaults in the ``setup.cfg`` of your Python project, e.g.:
 
 .. code-block:: ini
diff --git a/docs/source/index.rst b/docs/source/index.rst
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -14,7 +14,8 @@
    metadata
    pkgutil
    depgraph
-   new_commands
+   commands
+   command_hooks
    test_framework
    projects-index
    version
diff --git a/docs/source/pkgutil.rst b/docs/source/pkgutil.rst
--- a/docs/source/pkgutil.rst
+++ b/docs/source/pkgutil.rst
@@ -17,6 +17,17 @@
 first a complete documentation of the functions and classes
 is provided and then several use cases are presented.
 
+Caching
++++++++
+
+For performance purposes, the list of distributions is being internally
+cached. It is enabled by default, but you can turn it off or clear
+it using
+:func:`distutils2._backport.pkgutil.enable_cache`,
+:func:`distutils2._backport.pkgutil.disable_cache` and
+:func:`distutils2._backport.pkgutil.clear_cache`.
+
+
 API Reference
 =============
 
@@ -48,7 +59,7 @@
   print('=====')
   for (path, md5, size) in dist.get_installed_files():
       print('* Path: %s' % path)
-      print('  Hash %s, Size: %s bytes' % (md5, size)) 
+      print('  Hash %s, Size: %s bytes' % (md5, size))
   print('Metadata')
   print('========')
   for key, value in dist.metadata.items():
diff --git a/src/DEVNOTES.txt b/src/DEVNOTES.txt
--- a/src/DEVNOTES.txt
+++ b/src/DEVNOTES.txt
@@ -1,10 +1,21 @@
 Notes for developers
 ====================
 
-- Distutils2 runs from 2.4 to 3.2 (3.x not implemented yet), so
-  make sure you don't use a syntax that doesn't work under
+- Distutils2 runs on Python from 2.4 to 3.2 (3.x not implemented yet),
+  so make sure you don't use a syntax that doesn't work under
   one of these Python versions.
 
 - Always run tests.sh before you push a change. This implies
-  that you have all Python versions installed from 2.4 to 2.6.
+  that you have all Python versions installed from 2.4 to 2.7.
 
+- With Python 2.4, if you want to run tests with runtests.py, or run
+  just one test directly, be sure to run python2.4 setup.py build_ext
+  first, else tests won't find _hashlib or _md5. When using tests.sh,
+  build_ext is automatically done.
+
+- Renaming to do:
+
+  - DistributionMetadata > Metadata or ReleaseMetadata
+  - pkgutil > pkgutil.__init__ + new pkgutil.database (or better name)
+  - 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/Modules/_hashopenssl.c b/src/distutils2/_backport/_hashopenssl.c
rename from src/Modules/_hashopenssl.c
rename to src/distutils2/_backport/_hashopenssl.c
diff --git a/src/distutils2/_backport/hashlib.py b/src/distutils2/_backport/hashlib.py
--- a/src/distutils2/_backport/hashlib.py
+++ b/src/distutils2/_backport/hashlib.py
@@ -65,20 +65,20 @@
 
 def __get_builtin_constructor(name):
     if name in ('SHA1', 'sha1'):
-        import _sha
+        from distutils2._backport import _sha
         return _sha.new
     elif name in ('MD5', 'md5'):
-        import _md5
+        from distutils2._backport import _md5
         return _md5.new
     elif name in ('SHA256', 'sha256', 'SHA224', 'sha224'):
-        import _sha256
+        from distutils2._backport import _sha256
         bs = name[3:]
         if bs == '256':
             return _sha256.sha256
         elif bs == '224':
             return _sha256.sha224
     elif name in ('SHA512', 'sha512', 'SHA384', 'sha384'):
-        import _sha512
+        from distutils2._backport import _sha512
         bs = name[3:]
         if bs == '512':
             return _sha512.sha512
@@ -122,7 +122,7 @@
 
 
 try:
-    import _hashlib
+    from distutils2._backport import _hashlib
     new = __hash_new
     __get_hash = __get_openssl_constructor
 except ImportError:
diff --git a/src/Modules/md5.c b/src/distutils2/_backport/md5.c
rename from src/Modules/md5.c
rename to src/distutils2/_backport/md5.c
diff --git a/src/Modules/md5.h b/src/distutils2/_backport/md5.h
rename from src/Modules/md5.h
rename to src/distutils2/_backport/md5.h
diff --git a/src/Modules/md5module.c b/src/distutils2/_backport/md5module.c
rename from src/Modules/md5module.c
rename to src/distutils2/_backport/md5module.c
diff --git a/src/distutils2/_backport/pkgutil.py b/src/distutils2/_backport/pkgutil.py
--- a/src/distutils2/_backport/pkgutil.py
+++ b/src/distutils2/_backport/pkgutil.py
@@ -20,6 +20,7 @@
 import re
 import warnings
 
+
 __all__ = [
     'get_importer', 'iter_importers', 'get_loader', 'find_loader',
     'walk_packages', 'iter_modules', 'get_data',
@@ -27,6 +28,7 @@
     'Distribution', 'EggInfoDistribution', 'distinfo_dirname',
     'get_distributions', 'get_distribution', 'get_file_users',
     'provides_distribution', 'obsoletes_distribution',
+    'enable_cache', 'disable_cache', 'clear_cache'
 ]
 
 
@@ -187,8 +189,8 @@
     searches the current ``sys.path``, plus any modules that are frozen
     or built-in.
 
-    Note that :class:`ImpImporter` does not currently support being used by placement
-    on ``sys.meta_path``.
+    Note that :class:`ImpImporter` does not currently support being used by
+    placement on ``sys.meta_path``.
     """
 
     def __init__(self, path=None):
@@ -577,7 +579,8 @@
     argument should be the name of a package, in standard module format
     (``foo.bar``). The resource argument should be in the form of a relative
     filename, using ``'/'`` as the path separator. The parent directory name
-    ``'..'`` is not allowed, and nor is a rooted name (starting with a ``'/'``).
+    ``'..'`` is not allowed, and nor is a rooted name (starting with a
+    ``'/'``).
 
     The function returns a binary string, which is the contents of the
     specified resource.
@@ -613,6 +616,97 @@
 
 DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED',)
 
+# Cache
+_cache_name = {} # maps names to Distribution instances
+_cache_name_egg = {} # maps names to EggInfoDistribution instances
+_cache_path = {} # maps paths to Distribution instances
+_cache_path_egg = {} # maps paths to EggInfoDistribution instances
+_cache_generated = False # indicates if .dist-info distributions are cached
+_cache_generated_egg = False # indicates if .dist-info and .egg are cached
+_cache_enabled = True
+
+
+def enable_cache():
+    """
+    Enables the internal cache.
+
+    Note that this function will not clear the cache in any case, for that
+    functionality see :func:`clear_cache`.
+    """
+    global _cache_enabled
+
+    _cache_enabled = True
+
+def disable_cache():
+    """
+    Disables the internal cache.
+
+    Note that this function will not clear the cache in any case, for that
+    functionality see :func:`clear_cache`.
+    """
+    global _cache_enabled
+
+    _cache_enabled = False
+
+def clear_cache():
+    """ Clears the internal cache. """
+    global _cache_name, _cache_name_egg, cache_path, _cache_path_egg, \
+           _cache_generated, _cache_generated_egg
+
+    _cache_name = {}
+    _cache_name_egg = {}
+    _cache_path = {}
+    _cache_path_egg = {}
+    _cache_generated = False
+    _cache_generated_egg = False
+
+
+def _yield_distributions(include_dist, include_egg):
+    """
+    Yield .dist-info and .egg(-info) distributions, based on the arguments
+
+    :parameter include_dist: yield .dist-info distributions
+    :parameter include_egg: yield .egg(-info) distributions
+    """
+    for path in sys.path:
+        realpath = os.path.realpath(path)
+        if not os.path.isdir(realpath):
+            continue
+        for dir in os.listdir(realpath):
+            dist_path = os.path.join(realpath, dir)
+            if include_dist and dir.endswith('.dist-info'):
+                yield Distribution(dist_path)
+            elif include_egg and (dir.endswith('.egg-info') or
+                                  dir.endswith('.egg')):
+                yield EggInfoDistribution(dist_path)
+
+
+def _generate_cache(use_egg_info=False):
+    global _cache_generated, _cache_generated_egg
+
+    if _cache_generated_egg or (_cache_generated and not use_egg_info):
+        return
+    else:
+        gen_dist = not _cache_generated
+        gen_egg = use_egg_info
+
+        for dist in _yield_distributions(gen_dist, gen_egg):
+            if isinstance(dist, Distribution):
+                _cache_path[dist.path] = dist
+                if not dist.name in _cache_name:
+                    _cache_name[dist.name] = []
+                _cache_name[dist.name].append(dist)
+            else:
+                _cache_path_egg[dist.path] = dist
+                if not dist.name in _cache_name_egg:
+                    _cache_name_egg[dist.name] = []
+                _cache_name_egg[dist.name].append(dist)
+
+        if gen_dist:
+            _cache_generated = True
+        if gen_egg:
+            _cache_generated_egg = True
+
 
 class Distribution(object):
     """Created with the *path* of the ``.dist-info`` directory provided to the
@@ -627,15 +721,23 @@
     """A :class:`distutils2.metadata.DistributionMetadata` instance loaded with
     the distribution's ``METADATA`` file."""
     requested = False
-    """A boolean that indicates whether the ``REQUESTED`` metadata file is present
-    (in other words, whether the package was installed by user request)."""
+    """A boolean that indicates whether the ``REQUESTED`` metadata file is
+    present (in other words, whether the package was installed by user
+    request or it was installed as a dependency)."""
 
     def __init__(self, path):
+        if _cache_enabled and path in _cache_path:
+            self.metadata = _cache_path[path].metadata
+        else:
+            metadata_path = os.path.join(path, 'METADATA')
+            self.metadata = DistributionMetadata(path=metadata_path)
+
         self.path = path
-        metadata_path = os.path.join(path, 'METADATA')
-        self.metadata = DistributionMetadata(path=metadata_path)
         self.name = self.metadata['name']
 
+        if _cache_enabled and not path in _cache_path:
+            _cache_path[path] = self
+
     def _get_records(self, local=False):
         RECORD = os.path.join(self.path, 'RECORD')
         record_reader = csv_reader(open(RECORD, 'rb'), delimiter=',')
@@ -756,6 +858,11 @@
     def __init__(self, path):
         self.path = path
 
+        if _cache_enabled and path in _cache_path_egg:
+            self.metadata = _cache_path_egg[path].metadata
+            self.name = self.metadata['Name']
+            return
+
         # reused from Distribute's pkg_resources
         def yield_lines(strs):
             """Yield non-empty/non-comment lines of a ``basestring`` or sequence"""
@@ -787,7 +894,7 @@
                     requires = zipf.get_data('EGG-INFO/requires.txt')
                 except IOError:
                     requires = None
-            self.name = self.metadata['name']
+            self.name = self.metadata['Name']
         elif path.endswith('.egg-info'):
             if os.path.isdir(path):
                 path = os.path.join(path, 'PKG-INFO')
@@ -840,6 +947,9 @@
             else:
                 self.metadata['Requires'] += reqs
 
+        if _cache_enabled:
+            _cache_path_egg[self.path] = self
+
     def get_installed_files(self, local=False):
         return []
 
@@ -898,17 +1008,17 @@
 
     :rtype: iterator of :class:`Distribution` and :class:`EggInfoDistribution`
             instances"""
-    for path in sys.path:
-        realpath = os.path.realpath(path)
-        if not os.path.isdir(realpath):
-            continue
-        for dir in os.listdir(realpath):
-            if dir.endswith('.dist-info'):
-                dist = Distribution(os.path.join(realpath, dir))
-                yield dist
-            elif use_egg_info and (dir.endswith('.egg-info') or
-                                   dir.endswith('.egg')):
-                dist = EggInfoDistribution(os.path.join(realpath, dir))
+    if not _cache_enabled:
+        for dist in _yield_distributions(True, use_egg_info):
+            yield dist
+    else:
+        _generate_cache(use_egg_info)
+
+        for dist in _cache_path.itervalues():
+            yield dist
+
+        if use_egg_info:
+            for dist in _cache_path_egg.itervalues():
                 yield dist
 
 
@@ -928,17 +1038,19 @@
     value is expected. If the directory is not found, ``None`` is returned.
 
     :rtype: :class:`Distribution` or :class:`EggInfoDistribution` or None"""
-    found = None
-    for dist in get_distributions():
-        if dist.name == name:
-            found = dist
-            break
-    if use_egg_info:
-        for dist in get_distributions(True):
+    if not _cache_enabled:
+        for dist in _yield_distributions(True, use_egg_info):
             if dist.name == name:
-                found = dist
-                break
-    return found
+                return dist
+    else:
+        _generate_cache(use_egg_info)
+
+        if name in _cache_name:
+            return _cache_name[name][0]
+        elif use_egg_info and name in _cache_name_egg:
+            return _cache_name_egg[name][0]
+        else:
+            return None
 
 
 def obsoletes_distribution(name, version=None, use_egg_info=False):
diff --git a/src/Modules/sha256module.c b/src/distutils2/_backport/sha256module.c
rename from src/Modules/sha256module.c
rename to src/distutils2/_backport/sha256module.c
diff --git a/src/Modules/sha512module.c b/src/distutils2/_backport/sha512module.c
rename from src/Modules/sha512module.c
rename to src/distutils2/_backport/sha512module.c
diff --git a/src/Modules/shamodule.c b/src/distutils2/_backport/shamodule.c
rename from src/Modules/shamodule.c
rename to src/distutils2/_backport/shamodule.c
diff --git a/src/distutils2/_backport/tarfile.py b/src/distutils2/_backport/tarfile.py
--- a/src/distutils2/_backport/tarfile.py
+++ b/src/distutils2/_backport/tarfile.py
@@ -56,13 +56,6 @@
 if not hasattr(os, 'SEEK_SET'):
     os.SEEK_SET = 0
 
-if sys.platform == 'mac':
-    # This module needs work for MacOS9, especially in the area of pathname
-    # handling. In many places it is assumed a simple substitution of / by the
-    # local os.path.sep is good enough to convert pathnames, but this does not
-    # work with the mac rooted:path:name versus :nonrooted:path:name syntax
-    raise ImportError, "tarfile does not work for platform==mac"
-
 try:
     import grp, pwd
 except ImportError:
diff --git a/src/distutils2/_backport/tests/__init__.py b/src/distutils2/_backport/tests/__init__.py
--- a/src/distutils2/_backport/tests/__init__.py
+++ b/src/distutils2/_backport/tests/__init__.py
@@ -4,7 +4,7 @@
 from distutils2.tests.support import unittest
 
 
-here = os.path.dirname(__file__)
+here = os.path.dirname(__file__) or os.curdir
 
 def test_suite():
     suite = unittest.TestSuite()
@@ -16,4 +16,5 @@
             suite.addTest(module.test_suite())
     return suite
 
-
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
diff --git a/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA b/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
--- a/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
+++ b/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
@@ -2,3 +2,4 @@
 Name: grammar
 Version: 1.0a4
 Requires-Dist: truffles (>=1.2)
+Author: Sherlock Holmes
diff --git a/src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info b/src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info
new file mode 100644
--- /dev/null
+++ b/src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info
@@ -0,0 +1,3 @@
+Metadata-Version: 1.2
+Name: truffles
+Version: 5.0
diff --git a/src/distutils2/_backport/tests/test_pkgutil.py b/src/distutils2/_backport/tests/test_pkgutil.py
--- a/src/distutils2/_backport/tests/test_pkgutil.py
+++ b/src/distutils2/_backport/tests/test_pkgutil.py
@@ -10,7 +10,7 @@
 try:
     from hashlib import md5
 except ImportError:
-    from md5 import md5
+    from distutils2._backport.hashlib import md5
 
 from test.test_support import run_unittest, TESTFN
 from distutils2.tests.support import unittest
@@ -25,12 +25,14 @@
     except ImportError:
         from unittest2.compatibility import relpath
 
+# Adapted from Python 2.7's trunk
+
 # TODO Add a test for getting a distribution that is provided by another
 # distribution.
 
 # TODO Add a test for absolute pathed RECORD items (e.g. /etc/myapp/config.ini)
 
-# Adapted from Python 2.7's trunk
+
 class TestPkgUtilData(unittest.TestCase):
 
     def setUp(self):
@@ -108,10 +110,14 @@
 
         del sys.modules[pkg]
 
+
 # Adapted from Python 2.7's trunk
+
+
 class TestPkgUtilPEP302(unittest.TestCase):
 
     class MyTestLoader(object):
+
         def load_module(self, fullname):
             # Create an empty module
             mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
@@ -120,13 +126,14 @@
             # Make it a package
             mod.__path__ = []
             # Count how many times the module is reloaded
-            mod.__dict__['loads'] = mod.__dict__.get('loads',0) + 1
+            mod.__dict__['loads'] = mod.__dict__.get('loads', 0) + 1
             return mod
 
         def get_data(self, path):
             return "Hello, world!"
 
     class MyTestImporter(object):
+
         def find_module(self, fullname, path=None):
             return TestPkgUtilPEP302.MyTestLoader()
 
@@ -319,7 +326,7 @@
         current_path = os.path.abspath(os.path.dirname(__file__))
         self.sys_path = sys.path[:]
         self.fake_dists_path = os.path.join(current_path, 'fake_dists')
-        sys.path[0:0] = [self.fake_dists_path]
+        sys.path.insert(0, self.fake_dists_path)
 
     def tearDown(self):
         sys.path[:] = self.sys_path
@@ -366,8 +373,12 @@
             if not isinstance(dist, Distribution):
                 self.fail("item received was not a Distribution instance: "
                     "%s" % type(dist))
-            if dist.name in dict(fake_dists).keys():
+            if dist.name in dict(fake_dists).keys() and \
+               dist.path.startswith(self.fake_dists_path):
                 found_dists.append((dist.name, dist.metadata['version'],))
+            else:
+                # check that it doesn't find anything more than this
+                self.assertFalse(dist.path.startswith(self.fake_dists_path))
             # otherwise we don't care what other distributions are found
 
         # Finally, test that we found all that we were looking for
@@ -375,7 +386,8 @@
 
         # Now, test if the egg-info distributions are found correctly as well
         fake_dists += [('bacon', '0.1'), ('cheese', '2.0.2'),
-                       ('banana', '0.4'), ('strawberry', '0.6')]
+                       ('banana', '0.4'), ('strawberry', '0.6'),
+                       ('truffles', '5.0')]
         found_dists = []
 
         dists = [dist for dist in get_distributions(use_egg_info=True)]
@@ -384,8 +396,11 @@
                     isinstance(dist, EggInfoDistribution)):
                 self.fail("item received was not a Distribution or "
                           "EggInfoDistribution instance: %s" % type(dist))
-            if dist.name in dict(fake_dists).keys():
+            if dist.name in dict(fake_dists).keys() and \
+               dist.path.startswith(self.fake_dists_path):
                 found_dists.append((dist.name, dist.metadata['version']))
+            else:
+                self.assertFalse(dist.path.startswith(self.fake_dists_path))
 
         self.assertListEqual(sorted(fake_dists), sorted(found_dists))
 
@@ -485,7 +500,7 @@
 
         l = [dist.name for dist in provides_distribution('truffles', '>1.5',
                                                          use_egg_info=True)]
-        checkLists(l, ['bacon'])
+        checkLists(l, ['bacon', 'truffles'])
 
         l = [dist.name for dist in provides_distribution('truffles', '>=1.0')]
         checkLists(l, ['choxie', 'towel-stuff'])
@@ -549,6 +564,33 @@
         l = [dist.name for dist in obsoletes_distribution('truffles', '0.2')]
         checkLists(l, ['towel-stuff'])
 
+    def test_yield_distribution(self):
+        # tests the internal function _yield_distributions
+        from distutils2._backport.pkgutil import _yield_distributions
+        checkLists = lambda x, y: self.assertListEqual(sorted(x), sorted(y))
+
+        eggs = [('bacon', '0.1'), ('banana', '0.4'), ('strawberry', '0.6'),
+                ('truffles', '5.0'), ('cheese', '2.0.2')]
+        dists = [('choxie', '2.0.0.9'), ('grammar', '1.0a4'),
+                 ('towel-stuff', '0.1')]
+
+        checkLists([], _yield_distributions(False, False))
+
+        found = [(dist.name, dist.metadata['Version'])
+                for dist in _yield_distributions(False, True)
+                if dist.path.startswith(self.fake_dists_path)]
+        checkLists(eggs, found)
+
+        found = [(dist.name, dist.metadata['Version'])
+                for dist in _yield_distributions(True, False)
+                if dist.path.startswith(self.fake_dists_path)]
+        checkLists(dists, found)
+
+        found = [(dist.name, dist.metadata['Version'])
+                for dist in _yield_distributions(True, True)
+                if dist.path.startswith(self.fake_dists_path)]
+        checkLists(dists + eggs, found)
+
 
 def test_suite():
     suite = unittest.TestSuite()
diff --git a/src/distutils2/command/bdist_dumb.py b/src/distutils2/command/bdist_dumb.py
--- a/src/distutils2/command/bdist_dumb.py
+++ b/src/distutils2/command/bdist_dumb.py
@@ -77,9 +77,7 @@
                       ("don't know how to create dumb built distributions " +
                        "on platform %s") % os.name
 
-        self.set_undefined_options('bdist',
-                                   ('dist_dir', 'dist_dir'),
-                                   ('plat_name', 'plat_name'))
+        self.set_undefined_options('bdist', 'dist_dir', 'plat_name')
 
     def run(self):
         if not self.skip_build:
diff --git a/src/distutils2/command/bdist_msi.py b/src/distutils2/command/bdist_msi.py
--- a/src/distutils2/command/bdist_msi.py
+++ b/src/distutils2/command/bdist_msi.py
@@ -153,10 +153,7 @@
         else:
             self.versions = list(self.all_versions)
 
-        self.set_undefined_options('bdist',
-                                   ('dist_dir', 'dist_dir'),
-                                   ('plat_name', 'plat_name'),
-                                   )
+        self.set_undefined_options('bdist', 'dist_dir', 'plat_name')
 
         if self.pre_install_script:
             raise DistutilsOptionError, "the pre-install-script feature is not yet implemented"
diff --git a/src/distutils2/command/bdist_wininst.py b/src/distutils2/command/bdist_wininst.py
--- a/src/distutils2/command/bdist_wininst.py
+++ b/src/distutils2/command/bdist_wininst.py
@@ -100,10 +100,7 @@
                       " option must be specified" % (short_version,)
             self.target_version = short_version
 
-        self.set_undefined_options('bdist',
-                                   ('dist_dir', 'dist_dir'),
-                                   ('plat_name', 'plat_name'),
-                                  )
+        self.set_undefined_options('bdist', 'dist_dir', 'plat_name')
 
         if self.install_script:
             for script in self.distribution.scripts:
@@ -177,8 +174,8 @@
 
         # And make an archive relative to the root of the
         # pseudo-installation tree.
-        from tempfile import mktemp
-        archive_basename = mktemp()
+        from tempfile import NamedTemporaryFile
+        archive_basename = NamedTemporaryFile().name
         fullname = self.distribution.get_fullname()
         arcname = self.make_archive(archive_basename, "zip",
                                     root_dir=self.bdist_dir)
diff --git a/src/distutils2/command/build_clib.py b/src/distutils2/command/build_clib.py
--- a/src/distutils2/command/build_clib.py
+++ b/src/distutils2/command/build_clib.py
@@ -76,9 +76,7 @@
         self.set_undefined_options('build',
                                    ('build_temp', 'build_clib'),
                                    ('build_temp', 'build_temp'),
-                                   ('compiler', 'compiler'),
-                                   ('debug', 'debug'),
-                                   ('force', 'force'))
+                                   'compiler', 'debug', 'force')
 
         self.libraries = self.distribution.libraries
         if self.libraries:
diff --git a/src/distutils2/command/build_ext.py b/src/distutils2/command/build_ext.py
--- a/src/distutils2/command/build_ext.py
+++ b/src/distutils2/command/build_ext.py
@@ -177,13 +177,8 @@
 
     def finalize_options(self):
         self.set_undefined_options('build',
-                                   ('build_lib', 'build_lib'),
-                                   ('build_temp', 'build_temp'),
-                                   ('compiler', 'compiler'),
-                                   ('debug', 'debug'),
-                                   ('force', 'force'),
-                                   ('plat_name', 'plat_name'),
-                                   )
+                                   'build_lib', 'build_temp', 'compiler',
+                                   'debug', 'force', 'plat_name')
 
         if self.package is None:
             self.package = self.distribution.ext_package
@@ -292,7 +287,7 @@
                                                       "config"))
             else:
                 # building python standard extensions
-                self.library_dirs.append('.')
+                self.library_dirs.append(os.curdir)
 
         # for extensions under Linux or Solaris with a shared Python library,
         # Python's library directory must be appended to library_dirs
@@ -305,7 +300,7 @@
                 self.library_dirs.append(sysconfig.get_config_var('LIBDIR'))
             else:
                 # building python standard extensions
-                self.library_dirs.append('.')
+                self.library_dirs.append(os.curdir)
 
         # The argument parsing will result in self.define being a string, but
         # it has to be a list of 2-tuples.  All the preprocessor symbols
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
@@ -95,9 +95,7 @@
         self._doctests_2to3 = []
 
     def finalize_options(self):
-        self.set_undefined_options('build',
-                                   ('build_lib', 'build_lib'),
-                                   ('force', 'force'))
+        self.set_undefined_options('build', 'build_lib', 'force')
 
         # Get the distribution options that are aliases for build_py
         # options -- list of packages and list of modules.
diff --git a/src/distutils2/command/build_scripts.py b/src/distutils2/command/build_scripts.py
--- a/src/distutils2/command/build_scripts.py
+++ b/src/distutils2/command/build_scripts.py
@@ -40,8 +40,7 @@
     def finalize_options (self):
         self.set_undefined_options('build',
                                    ('build_scripts', 'build_dir'),
-                                   ('force', 'force'),
-                                   ('executable', 'executable'))
+                                   'force', 'executable')
         self.scripts = self.distribution.scripts
 
     def get_source_files(self):
diff --git a/src/distutils2/command/check.py b/src/distutils2/command/check.py
--- a/src/distutils2/command/check.py
+++ b/src/distutils2/command/check.py
@@ -8,13 +8,13 @@
 from distutils2.errors import DistutilsSetupError
 
 class check(Command):
-    """This command checks the meta-data of the package.
+    """This command checks the metadata of the package.
     """
     description = ("perform some checks on the package")
-    user_options = [('metadata', 'm', 'Verify meta-data'),
+    user_options = [('metadata', 'm', 'Verify metadata'),
                     ('restructuredtext', 'r',
-                     ('Checks if long string meta-data syntax '
-                      'are reStructuredText-compliant')),
+                     ('Checks if long string metadata syntax '
+                      'is reStructuredText-compliant')),
                     ('strict', 's',
                      'Will exit with an error if a check fails')]
 
@@ -53,7 +53,7 @@
             raise DistutilsSetupError(msg)
 
     def check_metadata(self):
-        """Ensures that all required elements of meta-data are supplied.
+        """Ensures that all required elements of metadata are supplied.
 
         name, version, URL, (author and author_email) or
         (maintainer and maintainer_email)).
@@ -62,7 +62,7 @@
         """
         missing, __ = self.distribution.metadata.check()
         if missing != []:
-            self.warn("missing required meta-data: %s"  % ', '.join(missing))
+            self.warn("missing required metadata: %s"  % ', '.join(missing))
 
     def check_restructuredtext(self):
         """Checks if the long string fields are reST-compliant."""
diff --git a/src/distutils2/command/clean.py b/src/distutils2/command/clean.py
--- a/src/distutils2/command/clean.py
+++ b/src/distutils2/command/clean.py
@@ -40,13 +40,9 @@
         self.all = None
 
     def finalize_options(self):
-        self.set_undefined_options('build',
-                                   ('build_base', 'build_base'),
-                                   ('build_lib', 'build_lib'),
-                                   ('build_scripts', 'build_scripts'),
-                                   ('build_temp', 'build_temp'))
-        self.set_undefined_options('bdist',
-                                   ('bdist_base', 'bdist_base'))
+        self.set_undefined_options('build', 'build_base', 'build_lib',
+                                   'build_scripts', 'build_temp')
+        self.set_undefined_options('bdist', 'bdist_base')
 
     def run(self):
         # remove the build/temp.<plat> directory (unless it's already
diff --git a/src/distutils2/command/cmd.py b/src/distutils2/command/cmd.py
--- a/src/distutils2/command/cmd.py
+++ b/src/distutils2/command/cmd.py
@@ -51,6 +51,12 @@
     # defined.  The canonical example is the "install" command.
     sub_commands = []
 
+    # Pre and post command hooks are run just before or just after the command
+    # itself. They are simple functions that receive the command instance. They
+    # should be specified as dotted strings.
+    pre_hook = None
+    post_hook = None
+
 
     # -- Creation/initialization methods -------------------------------
 
@@ -297,26 +303,30 @@
         else:
             return self.__class__.__name__
 
-    def set_undefined_options(self, src_cmd, *option_pairs):
-        """Set the values of any "undefined" options from corresponding
-        option values in some other command object.  "Undefined" here means
-        "is None", which is the convention used to indicate that an option
-        has not been changed between 'initialize_options()' and
-        'finalize_options()'.  Usually called from 'finalize_options()' for
-        options that depend on some other command rather than another
-        option of the same command.  'src_cmd' is the other command from
-        which option values will be taken (a command object will be created
-        for it if necessary); the remaining arguments are
-        '(src_option,dst_option)' tuples which mean "take the value of
-        'src_option' in the 'src_cmd' command object, and copy it to
-        'dst_option' in the current command object".
+    def set_undefined_options(self, src_cmd, *options):
+        """Set values of undefined options from another command.
+
+        Undefined options are options set to None, which is the convention
+        used to indicate that an option has not been changed between
+        'initialize_options()' and 'finalize_options()'.  This method is
+        usually called from 'finalize_options()' for options that depend on
+        some other command rather than another option of the same command,
+        typically subcommands.
+
+        The 'src_cmd' argument is the other command from which option values
+        will be taken (a command object will be created for it if necessary);
+        the remaining positional arguments are strings that give the name of
+        the option to set. If the name is different on the source and target
+        command, you can pass a tuple with '(name_on_source, name_on_dest)' so
+        that 'self.name_on_dest' will be set from 'src_cmd.name_on_source'.
         """
-
-        # Option_pairs: list of (src_option, dst_option) tuples
-
         src_cmd_obj = self.distribution.get_command_obj(src_cmd)
         src_cmd_obj.ensure_finalized()
-        for (src_option, dst_option) in option_pairs:
+        for obj in options:
+            if isinstance(obj, tuple):
+                src_option, dst_option = obj
+            else:
+                src_option, dst_option = obj, obj
             if getattr(self, dst_option) is None:
                 setattr(self, dst_option,
                         getattr(src_cmd_obj, src_option))
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
@@ -79,15 +79,23 @@
 
         ('record=', None,
          "filename in which to record list of installed files"),
+
+        # .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'),
         ]
 
-    boolean_options = ['compile', 'force', 'skip-build']
+    boolean_options = ['compile', 'force', 'skip-build', 'no-dist-info',
+                       'requested', 'no-dist-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'}
+    negative_opt = {'no-compile' : 'compile', 'no-requested': 'requested'}
 
 
     def initialize_options(self):
@@ -160,6 +168,12 @@
 
         self.record = None
 
+        # .dist-info related options
+        self.no_distinfo = None
+        self.installer = None
+        self.requested = None
+        self.no_record = None
+
 
     # -- Option finalizing methods -------------------------------------
     # (This is rather more involved than for most commands,
@@ -299,13 +313,14 @@
         self.dump_dirs("after prepending root")
 
         # Find out the build directories, ie. where to install from.
-        self.set_undefined_options('build',
-                                   ('build_base', 'build_base'),
-                                   ('build_lib', 'build_lib'))
+        self.set_undefined_options('build', 'build_base', 'build_lib')
 
         # Punt on doc directories for now -- after all, we're punting on
         # documentation completely!
 
+        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
@@ -586,5 +601,7 @@
                     ('install_headers', has_headers),
                     ('install_scripts', has_scripts),
                     ('install_data',    has_data),
-                    ('install_egg_info', lambda self:True),
+                    # 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
@@ -37,9 +37,7 @@
     def finalize_options(self):
         self.set_undefined_options('install',
                                    ('install_data', 'install_dir'),
-                                   ('root', 'root'),
-                                   ('force', 'force'),
-                                  )
+                                   'root', 'force')
 
     def run(self):
         self.mkpath(self.install_dir)
diff --git a/src/distutils2/command/install_distinfo.py b/src/distutils2/command/install_distinfo.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/command/install_distinfo.py
@@ -0,0 +1,175 @@
+"""
+distutils.command.install_distinfo
+==================================
+
+:Author: Josip Djolonga
+
+This module implements the ``install_distinfo`` command that creates the
+``.dist-info`` directory for the distribution, as specified in :pep:`376`.
+Usually, you do not have to call this command directly, it gets called
+automatically by the ``install`` command.
+"""
+
+# This file was created from the code for the former command install_egg_info
+
+import os
+import csv
+import re
+from distutils2.command.cmd import Command
+from distutils2 import log
+from distutils2._backport.shutil import rmtree
+try:
+    import hashlib
+except ImportError:
+    from distutils2._backport import hashlib
+
+
+class install_distinfo(Command):
+    """Install a .dist-info directory for the package"""
+
+    description = 'Install a .dist-info directory for the package'
+
+    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'),
+    ]
+
+    boolean_options = [
+        'requested',
+        'no-record',
+    ]
+
+    negative_opt = {'no-requested': 'requested'}
+
+    def initialize_options(self):
+        self.distinfo_dir = None
+        self.installer = None
+        self.requested = None
+        self.no_record = None
+
+    def finalize_options(self):
+        self.set_undefined_options('install',
+                                   ('installer', 'installer'),
+                                   ('requested', 'requested'),
+                                   ('no_record', 'no_record'))
+
+        self.set_undefined_options('install_lib',
+                                   ('install_dir', 'distinfo_dir'))
+
+        if self.installer is None:
+            self.installer = 'distutils'
+        if self.requested is None:
+            self.requested = True
+        if self.no_record is None:
+            self.no_record = False
+
+        metadata = self.distribution.metadata
+
+        basename = "%s-%s.dist-info" % (
+            to_filename(safe_name(metadata['Name'])),
+            to_filename(safe_version(metadata['Version'])),
+        )
+
+        self.distinfo_dir = os.path.join(self.distinfo_dir, basename)
+        self.outputs = []
+
+    def run(self):
+        if not self.dry_run:
+            target = self.distinfo_dir
+
+            if os.path.isdir(target) and not os.path.islink(target):
+                rmtree(target)
+            elif os.path.exists(target):
+                self.execute(os.unlink, (self.distinfo_dir,),
+                             "Removing " + target)
+
+            self.execute(os.makedirs, (target,), "Creating " + target)
+
+            metadata_path = os.path.join(self.distinfo_dir, 'METADATA')
+            log.info('Creating %s' % (metadata_path,))
+            self.distribution.metadata.write(metadata_path)
+            self.outputs.append(metadata_path)
+
+            installer_path = os.path.join(self.distinfo_dir, 'INSTALLER')
+            log.info('Creating %s' % (installer_path,))
+            f = open(installer_path, 'w')
+            try:
+                f.write(self.installer)
+            finally:
+                f.close()
+            self.outputs.append(installer_path)
+
+            if self.requested:
+                requested_path = os.path.join(self.distinfo_dir, 'REQUESTED')
+                log.info('Creating %s' % (requested_path,))
+                f = open(requested_path, 'w')
+                f.close()
+                self.outputs.append(requested_path)
+
+            if not self.no_record:
+                record_path = os.path.join(self.distinfo_dir, 'RECORD')
+                log.info('Creating %s' % (record_path,))
+                f = open(record_path, 'wb')
+                try:
+                    writer = csv.writer(f, delimiter=',',
+                                           lineterminator=os.linesep,
+                                           quotechar='"')
+
+                    install = self.get_finalized_command('install')
+
+                    for fpath in install.get_outputs():
+                        if fpath.endswith('.pyc') or fpath.endswith('.pyo'):
+                            # do not put size and md5 hash, as in PEP-376
+                            writer.writerow((fpath, '', ''))
+                        else:
+                            size = os.path.getsize(fpath)
+                            fd = open(fpath, 'r')
+                            hash = hashlib.md5()
+                            hash.update(fd.read())
+                            md5sum = hash.hexdigest()
+                            writer.writerow((fpath, md5sum, size))
+
+                    # add the RECORD file itself
+                    writer.writerow((record_path, '', ''))
+                    self.outputs.append(record_path)
+                finally:
+                    f.close()
+
+    def get_outputs(self):
+        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.
+
+
+def safe_name(name):
+    """Convert an arbitrary string to a standard distribution name
+
+    Any runs of non-alphanumeric/. characters are replaced with a single '-'.
+    """
+    return re.sub('[^A-Za-z0-9.]+', '-', name)
+
+
+def safe_version(version):
+    """Convert an arbitrary string to a standard version string
+
+    Spaces become dots, and all other non-alphanumeric characters become
+    dashes, with runs of multiple dashes condensed to a single dash.
+    """
+    version = version.replace(' ', '.')
+    return re.sub('[^A-Za-z0-9.]+', '-', version)
+
+
+def to_filename(name):
+    """Convert a project or version name to its filename-escaped form
+
+    Any '-' characters are currently replaced with '_'.
+    """
+    return name.replace('-', '_')
diff --git a/src/distutils2/command/install_egg_info.py b/src/distutils2/command/install_egg_info.py
deleted file mode 100644
--- a/src/distutils2/command/install_egg_info.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""distutils.command.install_egg_info
-
-Implements the Distutils 'install_egg_info' command, for installing
-a package's PKG-INFO metadata."""
-
-
-from distutils2.command.cmd import Command
-from distutils2 import log
-from distutils2._backport.shutil import rmtree
-import os, sys, re
-
-class install_egg_info(Command):
-    """Install an .egg-info file for the package"""
-
-    description = "Install package's PKG-INFO metadata as an .egg-info file"
-    user_options = [
-        ('install-dir=', 'd', "directory to install to"),
-    ]
-
-    def initialize_options(self):
-        self.install_dir = None
-
-    def finalize_options(self):
-        metadata = self.distribution.metadata
-        self.set_undefined_options('install_lib',('install_dir','install_dir'))
-        basename = "%s-%s-py%s.egg-info" % (
-            to_filename(safe_name(metadata['Name'])),
-            to_filename(safe_version(metadata['Version'])),
-            sys.version[:3]
-        )
-        self.target = os.path.join(self.install_dir, basename)
-        self.outputs = [self.target]
-
-    def run(self):
-        target = self.target
-        if os.path.isdir(target) and not os.path.islink(target):
-            if self.dry_run:
-                pass # XXX
-            else:
-                rmtree(target)
-        elif os.path.exists(target):
-            self.execute(os.unlink,(self.target,),"Removing "+target)
-        elif not os.path.isdir(self.install_dir):
-            self.execute(os.makedirs, (self.install_dir,),
-                         "Creating "+self.install_dir)
-        log.info("Writing %s", target)
-        if not self.dry_run:
-            f = open(target, 'w')
-            self.distribution.metadata.write_file(f)
-            f.close()
-
-    def get_outputs(self):
-        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.
-
-def safe_name(name):
-    """Convert an arbitrary string to a standard distribution name
-
-    Any runs of non-alphanumeric/. characters are replaced with a single '-'.
-    """
-    return re.sub('[^A-Za-z0-9.]+', '-', name)
-
-
-def safe_version(version):
-    """Convert an arbitrary string to a standard version string
-
-    Spaces become dots, and all other non-alphanumeric characters become
-    dashes, with runs of multiple dashes condensed to a single dash.
-    """
-    version = version.replace(' ','.')
-    return re.sub('[^A-Za-z0-9.]+', '-', version)
-
-
-def to_filename(name):
-    """Convert a project or version name to its filename-escaped form
-
-    Any '-' characters are currently replaced with '_'.
-    """
-    return name.replace('-','_')
diff --git a/src/distutils2/command/install_headers.py b/src/distutils2/command/install_headers.py
--- a/src/distutils2/command/install_headers.py
+++ b/src/distutils2/command/install_headers.py
@@ -29,8 +29,7 @@
     def finalize_options(self):
         self.set_undefined_options('install',
                                    ('install_headers', 'install_dir'),
-                                   ('force', 'force'))
-
+                                   'force')
 
     def run(self):
         headers = self.distribution.headers
diff --git a/src/distutils2/command/install_lib.py b/src/distutils2/command/install_lib.py
--- a/src/distutils2/command/install_lib.py
+++ b/src/distutils2/command/install_lib.py
@@ -68,11 +68,7 @@
         self.set_undefined_options('install',
                                    ('build_lib', 'build_dir'),
                                    ('install_lib', 'install_dir'),
-                                   ('force', 'force'),
-                                   ('compile', 'compile'),
-                                   ('optimize', 'optimize'),
-                                   ('skip_build', 'skip_build'),
-                                  )
+                                   'force', 'compile', 'optimize', 'skip_build')
 
         if self.compile is None:
             self.compile = 1
diff --git a/src/distutils2/command/install_scripts.py b/src/distutils2/command/install_scripts.py
--- a/src/distutils2/command/install_scripts.py
+++ b/src/distutils2/command/install_scripts.py
@@ -36,9 +36,7 @@
         self.set_undefined_options('build', ('build_scripts', 'build_dir'))
         self.set_undefined_options('install',
                                    ('install_scripts', 'install_dir'),
-                                   ('force', 'force'),
-                                   ('skip_build', 'skip_build'),
-                                  )
+                                   'force', 'skip_build')
 
     def run (self):
         if not self.skip_build:
diff --git a/src/distutils2/command/register.py b/src/distutils2/command/register.py
--- a/src/distutils2/command/register.py
+++ b/src/distutils2/command/register.py
@@ -21,15 +21,16 @@
 
 class register(Command):
 
-    description = ("register the distribution with the Python package index")
-    user_options = [('repository=', 'r',
-         "url of repository [default: %s]" % DEFAULT_REPOSITORY),
+    description = "register the distribution with the Python package index"
+    user_options = [
+        ('repository=', 'r',
+         "repository URL [default: %s]" % DEFAULT_REPOSITORY),
         ('show-response', None,
-         'display full response text from server'),
+         "display full response text from server"),
         ('list-classifiers', None,
-         'list the valid Trove classifiers'),
+         "list valid Trove classifiers"),
         ('strict', None ,
-         'Will stop the registering if the meta-data are not fully compliant')
+         "stop the registration if the metadata is not fully compliant")
         ]
 
     boolean_options = ['show-response', 'verify', 'list-classifiers',
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
@@ -70,8 +70,8 @@
         ('dist-dir=', 'd',
          "directory to put the source distribution archive(s) in "
          "[default: dist]"),
-        ('medata-check', None,
-         "Ensure that all required elements of meta-data "
+        ('check-metadata', None,
+         "Ensure that all required elements of metadata "
          "are supplied. Warn if any missing. [default]"),
         ('owner=', 'u',
          "Owner name used when creating a tar file [default: current user]"),
@@ -80,7 +80,7 @@
         ]
 
     boolean_options = ['use-defaults', 'prune',
-                       'manifest-only', 'keep-temp', 'metadata-check']
+                       'manifest-only', 'keep-temp', 'check-metadata']
 
     help_options = [
         ('help-formats', None,
@@ -362,7 +362,7 @@
         'self.keep_temp' is true).  The list of archive files created is
         stored so it can be retrieved later by 'get_archive_files()'.
         """
-        # Don't warn about missing meta-data here -- should be (and is!)
+        # Don't warn about missing metadata here -- should be (and is!)
         # done elsewhere.
         base_dir = self.distribution.get_fullname()
         base_name = os.path.join(self.dist_dir, base_dir)
diff --git a/src/distutils2/command/upload.py b/src/distutils2/command/upload.py
--- a/src/distutils2/command/upload.py
+++ b/src/distutils2/command/upload.py
@@ -11,7 +11,7 @@
 try:
     from hashlib import md5
 except ImportError:
-    from md5 import md5
+    from distutils2._backport.hashlib import md5
 
 from distutils2.errors import DistutilsOptionError
 from distutils2.util import spawn
@@ -24,16 +24,19 @@
 
 class upload(Command):
 
-    description = "upload binary package to PyPI"
+    description = "upload distribution to PyPI"
 
-    user_options = [('repository=', 'r',
-         "url of repository [default: %s]" % \
-            DEFAULT_REPOSITORY),
+    user_options = [
+        ('repository=', 'r',
+         "repository URL [default: %s]" % DEFAULT_REPOSITORY),
         ('show-response', None,
-         'display full response text from server'),
+         "display full response text from server"),
         ('sign', 's',
-         'sign files to upload using gpg'),
-        ('identity=', 'i', 'GPG identity used to sign files'),
+         "sign files to upload using gpg"),
+        ('identity=', 'i',
+         "GPG identity used to sign files"),
+        ('upload-docs', None,
+         "upload documentation too"),
         ]
 
     boolean_options = ['show-response', 'sign']
@@ -47,6 +50,7 @@
         self.show_response = 0
         self.sign = False
         self.identity = None
+        self.upload_docs = False
 
     def finalize_options(self):
         if self.repository is None:
@@ -74,6 +78,12 @@
             raise DistutilsOptionError("No dist file created in earlier command")
         for command, pyversion, filename in self.distribution.dist_files:
             self.upload_file(command, pyversion, filename)
+        if self.upload_docs:
+            upload_docs = self.get_finalized_command("upload_docs")
+            upload_docs.repository = self.repository
+            upload_docs.username = self.username
+            upload_docs.password = self.password
+            upload_docs.run()
 
     # XXX to be refactored with register.post_to_server
     def upload_file(self, command, pyversion, filename):
@@ -94,7 +104,7 @@
             spawn(gpg_args,
                   dry_run=self.dry_run)
 
-        # Fill in the data - send all the meta-data in case we need to
+        # Fill in the data - send all the metadata in case we need to
         # register a new release
         content = open(filename,'rb').read()
 
diff --git a/src/distutils2/command/upload_docs.py b/src/distutils2/command/upload_docs.py
--- a/src/distutils2/command/upload_docs.py
+++ b/src/distutils2/command/upload_docs.py
@@ -52,16 +52,19 @@
 class upload_docs(Command):
 
     user_options = [
-        ('repository=', 'r', "url of repository [default: %s]" % DEFAULT_REPOSITORY),
-        ('show-response', None, 'display full response text from server'),
-        ('upload-dir=', None, 'directory to upload'),
+        ('repository=', 'r',
+         "repository URL [default: %s]" % DEFAULT_REPOSITORY),
+        ('show-response', None,
+         "display full response text from server"),
+        ('upload-dir=', None,
+         "directory to upload"),
         ]
 
     def initialize_options(self):
         self.repository = None
         self.realm = None
         self.show_response = 0
-        self.upload_dir = "build/docs"
+        self.upload_dir = None
         self.username = ''
         self.password = ''
 
@@ -70,7 +73,7 @@
             self.repository = DEFAULT_REPOSITORY
         if self.realm is None:
             self.realm = DEFAULT_REALM
-        if self.upload_dir == None:
+        if self.upload_dir is None:
             build = self.get_finalized_command('build')
             self.upload_dir = os.path.join(build.build_base, "docs")
         self.announce('Using upload directory %s' % self.upload_dir)
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
@@ -155,7 +157,7 @@
 def run_setup(script_name, script_args=None, stop_after="run"):
     """Run a setup script in a somewhat controlled environment, and
     return the Distribution instance that drives things.  This is useful
-    if you need to find out the distribution meta-data (passed as
+    if you need to find out the distribution metadata (passed as
     keyword args from 'script' to 'setup()', or the contents of the
     config files or command-line.
 
@@ -205,8 +207,6 @@
         # Hmm, should we do something if exiting with a non-zero code
         # (ie. error)?
         pass
-    except:
-        raise
 
     if _setup_distribution is None:
         raise RuntimeError, \
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
diff --git a/src/distutils2/dist.py b/src/distutils2/dist.py
--- a/src/distutils2/dist.py
+++ b/src/distutils2/dist.py
@@ -6,19 +6,17 @@
 
 __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
 
 from distutils2.errors import (DistutilsOptionError, DistutilsArgError,
                                DistutilsModuleError, DistutilsClassError)
 from distutils2.fancy_getopt import FancyGetopt, translate_longopt
-from distutils2.util import check_environ, strtobool
+from distutils2.util import check_environ, strtobool, resolve_dotted_name
 from distutils2 import log
 from distutils2.metadata import DistributionMetadata
 
@@ -26,7 +24,7 @@
 # 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):
@@ -43,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
@@ -116,7 +113,7 @@
         ('use-2to3', None,
          "use 2to3 to make source python 3.x compatible"),
         ('convert-2to3-doctests', None,
-         "use 2to3 to convert doctests in seperate text files"), 
+         "use 2to3 to convert doctests in seperate text files"),
         ]
     display_option_names = map(lambda x: translate_longopt(x[0]),
                                display_options)
@@ -124,10 +121,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
@@ -145,17 +140,12 @@
         for attr in self.display_option_names:
             setattr(self, attr, 0)
 
-        # Store the distribution meta-data (name, version, author, and so
+        # Store the distribution metadata (name, version, author, and so
         # forth) in a separate object -- we're getting to have enough
         # information here (and enough command-line options) that it's
-        # worth it.  Also delegate 'get_XXX()' methods to the 'metadata'
-        # object in a sneaky and underhanded (but efficient!) way.
+        # worth it.
         self.metadata = DistributionMetadata()
 
-        #for basename in self.metadata._METHOD_BASENAMES:
-        #    method_name = "get_" + basename
-        #    setattr(self, method_name, getattr(self.metadata, method_name))
-
         # 'cmdclass' maps command names to class objects, so we
         # can 1) quickly figure out which class to instantiate when
         # we need to create a new command object, and 2) have a way
@@ -257,10 +247,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
@@ -286,10 +273,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()
@@ -385,9 +372,21 @@
 
                 for opt in options:
                     if opt != '__name__':
-                        val = parser.get(section,opt)
+                        val = parser.get(section, opt)
                         opt = opt.replace('-', '_')
-                        opt_dict[opt] = (filename, val)
+
+                        # ... although practicality beats purity :(
+                        if opt.startswith("pre_hook.") or opt.startswith("post_hook."):
+                            hook_type, alias = opt.split(".")
+                            # Hooks are somewhat exceptional, as they are
+                            # gathered from many config files (not overriden as
+                            # other options).
+                            # The option_dict expects {"command": ("filename", # "value")}
+                            # so for hooks, we store only the last config file processed
+                            hook_dict = opt_dict.setdefault(hook_type, (filename, {}))[1]
+                            hook_dict[alias] = val
+                        else:
+                            opt_dict[opt] = (filename, val)
 
             # Make the RawConfigParser forget everything (so we retain
             # the original filenames that options come from)
@@ -402,12 +401,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 ----------------------------------
 
@@ -473,7 +472,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
@@ -504,7 +503,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
@@ -513,22 +512,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.
@@ -545,7 +543,6 @@
         else:
             help_options = []
 
-
         # All commands support the global options too, just by adding
         # in 'global_options'.
         parser.set_option_table(self.global_options +
@@ -559,10 +556,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:
@@ -588,7 +585,7 @@
         objects.
         """
         if getattr(self, 'convert_2to3_doctests', None):
-            self.convert_2to3_doctests = [os.path.join(p) 
+            self.convert_2to3_doctests = [os.path.join(p)
                                 for p in self.convert_2to3_doctests]
         else:
             self.convert_2to3_doctests = []
@@ -801,7 +798,7 @@
             class_name = command
 
             try:
-                __import__ (module_name)
+                __import__(module_name)
                 module = sys.modules[module_name]
             except ImportError:
                 continue
@@ -809,16 +806,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
@@ -881,11 +877,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
@@ -953,15 +949,23 @@
         if self.have_run.get(command):
             return
 
-        log.info("running %s", command)
         cmd_obj = self.get_command_obj(command)
         cmd_obj.ensure_finalized()
+        self.run_command_hooks(cmd_obj, 'pre_hook')
+        log.info("running %s", command)
         cmd_obj.run()
+        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:
+            return
+        for hook in hooks.values():
+            hook_func = resolve_dotted_name(hook)
+            hook_func(cmd_obj)
 
     # -- Distribution query methods ------------------------------------
-
     def has_pure_modules(self):
         return len(self.packages or self.py_modules or []) > 0
 
@@ -988,13 +992,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/simple.py b/src/distutils2/index/simple.py
--- a/src/distutils2/index/simple.py
+++ b/src/distutils2/index/simple.py
@@ -25,7 +25,7 @@
 from distutils2.version import get_version_predicate
 from distutils2 import __version__ as __distutils2_version__
 
-__all__ = ['Crawler', 'DEFAULT_SIMPLE_INDEX_URL'] 
+__all__ = ['Crawler', 'DEFAULT_SIMPLE_INDEX_URL']
 
 # -- Constants -----------------------------------------------
 DEFAULT_SIMPLE_INDEX_URL = "http://a.pypi.python.org/simple/"
@@ -154,7 +154,7 @@
             matching_projects.append(self._get_project(project_name))
         return matching_projects
 
-    def get_releases(self, requirements, prefer_final=None, 
+    def get_releases(self, requirements, prefer_final=None,
                      force_update=False):
         """Search for releases and return a ReleaseList object containing
         the results.
@@ -167,7 +167,7 @@
 
         if not self._projects.has_key(predicate.name.lower()):
             raise ProjectNotFound()
-        
+
         releases = self._projects.get(predicate.name.lower())
         releases.sort_releases(prefer_final=prefer_final)
         return releases
@@ -260,7 +260,7 @@
         else:
             name = release_info['name']
         if not name.lower() in self._projects:
-            self._projects[name.lower()] = ReleasesList(name, 
+            self._projects[name.lower()] = ReleasesList(name,
                                                         index=self._index)
 
         if release:
diff --git a/src/distutils2/index/wrapper.py b/src/distutils2/index/wrapper.py
--- a/src/distutils2/index/wrapper.py
+++ b/src/distutils2/index/wrapper.py
@@ -3,7 +3,7 @@
 
 _WRAPPER_MAPPINGS = {'get_release': 'simple',
                      'get_releases': 'simple',
-                     'search_projects': 'simple', 
+                     'search_projects': 'simple',
                      'get_metadata': 'xmlrpc',
                      'get_distributions': 'simple'}
 
@@ -45,7 +45,7 @@
 
     :param index: tell wich index to rely on by default.
     :param index_classes: a dict of name:class to use as indexes.
-    :param indexes: a dict of name:index already instanciated
+    :param indexes: a dict of name:index already instantiated
     :param mappings: the mappings to use for this wrapper
     """
 
@@ -56,7 +56,7 @@
         self._indexes = indexes
         self._default_index = default_index
 
-        # instanciate the classes and set their _project attribute to the one
+        # instantiate the classes and set their _project attribute to the one
         # of the wrapper.
         for name, cls in index_classes.items():
             obj = self._indexes.setdefault(name, cls())
@@ -77,10 +77,10 @@
             # the method is not defined in the mappings, so we try first to get
             # it via the default index, and rely on others if needed.
             try:
-                real_method = getattr(self._indexes[self._default_index], 
+                real_method = getattr(self._indexes[self._default_index],
                                       method_name)
             except AttributeError:
-                other_indexes = [i for i in self._indexes 
+                other_indexes = [i for i in self._indexes
                                  if i != self._default_index]
                 for index in other_indexes:
                     real_method = getattr(self._indexes[index], method_name, None)
@@ -90,4 +90,4 @@
             return switch_index_if_fails(real_method, self)
         else:
             raise AttributeError("No index have attribute '%s'" % method_name)
-        
+
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
@@ -1,13 +1,12 @@
-"""
-Implementation of the Metadata for Python packages
+"""Implementation of the Metadata for Python packages PEPs.
 
-Supports all Metadata formats (1.0, 1.1, 1.2).
+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
@@ -53,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',
@@ -66,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',
@@ -78,12 +77,11 @@
                 'Obsoletes-Dist', 'Requires-External', 'Maintainer',
                 'Maintainer-email', 'Project-URL')
 
-_ALL_FIELDS = []
+_ALL_FIELDS = set()
+_ALL_FIELDS.update(_241_FIELDS)
+_ALL_FIELDS.update(_314_FIELDS)
+_ALL_FIELDS.update(_345_FIELDS)
 
-for field in _241_FIELDS + _314_FIELDS + _345_FIELDS:
-    if field in _ALL_FIELDS:
-        continue
-    _ALL_FIELDS.append(field)
 
 def _version2fieldlist(version):
     if version == '1.0':
@@ -94,8 +92,9 @@
         return _345_FIELDS
     raise MetadataUnrecognizedVersionError(version)
 
+
 def _best_version(fields):
-    """Will detect the best version depending on the fields used."""
+    """Detect the best version depending on the fields used."""
     def _has_marker(keys, markers):
         for marker in markers:
             if marker in keys:
@@ -182,23 +181,32 @@
 
 
 class DistributionMetadata(object):
-    """Distribution meta-data class (1.0 or 1.2).
+    """The metadata of a release.
 
-    if from_dict attribute is set, all key/values pairs will be sent to the
-    "set" method, building the metadata from the dict.
+    Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can
+    instantiate the class with one of these arguments (or none):
+    - *path*, the path to a METADATA file
+    - *fileobj* give a file-like object with METADATA as content
+    - *mapping* is a dict-like object
     """
+    # TODO document that execution_context and platform_dependent are used
+    # to filter on query, not when setting a key
+    # also document the mapping API and UNKNOWN default key
+
     def __init__(self, path=None, platform_dependent=False,
                  execution_context=None, fileobj=None, mapping=None):
         self._fields = {}
         self.version = None
         self.docutils_support = _HAS_DOCUTILS
         self.platform_dependent = platform_dependent
+        self.execution_context = execution_context
+        if [path, fileobj, mapping].count(None) < 2:
+            raise TypeError('path, fileobj and mapping are exclusive')
         if path is not None:
             self.read(path)
         elif fileobj is not None:
             self.read_file(fileobj)
-        self.execution_context = execution_context
-        if mapping:
+        elif mapping is not None:
             self.update(mapping)
 
     def _set_best_version(self):
@@ -208,10 +216,6 @@
     def _write_field(self, file, name, value):
         file.write('%s: %s\n' % (name, value))
 
-    def _write_list(self, file, name, values):
-        for value in values:
-            self._write_field(file, name, value)
-
     def _encode_field(self, value):
         if isinstance(value, unicode):
             return value.encode(PKG_INFO_ENCODING)
@@ -231,17 +235,15 @@
         if name in _ALL_FIELDS:
             return name
         name = name.replace('-', '_').lower()
-        if name in _ATTR2FIELD:
-            return _ATTR2FIELD[name]
-        return name
+        return _ATTR2FIELD.get(name, name)
 
     def _default_value(self, name):
-        if name in _LISTFIELDS + _ELEMENTSFIELD:
+        if name in _LISTFIELDS or name in _ELEMENTSFIELD:
             return []
         return 'UNKNOWN'
 
     def _check_rst_data(self, data):
-        """Returns warnings when the provided data doesn't compile."""
+        """Return warnings when the provided data has syntax errors."""
         source_path = StringIO()
         parser = Parser()
         settings = frontend.OptionParser().get_default_values()
@@ -276,7 +278,7 @@
         return _LINE_PREFIX.sub('\n', value)
 
     #
-    # Public APIs
+    # Public API
     #
     def get_fullname(self):
         return '%s-%s' % (self['Name'], self['Version'])
@@ -289,7 +291,7 @@
         self.read_file(open(filepath))
 
     def read_file(self, fileob):
-        """Reads the metadata values from a file object."""
+        """Read the metadata values from a file object."""
         msg = message_from_file(fileob)
         self.version = msg['metadata-version']
 
@@ -307,8 +309,7 @@
                     self.set(field, value)
 
     def write(self, filepath):
-        """Write the metadata fields into path.
-        """
+        """Write the metadata fields to filepath."""
         pkg_info = open(filepath, 'w')
         try:
             self.write_file(pkg_info)
@@ -316,8 +317,7 @@
             pkg_info.close()
 
     def write_file(self, fileobject):
-        """Write the PKG-INFO format data to a file object.
-        """
+        """Write the PKG-INFO format data to a file object."""
         self._set_best_version()
         for field in _version2fieldlist(self.version):
             values = self.get(field)
@@ -368,17 +368,17 @@
             self.update(kwargs)
 
     def set(self, name, value):
-        """Controls then sets a metadata field"""
+        """Control then set a metadata field."""
         name = self._convert_name(name)
 
-        if (name in _ELEMENTSFIELD + ('Platform',) and
+        if ((name in _ELEMENTSFIELD or name == 'Platform') and
             not isinstance(value, (list, tuple))):
             if isinstance(value, str):
                 value = value.split(',')
             else:
                 value = []
         elif (name in _LISTFIELDS and
-            not isinstance(value, (list, tuple))):
+              not isinstance(value, (list, tuple))):
             if isinstance(value, str):
                 value = [value]
             else:
@@ -408,7 +408,7 @@
         self._set_best_version()
 
     def get(self, name):
-        """Gets a metadata field."""
+        """Get a metadata field."""
         name = self._convert_name(name)
         if name not in self._fields:
             return self._default_value(name)
@@ -443,24 +443,21 @@
         return value
 
     def check(self):
-        """Checks if the metadata are compliant."""
+        """Check if the metadata is compliant."""
         # XXX should check the versions (if the file was loaded)
-        missing = []
+        missing, warnings = [], []
         for attr in ('Name', 'Version', 'Home-page'):
             value = self[attr]
             if value == 'UNKNOWN':
                 missing.append(attr)
 
         if _HAS_DOCUTILS:
-            warnings = self._check_rst_data(self['Description'])
-        else:
-            warnings = []
+            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':
             return missing, warnings
 
-
         def is_valid_predicates(value):
             for v in value:
                 if not is_valid_predicate(v.split(';')[0]):
@@ -468,16 +465,15 @@
             return True
 
         for fields, controller in ((_PREDICATE_FIELDS, is_valid_predicates),
-                                  (_VERSIONS_FIELDS, is_valid_versions),
-                                  (_VERSION_FIELDS, is_valid_version)):
+                                   (_VERSIONS_FIELDS, is_valid_versions),
+                                   (_VERSION_FIELDS, is_valid_version)):
             for field in fields:
                 value = self[field]
                 if value == 'UNKNOWN':
                     continue
 
                 if not controller(value):
-                    warnings.append('Wrong value for "%s": %s' \
-                            % (field, value))
+                    warnings.append('Wrong value for %r: %s' % (field, value))
 
         return missing, warnings
 
@@ -494,7 +490,6 @@
 #
 # micro-language for PEP 345 environment markers
 #
-_STR_LIMIT = "'\""
 
 # allowed operators
 _OPERATORS = {'==': lambda x, y: x == y,
@@ -506,17 +501,18 @@
               '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)
 
 # restricted set of variables
 _VARS = {'sys.platform': sys.platform,
-         'python_version': '%s.%s' % (sys.version_info[0],
-                                      sys.version_info[1]),
-         'python_full_version': sys.version.split()[0],
+         'python_version': sys.version[:3],
+         'python_full_version': sys.version.split(' ', 1)[0],
          'os.name': os.name,
-         'platform.version': platform.version,
-         'platform.machine': platform.machine}
+         'platform.version': platform.version(),
+         'platform.machine': platform.machine()}
+
 
 class _Operation(object):
 
@@ -539,7 +535,7 @@
     def _is_string(self, value):
         if value is None or len(value) < 2:
             return False
-        for delimiter in _STR_LIMIT:
+        for delimiter in '"\'':
             if value[0] == value[-1] == delimiter:
                 return True
         return False
@@ -550,7 +546,7 @@
     def _convert(self, value):
         if value in _VARS:
             return self._get_var(value)
-        return value.strip(_STR_LIMIT)
+        return value.strip('"\'')
 
     def _check_name(self, value):
         if value not in _VARS:
@@ -578,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
@@ -587,7 +584,7 @@
         return self.right is not None
 
     def __repr__(self):
-        return 'OR(%s, %s)' % (repr(self.left), repr(self.right))
+        return 'OR(%r, %r)' % (self.left, self.right)
 
     def __call__(self):
         return self.left() or self.right()
@@ -602,11 +599,12 @@
         return self.right is not None
 
     def __repr__(self):
-        return 'AND(%s, %s)' % (repr(self.left), repr(self.right))
+        return 'AND(%r, %r)' % (self.left, self.right)
 
     def __call__(self):
         return self.left() and self.right()
 
+
 class _CHAIN(object):
 
     def __init__(self, execution_context=None):
@@ -668,8 +666,9 @@
                 return False
         return True
 
+
 def _interpret(marker, execution_context=None):
-    """Interprets a marker and return a result given the environment."""
+    """Interpret a marker and return a result depending on environment."""
     marker = marker.strip()
     operations = _CHAIN(execution_context)
     tokenize(StringIO(marker).readline, operations.eat)
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':
@@ -737,14 +747,14 @@
             self.setupData['name'] = m.group(1)
             self.setupData['version'] = m.group(2)
 
-        for root, dirs, files in os.walk('.'):
+        for root, dirs, files in os.walk(os.curdir):
             for file in files:
-                if root == '.' and file == 'setup.py': continue
+                if root == os.curdir and file == 'setup.py': continue
                 fileName = os.path.join(root, file)
                 self.inspectFile(fileName)
 
                 if file == '__init__.py':
-                    trySrc = os.path.join('.', 'src')
+                    trySrc = os.path.join(os.curdir, 'src')
                     tmpRoot = root
                     if tmpRoot.startswith(trySrc):
                         tmpRoot = tmpRoot[len(trySrc):]
@@ -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/__init__.py b/src/distutils2/tests/__init__.py
--- a/src/distutils2/tests/__init__.py
+++ b/src/distutils2/tests/__init__.py
@@ -23,7 +23,7 @@
 
 from test.test_support import TESTFN    # use TESTFN from stdlib/test_support.
 
-here = os.path.dirname(__file__)
+here = os.path.dirname(__file__) or os.curdir
 
 verbose = 1
 
diff --git a/src/distutils2/tests/conversions/02_after.py b/src/distutils2/tests/conversions/02_after.py
--- a/src/distutils2/tests/conversions/02_after.py
+++ b/src/distutils2/tests/conversions/02_after.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf8 -*-
+# -*- encoding: utf-8 -*-
 import sys
 import os
 from distutils2.core import setup, Extension
diff --git a/src/distutils2/tests/conversions/02_before.py b/src/distutils2/tests/conversions/02_before.py
--- a/src/distutils2/tests/conversions/02_before.py
+++ b/src/distutils2/tests/conversions/02_before.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf8 -*-
+# -*- encoding: utf-8 -*-
 import sys
 import os
 from distutils.core import setup, Extension
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,14 +1,14 @@
 """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. 
+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
@@ -21,12 +21,12 @@
     >>> 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). 
+implementations (static HTTP and XMLRPC over HTTP).
 """
 
 import Queue
@@ -53,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):
@@ -79,18 +79,18 @@
         self.pypi.stop()
 
 class PyPIServer(threading.Thread):
-    """PyPI Mock server.
+    """PyPI Mocked server.
     Provides a mocked version of the PyPI API's, to ease tests.
 
     Support serving static content and serving previously given text.
     """
 
     def __init__(self, test_static_path=None,
-                 static_filesystem_paths=["default"], 
+                 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.
 
@@ -103,9 +103,9 @@
         threading.Thread.__init__(self)
         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: 
+        if not self._serve_xmlrpc:
             self.server = HTTPServer(('', 0), PyPIRequestHandler)
             self.server.RequestHandlerClass.pypi_server = self
 
@@ -114,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:
@@ -201,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.
@@ -239,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])
 
@@ -258,15 +258,15 @@
         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,
-             python_version="source", comment="comment", 
-             author="John Doe", author_email="john at doe.name", 
+             python_version="source", comment="comment",
+             author="John Doe", author_email="john at doe.name",
              maintainer="Main Tayner", maintainer_email="maintainer_mail",
              project_url="http://project_url/", homepage="http://homepage/",
-             keywords="", platform="UNKNOWN", classifiers=[], licence="", 
+             keywords="", platform="UNKNOWN", classifiers=[], licence="",
              description="Description", summary="Summary", stable_version="",
              ordering="", documentation_id="", code_kwalitee_id="",
              installability_id="", obsoletes=[], obsoletes_dist=[],
@@ -277,7 +277,7 @@
         self.name = name
         self.version = version
         self.hidden = hidden
-        
+
         # URL infos
         self.url = url
         self.digest = digest
@@ -286,7 +286,7 @@
         self.python_version = python_version
         self.comment = comment
         self.type = type
-        
+
         # metadata
         self.author = author
         self.author_email = author_email
@@ -305,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
@@ -314,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,
@@ -330,38 +330,38 @@
 
     def metadata(self):
         return {
-            'maintainer': self.maintainer, 
-            'project_url': [self.project_url], 
-            'maintainer_email': self.maintainer_email, 
-            'cheesecake_code_kwalitee_id': self.cheesecake_code_kwalitee_id, 
-            'keywords': self.keywords, 
-            'obsoletes_dist': self.obsoletes_dist, 
-            'requires_external': self.requires_external, 
-            'author': self.author, 
+            'maintainer': self.maintainer,
+            'project_url': [self.project_url],
+            'maintainer_email': self.maintainer_email,
+            'cheesecake_code_kwalitee_id': self.cheesecake_code_kwalitee_id,
+            'keywords': self.keywords,
+            'obsoletes_dist': self.obsoletes_dist,
+            'requires_external': self.requires_external,
+            'author': self.author,
             'author_email': self.author_email,
-            'download_url': self.url, 
-            'platform': self.platform, 
-            'version': self.version, 
-            'obsoletes': self.obsoletes, 
-            'provides': self.provides, 
-            'cheesecake_documentation_id': self.cheesecake_documentation_id, 
-            '_pypi_hidden': self.hidden, 
-            'description': self.description, 
-            '_pypi_ordering': 19, 
-            'requires_dist': self.requires_dist, 
-            'requires_python': self.requires_python, 
-            'classifiers': [], 
-            'name': self.name, 
-            'licence': self.licence, 
-            'summary': self.summary, 
-            'home_page': self.homepage, 
-            'stable_version': self.stable_version, 
+            'download_url': self.url,
+            'platform': self.platform,
+            'version': self.version,
+            'obsoletes': self.obsoletes,
+            'provides': self.provides,
+            'cheesecake_documentation_id': self.cheesecake_documentation_id,
+            '_pypi_hidden': self.hidden,
+            'description': self.description,
+            '_pypi_ordering': 19,
+            'requires_dist': self.requires_dist,
+            'requires_python': self.requires_python,
+            'classifiers': [],
+            'name': self.name,
+            'licence': self.licence,
+            'summary': self.summary,
+            'home_page': self.homepage,
+            'stable_version': self.stable_version,
             'provides_dist': self.provides_dist or "%s (%s)" % (self.name,
                                                               self.version),
-            'requires': self.requires, 
+            'requires': self.requires,
             'cheesecake_installability_id': self.cheesecake_installability_id,
         }
-    
+
     def search_result(self):
         return {
             '_pypi_ordering': 0,
@@ -372,7 +372,7 @@
 
 class XMLRPCMockIndex(object):
     """Mock XMLRPC server"""
-    
+
     def __init__(self, dists=[]):
         self._dists = dists
 
@@ -386,7 +386,7 @@
 
     def set_search_result(self, result):
         """set a predefined search result"""
-        self._search_result = result 
+        self._search_result = result
 
     def _get_search_results(self):
         results = []
@@ -401,7 +401,7 @@
         return [r.search_result() for r in results]
 
     def list_package(self):
-        return [d.name for d in self._dists] 
+        return [d.name for d in self._dists]
 
     def package_releases(self, package_name, show_hidden=False):
         if show_hidden:
@@ -411,14 +411,14 @@
             # 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 
+        return [d.url_infos() for d in self._dists
                 if d.name == package_name and d.version == version]
 
     def release_data(self, package_name, version):
-        release = [d for d in self._dists 
-                   if d.name == package_name and d.version == version] 
+        release = [d for d in self._dists
+                   if d.name == package_name and d.version == version]
         if release:
             return release[0].metadata()
         else:
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
@@ -3,6 +3,27 @@
 Always import unittest from this module, it will be the right version
 (standard library unittest for 2.7 and higher, third-party unittest2
 release for older versions).
+
+Three helper classes are provided: LoggingSilencer, TempdirManager and
+EnvironGuard. They are written to be used as mixins, e.g. ::
+
+    from distutils2.tests.support import unittest
+    from distutils2.tests.support import LoggingSilencer
+
+    class SomeTestCase(LoggingSilencer, unittest.TestCase):
+
+If you need to define a setUp method on your test class, you have to
+call the mixin class' setUp method or it won't work (same thing for
+tearDown):
+
+        def setUp(self):
+            super(self.__class__, self).setUp()
+            ... # other setup code
+
+Read each class' docstring to see their purpose and usage.
+
+Also provided is a DummyCommand class, useful to mock commands in the
+tests of another command that needs them (see docstring).
 """
 
 import os
@@ -10,26 +31,36 @@
 import shutil
 import tempfile
 from copy import deepcopy
-import warnings
 
 from distutils2 import log
 from distutils2.log import DEBUG, INFO, WARN, ERROR, FATAL
 
-if sys.version_info >= (2, 7):
-    # improved unittest package from 2.7's standard library
+if sys.version_info >= (3, 2):
+    # improved unittest package from 3.2's standard library
     import unittest
 else:
     # external release of same package for older versions
     import unittest2 as unittest
 
+__all__ = ['LoggingSilencer', 'TempdirManager', 'EnvironGuard',
+           'DummyCommand', 'unittest']
+
+
 class LoggingSilencer(object):
+    """TestCase-compatible mixin to catch logging calls.
+
+    Every log message that goes through distutils2.log will get appended to
+    self.logs instead of being printed. You can check that your code logs
+    warnings and errors as documented by inspecting that list; helper methods
+    get_logs and clear_logs are also provided.
+    """
 
     def setUp(self):
         super(LoggingSilencer, self).setUp()
-        self.threshold = log.set_threshold(log.FATAL)
+        self.threshold = log.set_threshold(FATAL)
         # catching warnings
-        # when log will be replaced by logging
-        # we won't need such monkey-patch anymore
+        # when log is replaced by logging we won't need
+        # such monkey-patching anymore
         self._old_log = log.Log._log
         log.Log._log = self._log
         self.logs = []
@@ -45,6 +76,10 @@
         self.logs.append((level, msg, args))
 
     def get_logs(self, *levels):
+        """Return a list of caught messages with level in `levels`.
+
+        Example: self.get_logs(log.WARN, log.DEBUG) -> list
+        """
         def _format(msg, args):
             if len(args) == 0:
                 return msg
@@ -53,48 +88,41 @@
                 in self.logs if level in levels]
 
     def clear_logs(self):
-        self.logs = []
+        """Empty the internal list of caught messages."""
+        del self.logs[:]
+
 
 class TempdirManager(object):
-    """Mix-in class that handles temporary directories for test cases.
+    """TestCase-compatible mixin to create temporary directories and files.
 
-    This is intended to be used with unittest.TestCase.
+    Directories and files created in a test_* method will be removed after it
+    has run.
     """
 
     def setUp(self):
         super(TempdirManager, self).setUp()
-        self.tempdirs = []
-        self.tempfiles = []
+        self._basetempdir = tempfile.mkdtemp()
 
     def tearDown(self):
         super(TempdirManager, self).tearDown()
-        while self.tempdirs:
-            d = self.tempdirs.pop()
-            shutil.rmtree(d, os.name in ('nt', 'cygwin'))
-        for file_ in self.tempfiles:
-            if os.path.exists(file_):
-                os.remove(file_)
+        shutil.rmtree(self._basetempdir, os.name in ('nt', 'cygwin'))
 
     def mktempfile(self):
-        """Create a temporary file that will be cleaned up."""
-        tempfile_ = tempfile.NamedTemporaryFile()
-        self.tempfiles.append(tempfile_.name)
-        return tempfile_
+        """Create a read-write temporary file and return it."""
+        fd, fn = tempfile.mkstemp(dir=self._basetempdir)
+        os.close(fd)
+        return open(fn, 'w+')
 
     def mkdtemp(self):
-        """Create a temporary directory that will be cleaned up.
-
-        Returns the path of the directory.
-        """
-        d = tempfile.mkdtemp()
-        self.tempdirs.append(d)
+        """Create a temporary directory and return its path."""
+        d = tempfile.mkdtemp(dir=self._basetempdir)
         return d
 
     def write_file(self, path, content='xxx'):
-        """Writes a file in the given path.
+        """Write a file at the given path.
 
-
-        path can be a string or a sequence.
+        path can be a string, a tuple or a list; if it's a tuple or list,
+        os.path.join will be used to produce a path.
         """
         if isinstance(path, (list, tuple)):
             path = os.path.join(*path)
@@ -105,41 +133,35 @@
             f.close()
 
     def create_dist(self, pkg_name='foo', **kw):
-        """Will generate a test environment.
+        """Create a stub distribution object and files.
 
-        This function creates:
-         - a Distribution instance using keywords
-         - a temporary directory with a package structure
+        This function creates a Distribution instance (use keyword arguments
+        to customize it) and a temporary directory with a project structure
+        (currently an empty directory).
 
-        It returns the package directory and the distribution
-        instance.
+        It returns the path to the directory and the Distribution instance.
+        You can use TempdirManager.write_file to write any file in that
+        directory, e.g. setup scripts or Python modules.
         """
+        # Late import so that third parties can import support without
+        # loading a ton of distutils2 modules in memory.
         from distutils2.dist import Distribution
         tmp_dir = self.mkdtemp()
         pkg_dir = os.path.join(tmp_dir, pkg_name)
         os.mkdir(pkg_dir)
         dist = Distribution(attrs=kw)
-
         return pkg_dir, dist
 
-class DummyCommand:
-    """Class to store options for retrieval via set_undefined_options()."""
-
-    def __init__(self, **kwargs):
-        for kw, val in kwargs.items():
-            setattr(self, kw, val)
-
-    def ensure_finalized(self):
-        pass
 
 class EnvironGuard(object):
+    """TestCase-compatible mixin to save and restore the environment."""
 
     def setUp(self):
         super(EnvironGuard, self).setUp()
         self.old_environ = deepcopy(os.environ)
 
     def tearDown(self):
-        for key, value in self.old_environ.items():
+        for key, value in self.old_environ.iteritems():
             if os.environ.get(key) != value:
                 os.environ[key] = value
 
@@ -148,3 +170,18 @@
                 del os.environ[key]
 
         super(EnvironGuard, self).tearDown()
+
+
+class DummyCommand(object):
+    """Class to store options for retrieval via set_undefined_options().
+
+    Useful for mocking one dependency command in the tests for another
+    command, see e.g. the dummy build command in test_build_scripts.
+    """
+
+    def __init__(self, **kwargs):
+        for kw, val in kwargs.iteritems():
+            setattr(self, kw, val)
+
+    def ensure_finalized(self):
+        pass
diff --git a/src/distutils2/tests/test_Mixin2to3.py b/src/distutils2/tests/test_Mixin2to3.py
--- a/src/distutils2/tests/test_Mixin2to3.py
+++ b/src/distutils2/tests/test_Mixin2to3.py
@@ -1,6 +1,5 @@
 """Tests for distutils.command.build_py."""
 import sys
-import tempfile
 
 import distutils2
 from distutils2.tests import support
@@ -10,7 +9,7 @@
 
 class Mixin2to3TestCase(support.TempdirManager, unittest.TestCase):
 
-    @unittest.skipUnless(sys.version > '2.6', 'Need >= 2.6')
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_convert_code_only(self):
         # used to check if code gets converted properly.
         code_content = "print 'test'\n"
@@ -27,7 +26,7 @@
 
         self.assertEquals(new_code_content, converted_code_content)
 
-    @unittest.skipUnless(sys.version > '2.6', 'Need >= 2.6')
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_doctests_only(self):
         # used to check if doctests gets converted properly.
         doctest_content = '"""\n>>> print test\ntest\n"""\nprint test\n\n'
diff --git a/src/distutils2/tests/test_bdist_msi.py b/src/distutils2/tests/test_bdist_msi.py
--- a/src/distutils2/tests/test_bdist_msi.py
+++ b/src/distutils2/tests/test_bdist_msi.py
@@ -10,8 +10,8 @@
                        support.LoggingSilencer,
                        unittest.TestCase):
 
-    @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
-    def test_minial(self):
+    @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
+    def test_minimal(self):
         # minimal test XXX need more tests
         from distutils2.command.bdist_msi import bdist_msi
         pkg_pth, dist = self.create_dist()
diff --git a/src/distutils2/tests/test_build_ext.py b/src/distutils2/tests/test_build_ext.py
--- a/src/distutils2/tests/test_build_ext.py
+++ b/src/distutils2/tests/test_build_ext.py
@@ -45,7 +45,7 @@
             build_ext.USER_BASE = site.USER_BASE
 
     # XXX only works with 2.6 > -- dunno why yet
-    @unittest.skipUnless(sys.version_info >= (2, 6,), 'works for >= 2.6')
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_build_ext(self):
         global ALREADY_TESTED
         xx_c = os.path.join(self.tmp_dir, 'xxmodule.c')
@@ -126,11 +126,8 @@
         # make sure we get some library dirs under solaris
         self.assertTrue(len(cmd.library_dirs) > 0)
 
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_user_site(self):
-        # site.USER_SITE was introduced in 2.6
-        if sys.version < '2.6':
-            return
-
         import site
         dist = Distribution({'name': 'xx'})
         cmd = build_ext(dist)
diff --git a/src/distutils2/tests/test_build_py.py b/src/distutils2/tests/test_build_py.py
--- a/src/distutils2/tests/test_build_py.py
+++ b/src/distutils2/tests/test_build_py.py
@@ -95,7 +95,7 @@
             sys.stdout = old_stdout
 
     @unittest.skipUnless(hasattr(sys, 'dont_write_bytecode'),
-                         'dont_write_bytecode support')
+                         'sys.dont_write_bytecode not supported')
     def test_dont_write_bytecode(self):
         # makes sure byte_compile is not used
         pkg_dir, dist = self.create_dist()
diff --git a/src/distutils2/tests/test_check.py b/src/distutils2/tests/test_check.py
--- a/src/distutils2/tests/test_check.py
+++ b/src/distutils2/tests/test_check.py
@@ -43,7 +43,7 @@
         # get an error if there are missing metadata
         self.assertRaises(DistutilsSetupError, self._run, {}, **{'strict': 1})
 
-        # and of course, no error when all metadata are present
+        # and of course, no error when all metadata fields are present
         cmd = self._run(metadata, strict=1)
         self.assertEqual(len(cmd._warnings), 0)
 
diff --git a/src/distutils2/tests/test_cmd.py b/src/distutils2/tests/test_cmd.py
--- a/src/distutils2/tests/test_cmd.py
+++ b/src/distutils2/tests/test_cmd.py
@@ -98,7 +98,7 @@
 
     def test_ensure_dirname(self):
         cmd = self.cmd
-        cmd.option1 = os.path.dirname(__file__)
+        cmd.option1 = os.path.dirname(__file__) or os.curdir
         cmd.ensure_dirname('option1')
         cmd.option2 = 'xxx'
         self.assertRaises(DistutilsOptionError, cmd.ensure_dirname, 'option2')
diff --git a/src/distutils2/tests/test_converter.py b/src/distutils2/tests/test_converter.py
--- a/src/distutils2/tests/test_converter.py
+++ b/src/distutils2/tests/test_converter.py
@@ -17,7 +17,7 @@
 
 class ConverterTestCase(unittest.TestCase):
 
-    @unittest.skipUnless(not sys.version < '2.6', 'Needs Python >=2.6')
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_conversions(self):
         # for all XX_before in the conversions/ dir
         # we run the refactoring tool
diff --git a/src/distutils2/tests/test_core.py b/src/distutils2/tests/test_core.py
--- a/src/distutils2/tests/test_core.py
+++ b/src/distutils2/tests/test_core.py
@@ -64,13 +64,13 @@
         f = self.write_setup(setup_using___file__)
         for s in ['init', 'config', 'commandline', 'run']:
             distutils2.core.run_setup(f, stop_after=s)
-        self.assertRaises(ValueError, distutils2.core.run_setup, 
+        self.assertRaises(ValueError, distutils2.core.run_setup,
                           f, stop_after='bob')
 
     def test_run_setup_args(self):
         f = self.write_setup(setup_using___file__)
-        d = distutils2.core.run_setup(f, script_args=["--help"], 
-                                        stop_after="init")
+        d = distutils2.core.run_setup(f, script_args=["--help"],
+                                      stop_after="init")
         self.assertEqual(['--help'], d.script_args)
 
     def test_run_setup_uses_current_dir(self):
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
@@ -1,4 +1,4 @@
-# -*- coding: utf8 -*-
+# -*- coding: utf-8 -*-
 
 """Tests for distutils2.dist."""
 import os
@@ -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',
@@ -240,6 +276,49 @@
         # make sure --no-user-cfg disables the user cfg file
         self.assertEqual(len(all_files)-1, len(files))
 
+    def test_special_hooks_parsing(self):
+        temp_home = self.mkdtemp()
+        config_files = [os.path.join(temp_home, "config1.cfg"),
+                        os.path.join(temp_home, "config2.cfg")]
+
+        # Store two aliased hooks in config files
+        self.write_file((temp_home, "config1.cfg"), '[test_dist]\npre-hook.a = type')
+        self.write_file((temp_home, "config2.cfg"), '[test_dist]\npre-hook.b = type')
+
+        sys.argv.extend(["--command-packages",
+                         "distutils2.tests",
+                         "test_dist"])
+        cmd = self.create_distribution(config_files).get_command_obj("test_dist")
+        self.assertEqual(cmd.pre_hook, {"a": 'type', "b": 'type'})
+
+
+    def test_hooks_get_run(self):
+        temp_home = self.mkdtemp()
+        config_file = os.path.join(temp_home, "config1.cfg")
+
+        self.write_file((temp_home, "config1.cfg"), textwrap.dedent('''
+            [test_dist]
+            pre-hook.test = distutils2.tests.test_dist.DistributionTestCase.log_pre_call
+            post-hook.test = distutils2.tests.test_dist.DistributionTestCase.log_post_call'''))
+
+        sys.argv.extend(["--command-packages",
+                         "distutils2.tests",
+                         "test_dist"])
+        d = self.create_distribution([config_file])
+        cmd = d.get_command_obj("test_dist")
+
+        # prepare the call recorders
+        record = []
+        DistributionTestCase.log_pre_call = staticmethod(lambda _cmd: record.append(('pre', _cmd)))
+        DistributionTestCase.log_post_call = staticmethod(lambda _cmd: record.append(('post', _cmd)))
+        test_dist.run = lambda _cmd: record.append(('run', _cmd))
+        test_dist.finalize_options = lambda _cmd: record.append(('finalize_options', _cmd))
+
+        d.run_command('test_dist')
+        self.assertEqual(record, [('finalize_options', cmd),
+                                  ('pre', cmd),
+                                  ('run', cmd),
+                                  ('post', cmd)])
 
 class MetadataTestCase(support.TempdirManager, support.EnvironGuard,
                        unittest.TestCase):
diff --git a/src/distutils2/tests/test_index_dist.py b/src/distutils2/tests/test_index_dist.py
--- a/src/distutils2/tests/test_index_dist.py
+++ b/src/distutils2/tests/test_index_dist.py
@@ -18,7 +18,7 @@
 
 class TestReleaseInfo(unittest.TestCase):
 
-    def test_instanciation(self):
+    def test_instantiation(self):
         # Test the DistInfo class provides us the good attributes when
         # given on construction
         release = ReleaseInfo("FooBar", "1.1")
@@ -106,7 +106,7 @@
         })
         self.assertEqual(2, len(d.urls))
 
-    def test_comparaison(self):
+    def test_comparison(self):
         # Test that we can compare DistInfoributionInfoList
         foo1 = ReleaseInfo("foo", "1.0")
         foo2 = ReleaseInfo("foo", "2.0")
@@ -123,23 +123,20 @@
     def test_download(self, server):
         # Download is possible, and the md5 is checked if given
 
-        add_to_tmpdirs = lambda x: self.tempdirs.append(os.path.dirname(x))
-
         url = "%s/simple/foobar/foobar-0.1.tar.gz" % server.full_address
         # check md5 if given
         dist = Dist(url=url, hashname="md5",
                     hashval="d41d8cd98f00b204e9800998ecf8427e")
-        add_to_tmpdirs(dist.download())
+        dist.download(self.mkdtemp())
 
         # a wrong md5 fails
         dist2 = Dist(url=url, hashname="md5", hashval="wrongmd5")
 
-        self.assertRaises(HashDoesNotMatch, dist2.download)
-        add_to_tmpdirs(dist2.downloaded_location)
+        self.assertRaises(HashDoesNotMatch, dist2.download, self.mkdtemp())
 
         # we can omit the md5 hash
         dist3 = Dist(url=url)
-        add_to_tmpdirs(dist3.download())
+        dist3.download(self.mkdtemp())
 
         # and specify a temporary location
         # for an already downloaded dist
@@ -177,7 +174,7 @@
         self.assertIn(releases[0], filtered)
         self.assertIn(releases[1], filtered)
 
-    def test_add_release(self):
+    def test_append(self):
         # When adding a new item to the list, the behavior is to test if
         # a release with the same name and version number already exists,
         # and if so, to add a new distribution for it. If the distribution type
diff --git a/src/distutils2/tests/test_index_simple.py b/src/distutils2/tests/test_index_simple.py
--- a/src/distutils2/tests/test_index_simple.py
+++ b/src/distutils2/tests/test_index_simple.py
@@ -222,7 +222,7 @@
             # create the index using both servers
             crawler = Crawler(server.full_address + "/simple/",
                 hosts=('*',), timeout=1,  # set the timeout to 1s for the tests
-                mirrors=[mirror.full_address, ])
+                mirrors=[mirror.full_address])
 
             # this should not raise a timeout
             self.assertEqual(4, len(crawler.get_releases("foo")))
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
@@ -75,11 +75,9 @@
         check_path(cmd.install_scripts, os.path.join(destination, "bin"))
         check_path(cmd.install_data, destination)
 
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_user_site(self):
-        # site.USER_SITE was introduced in 2.6
-        if sys.version < '2.6':
-            return
-
+        # test install with --user
         # preparing the environment for the test
         self.old_user_base = get_config_var('userbase')
         self.old_user_site = get_path('purelib', '%s_user' % os.name)
@@ -195,11 +193,12 @@
         cmd.ensure_finalized()
         cmd.run()
 
-        # let's check the RECORD file was created with one
-        # line (the egg info file)
+        # let's check the RECORD file was created with four
+        # lines, one for each .dist-info entry: METADATA,
+        # INSTALLER, REQUSTED, RECORD
         f = open(cmd.record)
         try:
-            self.assertEqual(len(f.readlines()), 1)
+            self.assertEqual(len(f.readlines()), 4)
         finally:
             f.close()
 
diff --git a/src/distutils2/tests/test_install_distinfo.py b/src/distutils2/tests/test_install_distinfo.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/tests/test_install_distinfo.py
@@ -0,0 +1,202 @@
+"""Tests for ``distutils2.command.install_distinfo``. """
+
+import os
+import sys
+import csv
+
+from distutils2.command.install_distinfo import install_distinfo
+from distutils2.core import Command
+from distutils2.metadata import DistributionMetadata
+from distutils2.tests import support
+from distutils2.tests.support import unittest
+
+try:
+    import hashlib
+except ImportError:
+    from distutils2._backport import hashlib
+
+
+class DummyInstallCmd(Command):
+
+    def __init__(self, dist=None):
+        self.outputs = []
+        self.distribution = dist
+
+    def __getattr__(self, name):
+            return None
+
+    def ensure_finalized(self):
+        pass
+
+    def get_outputs(self):
+        return self.outputs + \
+               self.get_finalized_command('install_distinfo').get_outputs()
+
+
+class InstallDistinfoTestCase(support.TempdirManager,
+                              support.LoggingSilencer,
+                              support.EnvironGuard,
+                              unittest.TestCase):
+
+    checkLists = lambda self, x, y: self.assertListEqual(sorted(x), sorted(y))
+
+    def test_empty_install(self):
+        pkg_dir, dist = self.create_dist(name='foo',
+                                         version='1.0')
+        install_dir = self.mkdtemp()
+
+        install = DummyInstallCmd(dist)
+        dist.command_obj['install'] = install
+
+        cmd = install_distinfo(dist)
+        dist.command_obj['install_distinfo'] = cmd
+
+        cmd.initialize_options()
+        cmd.distinfo_dir = install_dir
+        cmd.ensure_finalized()
+        cmd.run()
+
+        self.checkLists(os.listdir(install_dir), ['foo-1.0.dist-info'])
+
+        dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+        self.checkLists(os.listdir(dist_info),
+                        ['METADATA', 'RECORD', 'REQUESTED', 'INSTALLER'])
+        self.assertEqual(open(os.path.join(dist_info, 'INSTALLER')).read(),
+                         'distutils')
+        self.assertEqual(open(os.path.join(dist_info, 'REQUESTED')).read(),
+                         '')
+        meta_path = os.path.join(dist_info, 'METADATA')
+        self.assertTrue(DistributionMetadata(path=meta_path).check())
+
+    def test_installer(self):
+        pkg_dir, dist = self.create_dist(name='foo',
+                                         version='1.0')
+        install_dir = self.mkdtemp()
+
+        install = DummyInstallCmd(dist)
+        dist.command_obj['install'] = install
+
+        cmd = install_distinfo(dist)
+        dist.command_obj['install_distinfo'] = cmd
+
+        cmd.initialize_options()
+        cmd.distinfo_dir = install_dir
+        cmd.installer = 'bacon-python'
+        cmd.ensure_finalized()
+        cmd.run()
+
+        dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+        self.assertEqual(open(os.path.join(dist_info, 'INSTALLER')).read(),
+                         'bacon-python')
+
+    def test_requested(self):
+        pkg_dir, dist = self.create_dist(name='foo',
+                                         version='1.0')
+        install_dir = self.mkdtemp()
+
+        install = DummyInstallCmd(dist)
+        dist.command_obj['install'] = install
+
+        cmd = install_distinfo(dist)
+        dist.command_obj['install_distinfo'] = cmd
+
+        cmd.initialize_options()
+        cmd.distinfo_dir = install_dir
+        cmd.requested = False
+        cmd.ensure_finalized()
+        cmd.run()
+
+        dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+        self.checkLists(os.listdir(dist_info),
+                        ['METADATA', 'RECORD', 'INSTALLER'])
+
+    def test_no_record(self):
+        pkg_dir, dist = self.create_dist(name='foo',
+                                         version='1.0')
+        install_dir = self.mkdtemp()
+
+        install = DummyInstallCmd(dist)
+        dist.command_obj['install'] = install
+
+        cmd = install_distinfo(dist)
+        dist.command_obj['install_distinfo'] = cmd
+
+        cmd.initialize_options()
+        cmd.distinfo_dir = install_dir
+        cmd.no_record = True
+        cmd.ensure_finalized()
+        cmd.run()
+
+        dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+        self.checkLists(os.listdir(dist_info),
+                        ['METADATA', 'REQUESTED', 'INSTALLER'])
+
+    def test_record(self):
+        pkg_dir, dist = self.create_dist(name='foo',
+                                         version='1.0')
+        install_dir = self.mkdtemp()
+
+        install = DummyInstallCmd(dist)
+        dist.command_obj['install'] = install
+
+        fake_dists = os.path.join(os.path.dirname(__file__), '..',
+                                  '_backport', 'tests', 'fake_dists')
+        fake_dists = os.path.realpath(fake_dists)
+
+        # for testing, we simply add all files from _backport's fake_dists
+        dirs = []
+        for dir in os.listdir(fake_dists):
+                full_path = os.path.join(fake_dists, dir)
+                if (not dir.endswith('.egg') or dir.endswith('.egg-info') or
+                    dir.endswith('.dist-info')) and os.path.isdir(full_path):
+                    dirs.append(full_path)
+
+        for dir in dirs:
+            for (path, subdirs, files) in os.walk(dir):
+                install.outputs += [os.path.join(path, f) for f in files]
+                install.outputs += [os.path.join('path', f + 'c')
+                                    for f in files if f.endswith('.py')]
+
+
+        cmd = install_distinfo(dist)
+        dist.command_obj['install_distinfo'] = cmd
+
+        cmd.initialize_options()
+        cmd.distinfo_dir = install_dir
+        cmd.ensure_finalized()
+        cmd.run()
+
+        dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+
+        expected = []
+        for f in install.get_outputs():
+            if f.endswith('.pyc') or \
+               f == os.path.join(install_dir, 'foo-1.0.dist-info', 'RECORD'):
+                expected.append([f, '', ''])
+            else:
+                size = os.path.getsize(f)
+                md5 = hashlib.md5()
+                md5.update(open(f).read())
+                hash = md5.hexdigest()
+                expected.append([f, hash, str(size)])
+
+        parsed = []
+        f = open(os.path.join(dist_info, 'RECORD'), 'rb')
+        try:
+            reader = csv.reader(f, delimiter=',',
+                                   lineterminator=os.linesep,
+                                   quotechar='"')
+            parsed = list(reader)
+        finally:
+            f.close()
+
+        self.maxDiff = None
+        self.checkLists(parsed, expected)
+
+
+def test_suite():
+    return unittest.makeSuite(InstallDistinfoTestCase)
+
+
+if __name__ == "__main__":
+    unittest.main(defaultTest="test_suite")
diff --git a/src/distutils2/tests/test_install_lib.py b/src/distutils2/tests/test_install_lib.py
--- a/src/distutils2/tests/test_install_lib.py
+++ b/src/distutils2/tests/test_install_lib.py
@@ -57,9 +57,8 @@
         # setting up a dist environment
         cmd.compile = cmd.optimize = 1
         cmd.install_dir = pkg_dir
-        f = os.path.join(pkg_dir, 'foo.py')
-        self.write_file(f, '# python file')
-        cmd.distribution.py_modules = [pkg_dir]
+        f = os.path.join(pkg_dir, '__init__.py')
+        self.write_file(f, '# python package')
         cmd.distribution.ext_modules = [Extension('foo', ['xxx'])]
         cmd.distribution.packages = [pkg_dir]
         cmd.distribution.script_name = 'setup.py'
@@ -74,9 +73,8 @@
         # setting up a dist environment
         cmd.compile = cmd.optimize = 1
         cmd.install_dir = pkg_dir
-        f = os.path.join(pkg_dir, 'foo.py')
-        self.write_file(f, '# python file')
-        cmd.distribution.py_modules = [pkg_dir]
+        f = os.path.join(pkg_dir, '__init__.py')
+        self.write_file(f, '# python package')
         cmd.distribution.ext_modules = [Extension('foo', ['xxx'])]
         cmd.distribution.packages = [pkg_dir]
         cmd.distribution.script_name = 'setup.py'
@@ -84,7 +82,8 @@
         # get_input should return 2 elements
         self.assertEqual(len(cmd.get_inputs()), 2)
 
-    @unittest.skipUnless(bytecode_support, 'sys.dont_write_bytecode not supported')
+    @unittest.skipUnless(bytecode_support,
+                         'sys.dont_write_bytecode not supported')
     def test_dont_write_bytecode(self):
         # makes sure byte_compile is not used
         pkg_dir, dist = self.create_dist()
diff --git a/src/distutils2/tests/test_metadata.py b/src/distutils2/tests/test_metadata.py
--- a/src/distutils2/tests/test_metadata.py
+++ b/src/distutils2/tests/test_metadata.py
@@ -1,6 +1,7 @@
 """Tests for distutils.command.bdist."""
 import os
 import sys
+import platform
 from StringIO import StringIO
 
 from distutils2.metadata import (DistributionMetadata, _interpret,
@@ -12,25 +13,59 @@
 
 class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase):
 
+    def test_instantiation(self):
+        PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
+        fp = open(PKG_INFO)
+        try:
+            contents = fp.read()
+        finally:
+            fp.close()
+        fp = StringIO(contents)
+
+        m = DistributionMetadata()
+        self.assertRaises(MetadataUnrecognizedVersionError, m.items)
+
+        m = DistributionMetadata(PKG_INFO)
+        self.assertEqual(len(m.items()), 22)
+
+        m = DistributionMetadata(fileobj=fp)
+        self.assertEqual(len(m.items()), 22)
+
+        m = DistributionMetadata(mapping=dict(name='Test', version='1.0'))
+        self.assertEqual(len(m.items()), 11)
+
+        d = dict(m.items())
+        self.assertRaises(TypeError, DistributionMetadata,
+                          PKG_INFO, fileobj=fp)
+        self.assertRaises(TypeError, DistributionMetadata,
+                          PKG_INFO, mapping=d)
+        self.assertRaises(TypeError, DistributionMetadata,
+                          fileobj=fp, mapping=d)
+        self.assertRaises(TypeError, DistributionMetadata,
+                          PKG_INFO, mapping=m, fileobj=fp)
 
     def test_interpret(self):
-        platform = sys.platform
+        sys_platform = sys.platform
         version = sys.version.split()[0]
         os_name = os.name
+        platform_version = platform.version()
+        platform_machine = platform.machine()
 
-        self.assertTrue(_interpret("sys.platform == '%s'" % platform))
+        self.assertTrue(_interpret("sys.platform == '%s'" % sys_platform))
         self.assertTrue(_interpret(
-            "sys.platform == '%s' or python_version == '2.4'" % platform))
+            "sys.platform == '%s' or python_version == '2.4'" % sys_platform))
         self.assertTrue(_interpret(
             "sys.platform == '%s' and python_full_version == '%s'" %
-            (platform, version)))
-        self.assertTrue(_interpret("'%s' == sys.platform" % platform))
-
+            (sys_platform, version)))
+        self.assertTrue(_interpret("'%s' == sys.platform" % sys_platform))
         self.assertTrue(_interpret('os.name == "%s"' % os_name))
+        self.assertTrue(_interpret(
+            'platform.version == "%s" and platform.machine == "%s"' %
+            (platform_version, platform_machine)))
 
         # stuff that need to raise a syntax error
         ops = ('os.name == os.name', 'os.name == 2', "'2' == '2'",
-               'okpjonon', '', 'os.name ==')
+               'okpjonon', '', 'os.name ==', 'python_version == 2.4')
         for op in ops:
             self.assertRaises(SyntaxError, _interpret, op)
 
@@ -62,17 +97,13 @@
 
         PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
         metadata = DistributionMetadata(PKG_INFO)
-        res = StringIO()
-        metadata.write_file(res)
-        res.seek(0)
-        res = res.read()
-        f = open(PKG_INFO)
-        try:
-            # XXX this is not used
-            wanted = f.read()
-        finally:
-            f.close()
-        self.assertTrue('Keywords: keyring,password,crypt' in res)
+        out = StringIO()
+        metadata.write_file(out)
+        out.seek(0)
+        res = DistributionMetadata()
+        res.read_file(out)
+        for k in metadata.keys():
+            self.assertTrue(metadata[k] == res[k])
 
     def test_metadata_markers(self):
         # see if we can be platform-aware
@@ -82,6 +113,10 @@
         metadata = DistributionMetadata(platform_dependent=True)
         metadata.read_file(StringIO(content))
         self.assertEqual(metadata['Requires-Dist'], ['bar'])
+        metadata['Name'] = "baz; sys.platform == 'blah'"
+        # FIXME is None or 'UNKNOWN' correct here?
+        # where is that documented?
+        self.assertEquals(metadata['Name'], None)
 
         # test with context
         context = {'sys.platform': 'okook'}
@@ -109,15 +144,15 @@
         metadata.read_file(out)
         self.assertEqual(wanted, metadata['Description'])
 
-    def test_mapper_apis(self):
+    def test_mapping_api(self):
         PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
         content = open(PKG_INFO).read()
         content = content % sys.platform
-        metadata = DistributionMetadata()
-        metadata.read_file(StringIO(content))
+        metadata = DistributionMetadata(fileobj=StringIO(content))
         self.assertIn('Version', metadata.keys())
         self.assertIn('0.5', metadata.values())
         self.assertIn(('Version', '0.5'), metadata.items())
+        #TODO test update
 
     def test_versions(self):
         metadata = DistributionMetadata()
@@ -211,6 +246,10 @@
         metadata = DistributionMetadata()
         metadata['Version'] = 'rr'
         metadata['Requires-dist'] = ['Foo (a)']
+        if metadata.docutils_support:
+            missing, warnings = metadata.check()
+            self.assertEqual(len(warnings), 2)
+            metadata.docutils_support = False
         missing, warnings = metadata.check()
         self.assertEqual(missing, ['Name', 'Home-page'])
         self.assertEqual(len(warnings), 2)
diff --git a/src/distutils2/tests/test_msvc9compiler.py b/src/distutils2/tests/test_msvc9compiler.py
--- a/src/distutils2/tests/test_msvc9compiler.py
+++ b/src/distutils2/tests/test_msvc9compiler.py
@@ -64,7 +64,7 @@
 class msvc9compilerTestCase(support.TempdirManager,
                             unittest.TestCase):
 
-    @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
+    @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
     def test_no_compiler(self):
         # makes sure query_vcvarsall throws
         # a DistutilsPlatformError if the compiler
@@ -86,7 +86,7 @@
         finally:
             msvc9compiler.find_vcvarsall = old_find_vcvarsall
 
-    @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
+    @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
     def test_reg_class(self):
         from distutils2.msvccompiler import get_build_version
         if get_build_version() < 8.0:
@@ -110,7 +110,7 @@
         keys = Reg.read_keys(HKCU, r'Control Panel')
         self.assertTrue('Desktop' in keys)
 
-    @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
+    @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
     def test_remove_visual_c_ref(self):
         from distutils2.msvc9compiler import MSVCCompiler
         tempdir = self.mkdtemp()
diff --git a/src/distutils2/tests/test_pypi_versions.py b/src/distutils2/tests/test_pypi_versions.py
--- a/src/distutils2/tests/test_pypi_versions.py
+++ b/src/distutils2/tests/test_pypi_versions.py
@@ -1,43 +1,39 @@
-#
-## test_pypi_versions.py
-##
-##  A very simple test to see what percentage of the current pypi packages
-##  have versions that can be converted automatically by distutils' new
-##  suggest_normalized_version() into PEP-386 compatible versions.
-##
-##  Requires : Python 2.5+
-##
-##  Written by: ssteinerX at gmail.com
-#
+"""PEP 386 compatibility test with current distributions on PyPI.
+
+A very simple test to see what percentage of the current PyPI packages
+have versions that can be converted automatically by distutils2's new
+suggest_normalized_version into PEP 386-compatible versions.
+"""
+
+# XXX This file does not actually run tests, move it to a script
+
+# Written by ssteinerX at gmail.com
+
+import os
+import xmlrpclib
 
 try:
    import cPickle as pickle
-except:
+except ImportError:
    import pickle
 
-import xmlrpclib
-import os.path
-
 from distutils2.version import suggest_normalized_version
 from distutils2.tests import run_unittest
 from distutils2.tests.support import unittest
 
 def test_pypi():
-    #
-    ## To re-run from scratch, just delete these two .pkl files
-    #
+    # FIXME need a better way to do that
+    # To re-run from scratch, just delete these two .pkl files
     INDEX_PICKLE_FILE = 'pypi-index.pkl'
     VERSION_PICKLE_FILE = 'pypi-version.pkl'
 
     package_info = version_info = []
 
-    #
-    ## if there's a saved version of the package list
-    ##      restore it
-    ## else:
-    ##      pull the list down from pypi
-    ##      save a pickled version of it
-    #
+    # if there's a saved version of the package list
+    #      restore it
+    # else:
+    #      pull the list down from pypi
+    #      save a pickled version of it
     if os.path.exists(INDEX_PICKLE_FILE):
         print "Loading saved pypi data..."
         f = open(INDEX_PICKLE_FILE, 'rb')
@@ -57,13 +53,11 @@
         finally:
             f.close()
 
-    #
-    ## If there's a saved list of the versions from the packages
-    ##      restore it
-    ## else
-    ##     extract versions from the package list
-    ##     save a pickled version of it
-    #
+    # If there's a saved list of the versions from the packages
+    #      restore it
+    # else
+    #     extract versions from the package list
+    #     save a pickled version of it
     versions = []
     if os.path.exists(VERSION_PICKLE_FILE):
         print "Loading saved version info..."
diff --git a/src/distutils2/tests/test_register.py b/src/distutils2/tests/test_register.py
--- a/src/distutils2/tests/test_register.py
+++ b/src/distutils2/tests/test_register.py
@@ -1,5 +1,5 @@
+# -*- encoding: utf-8 -*-
 """Tests for distutils.command.register."""
-# -*- encoding: utf8 -*-
 import sys
 import os
 import getpass
@@ -161,7 +161,7 @@
         # therefore used afterwards by other commands
         self.assertEqual(cmd.distribution.password, 'password')
 
-    def test_registering(self):
+    def test_registration(self):
         # this test runs choice 2
         cmd = self._get_cmd()
         inputs = RawInputs('2', 'tarek', 'tarek at ziade.org')
@@ -210,7 +210,7 @@
         cmd.strict = 1
         self.assertRaises(DistutilsSetupError, cmd.run)
 
-        # metadata are OK but long_description is broken
+        # metadata is OK but long_description is broken
         metadata = {'home_page': 'xxx', 'author': 'xxx',
                     'author_email': u'éxéxé',
                     'name': 'xxx', 'version': 'xxx',
diff --git a/src/distutils2/tests/test_sdist.py b/src/distutils2/tests/test_sdist.py
--- a/src/distutils2/tests/test_sdist.py
+++ b/src/distutils2/tests/test_sdist.py
@@ -241,7 +241,7 @@
 
     @unittest.skipUnless(zlib, "requires zlib")
     def test_metadata_check_option(self):
-        # testing the `medata-check` option
+        # testing the `check-metadata` option
         dist, cmd = self.get_cmd(metadata={})
 
         # this should raise some warnings !
@@ -295,7 +295,7 @@
         self.assertRaises(DistutilsOptionError, cmd.finalize_options)
 
     @unittest.skipUnless(zlib, "requires zlib")
-    @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support")
+    @unittest.skipUnless(UID_GID_SUPPORT, "requires grp and pwd support")
     def test_make_distribution_owner_group(self):
 
         # check if tar and gzip are installed
diff --git a/src/distutils2/tests/test_upload.py b/src/distutils2/tests/test_upload.py
--- a/src/distutils2/tests/test_upload.py
+++ b/src/distutils2/tests/test_upload.py
@@ -1,5 +1,5 @@
+# -*- encoding: utf-8 -*-
 """Tests for distutils.command.upload."""
-# -*- encoding: utf8 -*-
 import os
 import sys
 
@@ -101,6 +101,38 @@
         self.assertEqual(handler.command, 'POST')
         self.assertNotIn('\n', headers['authorization'])
 
+    def test_upload_docs(self):
+        path = os.path.join(self.tmp_dir, 'xxx')
+        self.write_file(path)
+        command, pyversion, filename = 'xxx', '2.6', path
+        dist_files = [(command, pyversion, filename)]
+        docs_path = os.path.join(self.tmp_dir, "build", "docs")
+        os.makedirs(docs_path)
+        self.write_file(os.path.join(docs_path, "index.html"), "yellow")
+        self.write_file(self.rc, PYPIRC)
+
+        # lets run it
+        pkg_dir, dist = self.create_dist(dist_files=dist_files, author=u'dédé')
+
+        cmd = upload(dist)
+        cmd.get_finalized_command("build").run()
+        cmd.upload_docs = True
+        cmd.ensure_finalized()
+        cmd.repository = self.pypi.full_address
+        try:
+            prev_dir = os.getcwd()
+            os.chdir(self.tmp_dir)
+            cmd.run()
+        finally:
+            os.chdir(prev_dir)
+
+        handler, request_data = self.pypi.requests[-1]
+        action, name, content =\
+            request_data.split("----------------GHSKFJDLGDS7543FJKLFHRE75642756743254")[1:4]
+        
+        self.assertIn('name=":action"', action)
+        self.assertIn("doc_upload", action)
+
 def test_suite():
     return unittest.makeSuite(UploadTestCase)
 
diff --git a/src/distutils2/tests/test_upload_docs.py b/src/distutils2/tests/test_upload_docs.py
--- a/src/distutils2/tests/test_upload_docs.py
+++ b/src/distutils2/tests/test_upload_docs.py
@@ -1,13 +1,19 @@
+# -*- encoding: utf8 -*-
 """Tests for distutils.command.upload_docs."""
-# -*- encoding: utf8 -*-
-import httplib, os, os.path, shutil, sys, tempfile, zipfile
-from cStringIO import StringIO
+import os
+import sys
+import httplib
+import shutil
+import zipfile
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
 
 from distutils2.command import upload_docs as upload_docs_mod
 from distutils2.command.upload_docs import (upload_docs, zip_dir,
-                                    encode_multipart)
+                                            encode_multipart)
 from distutils2.core import Distribution
-
 from distutils2.errors import DistutilsFileError, DistutilsOptionError
 
 from distutils2.tests import support
@@ -59,7 +65,7 @@
         self.cmd = upload_docs(self.dist)
 
     def test_default_uploaddir(self):
-        sandbox = tempfile.mkdtemp()
+        sandbox = self.mkdtemp()
         previous = os.getcwd()
         os.chdir(sandbox)
         try:
@@ -72,7 +78,7 @@
 
     def prepare_sample_dir(self, sample_dir=None):
         if sample_dir is None:
-            sample_dir = tempfile.mkdtemp()
+            sample_dir = self.mkdtemp()
         os.mkdir(os.path.join(sample_dir, "docs"))
         self.write_file(os.path.join(sample_dir, "docs", "index.html"), "Ce mortel ennui")
         self.write_file(os.path.join(sample_dir, "index.html"), "Oh la la")
diff --git a/src/distutils2/tests/test_util.py b/src/distutils2/tests/test_util.py
--- a/src/distutils2/tests/test_util.py
+++ b/src/distutils2/tests/test_util.py
@@ -4,7 +4,6 @@
 from copy import copy
 from StringIO import StringIO
 import subprocess
-import tempfile
 import time
 
 from distutils2.tests import captured_stdout
@@ -19,7 +18,7 @@
                              _find_exe_version, _MAC_OS_X_LD_VERSION,
                              byte_compile, find_packages, spawn, find_executable,
                              _nt_quote_args, get_pypirc_path, generate_pypirc,
-                             read_pypirc)
+                             read_pypirc, resolve_dotted_name)
 
 from distutils2 import util
 from distutils2.tests import support
@@ -288,7 +287,7 @@
         self.assertEqual(res[2], None)
 
     @unittest.skipUnless(hasattr(sys, 'dont_write_bytecode'),
-                          'no dont_write_bytecode support')
+                         'sys.dont_write_bytecode not supported')
     def test_dont_write_bytecode(self):
         # makes sure byte_compile raise a DistutilsError
         # if sys.dont_write_bytecode is True
@@ -301,9 +300,9 @@
 
     def test_newer(self):
         self.assertRaises(DistutilsFileError, util.newer, 'xxx', 'xxx')
-        self.newer_f1 = tempfile.NamedTemporaryFile()
+        self.newer_f1 = self.mktempfile()
         time.sleep(1)
-        self.newer_f2 = tempfile.NamedTemporaryFile()
+        self.newer_f2 = self.mktempfile()
         self.assertTrue(util.newer(self.newer_f2.name, self.newer_f1.name))
 
     def test_find_packages(self):
@@ -343,7 +342,17 @@
         res = find_packages([root], ['pkg1.pkg2'])
         self.assertEqual(set(res), set(['pkg1', 'pkg5', 'pkg1.pkg3', 'pkg1.pkg3.pkg6']))
 
-    @unittest.skipUnless(sys.version > '2.6', 'Need Python 2.6 or more')
+    def test_resolve_dotted_name(self):
+        self.assertEqual(UtilTestCase, resolve_dotted_name("distutils2.tests.test_util.UtilTestCase"))
+        self.assertEqual(UtilTestCase.test_resolve_dotted_name,
+                         resolve_dotted_name("distutils2.tests.test_util.UtilTestCase.test_resolve_dotted_name"))
+
+        self.assertRaises(ImportError, resolve_dotted_name,
+                          "distutils2.tests.test_util.UtilTestCaseNot")
+        self.assertRaises(ImportError, resolve_dotted_name,
+                          "distutils2.tests.test_util.UtilTestCase.nonexistent_attribute")
+
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_run_2to3_on_code(self):
         content = "print 'test'"
         converted_content = "print('test')"
@@ -358,7 +367,7 @@
         file_handle.close()
         self.assertEquals(new_content, converted_content)
 
-    @unittest.skipUnless(sys.version > '2.6', 'Need Python 2.6 or more')
+    @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
     def test_run_2to3_on_doctests(self):
         # to check if text files containing doctests only get converted.
         content = ">>> print 'test'\ntest\n"
@@ -385,7 +394,7 @@
 
 
     @unittest.skipUnless(os.name in ('nt', 'posix'),
-                         'Runs only under posix or nt')
+                         'runs only under posix or nt')
     def test_spawn(self):
         tmpdir = self.mkdtemp()
 
diff --git a/src/distutils2/util.py b/src/distutils2/util.py
--- a/src/distutils2/util.py
+++ b/src/distutils2/util.py
@@ -88,8 +88,8 @@
         raise ValueError("path '%s' cannot end with '/'" % pathname)
 
     paths = pathname.split('/')
-    while '.' in paths:
-        paths.remove('.')
+    while os.curdir in paths:
+        paths.remove(os.curdir)
     if not paths:
         return os.curdir
     return os.path.join(*paths)
@@ -121,15 +121,6 @@
             path = path[1:]
         return os.path.join(new_root, path)
 
-    elif os.name == 'mac':
-        if not os.path.isabs(pathname):
-            return os.path.join(new_root, pathname)
-        else:
-            # Chop off volume name from start of path
-            elements = pathname.split(":", 1)
-            pathname = ":" + elements[1]
-            return os.path.join(new_root, pathname)
-
     else:
         raise DistutilsPlatformError("nothing known about "
                                      "platform '%s'" % os.name)
@@ -599,7 +590,7 @@
     return path[len(root_path) + 1:].replace(os.sep, '.')
 
 
-def find_packages(paths=('.',), exclude=()):
+def find_packages(paths=(os.curdir,), exclude=()):
     """Return a list all Python packages found recursively within
     directories 'paths'
 
@@ -646,6 +637,24 @@
     return packages
 
 
+def resolve_dotted_name(dotted_name):
+    module_name, rest = dotted_name.split('.')[0], dotted_name.split('.')[1:]
+    while len(rest) > 0:
+        try:
+            ret = __import__(module_name)
+            break
+        except ImportError:
+            if rest == []:
+                raise
+            module_name += ('.' + rest[0])
+            rest = rest[1:]
+    while len(rest) > 0:
+        try:
+            ret = getattr(ret, rest.pop(0))
+        except AttributeError:
+            raise ImportError
+    return ret
+
 # utility functions for 2to3 support
 
 def run_2to3(files, doctests_only=False, fixer_names=None, options=None,
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,20 @@
 """
 
 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
     we want to completely skip `module`.
@@ -16,16 +27,17 @@
     # distributions, such a Ubuntu, really like to build link farm in
     # /usr/lib in order to save a few bytes on the disk.
     dirnames = [dirname(module.__file__)]
-    
+
     pymod = module.__file__.rstrip("c")
     if islink(pymod):
         dirnames.append(dirname(realpath(pymod)))
     return dirnames
 
+
 def parse_opts():
     parser = OptionParser(usage="%prog [OPTIONS]",
                           description="run the distutils2 unittests")
-    
+
     parser.add_option("-q", "--quiet", help="do not print verbose messages",
                       action="store_true", default=False)
     parser.add_option("-c", "--coverage", action="store_true", default=False,
@@ -36,15 +48,23 @@
                       default=False,
                       help=("Show line numbers of statements in each module "
                             "that weren't executed."))
-    
+
     opts, args = parser.parse_args()
     return opts, args
 
+
 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)
@@ -63,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():
@@ -71,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:
@@ -89,6 +106,7 @@
 
     return ret
 
+
 def run_tests(verbose):
     import distutils2.tests
     from distutils2.tests import run_unittest, reap_children, TestFailed
@@ -108,12 +126,11 @@
     finally:
         reap_children()
 
+
 if __name__ == "__main__":
     try:
         from distutils2.tests.support import unittest
     except ImportError:
         sys.stderr.write('Error: You have to install unittest2')
         sys.exit(1)
-
     sys.exit(test_main())
-
diff --git a/src/runtests.py b/src/runtests.py
--- a/src/runtests.py
+++ b/src/runtests.py
@@ -1,9 +1,12 @@
+#!/usr/bin/env python
 """Tests for distutils2.
 
 The tests for distutils2 are defined in the distutils2.tests package.
 """
+
 import sys
 
+
 def test_main():
     import distutils2.tests
     from distutils2.tests import run_unittest, reap_children, TestFailed
@@ -23,12 +26,11 @@
     finally:
         reap_children()
 
+
 if __name__ == "__main__":
     try:
         from distutils2.tests.support import unittest
     except ImportError:
         sys.stderr.write('Error: You have to install unittest2')
         sys.exit(1)
-
     sys.exit(test_main())
-
diff --git a/src/setup.cfg b/src/setup.cfg
new file mode 100644
--- /dev/null
+++ b/src/setup.cfg
@@ -0,0 +1,3 @@
+[build_ext]
+# needed so that tests work without mucking with sys.path
+inplace = 1
diff --git a/src/setup.py b/src/setup.py
--- a/src/setup.py
+++ b/src/setup.py
@@ -168,29 +168,25 @@
 
         # The _hashlib module wraps optimized implementations
         # of hash functions from the OpenSSL library.
-        exts.append(Extension('_hashlib', ['_hashopenssl.c'],
+        exts.append(Extension('distutils2._backport._hashlib',
+                              ['distutils2/_backport/_hashopenssl.c'],
                               include_dirs = [ssl_inc_dir],
                               library_dirs = [os.path.dirname(ssl_lib)],
                               libraries = oslibs[os.name]))
     else:
-        exts.append(Extension('_sha', ['shamodule.c']) )
-        exts.append(Extension('_md5',
-                              sources=['md5module.c', 'md5.c'],
-                              depends=['md5.h']) )
+        exts.append(Extension('distutils2._backport._sha',
+                              ['distutils2/_backport/shamodule.c']))
+        exts.append(Extension('distutils2._backport._md5',
+                              sources=['distutils2/_backport/md5module.c',
+                                       'distutils2/_backport/md5.c'],
+                              depends=['distutils2/_backport/md5.h']) )
 
     if (not ssl_lib or openssl_ver < 0x00908000):
         # OpenSSL doesn't do these until 0.9.8 so we'll bring our own
-        exts.append(Extension('_sha256', ['sha256module.c']))
-        exts.append(Extension('_sha512', ['sha512module.c']))
-
-    def prepend_modules(filename):
-        return os.path.join('Modules', filename)
-
-    # all the C code is in the Modules subdirectory, prepend the path
-    for ext in exts:
-        ext.sources = [prepend_modules(fn) for fn in ext.sources]
-        if hasattr(ext, 'depends') and ext.depends is not None:
-            ext.depends = [prepend_modules(fn) for fn in ext.depends]
+        exts.append(Extension('distutils2._backport._sha256',
+                              ['distutils2/_backport/sha256module.c']))
+        exts.append(Extension('distutils2._backport._sha512',
+                              ['distutils2/_backport/sha512module.c']))
 
     return exts
 
diff --git a/src/tests.sh b/src/tests.sh
--- a/src/tests.sh
+++ b/src/tests.sh
@@ -1,20 +1,18 @@
 #!/bin/sh
 echo -n "Running tests for Python 2.4... "
-rm -rf *.so
-python2.4 setup.py build_ext -i -q 2> /dev/null > /dev/null
+rm -f distutils2/_backport/_hashlib.so
+python2.4 setup.py build_ext -f -q 2> /dev/null > /dev/null
 python2.4 -Wd runtests.py -q 2> /dev/null
-rm -rf *.so
 if [ $? -ne 0 ];then
     echo "Failed"
+    rm -f distutils2/_backport/_hashlib.so
     exit 1
 else
     echo "Success"
 fi
 
 echo -n "Running tests for Python 2.5... "
-python2.5 setup.py build_ext -i -q 2> /dev/null > /dev/null
 python2.5 -Wd runtests.py -q 2> /dev/null
-rm -rf *.so
 if [ $? -ne 0 ];then
     echo "Failed"
     exit 1
@@ -23,7 +21,7 @@
 fi
 
 echo -n "Running tests for Python 2.6... "
-python2.6 -Wd -bb -3 runtests.py -q 2> /dev/null
+python2.6 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
     echo "Failed"
     exit 1

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


More information about the Python-checkins mailing list