Issue #14605: Expose importlib.abc.FileLoader and
importlib.machinery.(FileFinder, SourceFileLoader,
_SourcelessFileLoader, ExtensionFileLoader).

This exposes all of importlib's mechanisms that will become public on
the sys module.
diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst
index e5cc27f..de29e4f 100644
--- a/Doc/library/importlib.rst
+++ b/Doc/library/importlib.rst
@@ -237,6 +237,34 @@
         module.
 
 
+.. class:: FileLoader(fullname, path)
+
+   An abstract base class which inherits from :class:`ResourceLoader` and
+   :class:`ExecutionLoader`, providing concreate implementations of
+   :meth:`ResourceLoader.get_data` and :meth:`ExecutionLoader.get_filename`.
+
+   The *fullname* argument is a fully resolved name of the module the loader is
+   to handle. The *path* argument is the path to the file for the module.
+
+   .. versionadded:: 3.3
+
+   .. attribute:: name
+
+      The name of the module the loader can handle.
+
+   .. attribute:: path
+
+      Path to the file of the module.
+
+   .. method:: get_filename(fullname)
+
+      Returns :attr:`path`.
+
+   .. method:: get_data(path)
+
+      Returns the open, binary file for *path*.
+
+
 .. class:: SourceLoader
 
     An abstract base class for implementing source (and optionally bytecode)
@@ -498,6 +526,163 @@
         module. If no finder is ever found then ``None`` is returned.
 
 
+.. class:: FileFinder(path, \*loader_details)
+
+   A concrete implementation of :class:`importlib.abc.Finder` which caches
+   results from the file system.
+
+   The *path* argument is the directory for which the finder is in charge of
+   searching.
+
+   The *loader_details* argument is a variable number of 3-item tuples each
+   containing a loader, file suffixes the loader recognizes, and a boolean
+   representing whether the loader handles packages.
+
+   The finder will cache the directory contents as necessary, making stat calls
+   for each module search to verify the cache is not outdated. Because cache
+   staleness relies upon the granularity of the operating system's state
+   information of the file system, there is a potential race condition of
+   searching for a module, creating a new file, and then searching for the
+   module the new file represents. If the operations happen fast enough to fit
+   within the granularity of stat calls, then the module search will fail. To
+   prevent this from happening, when you create a module dynamically, make sure
+   to call :func:`importlib.invalidate_caches`.
+
+   .. versionadded:: 3.3
+
+   .. attribute:: path
+
+      The path the finder will search in.
+
+   .. method:: find_module(fullname)
+
+      Attempt to find the loader to handle *fullname* within :attr:`path`.
+
+   .. method:: invalidate_caches()
+
+      Clear out the internal cache.
+
+   .. classmethod:: path_hook(\*loader_details)
+
+      A class method which returns a closure for use on :attr:`sys.path_hooks`.
+      An instance of :class:`FileFinder` is returned by the closure using the
+      path argument given to the closure directly and *loader_details*
+      indirectly.
+
+      If the argument to the closure is not an existing directory,
+      :exc:`ImportError` is raised.
+
+
+.. class:: SourceFileLoader(fullname, path)
+
+   A concrete implementation of :class:`importlib.abc.SourceLoader` by
+   subclassing :class:`importlib.abc.FileLoader` and providing some concrete
+   implementations of other methods.
+
+   .. versionadded:: 3.3
+
+   .. attribute:: name
+
+      The name of the module that this loader will handle.
+
+   .. attribute:: path
+
+      The path to the source file.
+
+   .. method:: is_package(fullname)
+
+      Return true if :attr:`path` appears to be for a package.
+
+   .. method:: path_stats(path)
+
+      Concrete implementation of :meth:`importlib.abc.SourceLoader.path_stats`.
+
+   .. method:: set_data(path, data)
+
+      Concrete implementation of :meth:`importlib.abc.SourceLoader.set_data`.
+
+   .. method:: load_module(fullname)
+
+      Load the specified module if it is the same as :attr:`name`.
+
+
+.. class:: _SourcelessFileLoader(fullname, path)
+
+   A concrete implementation of :class:`importlib.abc.FileLoader` which can
+   import bytecode files (i.e. no source code files exist).
+
+   It is **strongly** suggested you do not rely on this loader (hence the
+   leading underscore of the class). Direct use of bytecode files (and thus not
+   source code files) inhibits your modules from being usable by all Python
+   implementations. It also runs the risk of your bytecode files not being
+   usable by new versions of Python which change the bytecode format. This
+   class is only documented as it is directly used by import and thus can
+   potentially have instances show up as a module's ``__loader__`` attribute.
+
+   .. versionadded:: 3.3
+
+   .. attribute:: name
+
+      The name of the module the loader will handle.
+
+   .. attribute:: path
+
+      The path to the bytecode file.
+
+   .. method:: is_package(fullname)
+
+      Determines if the module is a package based on :attr:`path`.
+
+   .. method:: get_code(fullname)
+
+      Returns the code object for :attr:`name` created from :attr:`path`.
+
+   .. method:: get_source(fullname)
+
+      Returns ``None`` as bytecode files have no source when this loader is
+      used.
+
+   .. method:: load_module(fullname)
+
+      Loads the specified module if it is the same as :attr:`name`.
+
+
+.. class:: ExtensionFileLoader(fullname, path)
+
+   A concrete implementation of :class:`importlib.abc.InspectLoader` for
+   extension modules.
+
+   The *fullname* argument specifies the name of the module the loader is to
+   support. The *path* argument is the path to the extension module's file.
+
+   .. versionadded:: 3.3
+
+   .. attribute:: name
+
+      Name of the module the loader supports.
+
+   .. attribute:: path
+
+      Path to the extension module.
+
+   .. method:: load_module(fullname)
+
+      Loads the extension module if and only if *fullname** is the same as
+      :attr:`name`.
+
+   .. method:: is_package(fullname)
+
+      Returns ``False`` as extension modules can never be packages.
+
+   .. method:: get_code(fullname)
+
+      Returns ``None`` as extension modules lack a code object.
+
+   .. method:: get_source(fullname)
+
+      Returns ``None`` as extension modules do not have source code.
+
+
 :mod:`importlib.util` -- Utility code for importers
 ---------------------------------------------------
 
diff --git a/Lib/imp.py b/Lib/imp.py
index 3ab69e5..f35247c 100644
--- a/Lib/imp.py
+++ b/Lib/imp.py
@@ -71,7 +71,7 @@
 
     def get_data(self, path):
         """Gross hack to contort loader to deal w/ load_*()'s bad API."""
-        if self.file and path == self._path:
+        if self.file and path == self.path:
             with self.file:
                 # Technically should be returning bytes, but
                 # SourceLoader.get_code() just passed what is returned to
@@ -83,7 +83,7 @@
             return super().get_data(path)
 
 
-class _LoadSourceCompatibility(_HackedGetData, _bootstrap._SourceFileLoader):
+class _LoadSourceCompatibility(_HackedGetData, _bootstrap.SourceFileLoader):
 
     """Compatibility support for implementing load_source()."""
 
@@ -115,7 +115,7 @@
                 break
         else:
             raise ValueError('{!r} is not a package'.format(path))
-    return _bootstrap._SourceFileLoader(name, path).load_module(name)
+    return _bootstrap.SourceFileLoader(name, path).load_module(name)
 
 
 # XXX deprecate
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
index 04ceb56..d9df2b7 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -13,6 +13,9 @@
 # reference any injected objects! This includes not only global code but also
 # anything specified at the class level.
 
+# XXX Make sure all public names have no single leading underscore and all
+#     others do.
+
 
 # Bootstrap-related code ######################################################
 
@@ -283,7 +286,7 @@
 
     """
     def _check_name_wrapper(self, name, *args, **kwargs):
-        if self._name != name:
+        if self.name != name:
             raise ImportError("loader cannot handle %s" % name, name=name)
         return method(self, name, *args, **kwargs)
     _wrap(_check_name_wrapper, method)
@@ -423,7 +426,7 @@
 class _LoaderBasics:
 
     """Base class of common code needed by both SourceLoader and
-    _SourcelessFileLoader."""
+    SourcelessFileLoader."""
 
     def is_package(self, fullname):
         """Concrete implementation of InspectLoader.is_package by checking if
@@ -608,7 +611,7 @@
         return self._load_module(fullname)
 
 
-class _FileLoader:
+class FileLoader:
 
     """Base file loader class which implements the loader protocol methods that
     require file system usage."""
@@ -616,13 +619,13 @@
     def __init__(self, fullname, path):
         """Cache the module name and the path to the file found by the
         finder."""
-        self._name = fullname
-        self._path = path
+        self.name = fullname
+        self.path = path
 
     @_check_name
     def get_filename(self, fullname):
         """Return the path to the source file as found by the finder."""
-        return self._path
+        return self.path
 
     def get_data(self, path):
         """Return the data from path as raw bytes."""
@@ -630,7 +633,7 @@
             return file.read()
 
 
-class _SourceFileLoader(_FileLoader, SourceLoader):
+class SourceFileLoader(FileLoader, SourceLoader):
 
     """Concrete implementation of SourceLoader using the file system."""
 
@@ -668,7 +671,7 @@
             pass
 
 
-class _SourcelessFileLoader(_FileLoader, _LoaderBasics):
+class _SourcelessFileLoader(FileLoader, _LoaderBasics):
 
     """Loader which handles sourceless file imports."""
 
@@ -692,7 +695,7 @@
         return None
 
 
-class _ExtensionFileLoader:
+class ExtensionFileLoader:
 
     """Loader for extension modules.
 
@@ -701,8 +704,8 @@
     """
 
     def __init__(self, name, path):
-        self._name = name
-        self._path = path
+        self.name = name
+        self.path = path
 
     @_check_name
     @set_package
@@ -711,8 +714,8 @@
         """Load an extension module."""
         is_reload = fullname in sys.modules
         try:
-            module = _imp.load_dynamic(fullname, self._path)
-            verbose_message('extension module loaded from {!r}', self._path)
+            module = _imp.load_dynamic(fullname, self.path)
+            verbose_message('extension module loaded from {!r}', self.path)
             return module
         except:
             if not is_reload and fullname in sys.modules:
@@ -805,24 +808,25 @@
             return None
 
 
-class _FileFinder:
+class FileFinder:
 
     """File-based finder.
 
-    Constructor takes a list of objects detailing what file extensions their
-    loader supports along with whether it can be used for a package.
+    Interactions with the file system are cached for performance, being
+    refreshed when the directory the finder is handling has been modified.
 
     """
 
     def __init__(self, path, *details):
-        """Initialize with finder 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."""
         packages = []
         modules = []
-        for detail in details:
-            modules.extend((suffix, detail.loader) for suffix in detail.suffixes)
-            if detail.supports_packages:
-                packages.extend((suffix, detail.loader)
-                                for suffix in detail.suffixes)
+        for loader, suffixes, supports_packages in details:
+            modules.extend((suffix, loader) for suffix in suffixes)
+            if supports_packages:
+                packages.extend((suffix, loader) for suffix in suffixes)
         self.packages = packages
         self.modules = modules
         # Base (directory) path
@@ -898,46 +902,29 @@
         if sys.platform.startswith(CASE_INSENSITIVE_PLATFORMS):
             self._relaxed_path_cache = set(fn.lower() for fn in contents)
 
+    @classmethod
+    def path_hook(cls, *loader_details):
+        """A class method which returns a closure to use on sys.path_hook
+        which will return an instance using the specified loaders and the path
+        called on the closure.
 
-class _SourceFinderDetails:
+        If the path called on the closure is not a directory, ImportError is
+        raised.
 
-    loader = _SourceFileLoader
-    supports_packages = True
+        """
+        def path_hook_for_FileFinder(path):
+            """Path hook for importlib.machinery.FileFinder."""
+            if not _path_isdir(path):
+                raise ImportError("only directories are supported", path=path)
+            return cls(path, *loader_details)
 
-    def __init__(self):
-        self.suffixes = _suffix_list(_imp.PY_SOURCE)
+        return path_hook_for_FileFinder
 
-class _SourcelessFinderDetails:
-
-    loader = _SourcelessFileLoader
-    supports_packages = True
-
-    def __init__(self):
-        self.suffixes = _suffix_list(_imp.PY_COMPILED)
-
-
-class _ExtensionFinderDetails:
-
-    loader = _ExtensionFileLoader
-    supports_packages = False
-
-    def __init__(self):
-        self.suffixes = _suffix_list(_imp.C_EXTENSION)
 
 
 # Import itself ###############################################################
 
-def _file_path_hook(path):
-    """If the path is a directory, return a file-based finder."""
-    if _path_isdir(path):
-        return _FileFinder(path, _ExtensionFinderDetails(),
-                           _SourceFinderDetails(),
-                           _SourcelessFinderDetails())
-    else:
-        raise ImportError("only directories are supported", path=path)
-
-
-_DEFAULT_PATH_HOOK = _file_path_hook
+_DEFAULT_PATH_HOOK = None  # Set in _setup()
 
 class _DefaultPathFinder(PathFinder):
 
@@ -1209,6 +1196,12 @@
     if builtin_os == 'nt':
         SOURCE_SUFFIXES.append('.pyw')
 
+    supported_loaders = [(ExtensionFileLoader, _suffix_list(3), False),
+                         (SourceFileLoader, _suffix_list(1), True),
+                         (_SourcelessFileLoader, _suffix_list(2), True)]
+    setattr(self_module, '_DEFAULT_PATH_HOOK',
+            FileFinder.path_hook(*supported_loaders))
+
 
 def _install(sys_module, _imp_module):
     """Install importlib as the implementation of import.
diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py
index 43e4866..baa09fd 100644
--- a/Lib/importlib/abc.py
+++ b/Lib/importlib/abc.py
@@ -1,6 +1,12 @@
 """Abstract base classes related to import."""
 from . import _bootstrap
 from . import machinery
+try:
+    import _frozen_importlib
+except ImportError as exc:
+    if exc.name != '_frozen_importlib':
+        raise
+    _frozen_importlib = None
 import abc
 import imp
 import marshal
@@ -9,6 +15,14 @@
 import warnings
 
 
+def _register(abstract_cls, *classes):
+    for cls in classes:
+        abstract_cls.register(cls)
+        if _frozen_importlib is not None:
+            frozen_cls = getattr(_frozen_importlib, cls.__name__)
+            abstract_cls.register(frozen_cls)
+
+
 class Loader(metaclass=abc.ABCMeta):
 
     """Abstract base class for import loaders."""
@@ -32,9 +46,8 @@
         """
         raise NotImplementedError
 
-Finder.register(machinery.BuiltinImporter)
-Finder.register(machinery.FrozenImporter)
-Finder.register(machinery.PathFinder)
+_register(Finder, machinery.BuiltinImporter, machinery.FrozenImporter,
+          machinery.PathFinder, machinery.FileFinder)
 
 
 class ResourceLoader(Loader):
@@ -80,8 +93,8 @@
         module.  The fullname is a str.  Returns a str."""
         raise NotImplementedError
 
-InspectLoader.register(machinery.BuiltinImporter)
-InspectLoader.register(machinery.FrozenImporter)
+_register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter,
+            machinery.ExtensionFileLoader)
 
 
 class ExecutionLoader(InspectLoader):
@@ -100,6 +113,15 @@
         raise NotImplementedError
 
 
+class FileLoader(_bootstrap.FileLoader, ResourceLoader, ExecutionLoader):
+
+    """Abstract base class partially implementing the ResourceLoader and
+    ExecutionLoader ABCs."""
+
+_register(FileLoader, machinery.SourceFileLoader,
+            machinery._SourcelessFileLoader)
+
+
 class SourceLoader(_bootstrap.SourceLoader, ResourceLoader, ExecutionLoader):
 
     """Abstract base class for loading source code (and optionally any
@@ -146,6 +168,7 @@
         """
         raise NotImplementedError
 
+_register(SourceLoader, machinery.SourceFileLoader)
 
 class PyLoader(SourceLoader):
 
diff --git a/Lib/importlib/machinery.py b/Lib/importlib/machinery.py
index 5197744..c9906c7 100644
--- a/Lib/importlib/machinery.py
+++ b/Lib/importlib/machinery.py
@@ -3,3 +3,7 @@
 from ._bootstrap import BuiltinImporter
 from ._bootstrap import FrozenImporter
 from ._bootstrap import PathFinder
+from ._bootstrap import FileFinder
+from ._bootstrap import SourceFileLoader
+from ._bootstrap import _SourcelessFileLoader
+from ._bootstrap import ExtensionFileLoader
diff --git a/Lib/importlib/test/extension/test_case_sensitivity.py b/Lib/importlib/test/extension/test_case_sensitivity.py
index add830d..1ba2a33 100644
--- a/Lib/importlib/test/extension/test_case_sensitivity.py
+++ b/Lib/importlib/test/extension/test_case_sensitivity.py
@@ -1,3 +1,4 @@
+import imp
 import sys
 from test import support
 import unittest
@@ -13,8 +14,10 @@
         good_name = ext_util.NAME
         bad_name = good_name.upper()
         assert good_name != bad_name
-        finder = _bootstrap._FileFinder(ext_util.PATH,
-                                        _bootstrap._ExtensionFinderDetails())
+        finder = _bootstrap.FileFinder(ext_util.PATH,
+                                        (_bootstrap.ExtensionFileLoader,
+                                            _bootstrap._suffix_list(imp.C_EXTENSION),
+                                            False))
         return finder.find_module(bad_name)
 
     def test_case_sensitive(self):
diff --git a/Lib/importlib/test/extension/test_finder.py b/Lib/importlib/test/extension/test_finder.py
index ea97483..a28cd07 100644
--- a/Lib/importlib/test/extension/test_finder.py
+++ b/Lib/importlib/test/extension/test_finder.py
@@ -2,6 +2,7 @@
 from .. import abc
 from . import util
 
+import imp
 import unittest
 
 class FinderTests(abc.FinderTests):
@@ -9,8 +10,10 @@
     """Test the finder for extension modules."""
 
     def find_module(self, fullname):
-        importer = _bootstrap._FileFinder(util.PATH,
-                                          _bootstrap._ExtensionFinderDetails())
+        importer = _bootstrap.FileFinder(util.PATH,
+                                          (_bootstrap.ExtensionFileLoader,
+                                              _bootstrap._suffix_list(imp.C_EXTENSION),
+                                              False))
         return importer.find_module(fullname)
 
     def test_module(self):
diff --git a/Lib/importlib/test/extension/test_loader.py b/Lib/importlib/test/extension/test_loader.py
index 9e5edad..ab2b686 100644
--- a/Lib/importlib/test/extension/test_loader.py
+++ b/Lib/importlib/test/extension/test_loader.py
@@ -12,7 +12,7 @@
     """Test load_module() for extension modules."""
 
     def load_module(self, fullname):
-        loader = _bootstrap._ExtensionFileLoader(ext_util.NAME,
+        loader = _bootstrap.ExtensionFileLoader(ext_util.NAME,
                                                 ext_util.FILEPATH)
         return loader.load_module(fullname)
 
@@ -25,7 +25,7 @@
                 self.assertEqual(getattr(module, attr), value)
             self.assertTrue(ext_util.NAME in sys.modules)
             self.assertTrue(isinstance(module.__loader__,
-                                    _bootstrap._ExtensionFileLoader))
+                                    _bootstrap.ExtensionFileLoader))
 
     def test_package(self):
         # Extensions are not found in packages.
diff --git a/Lib/importlib/test/extension/test_path_hook.py b/Lib/importlib/test/extension/test_path_hook.py
index 4610420..673c300 100644
--- a/Lib/importlib/test/extension/test_path_hook.py
+++ b/Lib/importlib/test/extension/test_path_hook.py
@@ -14,7 +14,8 @@
     # XXX Should it only work for directories containing an extension module?
 
     def hook(self, entry):
-        return _bootstrap._file_path_hook(entry)
+        return _bootstrap.FileFinder.path_hook((_bootstrap.ExtensionFileLoader,
+            _bootstrap._suffix_list(imp.C_EXTENSION), False))(entry)
 
     def test_success(self):
         # Path hook should handle a directory where a known extension module
diff --git a/Lib/importlib/test/source/test_case_sensitivity.py b/Lib/importlib/test/source/test_case_sensitivity.py
index 569f516..d4bae8d 100644
--- a/Lib/importlib/test/source/test_case_sensitivity.py
+++ b/Lib/importlib/test/source/test_case_sensitivity.py
@@ -2,6 +2,7 @@
 from importlib import _bootstrap
 from .. import util
 from . import util as source_util
+import imp
 import os
 import sys
 from test import support as test_support
@@ -19,9 +20,13 @@
     assert name != name.lower()
 
     def find(self, path):
-        finder = _bootstrap._FileFinder(path,
-                                        _bootstrap._SourceFinderDetails(),
-                                        _bootstrap._SourcelessFinderDetails())
+        finder = _bootstrap.FileFinder(path,
+                                        (_bootstrap.SourceFileLoader,
+                                            _bootstrap._suffix_list(imp.PY_SOURCE),
+                                            True),
+                                        (_bootstrap._SourcelessFileLoader,
+                                            _bootstrap._suffix_list(imp.PY_COMPILED),
+                                            True))
         return finder.find_module(self.name)
 
     def sensitivity_test(self):
diff --git a/Lib/importlib/test/source/test_file_loader.py b/Lib/importlib/test/source/test_file_loader.py
index 710339c..764dcff 100644
--- a/Lib/importlib/test/source/test_file_loader.py
+++ b/Lib/importlib/test/source/test_file_loader.py
@@ -27,7 +27,7 @@
     # [basic]
     def test_module(self):
         with source_util.create_modules('_temp') as mapping:
-            loader = _bootstrap._SourceFileLoader('_temp', mapping['_temp'])
+            loader = _bootstrap.SourceFileLoader('_temp', mapping['_temp'])
             module = loader.load_module('_temp')
             self.assertTrue('_temp' in sys.modules)
             check = {'__name__': '_temp', '__file__': mapping['_temp'],
@@ -37,7 +37,7 @@
 
     def test_package(self):
         with source_util.create_modules('_pkg.__init__') as mapping:
-            loader = _bootstrap._SourceFileLoader('_pkg',
+            loader = _bootstrap.SourceFileLoader('_pkg',
                                                  mapping['_pkg.__init__'])
             module = loader.load_module('_pkg')
             self.assertTrue('_pkg' in sys.modules)
@@ -50,7 +50,7 @@
 
     def test_lacking_parent(self):
         with source_util.create_modules('_pkg.__init__', '_pkg.mod')as mapping:
-            loader = _bootstrap._SourceFileLoader('_pkg.mod',
+            loader = _bootstrap.SourceFileLoader('_pkg.mod',
                                                     mapping['_pkg.mod'])
             module = loader.load_module('_pkg.mod')
             self.assertTrue('_pkg.mod' in sys.modules)
@@ -65,7 +65,7 @@
 
     def test_module_reuse(self):
         with source_util.create_modules('_temp') as mapping:
-            loader = _bootstrap._SourceFileLoader('_temp', mapping['_temp'])
+            loader = _bootstrap.SourceFileLoader('_temp', mapping['_temp'])
             module = loader.load_module('_temp')
             module_id = id(module)
             module_dict_id = id(module.__dict__)
@@ -90,7 +90,7 @@
                 setattr(orig_module, attr, value)
             with open(mapping[name], 'w') as file:
                 file.write('+++ bad syntax +++')
-            loader = _bootstrap._SourceFileLoader('_temp', mapping['_temp'])
+            loader = _bootstrap.SourceFileLoader('_temp', mapping['_temp'])
             with self.assertRaises(SyntaxError):
                 loader.load_module(name)
             for attr in attributes:
@@ -101,7 +101,7 @@
         with source_util.create_modules('_temp') as mapping:
             with open(mapping['_temp'], 'w') as file:
                 file.write('=')
-            loader = _bootstrap._SourceFileLoader('_temp', mapping['_temp'])
+            loader = _bootstrap.SourceFileLoader('_temp', mapping['_temp'])
             with self.assertRaises(SyntaxError):
                 loader.load_module('_temp')
             self.assertTrue('_temp' not in sys.modules)
@@ -114,7 +114,7 @@
             file.write("# test file for importlib")
         try:
             with util.uncache('_temp'):
-                loader = _bootstrap._SourceFileLoader('_temp', file_path)
+                loader = _bootstrap.SourceFileLoader('_temp', file_path)
                 mod = loader.load_module('_temp')
                 self.assertEqual(file_path, mod.__file__)
                 self.assertEqual(imp.cache_from_source(file_path),
@@ -140,7 +140,7 @@
                 if e.errno != getattr(errno, 'EOVERFLOW', None):
                     raise
                 self.skipTest("cannot set modification time to large integer ({})".format(e))
-            loader = _bootstrap._SourceFileLoader('_temp', mapping['_temp'])
+            loader = _bootstrap.SourceFileLoader('_temp', mapping['_temp'])
             mod = loader.load_module('_temp')
             # Sanity checks.
             self.assertEqual(mod.__cached__, compiled)
@@ -255,7 +255,7 @@
 
 class SourceLoaderBadBytecodeTest(BadBytecodeTest):
 
-    loader = _bootstrap._SourceFileLoader
+    loader = _bootstrap.SourceFileLoader
 
     @source_util.writes_bytecode_files
     def test_empty_file(self):
diff --git a/Lib/importlib/test/source/test_finder.py b/Lib/importlib/test/source/test_finder.py
index 315aa77..f5de58a 100644
--- a/Lib/importlib/test/source/test_finder.py
+++ b/Lib/importlib/test/source/test_finder.py
@@ -3,6 +3,7 @@
 
 from importlib import _bootstrap
 import errno
+import imp
 import os
 import py_compile
 from test.support import make_legacy_pyc
@@ -35,9 +36,11 @@
     """
 
     def import_(self, root, module):
-        finder = _bootstrap._FileFinder(root,
-                                        _bootstrap._SourceFinderDetails(),
-                                        _bootstrap._SourcelessFinderDetails())
+        loader_details = [(_bootstrap.SourceFileLoader,
+                            _bootstrap._suffix_list(imp.PY_SOURCE), True),
+                          (_bootstrap._SourcelessFileLoader,
+                            _bootstrap._suffix_list(imp.PY_COMPILED), True)]
+        finder = _bootstrap.FileFinder(root, *loader_details)
         return finder.find_module(module)
 
     def run_test(self, test, create=None, *, compile_=None, unlink=None):
@@ -135,7 +138,8 @@
 
     def test_empty_string_for_dir(self):
         # The empty string from sys.path means to search in the cwd.
-        finder = _bootstrap._FileFinder('', _bootstrap._SourceFinderDetails())
+        finder = _bootstrap.FileFinder('', (_bootstrap.SourceFileLoader,
+            _bootstrap._suffix_list(imp.PY_SOURCE), True))
         with open('mod.py', 'w') as file:
             file.write("# test file for importlib")
         try:
@@ -146,7 +150,8 @@
 
     def test_invalidate_caches(self):
         # invalidate_caches() should reset the mtime.
-        finder = _bootstrap._FileFinder('', _bootstrap._SourceFinderDetails())
+        finder = _bootstrap.FileFinder('', (_bootstrap.SourceFileLoader,
+            _bootstrap._suffix_list(imp.PY_SOURCE), True))
         finder._path_mtime = 42
         finder.invalidate_caches()
         self.assertEqual(finder._path_mtime, -1)
diff --git a/Lib/importlib/test/source/test_path_hook.py b/Lib/importlib/test/source/test_path_hook.py
index 3de822c..663a128 100644
--- a/Lib/importlib/test/source/test_path_hook.py
+++ b/Lib/importlib/test/source/test_path_hook.py
@@ -1,6 +1,7 @@
 from . import util as source_util
 
 from importlib import _bootstrap
+import imp
 import unittest
 
 
@@ -8,14 +9,18 @@
 
     """Test the path hook for source."""
 
+    def path_hook(self):
+        return _bootstrap.FileFinder.path_hook((_bootstrap.SourceFileLoader,
+            _bootstrap._suffix_list(imp.PY_SOURCE), True))
+
     def test_success(self):
         with source_util.create_modules('dummy') as mapping:
-            self.assertTrue(hasattr(_bootstrap._file_path_hook(mapping['.root']),
+            self.assertTrue(hasattr(self.path_hook()(mapping['.root']),
                                  'find_module'))
 
     def test_empty_string(self):
         # The empty string represents the cwd.
-        self.assertTrue(hasattr(_bootstrap._file_path_hook(''), 'find_module'))
+        self.assertTrue(hasattr(self.path_hook()(''), 'find_module'))
 
 
 def test_main():
diff --git a/Lib/importlib/test/source/test_source_encoding.py b/Lib/importlib/test/source/test_source_encoding.py
index 72a1360..0ca5195 100644
--- a/Lib/importlib/test/source/test_source_encoding.py
+++ b/Lib/importlib/test/source/test_source_encoding.py
@@ -35,7 +35,7 @@
         with source_util.create_modules(self.module_name) as mapping:
             with open(mapping[self.module_name], 'wb') as file:
                 file.write(source)
-            loader = _bootstrap._SourceFileLoader(self.module_name,
+            loader = _bootstrap.SourceFileLoader(self.module_name,
                                                   mapping[self.module_name])
             return loader.load_module(self.module_name)
 
@@ -97,7 +97,7 @@
         with source_util.create_modules(module_name) as mapping:
             with open(mapping[module_name], 'wb') as file:
                 file.write(source)
-            loader = _bootstrap._SourceFileLoader(module_name,
+            loader = _bootstrap.SourceFileLoader(module_name,
                                                  mapping[module_name])
             return loader.load_module(module_name)
 
diff --git a/Lib/importlib/test/test_abc.py b/Lib/importlib/test/test_abc.py
index 0ecbe39..e9eec60 100644
--- a/Lib/importlib/test/test_abc.py
+++ b/Lib/importlib/test/test_abc.py
@@ -50,7 +50,7 @@
 
     superclasses = [abc.Loader]
     subclasses = [abc.PyLoader, machinery.BuiltinImporter,
-                    machinery.FrozenImporter]
+                    machinery.FrozenImporter, machinery.ExtensionFileLoader]
 
 
 class ExecutionLoader(InheritanceTests, unittest.TestCase):
@@ -59,9 +59,16 @@
     subclasses = [abc.PyLoader]
 
 
+class FileLoader(InheritanceTests, unittest.TestCase):
+
+    superclasses = [abc.ResourceLoader, abc.ExecutionLoader]
+    subclasses = [machinery.SourceFileLoader, machinery._SourcelessFileLoader]
+
+
 class SourceLoader(InheritanceTests, unittest.TestCase):
 
     superclasses = [abc.ResourceLoader, abc.ExecutionLoader]
+    subclasses = [machinery.SourceFileLoader]
 
 
 class PyLoader(InheritanceTests, unittest.TestCase):
diff --git a/Misc/NEWS b/Misc/NEWS
index 975e684..5bb9b92 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -61,6 +61,9 @@
 Library
 -------
 
+- Issue #14605: Add importlib.abc.FileLoader, importlib.machinery.(FileFinder,
+  SourceFileLoader, _SourcelessFileLoader, ExtensionFileLoader).
+
 - Issue #13959: imp.cache_from_source()/source_from_cache() now follow
   os.path.join()/split() semantics for path manipulation instead of its prior,
   custom semantics of caring the right-most path separator forward in path
diff --git a/Python/importlib.h b/Python/importlib.h
index 4b1001d..bc4c0d9 100644
--- a/Python/importlib.h
+++ b/Python/importlib.h
Binary files differ