Issues #18088, 18089: Introduce
importlib.abc.Loader.init_module_attrs() and implement
importlib.abc.InspectLoader.load_module().

The importlib.abc.Loader.init_module_attrs() method sets the various
attributes on the module being loaded. It is done unconditionally to
support reloading. Typically people used
importlib.util.module_for_loader, but since that's a decorator there
was no way to override it's actions, so init_module_attrs() came into
existence to allow for overriding. This is also why module_for_loader
is now pending deprecation (having its other use replaced by
importlib.util.module_to_load).

All of this allowed for importlib.abc.InspectLoader.load_module() to
be implemented. At this point you can now implement a loader with
nothing more than get_code() (which only requires get_source();
package support requires is_package()). Thanks to init_module_attrs()
the implementation of load_module() is basically a context manager
containing 2 methods calls, a call to exec(), and a return statement.
diff --git a/Lib/test/test_importlib/source/test_file_loader.py b/Lib/test/test_importlib/source/test_file_loader.py
index 80999e8..6e248f3 100644
--- a/Lib/test/test_importlib/source/test_file_loader.py
+++ b/Lib/test/test_importlib/source/test_file_loader.py
@@ -15,7 +15,7 @@
 import sys
 import unittest
 
-from test.support import make_legacy_pyc
+from test.support import make_legacy_pyc, unload
 
 
 class SimpleTest(unittest.TestCase):
@@ -26,23 +26,13 @@
     """
 
     def test_load_module_API(self):
-        # If fullname is not specified that assume self.name is desired.
-        class TesterMixin(importlib.abc.Loader):
-            def load_module(self, fullname): return fullname
-            def module_repr(self, module): return '<module>'
+        class Tester(importlib.abc.FileLoader):
+            def get_source(self, _): return 'attr = 42'
+            def is_package(self, _): return False
 
-        class Tester(importlib.abc.FileLoader, TesterMixin):
-            def get_code(self, _): pass
-            def get_source(self, _): pass
-            def is_package(self, _): pass
-
-        name = 'mod_name'
-        loader = Tester(name, 'some_path')
-        self.assertEqual(name, loader.load_module())
-        self.assertEqual(name, loader.load_module(None))
-        self.assertEqual(name, loader.load_module(name))
-        with self.assertRaises(ImportError):
-            loader.load_module(loader.name + 'XXX')
+        loader = Tester('blah', 'blah.py')
+        self.addCleanup(unload, 'blah')
+        module = loader.load_module()  # Should not raise an exception.
 
     def test_get_filename_API(self):
         # If fullname is not set then assume self.path is desired.
@@ -473,13 +463,6 @@
         self._test_non_code_marshal(del_source=True)
 
 
-def test_main():
-    from test.support import run_unittest
-    run_unittest(SimpleTest,
-                 SourceLoaderBadBytecodeTest,
-                 SourcelessLoaderBadBytecodeTest
-                )
-
 
 if __name__ == '__main__':
-    test_main()
+    unittest.main()
diff --git a/Lib/test/test_importlib/test_abc.py b/Lib/test/test_importlib/test_abc.py
index b443337..ea417f5 100644
--- a/Lib/test/test_importlib/test_abc.py
+++ b/Lib/test/test_importlib/test_abc.py
@@ -2,12 +2,14 @@
 from importlib import abc
 from importlib import machinery
 
+import contextlib
 import imp
 import inspect
 import io
 import marshal
 import os
 import sys
+from test import support
 import unittest
 from unittest import mock
 
@@ -198,6 +200,15 @@
         with self.assertRaises(ImportError):
             self.ins.get_filename('blah')
 
+##### Loader concrete methods ##################################################
+class LoaderConcreteMethodTests(unittest.TestCase):
+
+    def test_init_module_attrs(self):
+        loader = LoaderSubclass()
+        module = imp.new_module('blah')
+        loader.init_module_attrs(module)
+        self.assertEqual(module.__loader__, loader)
+
 
 ##### InspectLoader concrete methods ###########################################
 class InspectLoaderSourceToCodeTests(unittest.TestCase):
@@ -269,6 +280,93 @@
             loader.get_code('blah')
 
 
+class InspectLoaderInitModuleTests(unittest.TestCase):
+
+    @staticmethod
+    def mock_is_package(return_value):
+        return mock.patch.object(InspectLoaderSubclass, 'is_package',
+                                 return_value=return_value)
+
+    def init_module_attrs(self, name):
+        loader = InspectLoaderSubclass()
+        module = imp.new_module(name)
+        loader.init_module_attrs(module)
+        self.assertEqual(module.__loader__, loader)
+        return module
+
+    def test_package(self):
+        # If a package, then __package__ == __name__, __path__ == []
+        with self.mock_is_package(True):
+            name = 'blah'
+            module = self.init_module_attrs(name)
+            self.assertEqual(module.__package__, name)
+            self.assertEqual(module.__path__, [])
+
+    def test_toplevel(self):
+        # If a module is top-level, __package__ == ''
+        with self.mock_is_package(False):
+            name = 'blah'
+            module = self.init_module_attrs(name)
+            self.assertEqual(module.__package__, '')
+
+    def test_submodule(self):
+        # If a module is contained within a package then set __package__ to the
+        # package name.
+        with self.mock_is_package(False):
+            name = 'pkg.mod'
+            module = self.init_module_attrs(name)
+            self.assertEqual(module.__package__, 'pkg')
+
+    def test_is_package_ImportError(self):
+        # If is_package() raises ImportError, __package__ should be None and
+        # __path__ should not be set.
+        with self.mock_is_package(False) as mocked_method:
+            mocked_method.side_effect = ImportError
+            name = 'mod'
+            module = self.init_module_attrs(name)
+            self.assertIsNone(module.__package__)
+            self.assertFalse(hasattr(module, '__path__'))
+
+
+class InspectLoaderLoadModuleTests(unittest.TestCase):
+
+    """Test InspectLoader.load_module()."""
+
+    module_name = 'blah'
+
+    def setUp(self):
+        support.unload(self.module_name)
+        self.addCleanup(support.unload, self.module_name)
+
+    def mock_get_code(self):
+        return mock.patch.object(InspectLoaderSubclass, 'get_code')
+
+    def test_get_code_ImportError(self):
+        # If get_code() raises ImportError, it should propagate.
+        with self.mock_get_code() as mocked_get_code:
+            mocked_get_code.side_effect = ImportError
+            with self.assertRaises(ImportError):
+                loader = InspectLoaderSubclass()
+                loader.load_module(self.module_name)
+
+    def test_get_code_None(self):
+        # If get_code() returns None, raise ImportError.
+        with self.mock_get_code() as mocked_get_code:
+            mocked_get_code.return_value = None
+            with self.assertRaises(ImportError):
+                loader = InspectLoaderSubclass()
+                loader.load_module(self.module_name)
+
+    def test_module_returned(self):
+        # The loaded module should be returned.
+        code = compile('attr = 42', '<string>', 'exec')
+        with self.mock_get_code() as mocked_get_code:
+            mocked_get_code.return_value = code
+            loader = InspectLoaderSubclass()
+            module = loader.load_module(self.module_name)
+            self.assertEqual(module, sys.modules[self.module_name])
+
+
 ##### ExecutionLoader concrete methods #########################################
 class ExecutionLoaderGetCodeTests(unittest.TestCase):
 
@@ -327,6 +425,69 @@
         self.assertEqual(module.attr, 42)
 
 
+class ExecutionLoaderInitModuleTests(unittest.TestCase):
+
+    @staticmethod
+    @contextlib.contextmanager
+    def mock_methods(is_package, filename):
+        is_package_manager = InspectLoaderInitModuleTests.mock_is_package(is_package)
+        get_filename_manager = mock.patch.object(ExecutionLoaderSubclass,
+                'get_filename', return_value=filename)
+        with is_package_manager as mock_is_package:
+            with get_filename_manager as mock_get_filename:
+                yield {'is_package': mock_is_package,
+                       'get_filename': mock_get_filename}
+
+    def test_toplevel(self):
+        # Verify __loader__, __file__, and __package__; no __path__.
+        name = 'blah'
+        path = os.path.join('some', 'path', '{}.py'.format(name))
+        with self.mock_methods(False, path):
+            loader = ExecutionLoaderSubclass()
+            module = imp.new_module(name)
+            loader.init_module_attrs(module)
+        self.assertIs(module.__loader__, loader)
+        self.assertEqual(module.__file__, path)
+        self.assertEqual(module.__package__, '')
+        self.assertFalse(hasattr(module, '__path__'))
+
+    def test_package(self):
+        # Verify __loader__, __file__, __package__, and __path__.
+        name = 'pkg'
+        path = os.path.join('some', 'pkg', '__init__.py')
+        with self.mock_methods(True, path):
+            loader = ExecutionLoaderSubclass()
+            module = imp.new_module(name)
+            loader.init_module_attrs(module)
+        self.assertIs(module.__loader__, loader)
+        self.assertEqual(module.__file__, path)
+        self.assertEqual(module.__package__, 'pkg')
+        self.assertEqual(module.__path__, [os.path.dirname(path)])
+
+    def test_submodule(self):
+        # Verify __package__ and not __path__; test_toplevel() takes care of
+        # other attributes.
+        name = 'pkg.submodule'
+        path = os.path.join('some', 'pkg', 'submodule.py')
+        with self.mock_methods(False, path):
+            loader = ExecutionLoaderSubclass()
+            module = imp.new_module(name)
+            loader.init_module_attrs(module)
+        self.assertEqual(module.__package__, 'pkg')
+        self.assertEqual(module.__file__, path)
+        self.assertFalse(hasattr(module, '__path__'))
+
+    def test_get_filename_ImportError(self):
+        # If get_filename() raises ImportError, don't set __file__.
+        name = 'blah'
+        path = 'blah.py'
+        with self.mock_methods(False, path) as mocked_methods:
+            mocked_methods['get_filename'].side_effect = ImportError
+            loader = ExecutionLoaderSubclass()
+            module = imp.new_module(name)
+            loader.init_module_attrs(module)
+        self.assertFalse(hasattr(module, '__file__'))
+
 
 ##### SourceLoader concrete methods ############################################
 class SourceOnlyLoaderMock(abc.SourceLoader):
@@ -621,6 +782,47 @@
         self.assertEqual(mock.get_source(name), expect)
 
 
+class SourceLoaderInitModuleAttrTests(unittest.TestCase):
+
+    """Tests for importlib.abc.SourceLoader.init_module_attrs()."""
+
+    def test_init_module_attrs(self):
+        # If __file__ set, __cached__ == imp.cached_from_source(__file__).
+        name = 'blah'
+        path = 'blah.py'
+        loader = SourceOnlyLoaderMock(path)
+        module = imp.new_module(name)
+        loader.init_module_attrs(module)
+        self.assertEqual(module.__loader__, loader)
+        self.assertEqual(module.__package__, '')
+        self.assertEqual(module.__file__, path)
+        self.assertEqual(module.__cached__, imp.cache_from_source(path))
+
+    @mock.patch('importlib._bootstrap.cache_from_source')
+    def test_cache_from_source_NotImplementedError(self, mock_cache_from_source):
+        # If imp.cache_from_source() raises NotImplementedError don't set
+        # __cached__.
+        mock_cache_from_source.side_effect = NotImplementedError
+        name = 'blah'
+        path = 'blah.py'
+        loader = SourceOnlyLoaderMock(path)
+        module = imp.new_module(name)
+        loader.init_module_attrs(module)
+        self.assertEqual(module.__file__, path)
+        self.assertFalse(hasattr(module, '__cached__'))
+
+    def test_no_get_filename(self):
+        # No __file__, no __cached__.
+        with mock.patch.object(SourceOnlyLoaderMock, 'get_filename') as mocked:
+            mocked.side_effect = ImportError
+            name = 'blah'
+            loader = SourceOnlyLoaderMock('blah.py')
+            module = imp.new_module(name)
+            loader.init_module_attrs(module)
+        self.assertFalse(hasattr(module, '__file__'))
+        self.assertFalse(hasattr(module, '__cached__'))
+
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py
index 9897def..e6b0084 100644
--- a/Lib/test/test_importlib/test_util.py
+++ b/Lib/test/test_importlib/test_util.py
@@ -5,6 +5,7 @@
 from test import support
 import types
 import unittest
+import warnings
 
 
 class ModuleToLoadTests(unittest.TestCase):
@@ -72,14 +73,27 @@
 
     """Tests for importlib.util.module_for_loader."""
 
+    @staticmethod
+    def module_for_loader(func):
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', PendingDeprecationWarning)
+            return util.module_for_loader(func)
+
+    def test_warning(self):
+        # Should raise a PendingDeprecationWarning when used.
+        with warnings.catch_warnings():
+            warnings.simplefilter('error', PendingDeprecationWarning)
+            with self.assertRaises(PendingDeprecationWarning):
+                func = util.module_for_loader(lambda x: x)
+
     def return_module(self, name):
-        fxn = util.module_for_loader(lambda self, module: module)
+        fxn = self.module_for_loader(lambda self, module: module)
         return fxn(self, name)
 
     def raise_exception(self, name):
         def to_wrap(self, module):
             raise ImportError
-        fxn = util.module_for_loader(to_wrap)
+        fxn = self.module_for_loader(to_wrap)
         try:
             fxn(self, name)
         except ImportError:
@@ -100,7 +114,7 @@
         class FakeLoader:
             def is_package(self, name):
                 return True
-            @util.module_for_loader
+            @self.module_for_loader
             def load_module(self, module):
                 return module
         name = 'a.b.c'
@@ -134,7 +148,7 @@
 
     def test_decorator_attrs(self):
         def fxn(self, module): pass
-        wrapped = util.module_for_loader(fxn)
+        wrapped = self.module_for_loader(fxn)
         self.assertEqual(wrapped.__name__, fxn.__name__)
         self.assertEqual(wrapped.__qualname__, fxn.__qualname__)
 
@@ -160,7 +174,7 @@
                 self._pkg = is_package
             def is_package(self, name):
                 return self._pkg
-            @util.module_for_loader
+            @self.module_for_loader
             def load_module(self, module):
                 return module