Branch merge
diff --git a/Doc/packaging/setupcfg.rst b/Doc/packaging/setupcfg.rst
index 1a394b5..9b09a4c 100644
--- a/Doc/packaging/setupcfg.rst
+++ b/Doc/packaging/setupcfg.rst
@@ -19,6 +19,8 @@
    :local:
 
 
+.. _setupcfg-syntax:
+
 Syntax
 ======
 
@@ -117,6 +119,8 @@
 file**.  This will be useful to let users publish a single file.
 
 
+.. _setupcfg-sections:
+
 Description of sections and fields
 ==================================
 
@@ -149,6 +153,8 @@
    on the command line.
 
 
+.. _setupcfg-section-global:
+
 Global options
 --------------
 
@@ -194,6 +200,9 @@
       setup_hooks = _setuphooks.customize_config
 
 
+
+.. _setupcfg-section-metadata:
+
 Metadata
 --------
 
@@ -318,6 +327,8 @@
 from the fields present in the file.
 
 
+.. _setupcfg-section-files:
+
 Files
 -----
 
@@ -325,7 +336,8 @@
 
 packages_root
    the root directory containing all packages and modules
-   (default: current directory).  *optional*
+   (default: current directory, i.e. the project's top-level
+   directory where :file:`setup.cfg` lives).  *optional*
 
 packages
    a list of packages the project includes *optional*, *multi*
@@ -337,8 +349,8 @@
    a list of scripts the project includes *optional*, *multi*
 
 extra_files
-   a list of patterns to include extra files *optional*,
-   *multi*
+   a list of patterns for additional files to include in source distributions
+   (see :ref:`packaging-manifest`) *optional*, *multi*
 
 Example::
 
@@ -747,8 +759,10 @@
 {scripts} category.
 
 
-Extension sections
-------------------
+.. _setupcfg-section-extensions:
+
+Extension modules sections
+--------------------------
 
 If a project includes extension modules written in C or C++, each one of them
 needs to have its options defined in a dedicated section.  Here's an example::
@@ -779,8 +793,10 @@
 ``--``.
 
 
-Command sections
-----------------
+.. _setupcfg-section-commands:
+
+Commands sections
+-----------------
 
 To pass options to commands without having to type them on the command line
 for each invocation, you can write them in the :file:`setup.cfg` file, in a
@@ -803,6 +819,11 @@
 Option values given in the configuration file can be overriden on the command
 line.  See :ref:`packaging-setup-config` for more information.
 
+These sections are also used to define :ref:`command hooks
+<packaging-command-hooks>`.
+
+
+.. _setupcfg-extensibility:
 
 Extensibility
 =============
@@ -817,6 +838,8 @@
    X-Debian-Name = python-distribute
 
 
+.. _setupcfg-changes:
+
 Changes in the specification
 ============================
 
@@ -852,6 +875,8 @@
 - May write optional fields.
 
 
+.. _setupcfg-acks:
+
 Acknowledgments
 ===============
 
diff --git a/Lib/packaging/command/build_py.py b/Lib/packaging/command/build_py.py
index 0eafffa..e5b10b0 100644
--- a/Lib/packaging/command/build_py.py
+++ b/Lib/packaging/command/build_py.py
@@ -1,6 +1,7 @@
 """Build pure Python modules (just copy to build directory)."""
 
 import os
+import imp
 import sys
 from glob import glob
 
@@ -330,9 +331,10 @@
             outputs.append(filename)
             if include_bytecode:
                 if self.compile:
-                    outputs.append(filename + "c")
+                    outputs.append(imp.cache_from_source(filename))
                 if self.optimize > 0:
-                    outputs.append(filename + "o")
+                    outputs.append(imp.cache_from_source(filename,
+                                                         debug_override=False))
 
         outputs += [
             os.path.join(build_dir, filename)
diff --git a/Lib/packaging/command/install_lib.py b/Lib/packaging/command/install_lib.py
index 5e81b41..558966d 100644
--- a/Lib/packaging/command/install_lib.py
+++ b/Lib/packaging/command/install_lib.py
@@ -1,6 +1,7 @@
 """Install all modules (extensions and pure Python)."""
 
 import os
+import imp
 import sys
 import logging
 
@@ -172,9 +173,10 @@
             if ext != PYTHON_SOURCE_EXTENSION:
                 continue
             if self.compile:
-                bytecode_files.append(py_file + "c")
+                bytecode_files.append(imp.cache_from_source(py_file))
             if self.optimize > 0:
-                bytecode_files.append(py_file + "o")
+                bytecode_files.append(imp.cache_from_source(
+                    py_file, debug_override=False))
 
         return bytecode_files
 
diff --git a/Lib/packaging/metadata.py b/Lib/packaging/metadata.py
index 9d3f8ab..2d0ffa4 100644
--- a/Lib/packaging/metadata.py
+++ b/Lib/packaging/metadata.py
@@ -185,6 +185,7 @@
 
 _FILESAFE = re.compile('[^A-Za-z0-9.]+')
 
+
 class Metadata:
     """The metadata of a release.
 
@@ -228,10 +229,8 @@
 
     def __delitem__(self, name):
         field_name = self._convert_name(name)
-        try:
-            del self._fields[field_name]
-        except KeyError:
-            raise KeyError(name)
+        # we let a KeyError propagate
+        del self._fields[field_name]
         self._set_best_version()
 
     def __contains__(self, name):
diff --git a/Lib/packaging/tests/__main__.py b/Lib/packaging/tests/__main__.py
index 51daba4..00f323e 100644
--- a/Lib/packaging/tests/__main__.py
+++ b/Lib/packaging/tests/__main__.py
@@ -3,7 +3,6 @@
 # Ripped from importlib tests, thanks Brett!
 
 import os
-import sys
 import unittest
 from test.support import run_unittest, reap_children, reap_threads
 
diff --git a/Lib/packaging/tests/support.py b/Lib/packaging/tests/support.py
index cfcfe01..6e26ea4 100644
--- a/Lib/packaging/tests/support.py
+++ b/Lib/packaging/tests/support.py
@@ -82,10 +82,13 @@
     configured to record all messages logged to the 'packaging' logger.
 
     Use get_logs to retrieve messages and self.loghandler.flush to discard
-    them.  get_logs automatically flushes the logs; if you test code that
-    generates logging messages but don't use get_logs, you have to flush
-    manually before doing other checks on logging message, otherwise you
-    will get irrelevant results.  See example in test_command_check.
+    them.  get_logs automatically flushes the logs, unless you pass
+    *flush=False*, for example to make multiple calls to the method with
+    different level arguments.  If your test calls some code that generates
+    logging message and then you don't call get_logs, you will need to flush
+    manually before testing other code in the same test_* method, otherwise
+    get_logs in the next lines will see messages from the previous lines.
+    See example in test_command_check.
     """
 
     def setUp(self):
@@ -109,25 +112,23 @@
         logger2to3.setLevel(self._old_levels[1])
         super(LoggingCatcher, self).tearDown()
 
-    def get_logs(self, *levels):
-        """Return all log messages with level in *levels*.
+    def get_logs(self, level=logging.WARNING, flush=True):
+        """Return all log messages with given level.
 
-        Without explicit levels given, returns all messages.  *levels* defaults
-        to all levels.  For log calls with arguments (i.e.
-        logger.info('bla bla %r', arg)), the messages will be formatted before
-        being returned (e.g. "bla bla 'thing'").
+        *level* defaults to logging.WARNING.
+
+        For log calls with arguments (i.e.  logger.info('bla bla %r', arg)),
+        the messages will be formatted before being returned (e.g. "bla bla
+        'thing'").
 
         Returns a list.  Automatically flushes the loghandler after being
-        called.
-
-        Example: self.get_logs(logging.WARN, logging.DEBUG).
+        called, unless *flush* is False (this is useful to get e.g. all
+        warnings then all info messages).
         """
-        if not levels:
-            messages = [log.getMessage() for log in self.loghandler.buffer]
-        else:
-            messages = [log.getMessage() for log in self.loghandler.buffer
-                        if log.levelno in levels]
-        self.loghandler.flush()
+        messages = [log.getMessage() for log in self.loghandler.buffer
+                    if log.levelno == level]
+        if flush:
+            self.loghandler.flush()
         return messages
 
 
diff --git a/Lib/packaging/tests/test_command_build_py.py b/Lib/packaging/tests/test_command_build_py.py
index a978c91..9d519e3 100644
--- a/Lib/packaging/tests/test_command_build_py.py
+++ b/Lib/packaging/tests/test_command_build_py.py
@@ -102,6 +102,40 @@
             os.chdir(cwd)
             sys.stdout = old_stdout
 
+    @unittest.skipIf(sys.dont_write_bytecode, 'byte-compile disabled')
+    def test_byte_compile(self):
+        project_dir, dist = self.create_dist(py_modules=['boiledeggs'])
+        os.chdir(project_dir)
+        self.write_file('boiledeggs.py', 'import antigravity')
+        cmd = build_py(dist)
+        cmd.compile = True
+        cmd.build_lib = 'here'
+        cmd.finalize_options()
+        cmd.run()
+
+        found = os.listdir(cmd.build_lib)
+        self.assertEqual(sorted(found), ['__pycache__', 'boiledeggs.py'])
+        found = os.listdir(os.path.join(cmd.build_lib, '__pycache__'))
+        self.assertEqual(found, ['boiledeggs.%s.pyc' % imp.get_tag()])
+
+    @unittest.skipIf(sys.dont_write_bytecode, 'byte-compile disabled')
+    def test_byte_compile_optimized(self):
+        project_dir, dist = self.create_dist(py_modules=['boiledeggs'])
+        os.chdir(project_dir)
+        self.write_file('boiledeggs.py', 'import antigravity')
+        cmd = build_py(dist)
+        cmd.compile = True
+        cmd.optimize = 1
+        cmd.build_lib = 'here'
+        cmd.finalize_options()
+        cmd.run()
+
+        found = os.listdir(cmd.build_lib)
+        self.assertEqual(sorted(found), ['__pycache__', 'boiledeggs.py'])
+        found = os.listdir(os.path.join(cmd.build_lib, '__pycache__'))
+        self.assertEqual(sorted(found), ['boiledeggs.%s.pyc' % imp.get_tag(),
+                                         'boiledeggs.%s.pyo' % imp.get_tag()])
+
     def test_dont_write_bytecode(self):
         # makes sure byte_compile is not used
         pkg_dir, dist = self.create_dist()
@@ -118,6 +152,7 @@
 
         self.assertIn('byte-compiling is disabled', self.get_logs()[0])
 
+
 def test_suite():
     return unittest.makeSuite(BuildPyTestCase)
 
diff --git a/Lib/packaging/tests/test_command_check.py b/Lib/packaging/tests/test_command_check.py
index 3a4ab42..0b91050 100644
--- a/Lib/packaging/tests/test_command_check.py
+++ b/Lib/packaging/tests/test_command_check.py
@@ -1,6 +1,5 @@
 """Tests for distutils.command.check."""
 
-import logging
 from packaging.command.check import check
 from packaging.metadata import _HAS_DOCUTILS
 from packaging.errors import PackagingSetupError, MetadataMissingError
@@ -27,11 +26,11 @@
         # let's run the command with no metadata at all
         # by default, check is checking the metadata
         # should have some warnings
-        cmd = self._run()
+        self._run()
         # trick: using assertNotEqual with an empty list will give us a more
         # useful error message than assertGreater(.., 0) when the code change
         # and the test fails
-        self.assertNotEqual([], self.get_logs(logging.WARNING))
+        self.assertNotEqual(self.get_logs(), [])
 
         # now let's add the required fields
         # and run it again, to make sure we don't get
@@ -40,8 +39,8 @@
                     'author_email': 'xxx',
                     'name': 'xxx', 'version': '4.2',
                     }
-        cmd = self._run(metadata)
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self._run(metadata)
+        self.assertEqual(self.get_logs(), [])
 
         # now with the strict mode, we should
         # get an error if there are missing metadata
@@ -53,8 +52,8 @@
         self.loghandler.flush()
 
         # and of course, no error when all metadata fields are present
-        cmd = self._run(metadata, strict=True)
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self._run(metadata, strict=True)
+        self.assertEqual(self.get_logs(), [])
 
         # now a test with non-ASCII characters
         metadata = {'home_page': 'xxx', 'author': '\u00c9ric',
@@ -62,15 +61,15 @@
                     'version': '1.2',
                     'summary': 'Something about esszet \u00df',
                     'description': 'More things about esszet \u00df'}
-        cmd = self._run(metadata)
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self._run(metadata)
+        self.assertEqual(self.get_logs(), [])
 
     def test_check_metadata_1_2(self):
         # let's run the command with no metadata at all
         # by default, check is checking the metadata
         # should have some warnings
-        cmd = self._run()
-        self.assertNotEqual([], self.get_logs(logging.WARNING))
+        self._run()
+        self.assertNotEqual(self.get_logs(), [])
 
         # now let's add the required fields and run it again, to make sure we
         # don't get any warning anymore let's use requires_python as a marker
@@ -80,8 +79,8 @@
                     'name': 'xxx', 'version': '4.2',
                     'requires_python': '2.4',
                     }
-        cmd = self._run(metadata)
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self._run(metadata)
+        self.assertEqual(self.get_logs(), [])
 
         # now with the strict mode, we should
         # get an error if there are missing metadata
@@ -99,8 +98,8 @@
 
         # now with correct version format again
         metadata['version'] = '4.2'
-        cmd = self._run(metadata, strict=True)
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self._run(metadata, strict=True)
+        self.assertEqual(self.get_logs(), [])
 
     @unittest.skipUnless(_HAS_DOCUTILS, "requires docutils")
     def test_check_restructuredtext(self):
@@ -109,9 +108,7 @@
         pkg_info, dist = self.create_dist(description=broken_rest)
         cmd = check(dist)
         cmd.check_restructuredtext()
-        self.assertEqual(len(self.get_logs(logging.WARNING)), 1)
-        # clear warnings from the previous call
-        self.loghandler.flush()
+        self.assertEqual(len(self.get_logs()), 1)
 
         # let's see if we have an error with strict=1
         metadata = {'home_page': 'xxx', 'author': 'xxx',
@@ -126,7 +123,7 @@
         dist = self.create_dist(description='title\n=====\n\ntest \u00df')[1]
         cmd = check(dist)
         cmd.check_restructuredtext()
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self.assertEqual(self.get_logs(), [])
 
     def test_check_all(self):
         self.assertRaises(PackagingSetupError, self._run,
@@ -143,18 +140,18 @@
         }
         cmd = check(dist)
         cmd.check_hooks_resolvable()
-        self.assertEqual(len(self.get_logs(logging.WARNING)), 1)
+        self.assertEqual(len(self.get_logs()), 1)
 
     def test_warn(self):
         _, dist = self.create_dist()
         cmd = check(dist)
-        self.assertEqual([], self.get_logs())
+        self.assertEqual(self.get_logs(), [])
         cmd.warn('hello')
-        self.assertEqual(['check: hello'], self.get_logs())
+        self.assertEqual(self.get_logs(), ['check: hello'])
         cmd.warn('hello %s', 'world')
-        self.assertEqual(['check: hello world'], self.get_logs())
+        self.assertEqual(self.get_logs(), ['check: hello world'])
         cmd.warn('hello %s %s', 'beautiful', 'world')
-        self.assertEqual(['check: hello beautiful world'], self.get_logs())
+        self.assertEqual(self.get_logs(), ['check: hello beautiful world'])
 
 
 def test_suite():
diff --git a/Lib/packaging/tests/test_command_clean.py b/Lib/packaging/tests/test_command_clean.py
index 8d29e4d..ab944ed 100644
--- a/Lib/packaging/tests/test_command_clean.py
+++ b/Lib/packaging/tests/test_command_clean.py
@@ -23,7 +23,7 @@
             if name == 'build_base':
                 continue
             for f in ('one', 'two', 'three'):
-                self.write_file(os.path.join(path, f))
+                self.write_file((path, f))
 
         # let's run the command
         cmd.all = True
diff --git a/Lib/packaging/tests/test_command_cmd.py b/Lib/packaging/tests/test_command_cmd.py
index 8ac9dce..6d00ec3 100644
--- a/Lib/packaging/tests/test_command_cmd.py
+++ b/Lib/packaging/tests/test_command_cmd.py
@@ -1,5 +1,6 @@
 """Tests for distutils.cmd."""
 import os
+import logging
 
 from packaging.command.cmd import Command
 from packaging.dist import Distribution
@@ -43,7 +44,7 @@
 
         wanted = ["command options for 'MyCmd':", '  option1 = 1',
                   '  option2 = 1']
-        msgs = self.get_logs()
+        msgs = self.get_logs(logging.INFO)
         self.assertEqual(msgs, wanted)
 
     def test_ensure_string(self):
diff --git a/Lib/packaging/tests/test_command_install_data.py b/Lib/packaging/tests/test_command_install_data.py
index 44a14d2..7f4af41 100644
--- a/Lib/packaging/tests/test_command_install_data.py
+++ b/Lib/packaging/tests/test_command_install_data.py
@@ -117,6 +117,11 @@
         dist.command_obj['install_distinfo'] = cmd
         cmd.run()
 
+        # first a few sanity checks
+        self.assertEqual(os.listdir(scripts_dir), ['spamd'])
+        self.assertEqual(os.listdir(install_dir), ['Spamlib-0.1.dist-info'])
+
+        # now the real test
         fn = os.path.join(install_dir, 'Spamlib-0.1.dist-info', 'RESOURCES')
         with open(fn, encoding='utf-8') as fp:
             content = fp.read().strip()
diff --git a/Lib/packaging/tests/test_command_install_dist.py b/Lib/packaging/tests/test_command_install_dist.py
index 808b568..3345d2e 100644
--- a/Lib/packaging/tests/test_command_install_dist.py
+++ b/Lib/packaging/tests/test_command_install_dist.py
@@ -1,6 +1,7 @@
 """Tests for packaging.command.install."""
 
 import os
+import imp
 import sys
 from sysconfig import (get_scheme_names, get_config_vars,
                        _SCHEMES, get_config_var, get_path)
@@ -92,21 +93,20 @@
         self.old_expand = os.path.expanduser
         os.path.expanduser = _expanduser
 
-        try:
-            # this is the actual test
-            self._test_user_site()
-        finally:
+        def cleanup():
             _CONFIG_VARS['userbase'] = self.old_user_base
             _SCHEMES.set(scheme, 'purelib', self.old_user_site)
             os.path.expanduser = self.old_expand
 
-    def _test_user_site(self):
+        self.addCleanup(cleanup)
+
         schemes = get_scheme_names()
         for key in ('nt_user', 'posix_user', 'os2_home'):
             self.assertIn(key, schemes)
 
         dist = Distribution({'name': 'xx'})
         cmd = install_dist(dist)
+
         # making sure the user option is there
         options = [name for name, short, lable in
                    cmd.user_options]
@@ -181,9 +181,11 @@
     def test_old_record(self):
         # test pre-PEP 376 --record option (outside dist-info dir)
         install_dir = self.mkdtemp()
-        project_dir, dist = self.create_dist(scripts=['hello'])
+        project_dir, dist = self.create_dist(py_modules=['hello'],
+                                             scripts=['sayhi'])
         os.chdir(project_dir)
-        self.write_file('hello', "print('o hai')")
+        self.write_file('hello.py', "def main(): print('o hai')")
+        self.write_file('sayhi', 'from hello import main; main()')
 
         cmd = install_dist(dist)
         dist.command_obj['install_dist'] = cmd
@@ -196,8 +198,9 @@
             content = f.read()
 
         found = [os.path.basename(line) for line in content.splitlines()]
-        expected = ['hello', 'METADATA', 'INSTALLER', 'REQUESTED', 'RECORD']
-        self.assertEqual(found, expected)
+        expected = ['hello.py', 'hello.%s.pyc' % imp.get_tag(), 'sayhi',
+                    'METADATA', 'INSTALLER', 'REQUESTED', 'RECORD']
+        self.assertEqual(sorted(found), sorted(expected))
 
         # XXX test that fancy_getopt is okay with options named
         # record and no-record but unrelated
diff --git a/Lib/packaging/tests/test_command_sdist.py b/Lib/packaging/tests/test_command_sdist.py
index ddc6bf7..7ea138c 100644
--- a/Lib/packaging/tests/test_command_sdist.py
+++ b/Lib/packaging/tests/test_command_sdist.py
@@ -2,7 +2,6 @@
 import os
 import zipfile
 import tarfile
-import logging
 
 from packaging.tests.support import requires_zlib
 
@@ -221,7 +220,7 @@
         # with the check subcommand
         cmd.ensure_finalized()
         cmd.run()
-        warnings = self.get_logs(logging.WARN)
+        warnings = self.get_logs()
         self.assertEqual(len(warnings), 4)
 
         # trying with a complete set of metadata
@@ -230,13 +229,10 @@
         cmd.ensure_finalized()
         cmd.metadata_check = False
         cmd.run()
-        warnings = self.get_logs(logging.WARN)
-        # removing manifest generated warnings
-        warnings = [warn for warn in warnings if
-                    not warn.endswith('-- skipping')]
-        # the remaining warnings are about the use of the default file list and
-        # the absence of setup.cfg
+        warnings = self.get_logs()
         self.assertEqual(len(warnings), 2)
+        self.assertIn('using default file list', warnings[0])
+        self.assertIn("'setup.cfg' file not found", warnings[1])
 
     def test_show_formats(self):
         __, stdout = captured_stdout(show_formats)
diff --git a/Lib/packaging/tests/test_command_test.py b/Lib/packaging/tests/test_command_test.py
index f780723..7aa1f79 100644
--- a/Lib/packaging/tests/test_command_test.py
+++ b/Lib/packaging/tests/test_command_test.py
@@ -2,7 +2,6 @@
 import re
 import sys
 import shutil
-import logging
 import unittest as ut1
 import packaging.database
 
@@ -140,7 +139,8 @@
         cmd.run()
         self.assertEqual(['build has run'], record)
 
-    def _test_works_with_2to3(self):
+    @unittest.skip('needs to be written')
+    def test_works_with_2to3(self):
         pass
 
     def test_checks_requires(self):
@@ -149,7 +149,7 @@
         phony_project = 'ohno_ohno-impossible_1234-name_stop-that!'
         cmd.tests_require = [phony_project]
         cmd.ensure_finalized()
-        logs = self.get_logs(logging.WARNING)
+        logs = self.get_logs()
         self.assertIn(phony_project, logs[-1])
 
     def prepare_a_module(self):
diff --git a/Lib/packaging/tests/test_command_upload.py b/Lib/packaging/tests/test_command_upload.py
index 82d7360..1f68c1d 100644
--- a/Lib/packaging/tests/test_command_upload.py
+++ b/Lib/packaging/tests/test_command_upload.py
@@ -129,7 +129,7 @@
         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((docs_path, "index.html"), "yellow")
         self.write_file(self.rc, PYPIRC)
 
         # let's run it
diff --git a/Lib/packaging/tests/test_command_upload_docs.py b/Lib/packaging/tests/test_command_upload_docs.py
index f443727..803e733 100644
--- a/Lib/packaging/tests/test_command_upload_docs.py
+++ b/Lib/packaging/tests/test_command_upload_docs.py
@@ -1,6 +1,7 @@
 """Tests for packaging.command.upload_docs."""
 import os
 import shutil
+import logging
 import zipfile
 try:
     import _ssl
@@ -70,9 +71,8 @@
         if sample_dir is None:
             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")
+        self.write_file((sample_dir, "docs", "index.html"), "Ce mortel ennui")
+        self.write_file((sample_dir, "index.html"), "Oh la la")
         return sample_dir
 
     def test_zip_dir(self):
@@ -141,13 +141,16 @@
         self.pypi.default_response_status = '403 Forbidden'
         self.prepare_command()
         self.cmd.run()
-        self.assertIn('Upload failed (403): Forbidden', self.get_logs()[-1])
+        errors = self.get_logs(logging.ERROR)
+        self.assertEqual(len(errors), 1)
+        self.assertIn('Upload failed (403): Forbidden', errors[0])
 
         self.pypi.default_response_status = '301 Moved Permanently'
         self.pypi.default_response_headers.append(
             ("Location", "brand_new_location"))
         self.cmd.run()
-        self.assertIn('brand_new_location', self.get_logs()[-1])
+        lastlog = self.get_logs(logging.INFO)[-1]
+        self.assertIn('brand_new_location', lastlog)
 
     def test_reads_pypirc_data(self):
         self.write_file(self.rc, PYPIRC % self.pypi.full_address)
@@ -171,7 +174,7 @@
         self.prepare_command()
         self.cmd.show_response = True
         self.cmd.run()
-        record = self.get_logs()[-1]
+        record = self.get_logs(logging.INFO)[-1]
         self.assertTrue(record, "should report the response")
         self.assertIn(self.pypi.default_response_data, record)
 
diff --git a/Lib/packaging/tests/test_config.py b/Lib/packaging/tests/test_config.py
index bcb55fb..e45fc11 100644
--- a/Lib/packaging/tests/test_config.py
+++ b/Lib/packaging/tests/test_config.py
@@ -1,7 +1,6 @@
 """Tests for packaging.config."""
 import os
 import sys
-import logging
 from io import StringIO
 
 from packaging import command
@@ -375,15 +374,14 @@
         self.write_file('README', 'yeah')
         self.write_file('hooks.py', HOOKS_MODULE)
         self.get_dist()
-        logs = self.get_logs(logging.WARNING)
-        self.assertEqual(['logging_hook called'], logs)
+        self.assertEqual(['logging_hook called'], self.get_logs())
         self.assertIn('hooks', sys.modules)
 
     def test_missing_setup_hook_warns(self):
         self.write_setup({'setup-hooks': 'this.does._not.exist'})
         self.write_file('README', 'yeah')
         self.get_dist()
-        logs = self.get_logs(logging.WARNING)
+        logs = self.get_logs()
         self.assertEqual(1, len(logs))
         self.assertIn('cannot find setup hook', logs[0])
 
@@ -397,7 +395,7 @@
         dist = self.get_dist()
 
         self.assertEqual(['haven', 'first', 'third'], dist.py_modules)
-        logs = self.get_logs(logging.WARNING)
+        logs = self.get_logs()
         self.assertEqual(1, len(logs))
         self.assertIn('cannot find setup hook', logs[0])
 
diff --git a/Lib/packaging/tests/test_create.py b/Lib/packaging/tests/test_create.py
index b896619..16dbfb7 100644
--- a/Lib/packaging/tests/test_create.py
+++ b/Lib/packaging/tests/test_create.py
@@ -80,8 +80,7 @@
             os.mkdir(os.path.join(tempdir, dir_))
 
         for file_ in files:
-            path = os.path.join(tempdir, file_)
-            self.write_file(path, 'xxx')
+            self.write_file((tempdir, file_), 'xxx')
 
         mainprogram._find_files()
         mainprogram.data['packages'].sort()
diff --git a/Lib/packaging/tests/test_dist.py b/Lib/packaging/tests/test_dist.py
index f912c6f..3089f36 100644
--- a/Lib/packaging/tests/test_dist.py
+++ b/Lib/packaging/tests/test_dist.py
@@ -1,13 +1,12 @@
 """Tests for packaging.dist."""
 import os
 import sys
-import logging
 import textwrap
 
 import packaging.dist
 
 from packaging.dist import Distribution
-from packaging.command import set_command
+from packaging.command import set_command, _COMMANDS
 from packaging.command.cmd import Command
 from packaging.errors import PackagingModuleError, PackagingOptionError
 from packaging.tests import captured_stdout
@@ -29,6 +28,9 @@
     def finalize_options(self):
         pass
 
+    def run(self):
+        pass
+
 
 class DistributionTestCase(support.TempdirManager,
                            support.LoggingCatcher,
@@ -39,12 +41,18 @@
 
     def setUp(self):
         super(DistributionTestCase, self).setUp()
+        # XXX this is ugly, we should fix the functions to accept args
+        # (defaulting to sys.argv)
         self.argv = sys.argv, sys.argv[:]
         del sys.argv[1:]
+        self._commands = _COMMANDS.copy()
 
     def tearDown(self):
         sys.argv = self.argv[0]
         sys.argv[:] = self.argv[1]
+        # XXX maybe we need a public API to remove commands
+        _COMMANDS.clear()
+        _COMMANDS.update(self._commands)
         super(DistributionTestCase, self).tearDown()
 
     @unittest.skip('needs to be updated')
@@ -74,7 +82,7 @@
                             'version': '1.2',
                             'home_page': 'xxxx',
                             'badoptname': 'xxx'})
-        logs = self.get_logs(logging.WARNING)
+        logs = self.get_logs()
         self.assertEqual(len(logs), 1)
         self.assertIn('unknown argument', logs[0])
 
@@ -85,7 +93,7 @@
                                    'version': '1.2', 'home_page': 'xxxx',
                                    'options': {}})
 
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self.assertEqual(self.get_logs(), [])
         self.assertNotIn('options', dir(dist))
 
     def test_non_empty_options(self):
diff --git a/Lib/packaging/tests/test_manifest.py b/Lib/packaging/tests/test_manifest.py
index 1c7aa93..5f89331 100644
--- a/Lib/packaging/tests/test_manifest.py
+++ b/Lib/packaging/tests/test_manifest.py
@@ -1,7 +1,6 @@
 """Tests for packaging.manifest."""
 import os
 import re
-import logging
 from io import StringIO
 from packaging.errors import PackagingTemplateError
 from packaging.manifest import Manifest, _translate_pattern, _glob_to_re
@@ -37,10 +36,10 @@
         super(ManifestTestCase, self).tearDown()
 
     def assertNoWarnings(self):
-        self.assertEqual(self.get_logs(logging.WARNING), [])
+        self.assertEqual(self.get_logs(), [])
 
     def assertWarnings(self):
-        self.assertGreater(len(self.get_logs(logging.WARNING)), 0)
+        self.assertNotEqual(self.get_logs(), [])
 
     def test_manifest_reader(self):
         tmpdir = self.mkdtemp()
@@ -51,7 +50,7 @@
         manifest = Manifest()
         manifest.read_template(MANIFEST)
 
-        warnings = self.get_logs(logging.WARNING)
+        warnings = self.get_logs()
         # the manifest should have been read and 3 warnings issued
         # (we didn't provide the files)
         self.assertEqual(3, len(warnings))
diff --git a/Lib/packaging/tests/test_metadata.py b/Lib/packaging/tests/test_metadata.py
index 6b7dd38..54a7af3 100644
--- a/Lib/packaging/tests/test_metadata.py
+++ b/Lib/packaging/tests/test_metadata.py
@@ -1,7 +1,6 @@
 """Tests for packaging.metadata."""
 import os
 import sys
-import logging
 from textwrap import dedent
 from io import StringIO
 
@@ -302,7 +301,7 @@
                           'name': 'xxx',
                           'version': 'xxx',
                           'home_page': 'xxxx'})
-        logs = self.get_logs(logging.WARNING)
+        logs = self.get_logs()
         self.assertEqual(1, len(logs))
         self.assertIn('not a valid version', logs[0])
 
@@ -418,7 +417,7 @@
         # XXX check PEP and see if 3 == 3.0
         metadata['Requires-Python'] = '>=2.6, <3.0'
         metadata['Requires-Dist'] = ['Foo (>=2.6, <3.0)']
-        self.assertEqual([], self.get_logs(logging.WARNING))
+        self.assertEqual(self.get_logs(), [])
 
     @unittest.skip('needs to be implemented')
     def test_requires_illegal(self):
diff --git a/Lib/packaging/tests/test_mixin2to3.py b/Lib/packaging/tests/test_mixin2to3.py
index 14a7487..000a992 100644
--- a/Lib/packaging/tests/test_mixin2to3.py
+++ b/Lib/packaging/tests/test_mixin2to3.py
@@ -1,4 +1,3 @@
-import sys
 import textwrap
 
 from packaging.tests import unittest, support
diff --git a/Lib/packaging/tests/test_uninstall.py b/Lib/packaging/tests/test_uninstall.py
index 8f094f4..2168b6f 100644
--- a/Lib/packaging/tests/test_uninstall.py
+++ b/Lib/packaging/tests/test_uninstall.py
@@ -61,8 +61,7 @@
         kw['pkg'] = pkg
 
         pkg_dir = os.path.join(project_dir, pkg)
-        os.mkdir(pkg_dir)
-        os.mkdir(os.path.join(pkg_dir, 'sub'))
+        os.makedirs(os.path.join(pkg_dir, 'sub'))
 
         self.write_file((project_dir, 'setup.cfg'), SETUP_CFG % kw)
         self.write_file((pkg_dir, '__init__.py'), '#')
diff --git a/Lib/packaging/tests/test_util.py b/Lib/packaging/tests/test_util.py
index 3d71a96..92aa72a 100644
--- a/Lib/packaging/tests/test_util.py
+++ b/Lib/packaging/tests/test_util.py
@@ -4,6 +4,7 @@
 import time
 import logging
 import tempfile
+import textwrap
 import subprocess
 from io import StringIO
 
@@ -355,55 +356,64 @@
         #
         root = self.mkdtemp()
         pkg1 = os.path.join(root, 'pkg1')
-        os.mkdir(pkg1)
-        self.write_file(os.path.join(pkg1, '__init__.py'))
-        os.mkdir(os.path.join(pkg1, 'pkg2'))
-        self.write_file(os.path.join(pkg1, 'pkg2', '__init__.py'))
-        os.mkdir(os.path.join(pkg1, 'pkg3'))
-        self.write_file(os.path.join(pkg1, 'pkg3', '__init__.py'))
-        os.mkdir(os.path.join(pkg1, 'pkg3', 'pkg6'))
-        self.write_file(os.path.join(pkg1, 'pkg3', 'pkg6', '__init__.py'))
-        os.mkdir(os.path.join(pkg1, 'pkg4'))
-        os.mkdir(os.path.join(pkg1, 'pkg4', 'pkg8'))
-        self.write_file(os.path.join(pkg1, 'pkg4', 'pkg8', '__init__.py'))
-        pkg5 = os.path.join(root, 'pkg5')
-        os.mkdir(pkg5)
-        self.write_file(os.path.join(pkg5, '__init__.py'))
+        os.makedirs(os.path.join(pkg1, 'pkg2'))
+        os.makedirs(os.path.join(pkg1, 'pkg3', 'pkg6'))
+        os.makedirs(os.path.join(pkg1, 'pkg4', 'pkg8'))
+        os.makedirs(os.path.join(root, 'pkg5'))
+        self.write_file((pkg1, '__init__.py'))
+        self.write_file((pkg1, 'pkg2', '__init__.py'))
+        self.write_file((pkg1, 'pkg3', '__init__.py'))
+        self.write_file((pkg1, 'pkg3', 'pkg6', '__init__.py'))
+        self.write_file((pkg1, 'pkg4', 'pkg8', '__init__.py'))
+        self.write_file((root, 'pkg5', '__init__.py'))
 
         res = find_packages([root], ['pkg1.pkg2'])
-        self.assertEqual(set(res), set(['pkg1', 'pkg5', 'pkg1.pkg3',
-                                        'pkg1.pkg3.pkg6']))
+        self.assertEqual(sorted(res),
+                         ['pkg1', 'pkg1.pkg3', 'pkg1.pkg3.pkg6', 'pkg5'])
 
     def test_resolve_name(self):
-        self.assertIs(str, resolve_name('builtins.str'))
-        self.assertEqual(
-            UtilTestCase.__name__,
-            resolve_name("packaging.tests.test_util.UtilTestCase").__name__)
-        self.assertEqual(
-            UtilTestCase.test_resolve_name.__name__,
-            resolve_name("packaging.tests.test_util.UtilTestCase."
-                         "test_resolve_name").__name__)
+        # test raw module name
+        tmpdir = self.mkdtemp()
+        sys.path.append(tmpdir)
+        self.addCleanup(sys.path.remove, tmpdir)
+        self.write_file((tmpdir, 'hello.py'), '')
 
-        self.assertRaises(ImportError, resolve_name,
-                          "packaging.tests.test_util.UtilTestCaseNot")
-        self.assertRaises(ImportError, resolve_name,
-                          "packaging.tests.test_util.UtilTestCase."
-                          "nonexistent_attribute")
+        os.makedirs(os.path.join(tmpdir, 'a', 'b'))
+        self.write_file((tmpdir, 'a', '__init__.py'), '')
+        self.write_file((tmpdir, 'a', 'b', '__init__.py'), '')
+        self.write_file((tmpdir, 'a', 'b', 'c.py'), 'class Foo: pass')
+        self.write_file((tmpdir, 'a', 'b', 'd.py'), textwrap.dedent("""\
+                         class FooBar:
+                             class Bar:
+                                 def baz(self):
+                                     pass
+                            """))
 
-    def test_import_nested_first_time(self):
-        tmp_dir = self.mkdtemp()
-        os.makedirs(os.path.join(tmp_dir, 'a', 'b'))
-        self.write_file(os.path.join(tmp_dir, 'a', '__init__.py'), '')
-        self.write_file(os.path.join(tmp_dir, 'a', 'b', '__init__.py'), '')
-        self.write_file(os.path.join(tmp_dir, 'a', 'b', 'c.py'),
-                                    'class Foo: pass')
+        # check Python, C and built-in module
+        self.assertEqual(resolve_name('hello').__name__, 'hello')
+        self.assertEqual(resolve_name('_csv').__name__, '_csv')
+        self.assertEqual(resolve_name('sys').__name__, 'sys')
 
-        try:
-            sys.path.append(tmp_dir)
-            resolve_name("a.b.c.Foo")
-            # assert nothing raised
-        finally:
-            sys.path.remove(tmp_dir)
+        # test module.attr
+        self.assertIs(resolve_name('builtins.str'), str)
+        self.assertIsNone(resolve_name('hello.__doc__'))
+        self.assertEqual(resolve_name('a.b.c.Foo').__name__, 'Foo')
+        self.assertEqual(resolve_name('a.b.d.FooBar.Bar.baz').__name__, 'baz')
+
+        # error if module not found
+        self.assertRaises(ImportError, resolve_name, 'nonexistent')
+        self.assertRaises(ImportError, resolve_name, 'non.existent')
+        self.assertRaises(ImportError, resolve_name, 'a.no')
+        self.assertRaises(ImportError, resolve_name, 'a.b.no')
+        self.assertRaises(ImportError, resolve_name, 'a.b.no.no')
+        self.assertRaises(ImportError, resolve_name, 'inva-lid')
+
+        # looking up built-in names is not supported
+        self.assertRaises(ImportError, resolve_name, 'str')
+
+        # error if module found but not attr
+        self.assertRaises(ImportError, resolve_name, 'a.b.Spam')
+        self.assertRaises(ImportError, resolve_name, 'a.b.c.Spam')
 
     def test_run_2to3_on_code(self):
         content = "print 'test'"
diff --git a/Lib/packaging/tests/test_version.py b/Lib/packaging/tests/test_version.py
index 2c86111..c863362 100644
--- a/Lib/packaging/tests/test_version.py
+++ b/Lib/packaging/tests/test_version.py
@@ -1,6 +1,5 @@
 """Tests for packaging.version."""
 import doctest
-import os
 
 from packaging.version import NormalizedVersion as V
 from packaging.version import HugeMajorVersionNumError, IrrationalVersionError
@@ -46,7 +45,6 @@
     def test_from_parts(self):
 
         for v, s in self.versions:
-            parts = v.parts
             v2 = V.from_parts(*v.parts)
             self.assertEqual(v, v2)
             self.assertEqual(str(v), str(v2))
@@ -192,7 +190,7 @@
                       'Hey (>=2.5,<2.7)')
 
         for predicate in predicates:
-            v = VersionPredicate(predicate)
+            VersionPredicate(predicate)
 
         self.assertTrue(VersionPredicate('Hey (>=2.5,<2.7)').match('2.6'))
         self.assertTrue(VersionPredicate('Ho').match('2.6'))
diff --git a/Lib/packaging/util.py b/Lib/packaging/util.py
index f8a8058..2af1149 100644
--- a/Lib/packaging/util.py
+++ b/Lib/packaging/util.py
@@ -630,22 +630,35 @@
 def resolve_name(name):
     """Resolve a name like ``module.object`` to an object and return it.
 
-    Raise ImportError if the module or name is not found.
+    This functions supports packages and attributes without depth limitation:
+    ``package.package.module.class.class.function.attr`` is valid input.
+    However, looking up builtins is not directly supported: use
+    ``builtins.name``.
+
+    Raises ImportError if importing the module fails or if one requested
+    attribute is not found.
     """
+    if '.' not in name:
+        # shortcut
+        __import__(name)
+        return sys.modules[name]
+
+    # FIXME clean up this code!
     parts = name.split('.')
     cursor = len(parts)
     module_name = parts[:cursor]
+    ret = ''
 
     while cursor > 0:
         try:
             ret = __import__('.'.join(module_name))
             break
         except ImportError:
-            if cursor == 0:
-                raise
             cursor -= 1
             module_name = parts[:cursor]
-            ret = ''
+
+    if ret == '':
+        raise ImportError(parts[0])
 
     for part in parts[1:]:
         try:
diff --git a/Misc/NEWS b/Misc/NEWS
index 178a0b3..8774334 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -1455,7 +1455,7 @@
   the amount of time needed to run the tests. "make test" and "make quicktest"
   now include some resource-intensive tests, but no longer run the test suite
   twice to check for bugs in .pyc generation. Tools/scripts/run_test.py provides
-  as an easy platform-independent way to run test suite with sensible defaults.
+  an easy platform-independent way to run test suite with sensible defaults.
 
 - Issue #12331: The test suite for the packaging module can now run from an
   installed Python.
diff --git a/setup.py b/setup.py
index 542dc49..76566bd 100644
--- a/setup.py
+++ b/setup.py
@@ -1380,8 +1380,7 @@
         # End multiprocessing
 
         # Platform-specific libraries
-        if any(platform.startswith(prefix)
-               for prefix in ("linux", "freebsd", "gnukfreebsd")):
+        if platform.startswith(('linux', 'freebsd', 'gnukfreebsd')):
             exts.append( Extension('ossaudiodev', ['ossaudiodev.c']) )
         else:
             missing.append('ossaudiodev')