issue 14660: Implement PEP 420, namespace packages.
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
index 3069bd8..3dcd05a 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -468,6 +468,10 @@
     """
 
     @classmethod
+    def module_repr(cls, module):
+        return "<module '{}' (built-in)>".format(module.__name__)
+
+    @classmethod
     def find_module(cls, fullname, path=None):
         """Find the built-in module.
 
@@ -521,6 +525,10 @@
     """
 
     @classmethod
+    def module_repr(cls, m):
+        return "<module '{}' (frozen)>".format(m.__name__)
+
+    @classmethod
     def find_module(cls, fullname, path=None):
         """Find a frozen module."""
         return cls if _imp.is_frozen(fullname) else None
@@ -533,7 +541,10 @@
         """Load a frozen module."""
         is_reload = fullname in sys.modules
         try:
-            return _imp.init_frozen(fullname)
+            m = _imp.init_frozen(fullname)
+            # Let our own module_repr() method produce a suitable repr.
+            del m.__file__
+            return m
         except:
             if not is_reload and fullname in sys.modules:
                 del sys.modules[fullname]
@@ -875,6 +886,79 @@
         return None
 
 
+class _NamespacePath:
+    """Represents a namespace package's path.  It uses the module name
+    to find its parent module, and from there it looks up the parent's
+    __path__.  When this changes, the module's own path is recomputed,
+    using path_finder.  For top-leve modules, the parent module's path
+    is sys.path."""
+
+    def __init__(self, name, path, path_finder):
+        self._name = name
+        self._path = path
+        self._last_parent_path = tuple(self._get_parent_path())
+        self._path_finder = path_finder
+
+    def _find_parent_path_names(self):
+        """Returns a tuple of (parent-module-name, parent-path-attr-name)"""
+        parent, dot, me = self._name.rpartition('.')
+        if dot == '':
+            # This is a top-level module. sys.path contains the parent path.
+            return 'sys', 'path'
+        # Not a top-level module. parent-module.__path__ contains the
+        #  parent path.
+        return parent, '__path__'
+
+    def _get_parent_path(self):
+        parent_module_name, path_attr_name = self._find_parent_path_names()
+        return getattr(sys.modules[parent_module_name], path_attr_name)
+
+    def _recalculate(self):
+        # If the parent's path has changed, recalculate _path
+        parent_path = tuple(self._get_parent_path()) # Make a copy
+        if parent_path != self._last_parent_path:
+            loader, new_path = self._path_finder(self._name, parent_path)
+            # Note that no changes are made if a loader is returned, but we
+            #  do remember the new parent path
+            if loader is None:
+                self._path = new_path
+            self._last_parent_path = parent_path     # Save the copy
+        return self._path
+
+    def __iter__(self):
+        return iter(self._recalculate())
+
+    def __len__(self):
+        return len(self._recalculate())
+
+    def __repr__(self):
+        return "_NamespacePath({0!r})".format(self._path)
+
+    def __contains__(self, item):
+        return item in self._recalculate()
+
+    def append(self, item):
+        self._path.append(item)
+
+
+class NamespaceLoader:
+    def __init__(self, name, path, path_finder):
+        self._path = _NamespacePath(name, path, path_finder)
+
+    @classmethod
+    def module_repr(cls, module):
+        return "<module '{}' (namespace)>".format(module.__name__)
+
+    @set_package
+    @set_loader
+    @module_for_loader
+    def load_module(self, module):
+        """Load a namespace module."""
+        _verbose_message('namespace module loaded with path {!r}', self._path)
+        module.__path__ = self._path
+        return module
+
+
 # Finders #####################################################################
 
 class PathFinder:
@@ -916,19 +1000,46 @@
         return finder
 
     @classmethod
+    def _get_loader(cls, fullname, path):
+        """Find the loader or namespace_path for this module/package name."""
+        # If this ends up being a namespace package, namespace_path is
+        #  the list of paths that will become its __path__
+        namespace_path = []
+        for entry in path:
+            finder = cls._path_importer_cache(entry)
+            if finder is not None:
+                if hasattr(finder, 'find_loader'):
+                    loader, portions = finder.find_loader(fullname)
+                else:
+                    loader = finder.find_module(fullname)
+                    portions = []
+                if loader is not None:
+                    # We found a loader: return it immediately.
+                    return (loader, namespace_path)
+                # This is possibly part of a namespace package.
+                #  Remember these path entries (if any) for when we
+                #  create a namespace package, and continue iterating
+                #  on path.
+                namespace_path.extend(portions)
+        else:
+            return (None, namespace_path)
+
+    @classmethod
     def find_module(cls, fullname, path=None):
         """Find the module on sys.path or 'path' based on sys.path_hooks and
         sys.path_importer_cache."""
         if path is None:
             path = sys.path
-        for entry in path:
-            finder = cls._path_importer_cache(entry)
-            if finder is not None:
-                loader = finder.find_module(fullname)
-                if loader:
-                    return loader
+        loader, namespace_path = cls._get_loader(fullname, path)
+        if loader is not None:
+            return loader
         else:
-            return None
+            if namespace_path:
+                # We found at least one namespace path.  Return a
+                #  loader which can create the namespace package.
+                return NamespaceLoader(fullname, namespace_path, cls._get_loader)
+            else:
+                return None
 
 
 class FileFinder:
@@ -942,8 +1053,8 @@
 
     def __init__(self, path, *details):
         """Initialize with the path to search on and a variable number of
-        3-tuples containing the loader, file suffixes the loader recognizes, and
-        a boolean of whether the loader handles packages."""
+        3-tuples containing the loader, file suffixes the loader recognizes,
+        and a boolean of whether the loader handles packages."""
         packages = []
         modules = []
         for loader, suffixes, supports_packages in details:
@@ -964,6 +1075,19 @@
 
     def find_module(self, fullname):
         """Try to find a loader for the specified module."""
+        # Call find_loader(). If it returns a string (indicating this
+        # is a namespace package portion), generate a warning and
+        # return None.
+        loader, portions = self.find_loader(fullname)
+        assert len(portions) in [0, 1]
+        if loader is None and len(portions):
+            msg = "Not importing directory {}: missing __init__"
+            _warnings.warn(msg.format(portions[0]), ImportWarning)
+        return loader
+
+    def find_loader(self, fullname):
+        """Try to find a loader for the specified module, or the namespace
+        package portions. Returns (loader, list-of-portions)."""
         tail_module = fullname.rpartition('.')[2]
         try:
             mtime = _os.stat(self.path).st_mtime
@@ -987,17 +1111,17 @@
                     init_filename = '__init__' + suffix
                     full_path = _path_join(base_path, init_filename)
                     if _path_isfile(full_path):
-                        return loader(fullname, full_path)
+                        return (loader(fullname, full_path), [base_path])
                 else:
-                    msg = "Not importing directory {}: missing __init__"
-                    _warnings.warn(msg.format(base_path), ImportWarning)
+                    # A namespace package, return the path
+                    return (None, [base_path])
         # Check for a file w/ a proper suffix exists.
         for suffix, loader in self.modules:
             if cache_module + suffix in cache:
                 full_path = _path_join(self.path, tail_module + suffix)
                 if _path_isfile(full_path):
-                    return loader(fullname, full_path)
-        return None
+                    return (loader(fullname, full_path), [])
+        return (None, [])
 
     def _fill_cache(self):
         """Fill the cache of potential modules and packages for this directory."""
diff --git a/Lib/importlib/test/frozen/test_loader.py b/Lib/importlib/test/frozen/test_loader.py
index 91d73fa..ba512d9 100644
--- a/Lib/importlib/test/frozen/test_loader.py
+++ b/Lib/importlib/test/frozen/test_loader.py
@@ -10,38 +10,46 @@
     def test_module(self):
         with util.uncache('__hello__'), captured_stdout() as stdout:
             module = machinery.FrozenImporter.load_module('__hello__')
-            check = {'__name__': '__hello__', '__file__': '<frozen>',
-                    '__package__': '', '__loader__': machinery.FrozenImporter}
+            check = {'__name__': '__hello__',
+                    '__package__': '', 
+                    '__loader__': machinery.FrozenImporter,
+                    }
             for attr, value in check.items():
                 self.assertEqual(getattr(module, attr), value)
             self.assertEqual(stdout.getvalue(), 'Hello world!\n')
+            self.assertFalse(hasattr(module, '__file__'))
 
     def test_package(self):
         with util.uncache('__phello__'),  captured_stdout() as stdout:
             module = machinery.FrozenImporter.load_module('__phello__')
-            check = {'__name__': '__phello__', '__file__': '<frozen>',
-                     '__package__': '__phello__', '__path__': ['__phello__'],
-                     '__loader__': machinery.FrozenImporter}
+            check = {'__name__': '__phello__', 
+                     '__package__': '__phello__', 
+                     '__path__': ['__phello__'],
+                     '__loader__': machinery.FrozenImporter,
+                     }
             for attr, value in check.items():
                 attr_value = getattr(module, attr)
                 self.assertEqual(attr_value, value,
                                  "for __phello__.%s, %r != %r" %
                                  (attr, attr_value, value))
             self.assertEqual(stdout.getvalue(), 'Hello world!\n')
+            self.assertFalse(hasattr(module, '__file__'))
 
     def test_lacking_parent(self):
         with util.uncache('__phello__', '__phello__.spam'), \
              captured_stdout() as stdout:
             module = machinery.FrozenImporter.load_module('__phello__.spam')
-            check = {'__name__': '__phello__.spam', '__file__': '<frozen>',
+            check = {'__name__': '__phello__.spam',
                     '__package__': '__phello__',
-                    '__loader__': machinery.FrozenImporter}
+                    '__loader__': machinery.FrozenImporter,
+                    }
             for attr, value in check.items():
                 attr_value = getattr(module, attr)
                 self.assertEqual(attr_value, value,
                                  "for __phello__.spam.%s, %r != %r" %
                                  (attr, attr_value, value))
             self.assertEqual(stdout.getvalue(), 'Hello world!\n')
+            self.assertFalse(hasattr(module, '__file__'))
 
     def test_module_reuse(self):
         with util.uncache('__hello__'), captured_stdout() as stdout:
@@ -51,6 +59,12 @@
             self.assertEqual(stdout.getvalue(),
                              'Hello world!\nHello world!\n')
 
+    def test_module_repr(self):
+        with util.uncache('__hello__'), captured_stdout():
+            module = machinery.FrozenImporter.load_module('__hello__')
+            self.assertEqual(repr(module), 
+                             "<module '__hello__' (frozen)>")
+
     def test_state_after_failure(self):
         # No way to trigger an error in a frozen module.
         pass
diff --git a/Lib/importlib/test/source/test_finder.py b/Lib/importlib/test/source/test_finder.py
index bbe0163..a3fa21d 100644
--- a/Lib/importlib/test/source/test_finder.py
+++ b/Lib/importlib/test/source/test_finder.py
@@ -106,36 +106,17 @@
             loader = self.import_(pkg_dir, 'pkg.sub')
             self.assertTrue(hasattr(loader, 'load_module'))
 
-    # [sub empty]
-    def test_empty_sub_directory(self):
-        context = source_util.create_modules('pkg.__init__', 'pkg.sub.__init__')
-        with warnings.catch_warnings():
-            warnings.simplefilter("error", ImportWarning)
-            with context as mapping:
-                os.unlink(mapping['pkg.sub.__init__'])
-                pkg_dir = os.path.dirname(mapping['pkg.__init__'])
-                with self.assertRaises(ImportWarning):
-                    self.import_(pkg_dir, 'pkg.sub')
-
     # [package over modules]
     def test_package_over_module(self):
         name = '_temp'
         loader = self.run_test(name, {'{0}.__init__'.format(name), name})
         self.assertTrue('__init__' in loader.get_filename(name))
 
-
     def test_failure(self):
         with source_util.create_modules('blah') as mapping:
             nothing = self.import_(mapping['.root'], 'sdfsadsadf')
             self.assertTrue(nothing is None)
 
-    # [empty dir]
-    def test_empty_dir(self):
-        with warnings.catch_warnings():
-            warnings.simplefilter("error", ImportWarning)
-            with self.assertRaises(ImportWarning):
-                self.run_test('pkg', {'pkg.__init__'}, unlink={'pkg.__init__'})
-
     def test_empty_string_for_dir(self):
         # The empty string from sys.path means to search in the cwd.
         finder = machinery.FileFinder('', (machinery.SourceFileLoader,
diff --git a/Lib/pkgutil.py b/Lib/pkgutil.py
index 8e062a4..c5b0c4d 100644
--- a/Lib/pkgutil.py
+++ b/Lib/pkgutil.py
@@ -515,19 +515,29 @@
 
     pname = os.path.join(*name.split('.')) # Reconstitute as relative path
     sname_pkg = name + ".pkg"
-    init_py = "__init__.py"
 
     path = path[:] # Start with a copy of the existing path
 
     for dir in sys.path:
-        if not isinstance(dir, str) or not os.path.isdir(dir):
+        if not isinstance(dir, str):
             continue
-        subdir = os.path.join(dir, pname)
-        # XXX This may still add duplicate entries to path on
-        # case-insensitive filesystems
-        initfile = os.path.join(subdir, init_py)
-        if subdir not in path and os.path.isfile(initfile):
-            path.append(subdir)
+
+        finder = get_importer(dir)
+        if finder is not None:
+            # Is this finder PEP 420 compliant?
+            if hasattr(finder, 'find_loader'):
+                loader, portions = finder.find_loader(name)
+            else:
+                # No, no need to call it
+                loader = None
+                portions = []
+
+            for portion in portions:
+                # XXX This may still add duplicate entries to path on
+                # case-insensitive filesystems
+                if portion not in path:
+                    path.append(portion)
+
         # XXX Is this the right thing for subpackages like zope.app?
         # It looks for a file named "zope.app.pkg"
         pkgfile = os.path.join(dir, sname_pkg)
diff --git a/Lib/test/namespace_pkgs/both_portions/foo/one.py b/Lib/test/namespace_pkgs/both_portions/foo/one.py
new file mode 100644
index 0000000..3080f6f
--- /dev/null
+++ b/Lib/test/namespace_pkgs/both_portions/foo/one.py
@@ -0,0 +1 @@
+attr = 'both_portions foo one'
diff --git a/Lib/test/namespace_pkgs/both_portions/foo/two.py b/Lib/test/namespace_pkgs/both_portions/foo/two.py
new file mode 100644
index 0000000..4131d3d
--- /dev/null
+++ b/Lib/test/namespace_pkgs/both_portions/foo/two.py
@@ -0,0 +1 @@
+attr = 'both_portions foo two'
diff --git a/Lib/test/namespace_pkgs/missing_directory.zip b/Lib/test/namespace_pkgs/missing_directory.zip
new file mode 100644
index 0000000..836a910
--- /dev/null
+++ b/Lib/test/namespace_pkgs/missing_directory.zip
Binary files differ
diff --git a/Lib/test/namespace_pkgs/nested_portion1.zip b/Lib/test/namespace_pkgs/nested_portion1.zip
new file mode 100644
index 0000000..8d22406
--- /dev/null
+++ b/Lib/test/namespace_pkgs/nested_portion1.zip
Binary files differ
diff --git a/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/__init__.py b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/__init__.py
diff --git a/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/one.py b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/one.py
new file mode 100644
index 0000000..d8f5c83
--- /dev/null
+++ b/Lib/test/namespace_pkgs/not_a_namespace_pkg/foo/one.py
@@ -0,0 +1 @@
+attr = 'portion1 foo one'
diff --git a/Lib/test/namespace_pkgs/portion1/foo/one.py b/Lib/test/namespace_pkgs/portion1/foo/one.py
new file mode 100644
index 0000000..d8f5c83
--- /dev/null
+++ b/Lib/test/namespace_pkgs/portion1/foo/one.py
@@ -0,0 +1 @@
+attr = 'portion1 foo one'
diff --git a/Lib/test/namespace_pkgs/portion2/foo/two.py b/Lib/test/namespace_pkgs/portion2/foo/two.py
new file mode 100644
index 0000000..d092e1e
--- /dev/null
+++ b/Lib/test/namespace_pkgs/portion2/foo/two.py
@@ -0,0 +1 @@
+attr = 'portion2 foo two'
diff --git a/Lib/test/namespace_pkgs/project1/parent/child/one.py b/Lib/test/namespace_pkgs/project1/parent/child/one.py
new file mode 100644
index 0000000..2776fcd
--- /dev/null
+++ b/Lib/test/namespace_pkgs/project1/parent/child/one.py
@@ -0,0 +1 @@
+attr = 'parent child one'
diff --git a/Lib/test/namespace_pkgs/project2/parent/child/two.py b/Lib/test/namespace_pkgs/project2/parent/child/two.py
new file mode 100644
index 0000000..8b037bc
--- /dev/null
+++ b/Lib/test/namespace_pkgs/project2/parent/child/two.py
@@ -0,0 +1 @@
+attr = 'parent child two'
diff --git a/Lib/test/namespace_pkgs/project3/parent/child/three.py b/Lib/test/namespace_pkgs/project3/parent/child/three.py
new file mode 100644
index 0000000..f8abfe1
--- /dev/null
+++ b/Lib/test/namespace_pkgs/project3/parent/child/three.py
@@ -0,0 +1 @@
+attr = 'parent child three'
diff --git a/Lib/test/namespace_pkgs/top_level_portion1.zip b/Lib/test/namespace_pkgs/top_level_portion1.zip
new file mode 100644
index 0000000..3b866c9
--- /dev/null
+++ b/Lib/test/namespace_pkgs/top_level_portion1.zip
Binary files differ
diff --git a/Lib/test/test_frozen.py b/Lib/test/test_frozen.py
index dbd229b..fd6761c 100644
--- a/Lib/test/test_frozen.py
+++ b/Lib/test/test_frozen.py
@@ -7,7 +7,7 @@
 class FrozenTests(unittest.TestCase):
 
     module_attrs = frozenset(['__builtins__', '__cached__', '__doc__',
-                              '__file__', '__loader__', '__name__',
+                              '__loader__', '__name__',
                               '__package__'])
     package_attrs = frozenset(list(module_attrs) + ['__path__'])
 
diff --git a/Lib/test/test_import.py b/Lib/test/test_import.py
index 890041f..a90e627 100644
--- a/Lib/test/test_import.py
+++ b/Lib/test/test_import.py
@@ -286,12 +286,6 @@
         import test.support as y
         self.assertIs(y, test.support, y.__name__)
 
-    def test_import_initless_directory_warning(self):
-        with check_warnings(('', ImportWarning)):
-            # Just a random non-package directory we always expect to be
-            # somewhere in sys.path...
-            self.assertRaises(ImportError, __import__, "site-packages")
-
     def test_import_by_filename(self):
         path = os.path.abspath(TESTFN)
         encoding = sys.getfilesystemencoding()
diff --git a/Lib/test/test_module.py b/Lib/test/test_module.py
index 5617789..e5a2525 100644
--- a/Lib/test/test_module.py
+++ b/Lib/test/test_module.py
@@ -5,6 +5,15 @@
 import sys
 ModuleType = type(sys)
 
+class FullLoader:
+    @classmethod
+    def module_repr(cls, m):
+        return "<module '{}' (crafted)>".format(m.__name__)
+
+class BareLoader:
+    pass
+
+
 class ModuleTests(unittest.TestCase):
     def test_uninitialized(self):
         # An uninitialized module has no __dict__ or __name__,
@@ -80,8 +89,90 @@
         gc_collect()
         self.assertEqual(destroyed, [1])
 
+    def test_module_repr_minimal(self):
+        # reprs when modules have no __file__, __name__, or __loader__
+        m = ModuleType('foo')
+        del m.__name__
+        self.assertEqual(repr(m), "<module '?'>")
+
+    def test_module_repr_with_name(self):
+        m = ModuleType('foo')
+        self.assertEqual(repr(m), "<module 'foo'>")
+
+    def test_module_repr_with_name_and_filename(self):
+        m = ModuleType('foo')
+        m.__file__ = '/tmp/foo.py'
+        self.assertEqual(repr(m), "<module 'foo' from '/tmp/foo.py'>")
+
+    def test_module_repr_with_filename_only(self):
+        m = ModuleType('foo')
+        del m.__name__
+        m.__file__ = '/tmp/foo.py'
+        self.assertEqual(repr(m), "<module '?' from '/tmp/foo.py'>")
+
+    def test_module_repr_with_bare_loader_but_no_name(self):
+        m = ModuleType('foo')
+        del m.__name__
+        # Yes, a class not an instance.
+        m.__loader__ = BareLoader
+        self.assertEqual(
+            repr(m), "<module '?' (<class 'test.test_module.BareLoader'>)>")
+
+    def test_module_repr_with_full_loader_but_no_name(self):
+        # m.__loader__.module_repr() will fail because the module has no
+        # m.__name__.  This exception will get suppressed and instead the
+        # loader's repr will be used.
+        m = ModuleType('foo')
+        del m.__name__
+        # Yes, a class not an instance.
+        m.__loader__ = FullLoader
+        self.assertEqual(
+            repr(m), "<module '?' (<class 'test.test_module.FullLoader'>)>")
+
+    def test_module_repr_with_bare_loader(self):
+        m = ModuleType('foo')
+        # Yes, a class not an instance.
+        m.__loader__ = BareLoader
+        self.assertEqual(
+            repr(m), "<module 'foo' (<class 'test.test_module.BareLoader'>)>")
+
+    def test_module_repr_with_full_loader(self):
+        m = ModuleType('foo')
+        # Yes, a class not an instance.
+        m.__loader__ = FullLoader
+        self.assertEqual(
+            repr(m), "<module 'foo' (crafted)>")
+
+    def test_module_repr_with_bare_loader_and_filename(self):
+        # Because the loader has no module_repr(), use the file name.
+        m = ModuleType('foo')
+        # Yes, a class not an instance.
+        m.__loader__ = BareLoader
+        m.__file__ = '/tmp/foo.py'
+        self.assertEqual(repr(m), "<module 'foo' from '/tmp/foo.py'>")
+
+    def test_module_repr_with_full_loader_and_filename(self):
+        # Even though the module has an __file__, use __loader__.module_repr()
+        m = ModuleType('foo')
+        # Yes, a class not an instance.
+        m.__loader__ = FullLoader
+        m.__file__ = '/tmp/foo.py'
+        self.assertEqual(repr(m), "<module 'foo' (crafted)>")
+
+    def test_module_repr_builtin(self):
+        self.assertEqual(repr(sys), "<module 'sys' (built-in)>")
+
+    def test_module_repr_source(self):
+        r = repr(unittest)
+        self.assertEqual(r[:25], "<module 'unittest' from '")
+        self.assertEqual(r[-13:], "__init__.py'>")
+
+    # frozen and namespace module reprs are tested in importlib.
+
+
 def test_main():
     run_unittest(ModuleTests)
 
+
 if __name__ == '__main__':
     test_main()
diff --git a/Lib/test/test_namespace_pkgs.py b/Lib/test/test_namespace_pkgs.py
new file mode 100644
index 0000000..176ddd3
--- /dev/null
+++ b/Lib/test/test_namespace_pkgs.py
@@ -0,0 +1,239 @@
+import sys
+import contextlib
+import unittest
+import os
+
+import importlib.test.util
+from test.support import run_unittest
+
+# needed tests:
+#
+# need to test when nested, so that the top-level path isn't sys.path
+# need to test dynamic path detection, both at top-level and nested
+# with dynamic path, check when a loader is returned on path reload (that is,
+#  trying to switch from a namespace package to a regular package)
+
+
+@contextlib.contextmanager
+def sys_modules_context():
+    """
+    Make sure sys.modules is the same object and has the same content
+    when exiting the context as when entering.
+
+    Similar to importlib.test.util.uncache, but doesn't require explicit
+    names.
+    """
+    sys_modules_saved = sys.modules
+    sys_modules_copy = sys.modules.copy()
+    try:
+        yield
+    finally:
+        sys.modules = sys_modules_saved
+        sys.modules.clear()
+        sys.modules.update(sys_modules_copy)
+
+
+@contextlib.contextmanager
+def namespace_tree_context(**kwargs):
+    """
+    Save import state and sys.modules cache and restore it on exit.
+    Typical usage:
+
+    >>> with namespace_tree_context(path=['/tmp/xxyy/portion1',
+    ...         '/tmp/xxyy/portion2']):
+    ...     pass
+    """
+    # use default meta_path and path_hooks unless specified otherwise
+    kwargs.setdefault('meta_path', sys.meta_path)
+    kwargs.setdefault('path_hooks', sys.path_hooks)
+    import_context = importlib.test.util.import_state(**kwargs)
+    with import_context, sys_modules_context():
+        yield
+
+class NamespacePackageTest(unittest.TestCase):
+    """
+    Subclasses should define self.root and self.paths (under that root)
+    to be added to sys.path.
+    """
+    root = os.path.join(os.path.dirname(__file__), 'namespace_pkgs')
+
+    def setUp(self):
+        self.resolved_paths = [
+            os.path.join(self.root, path) for path in self.paths
+        ]
+        self.ctx = namespace_tree_context(path=self.resolved_paths)
+        self.ctx.__enter__()
+
+    def tearDown(self):
+        # TODO: will we ever want to pass exc_info to __exit__?
+        self.ctx.__exit__(None, None, None)
+
+class SingleNamespacePackage(NamespacePackageTest):
+    paths = ['portion1']
+
+    def test_simple_package(self):
+        import foo.one
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+
+    def test_cant_import_other(self):
+        with self.assertRaises(ImportError):
+            import foo.two
+
+    def test_module_repr(self):
+        import foo.one
+        self.assertEqual(repr(foo), "<module 'foo' (namespace)>")
+
+
+class DynamicPatheNamespacePackage(NamespacePackageTest):
+    paths = ['portion1']
+
+    def test_dynamic_path(self):
+        # Make sure only 'foo.one' can be imported
+        import foo.one
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+
+        with self.assertRaises(ImportError):
+            import foo.two
+
+        # Now modify sys.path
+        sys.path.append(os.path.join(self.root, 'portion2'))
+
+        # And make sure foo.two is now importable
+        import foo.two
+        self.assertEqual(foo.two.attr, 'portion2 foo two')
+
+
+class CombinedNamespacePackages(NamespacePackageTest):
+    paths = ['both_portions']
+
+    def test_imports(self):
+        import foo.one
+        import foo.two
+        self.assertEqual(foo.one.attr, 'both_portions foo one')
+        self.assertEqual(foo.two.attr, 'both_portions foo two')
+
+
+class SeparatedNamespacePackages(NamespacePackageTest):
+    paths = ['portion1', 'portion2']
+
+    def test_imports(self):
+        import foo.one
+        import foo.two
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+        self.assertEqual(foo.two.attr, 'portion2 foo two')
+
+
+class SeparatedOverlappingNamespacePackages(NamespacePackageTest):
+    paths = ['portion1', 'both_portions']
+
+    def test_first_path_wins(self):
+        import foo.one
+        import foo.two
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+        self.assertEqual(foo.two.attr, 'both_portions foo two')
+
+    def test_first_path_wins_again(self):
+        sys.path.reverse()
+        import foo.one
+        import foo.two
+        self.assertEqual(foo.one.attr, 'both_portions foo one')
+        self.assertEqual(foo.two.attr, 'both_portions foo two')
+
+    def test_first_path_wins_importing_second_first(self):
+        import foo.two
+        import foo.one
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+        self.assertEqual(foo.two.attr, 'both_portions foo two')
+
+
+class SingleZipNamespacePackage(NamespacePackageTest):
+    paths = ['top_level_portion1.zip']
+
+    def test_simple_package(self):
+        import foo.one
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+
+    def test_cant_import_other(self):
+        with self.assertRaises(ImportError):
+            import foo.two
+
+
+class SeparatedZipNamespacePackages(NamespacePackageTest):
+    paths = ['top_level_portion1.zip', 'portion2']
+
+    def test_imports(self):
+        import foo.one
+        import foo.two
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+        self.assertEqual(foo.two.attr, 'portion2 foo two')
+        self.assertIn('top_level_portion1.zip', foo.one.__file__)
+        self.assertNotIn('.zip', foo.two.__file__)
+
+
+class SingleNestedZipNamespacePackage(NamespacePackageTest):
+    paths = ['nested_portion1.zip/nested_portion1']
+
+    def test_simple_package(self):
+        import foo.one
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+
+    def test_cant_import_other(self):
+        with self.assertRaises(ImportError):
+            import foo.two
+
+
+class SeparatedNestedZipNamespacePackages(NamespacePackageTest):
+    paths = ['nested_portion1.zip/nested_portion1', 'portion2']
+
+    def test_imports(self):
+        import foo.one
+        import foo.two
+        self.assertEqual(foo.one.attr, 'portion1 foo one')
+        self.assertEqual(foo.two.attr, 'portion2 foo two')
+        fn = os.path.join('nested_portion1.zip', 'nested_portion1')
+        self.assertIn(fn, foo.one.__file__)
+        self.assertNotIn('.zip', foo.two.__file__)
+
+
+class LegacySupport(NamespacePackageTest):
+    paths = ['not_a_namespace_pkg', 'portion1', 'portion2', 'both_portions']
+
+    def test_non_namespace_package_takes_precedence(self):
+        import foo.one
+        with self.assertRaises(ImportError):
+            import foo.two
+        self.assertIn('__init__', foo.__file__)
+        self.assertNotIn('namespace', str(foo.__loader__).lower())
+
+
+class ZipWithMissingDirectory(NamespacePackageTest):
+    paths = ['missing_directory.zip']
+
+    @unittest.expectedFailure
+    def test_missing_directory(self):
+        # This will fail because missing_directory.zip contains:
+        #   Length      Date    Time    Name
+        # ---------  ---------- -----   ----
+        #        29  2012-05-03 18:13   foo/one.py
+        #         0  2012-05-03 20:57   bar/
+        #        38  2012-05-03 20:57   bar/two.py
+        # ---------                     -------
+        #        67                     3 files
+
+        # Because there is no 'foo/', the zipimporter currently doesn't
+        #  know that foo is a namespace package
+
+        import foo.one
+
+    def test_present_directory(self):
+        # This succeeds because there is a "bar/" in the zip file
+        import bar.two
+        self.assertEqual(bar.two.attr, 'missing_directory foo two')
+
+
+def test_main():
+    run_unittest(*NamespacePackageTest.__subclasses__())
+
+
+if __name__ == "__main__":
+    test_main()
diff --git a/Lib/test/test_pkgutil.py b/Lib/test/test_pkgutil.py
index 6025bcd..a41b5f5 100644
--- a/Lib/test/test_pkgutil.py
+++ b/Lib/test/test_pkgutil.py
@@ -138,10 +138,11 @@
         del sys.modules['foo']
 
 
+# These tests, especially the setup and cleanup, are hideous. They
+# need to be cleaned up once issue 14715 is addressed.
 class ExtendPathTests(unittest.TestCase):
     def create_init(self, pkgname):
         dirname = tempfile.mkdtemp()
-        self.addCleanup(shutil.rmtree, dirname)
         sys.path.insert(0, dirname)
 
         pkgdir = os.path.join(dirname, pkgname)
@@ -156,22 +157,12 @@
         with open(module_name, 'w') as fl:
             print('value={}'.format(value), file=fl)
 
-    def setUp(self):
-        # Create 2 directories on sys.path
-        self.pkgname = 'foo'
-        self.dirname_0 = self.create_init(self.pkgname)
-        self.dirname_1 = self.create_init(self.pkgname)
-
-    def tearDown(self):
-        del sys.path[0]
-        del sys.path[0]
-        del sys.modules['foo']
-        del sys.modules['foo.bar']
-        del sys.modules['foo.baz']
-
     def test_simple(self):
-        self.create_submodule(self.dirname_0, self.pkgname, 'bar', 0)
-        self.create_submodule(self.dirname_1, self.pkgname, 'baz', 1)
+        pkgname = 'foo'
+        dirname_0 = self.create_init(pkgname)
+        dirname_1 = self.create_init(pkgname)
+        self.create_submodule(dirname_0, pkgname, 'bar', 0)
+        self.create_submodule(dirname_1, pkgname, 'baz', 1)
         import foo.bar
         import foo.baz
         # Ensure we read the expected values
@@ -180,8 +171,45 @@
 
         # Ensure the path is set up correctly
         self.assertEqual(sorted(foo.__path__),
-                         sorted([os.path.join(self.dirname_0, self.pkgname),
-                                 os.path.join(self.dirname_1, self.pkgname)]))
+                         sorted([os.path.join(dirname_0, pkgname),
+                                 os.path.join(dirname_1, pkgname)]))
+
+        # Cleanup
+        shutil.rmtree(dirname_0)
+        shutil.rmtree(dirname_1)
+        del sys.path[0]
+        del sys.path[0]
+        del sys.modules['foo']
+        del sys.modules['foo.bar']
+        del sys.modules['foo.baz']
+
+    def test_mixed_namespace(self):
+        pkgname = 'foo'
+        dirname_0 = self.create_init(pkgname)
+        dirname_1 = self.create_init(pkgname)
+        self.create_submodule(dirname_0, pkgname, 'bar', 0)
+        # Turn this into a PEP 420 namespace package
+        os.unlink(os.path.join(dirname_0, pkgname, '__init__.py'))
+        self.create_submodule(dirname_1, pkgname, 'baz', 1)
+        import foo.bar
+        import foo.baz
+        # Ensure we read the expected values
+        self.assertEqual(foo.bar.value, 0)
+        self.assertEqual(foo.baz.value, 1)
+
+        # Ensure the path is set up correctly
+        self.assertEqual(sorted(foo.__path__),
+                         sorted([os.path.join(dirname_0, pkgname),
+                                 os.path.join(dirname_1, pkgname)]))
+
+        # Cleanup
+        shutil.rmtree(dirname_0)
+        shutil.rmtree(dirname_1)
+        del sys.path[0]
+        del sys.path[0]
+        del sys.modules['foo']
+        del sys.modules['foo.bar']
+        del sys.modules['foo.baz']
 
     # XXX: test .pkg files